diff --git a/packages/design-system/lib/components/Select/SelectFilter.tsx b/packages/design-system/lib/components/Select/SelectFilter.tsx index d6caeda7f..6bf693855 100644 --- a/packages/design-system/lib/components/Select/SelectFilter.tsx +++ b/packages/design-system/lib/components/Select/SelectFilter.tsx @@ -1,3 +1,5 @@ +'use client' + import { Button, ComboBox, @@ -6,8 +8,16 @@ import { ListBox, Popover, Label, + ComboBoxStateContext, } from 'react-aria-components' -import { useState } from 'react' +import { + PropsWithChildren, + RefObject, + useContext, + useEffect, + useRef, + useState, +} from 'react' import { MaterialIcon } from '../Icons/MaterialIcon' import { Typography } from '../Typography' @@ -17,6 +27,60 @@ import type { SelectFilterProps } from './types' import styles from './select.module.css' +/** + * ComboBoxInner + * Used to simulate a press on down arrow key to highlight the first item. + * We do this so that a user can type some characters and then select the first + * option directly with tab. Might require revisit later when assistive + * technologies test it out as it might be a bit verbose for the announcements + * because it will most likely announce the "first item" for each typed + * character from the user. + */ +function ComboBoxInner({ + inputRef, + children, +}: PropsWithChildren<{ + inputRef: RefObject +}>) { + // Get the state for the ComboBox from RAC + const comboBoxState = useContext(ComboBoxStateContext) + + const { isFocused, selectedKey, inputValue } = comboBoxState ?? {} + + // Act after render + useEffect(() => { + let timeout: Timer + + // We only want to act on focused field which has rerendered due to + // changes to its input value. Selecting a value will also rerender, but + // that we want to ignore as that is not a change to its input value. + if (isFocused && selectedKey !== inputValue) { + // The simlulated event has to originate from the input field. + if (inputRef.current) { + // Simulate a press on down arrow key. + const e = new KeyboardEvent('keydown', { + key: 'ArrowDown', + bubbles: true /* important so RAC can act on it */, + }) + + // Dispatch after everything has rendered completely + timeout = setTimeout(() => { + inputRef.current?.dispatchEvent(e) + }, 0) + } + } + + // Clean up + return () => { + if (timeout) { + clearTimeout(timeout) + } + } + }, [inputRef, inputValue, isFocused, selectedKey]) + + return children +} + export function SelectFilter({ name, label, @@ -35,6 +99,8 @@ export function SelectFilter({ const [value, setValue] = useState(defaultSelectedKey ?? null) const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default' + const inputRef = useRef(null) + return ( - + + + + {items.map((item) => ( + + {item.label} + + ))} + + + ) }