Skip to content

Commit 9b63299

Browse files
Add openOnFocus prop to AutocompleteInput (#4555)
* Add `openOnFocus` prop to `AutocompleteInput` Towards github/accessibility-audits#7437 * Add changeset * Fix JSON formatting * Allow up and down to open menu if not already open * update hook * Update packages/react/src/Autocomplete/AutocompleteInput.tsx Co-authored-by: Kendall Gassner <[email protected]> --------- Co-authored-by: Kendall Gassner <[email protected]>
1 parent 68e6326 commit 9b63299

File tree

4 files changed

+62
-8
lines changed

4 files changed

+62
-8
lines changed

.changeset/few-weeks-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@primer/react': patch
3+
---
4+
5+
Add `openInFocus` prop (default: true) to `AutocompleteInput`

packages/react/src/Autocomplete/Autocomplete.docs.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
"name": "as",
2020
"type": "React.ElementType",
2121
"defaultValue": "TextInput"
22+
},
23+
{
24+
"name": "openOnFocus",
25+
"type": "boolean",
26+
"defaultValue": "true",
27+
"description": "Whether the associated autocomplete menu should open on an input focus event"
2228
}
2329
],
2430
"passthrough": {

packages/react/src/Autocomplete/Autocomplete.stories.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ const getArgsByChildComponent = ({
2929
menuLoading,
3030
selectionVariant,
3131

32+
// Autocomplete.Input
33+
openOnFocus,
34+
3235
// Autocomplete.Overlay
3336
anchorSide,
3437
height,
@@ -65,6 +68,7 @@ any) => {
6568
}
6669
return {
6770
menuArgs: {emptyStateText, loading: menuLoading, selectionVariant},
71+
inputArgs: {openOnFocus},
6872
overlayArgs: {anchorSide, height, maxHeight: overlayMaxHeight, width},
6973
textInputArgs,
7074
textInputWithTokensArgs: {
@@ -131,6 +135,7 @@ const autocompleteStoryMeta: Meta = {
131135
emptyStateText: 'No selectable options',
132136
menuLoading: false,
133137
selectionVariant: 'single',
138+
openOnFocus: true,
134139
anchorSide: undefined,
135140
height: 'auto',
136141
overlayMaxHeight: undefined,
@@ -161,6 +166,16 @@ const autocompleteStoryMeta: Meta = {
161166
},
162167
},
163168

169+
// Autocomplete.Input
170+
openOnFocus: {
171+
control: {
172+
type: 'boolean',
173+
},
174+
table: {
175+
category: 'Autocomplete.Input',
176+
},
177+
},
178+
164179
// Autocomplete.Overlay
165180
anchorSide: {
166181
control: {
@@ -217,7 +232,7 @@ const autocompleteStoryMeta: Meta = {
217232

218233
export const Default = (args: FormControlArgs<AutocompleteArgs>) => {
219234
const {parentArgs, labelArgs, captionArgs, validationArgs} = getFormControlArgsByChildComponent(args)
220-
const {menuArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args)
235+
const {menuArgs, inputArgs, overlayArgs, textInputArgs} = getArgsByChildComponent(args)
221236
const isMultiselect = menuArgs.selectionVariant === 'multiple'
222237
const [selectedItemIds, setSelectedItemIds] = useState<Array<string>>([])
223238
const onSelectedChange = (newlySelectedItems: Datum | Datum[]) => {
@@ -228,12 +243,19 @@ export const Default = (args: FormControlArgs<AutocompleteArgs>) => {
228243
setSelectedItemIds(newlySelectedItems.map(item => item.id))
229244
}
230245

246+
const autocompleteInput = {...inputArgs, ...textInputArgs}
247+
const formValidationId = 'validation-field'
231248
return (
232249
<Box as="form" sx={{p: 3}} onSubmit={event => event.preventDefault()}>
233250
<FormControl {...parentArgs}>
234251
<FormControl.Label id="autocompleteLabel" {...labelArgs} />
235252
<Autocomplete>
236-
<Autocomplete.Input {...textInputArgs} size={textInputArgs.inputSize} data-testid="autocompleteInput" />
253+
<Autocomplete.Input
254+
aria-describedby={formValidationId}
255+
{...autocompleteInput}
256+
size={textInputArgs.inputSize}
257+
data-testid="autocompleteInput"
258+
/>
237259
<Autocomplete.Overlay {...overlayArgs}>
238260
<Autocomplete.Menu
239261
items={items}
@@ -246,7 +268,7 @@ export const Default = (args: FormControlArgs<AutocompleteArgs>) => {
246268
</Autocomplete>
247269
{captionArgs.children && <FormControl.Caption {...captionArgs} />}
248270
{validationArgs.children && validationArgs.variant && (
249-
<FormControl.Validation {...validationArgs} variant={validationArgs.variant} />
271+
<FormControl.Validation id={formValidationId} {...validationArgs} variant={validationArgs.variant} />
250272
)}
251273
</FormControl>
252274
</Box>

packages/react/src/Autocomplete/AutocompleteInput.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,27 @@ import useSafeTimeout from '../hooks/useSafeTimeout'
1010
type InternalAutocompleteInputProps = {
1111
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1212
as?: React.ComponentType<React.PropsWithChildren<any>>
13+
// When false, the autocomplete menu will not render either on mouse click or
14+
// keyboard focus.
15+
openOnFocus?: boolean
1316
}
1417

18+
const ARROW_KEYS_NAV = new Set(['ArrowUp', 'ArrowDown'])
19+
1520
const AutocompleteInput = React.forwardRef(
1621
(
17-
{as: Component = TextInput, onFocus, onBlur, onChange, onKeyDown, onKeyUp, onKeyPress, value, ...props},
22+
{
23+
as: Component = TextInput,
24+
onFocus,
25+
onBlur,
26+
onChange,
27+
onKeyDown,
28+
onKeyUp,
29+
onKeyPress,
30+
value,
31+
openOnFocus = true,
32+
...props
33+
},
1834
forwardedRef,
1935
) => {
2036
const autocompleteContext = useContext(AutocompleteContext)
@@ -38,10 +54,12 @@ const AutocompleteInput = React.forwardRef(
3854

3955
const handleInputFocus: FocusEventHandler<HTMLInputElement> = useCallback(
4056
event => {
41-
onFocus && onFocus(event)
42-
setShowMenu(true)
57+
if (openOnFocus) {
58+
onFocus?.(event)
59+
setShowMenu(true)
60+
}
4361
},
44-
[onFocus, setShowMenu],
62+
[onFocus, setShowMenu, openOnFocus],
4563
)
4664

4765
const handleInputBlur: FocusEventHandler<HTMLInputElement> = useCallback(
@@ -83,8 +101,11 @@ const AutocompleteInput = React.forwardRef(
83101
setInputValue('')
84102
inputRef.current.value = ''
85103
}
104+
if (!showMenu && ARROW_KEYS_NAV.has(event.key) && !event.altKey) {
105+
setShowMenu(true)
106+
}
86107
},
87-
[inputRef, setInputValue, setHighlightRemainingText, onKeyDown],
108+
[inputRef, setInputValue, setHighlightRemainingText, onKeyDown, showMenu, setShowMenu],
88109
)
89110

90111
const handleInputKeyUp: KeyboardEventHandler<HTMLInputElement> = useCallback(

0 commit comments

Comments
 (0)