AutoRpa/state_center_web/src/components/ToolbarSelect.tsx

98 lines
3.1 KiB
TypeScript

import { useEffect, useId, useRef, useState } from 'react';
export type ToolbarSelectOption = {
value: string;
label: string;
};
type Props = {
value: string;
options: ToolbarSelectOption[];
ariaLabel: string;
onChange(value: string): void;
};
export function ToolbarSelect({ value, options, ariaLabel, onChange }: Props) {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
const listId = useId();
const selected = options.find((option) => option.value === value) || options[0];
useEffect(() => {
if (!open) return;
function onPointer(event: MouseEvent) {
if (!rootRef.current?.contains(event.target as Node)) setOpen(false);
}
function onKey(event: KeyboardEvent) {
if (event.key === 'Escape') {
event.preventDefault();
setOpen(false);
return;
}
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
event.preventDefault();
const idx = options.findIndex((option) => option.value === value);
const nextIndex = event.key === 'ArrowDown'
? Math.min(options.length - 1, Math.max(0, idx + 1))
: Math.max(0, idx < 0 ? 0 : idx - 1);
const next = options[nextIndex];
if (next) onChange(next.value);
}
document.addEventListener('mousedown', onPointer);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onPointer);
document.removeEventListener('keydown', onKey);
};
}, [open, options, value, onChange]);
return (
<div className={`toolbar-select${open ? ' is-open' : ''}`} ref={rootRef}>
<button
type="button"
className="toolbar-select-trigger"
aria-haspopup="listbox"
aria-expanded={open}
aria-controls={listId}
aria-label={ariaLabel}
onClick={() => setOpen((current) => !current)}
>
<span>{selected?.label}</span>
<svg className="toolbar-select-chevron" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
d="M3 4.5L6 7.5L9 4.5"
/>
</svg>
</button>
{open ? (
<ul className="toolbar-select-menu" id={listId} role="listbox" aria-label={ariaLabel}>
{options.map((option) => {
const active = option.value === value;
return (
<li key={option.value} role="presentation">
<button
type="button"
role="option"
aria-selected={active}
className={active ? 'toolbar-select-option is-selected' : 'toolbar-select-option'}
onClick={() => {
onChange(option.value);
setOpen(false);
}}
>
{option.label}
</button>
</li>
);
})}
</ul>
) : null}
</div>
);
}