fix: preselect combobox on focus
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ComboBox,
|
ComboBox,
|
||||||
@@ -6,8 +8,16 @@ import {
|
|||||||
ListBox,
|
ListBox,
|
||||||
Popover,
|
Popover,
|
||||||
Label,
|
Label,
|
||||||
|
ComboBoxStateContext,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { useState } from 'react'
|
import {
|
||||||
|
PropsWithChildren,
|
||||||
|
RefObject,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
|
||||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||||
import { Typography } from '../Typography'
|
import { Typography } from '../Typography'
|
||||||
@@ -17,6 +27,60 @@ import type { SelectFilterProps } from './types'
|
|||||||
|
|
||||||
import styles from './select.module.css'
|
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({
|
export function SelectFilter({
|
||||||
name,
|
name,
|
||||||
label,
|
label,
|
||||||
@@ -35,6 +99,8 @@ export function SelectFilter({
|
|||||||
const [value, setValue] = useState<Key | null>(defaultSelectedKey ?? null)
|
const [value, setValue] = useState<Key | null>(defaultSelectedKey ?? null)
|
||||||
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default'
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComboBox
|
<ComboBox
|
||||||
className={styles.select}
|
className={styles.select}
|
||||||
@@ -56,58 +122,64 @@ export function SelectFilter({
|
|||||||
defaultSelectedKey={defaultSelectedKey}
|
defaultSelectedKey={defaultSelectedKey}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Label className={styles.inner}>
|
<ComboBoxInner inputRef={inputRef}>
|
||||||
{icon ? (
|
<Label className={styles.inner}>
|
||||||
<MaterialIcon
|
{icon ? (
|
||||||
icon={icon}
|
<MaterialIcon
|
||||||
size={24}
|
icon={icon}
|
||||||
color={iconColor}
|
size={24}
|
||||||
aria-hidden="true"
|
color={iconColor}
|
||||||
/>
|
aria-hidden="true"
|
||||||
) : null}
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<span className={styles.displayText}>
|
<span className={styles.displayText}>
|
||||||
<Typography
|
<Typography
|
||||||
variant={
|
variant={
|
||||||
focus || value ? 'Label/xsRegular' : 'Body/Paragraph/mdRegular'
|
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}
|
|
||||||
>
|
>
|
||||||
{item.label}
|
<span className={styles.label}>{label}</span>
|
||||||
</SelectItem>
|
</Typography>
|
||||||
))}
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
</ListBox>
|
<Input
|
||||||
</Popover>
|
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>
|
</ComboBox>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user