feat/custom-select-dropdowns#624
Conversation
Replace native HTML <select> elements with a custom dropdown component that matches the dashboard design system (colors, typography, spacing, border radius, theme support, RTL, responsive). Includes full keyboard navigation, ARIA attributes, and outside-click dismissal.
Use the new CustomSelect component for the 'All Status' (Sessions) and 'All Severities' (Logs) dropdowns.
Use the CustomSelect component for the language picker for UI consistency.
rmyndharis
left a comment
There was a problem hiding this comment.
Thanks for this — the component matches the design system nicely and I verified the branch locally (tsc + vite build, lint, and i18n:check all pass). One accessibility regression needs fixing before merge, since this replaces a native <select>:
Focus is dropped to <body> on every selection and on Escape.
While the dropdown is open, real DOM focus is moved onto an option <button> via the effect:
useEffect(() => {
if (isOpen && focusedIndex >= 0 && listRef.current) {
const items = listRef.current.querySelectorAll<HTMLButtonElement>('.custom-select-option');
items[focusedIndex]?.focus();
}
}, [isOpen, focusedIndex]);On close() (selecting an option, Escape, or Tab) isOpen becomes false, which unmounts the entire {isOpen && (...)} dropdown subtree — including the option button that currently holds focus. Nothing returns focus to the trigger (there's no triggerRef), so the browser resets focus to document.body.
Impact: a keyboard/screen-reader user opens the filter, arrows to an option, presses Enter — and is silently dumped to the top of the page. The next Tab starts from the first focusable element and the screen reader loses its reading position. A native <select> returns focus to the control after selection/Escape; this is a WCAG 2.4.3 (Focus Order) regression, and it happens on all three call sites (Sessions, Logs, Login) on every selection.
Fix (~5 lines): add a ref to the trigger button and call triggerRef.current?.focus() inside close().
Non-blocking nice-to-haves:
- Restore native type-ahead (first-letter jump) and Home/End key support — the native
<select>this replaces had them. - The trigger sets
aria-activedescendantwhile real DOM focus roves to the option buttons. Those are two different, mutually-exclusive ARIA patterns — pick the roving-focus one you already use and droparia-activedescendantwhen you touch this.
Once focus returns to the trigger on close, this is good to go.
Adds a triggerRef and calls triggerRef.current?.focus() inside close() so focus returns to the trigger button when an option is selected or Escape is pressed. Without this, unmounting the dropdown drops focus to document.body (WCAG 2.4.3 regression). Also removes aria-activedescendant (mutually exclusive with the roving-focus pattern used), and adds Home/End and type-ahead (first-letter jump) support to restore native <select> behavior.
|
Thanks for the thorough review! I added a Also addressed both suggestions:
Also resolved the CHANGELOG conflict. |
Description
Replaces native HTML
<select>elements in the dashboard with a reusableCustomSelectcomponent that matches the existing design system (colors,typography, spacing, border radius, animations, dark/light themes).
Changes
CustomSelectcomponent with keyboard navigation, ARIAaccessibility, theme support, responsive design, RTL support
Type of Change
Checklist
Screenshots (if applicable)
Related Issues
Closes #