fix(ui): report category dropdown uses folds menu, not native select (N56)
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>
This commit is contained in:
+1
-2
@@ -397,9 +397,8 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
**N56 — Report Modal Category Dropdown: Native `<select>` Instead of folds `Chip`+`PopOut`+`Menu`**
|
||||
|
||||
- **File:** `src/app/features/room/ReportRoomModal.tsx` lines 138–163; `src/app/features/room/ReportUserModal.tsx` lines 144–169
|
||||
- **Status:** **OPEN**
|
||||
- **Status:** **FIXED** — extracted a shared `ReportCategorySelect` component (`src/app/features/room/ReportCategorySelect.tsx`) using the folds `Button` trigger + `PopOut` + `FocusTrap` + `Menu` + `MenuItem` pattern (with `escapeDeactivates`/arrow-key nav, matching `OrderButton`); both modals now use it instead of the native `<select>`.
|
||||
- **Issue:** Both report modals render the "Category" field as `<Box as="select">` with hand-rolled inline styles (padding, border, background, color, fontSize, fontFamily). No other selector in the message-action modal context uses `<select>` — the established pattern for all dropdowns in both message modals and search filters is `Chip onClick → setMenuAnchor → PopOut → FocusTrap → Menu → MenuItem` (reference: `OrderButton` in `SearchFilters.tsx` lines 63–114).
|
||||
- **Fix:** Replace native `<select>` with `<Chip>` trigger + `<PopOut>` + `<Menu>` + `<MenuItem>` pattern.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Spinner,
|
||||
} from 'folds';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { ReportCategorySelect } from './ReportCategorySelect';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
@@ -131,31 +132,12 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) {
|
||||
<Text as="label" htmlFor="report-category" size="L400">
|
||||
Category
|
||||
</Text>
|
||||
<Box
|
||||
as="select"
|
||||
<ReportCategorySelect
|
||||
id="report-category"
|
||||
aria-label="Report category"
|
||||
value={category}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setCategory(e.target.value as ReportCategory)
|
||||
}
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
borderRadius: config.radii.R300,
|
||||
border: `1px solid ${color.Surface.ContainerLine}`,
|
||||
background: color.Surface.Container,
|
||||
color: color.Surface.OnContainer,
|
||||
fontSize: 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{(Object.keys(CATEGORY_LABELS) as ReportCategory[]).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{CATEGORY_LABELS[key]}
|
||||
</option>
|
||||
))}
|
||||
</Box>
|
||||
labels={CATEGORY_LABELS}
|
||||
onChange={(v) => setCategory(v as ReportCategory)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box direction="Column" gap="100">
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from 'folds';
|
||||
import { Method } from 'matrix-js-sdk';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { ReportCategorySelect } from './ReportCategorySelect';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
@@ -137,31 +138,12 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
|
||||
<Text as="label" htmlFor="report-user-category" size="L400">
|
||||
Category
|
||||
</Text>
|
||||
<Box
|
||||
as="select"
|
||||
<ReportCategorySelect
|
||||
id="report-user-category"
|
||||
aria-label="Report category"
|
||||
value={category}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setCategory(e.target.value as ReportCategory)
|
||||
}
|
||||
style={{
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
borderRadius: config.radii.R300,
|
||||
border: `1px solid ${color.Surface.ContainerLine}`,
|
||||
background: color.Surface.Container,
|
||||
color: color.Surface.OnContainer,
|
||||
fontSize: 'inherit',
|
||||
fontFamily: 'inherit',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{(Object.keys(CATEGORY_LABELS) as ReportCategory[]).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{CATEGORY_LABELS[key]}
|
||||
</option>
|
||||
))}
|
||||
</Box>
|
||||
labels={CATEGORY_LABELS}
|
||||
onChange={(v) => setCategory(v as ReportCategory)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box direction="Column" gap="100">
|
||||
|
||||
Reference in New Issue
Block a user