5470e25bb0
Extract a shared ReportCategorySelect: folds Button trigger + PopOut + FocusTrap + Menu + MenuItem (escape + arrow-key nav, like OrderButton), replacing the OS-styled native <select> in both ReportRoomModal and ReportUserModal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
import React, { MouseEventHandler, useState } from 'react';
|
|
import { Box, Button, Icon, Icons, Menu, MenuItem, PopOut, RectCords, Text, config } from 'folds';
|
|
import FocusTrap from 'focus-trap-react';
|
|
import { stopPropagation } from '../../utils/keyboard';
|
|
|
|
type ReportCategorySelectProps = {
|
|
id?: string;
|
|
value: string;
|
|
labels: Record<string, string>;
|
|
onChange: (value: string) => void;
|
|
};
|
|
|
|
/**
|
|
* Category dropdown for the report modals — folds `Button` + `PopOut` + `Menu`
|
|
* pattern (matching `OrderButton` in SearchFilters), replacing the OS-styled
|
|
* native `<select>` that looked foreign inside the modal.
|
|
*/
|
|
export function ReportCategorySelect({ id, value, labels, onChange }: ReportCategorySelectProps) {
|
|
const [anchor, setAnchor] = useState<RectCords>();
|
|
|
|
const handleOpen: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
|
setAnchor(evt.currentTarget.getBoundingClientRect());
|
|
};
|
|
|
|
const handleSelect = (key: string) => {
|
|
onChange(key);
|
|
setAnchor(undefined);
|
|
};
|
|
|
|
return (
|
|
<PopOut
|
|
anchor={anchor}
|
|
position="Bottom"
|
|
align="Start"
|
|
offset={4}
|
|
content={
|
|
<FocusTrap
|
|
focusTrapOptions={{
|
|
initialFocus: false,
|
|
onDeactivate: () => setAnchor(undefined),
|
|
clickOutsideDeactivates: true,
|
|
escapeDeactivates: stopPropagation,
|
|
isKeyForward: (evt: KeyboardEvent) =>
|
|
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
|
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
|
|
}}
|
|
>
|
|
<Menu style={{ padding: config.space.S100 }}>
|
|
<Box direction="Column" gap="100">
|
|
{Object.keys(labels).map((key) => (
|
|
<MenuItem
|
|
key={key}
|
|
size="300"
|
|
variant={key === value ? 'Primary' : 'Surface'}
|
|
radii="300"
|
|
aria-pressed={key === value}
|
|
onClick={() => handleSelect(key)}
|
|
>
|
|
<Text size="T300">{labels[key]}</Text>
|
|
</MenuItem>
|
|
))}
|
|
</Box>
|
|
</Menu>
|
|
</FocusTrap>
|
|
}
|
|
>
|
|
<Button
|
|
id={id}
|
|
type="button"
|
|
variant="Secondary"
|
|
fill="Soft"
|
|
size="400"
|
|
radii="300"
|
|
outlined
|
|
onClick={handleOpen}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={!!anchor}
|
|
after={<Icon size="100" src={Icons.ChevronBottom} />}
|
|
style={{ width: '100%', justifyContent: 'space-between' }}
|
|
>
|
|
<Text size="T300">{labels[value] ?? value}</Text>
|
|
</Button>
|
|
</PopOut>
|
|
);
|
|
}
|