fix: preselect combobox on focus
This commit is contained in:
@@ -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<HTMLInputElement>
|
||||
}>) {
|
||||
// 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<Key | null>(defaultSelectedKey ?? null)
|
||||
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
return (
|
||||
<ComboBox
|
||||
className={styles.select}
|
||||
@@ -56,58 +122,64 @@ export function SelectFilter({
|
||||
defaultSelectedKey={defaultSelectedKey}
|
||||
{...props}
|
||||
>
|
||||
<Label className={styles.inner}>
|
||||
{icon ? (
|
||||
<MaterialIcon
|
||||
icon={icon}
|
||||
size={24}
|
||||
color={iconColor}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<ComboBoxInner inputRef={inputRef}>
|
||||
<Label className={styles.inner}>
|
||||
{icon ? (
|
||||
<MaterialIcon
|
||||
icon={icon}
|
||||
size={24}
|
||||
color={iconColor}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<span className={styles.displayText}>
|
||||
<Typography
|
||||
variant={
|
||||
focus || value ? 'Label/xsRegular' : 'Body/Paragraph/mdRegular'
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{label}</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<Input className={styles.input} autoComplete={autoComplete} />
|
||||
</Typography>
|
||||
</span>
|
||||
<Button className={styles.button}>
|
||||
<MaterialIcon
|
||||
icon="chevron_right"
|
||||
size={24}
|
||||
color={iconColor}
|
||||
aria-hidden="true"
|
||||
className={styles.chevron}
|
||||
/>
|
||||
</Button>
|
||||
</Label>
|
||||
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
shouldFlip={false}
|
||||
crossOffset={icon ? -40 : -8}
|
||||
offset={22}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
id={item.value}
|
||||
icon={item.icon || itemIcon}
|
||||
isDisabled={item.isDisabled}
|
||||
<span className={styles.displayText}>
|
||||
<Typography
|
||||
variant={
|
||||
focus || value ? 'Label/xsRegular' : 'Body/Paragraph/mdRegular'
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
<span className={styles.label}>{label}</span>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={styles.input}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
</Typography>
|
||||
</span>
|
||||
<Button className={styles.button}>
|
||||
<MaterialIcon
|
||||
icon="chevron_right"
|
||||
size={24}
|
||||
color={iconColor}
|
||||
aria-hidden="true"
|
||||
className={styles.chevron}
|
||||
/>
|
||||
</Button>
|
||||
</Label>
|
||||
|
||||
<Popover
|
||||
className={styles.popover}
|
||||
shouldFlip={false}
|
||||
crossOffset={icon ? -40 : -8}
|
||||
offset={22}
|
||||
>
|
||||
<ListBox className={styles.listBox}>
|
||||
{items.map((item) => (
|
||||
<SelectItem
|
||||
key={item.value}
|
||||
id={item.value}
|
||||
icon={item.icon || itemIcon}
|
||||
isDisabled={item.isDisabled}
|
||||
>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</ComboBoxInner>
|
||||
</ComboBox>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user