Skip to content

Commit

Permalink
feat(Combobox): allow combobox to take an input ref (#1297)
Browse files Browse the repository at this point in the history
  • Loading branch information
thuey authored Dec 1, 2023
1 parent ec51188 commit 6dd5a34
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 7 deletions.
7 changes: 6 additions & 1 deletion docs/pages/components/Combobox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,12 @@ When autocomplete is set to "automatic" the listbox will provide a filtered list
name: 'portal',
type: ['React.Ref<HTMLElement>', 'HTMLElement'],
description: 'Alternative placement of combobox listbox. When not set, the listbox will be absolutely positioned inline.'
}
},
{
name: 'inputRef',
type: 'React.Ref<HTMLInputElement>',
description: 'Ref for the input.'
},
]} />
### ComboboxOption
Expand Down
27 changes: 24 additions & 3 deletions packages/react/__tests__/src/components/Combobox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,27 @@ test('should render required combobox', () => {
});

test('should render combobox with error', () => {
const errorId = 'combo-error';
const wrapper = mount(
<Combobox required error="You forgot to choose a value.">
<Combobox
id="combo"
aria-describedby="other-id"
required
error="You forgot to choose a value."
>
<ComboboxOption>Apple</ComboboxOption>
<ComboboxOption>Banana</ComboboxOption>
<ComboboxOption>Cantaloupe</ComboboxOption>
</Combobox>
);

expect(wrapper.find('.Error').exists()).toBeTruthy();
expect(wrapper.find('.Error').text()).toEqual(
expect(wrapper.find(`#${errorId}`).exists()).toBeTruthy();
expect(wrapper.find(`#${errorId}`).text()).toEqual(
'You forgot to choose a value.'
);
expect(
wrapper.find('input').getDOMNode().getAttribute('aria-describedby')
).toBe(`other-id ${errorId}`);
});

test('should open combobox listbox on click', () => {
Expand Down Expand Up @@ -260,6 +269,18 @@ test('should focus combobox input on click', () => {
expect(onFocus.calledOnce).toBeTruthy();
});

test('should allow an input ref to be passed to the combobox', () => {
const inputRef = React.createRef();
const wrapper = mount(
<Combobox inputRef={inputRef}>
<ComboboxOption>Apple</ComboboxOption>
</Combobox>
);

expect(inputRef.current).toBeTruthy();
expect(inputRef.current).toEqual(wrapper.find('input').getDOMNode());
});

test('should open combobox listbox on focus', () => {
const wrapper = mount(
<Combobox>
Expand Down
18 changes: 15 additions & 3 deletions packages/react/src/components/Combobox/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { ComboboxOptionState } from './ComboboxContext';
import type { ComboboxValue } from './ComboboxOption';
import type { ListboxOption } from '../Listbox/ListboxContext';
import useSharedRef from '../../utils/useSharedRef';
import tokenList from '../../utils/token-list';

// Event Keys
const [Enter, Escape, Home, End] = ['Enter', 'Escape', 'Home', 'End'];
Expand Down Expand Up @@ -51,6 +52,7 @@ interface ComboboxProps
onActiveChange?: (option: ListboxOption) => void;
renderNoResults?: (() => JSX.Element) | React.ReactElement;
portal?: React.RefObject<HTMLElement> | HTMLElement;
inputRef?: React.Ref<HTMLInputElement>;
}

const defaultAutoCompleteMatches = (inputValue: string, value: string) => {
Expand Down Expand Up @@ -95,6 +97,8 @@ const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
name,
renderNoResults,
portal,
inputRef: propInputRef = null,
'aria-describedby': ariaDescribedby,
...props
},
ref
Expand All @@ -110,7 +114,7 @@ const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
useState<ListboxOption | null>(null);
const [id] = propId ? [propId] : useId(1, 'combobox');
const comboboxRef = useSharedRef<HTMLDivElement>(ref);
const inputRef = useRef<HTMLInputElement>(null);
const inputRef = useSharedRef<HTMLInputElement>(propInputRef);
const listboxRef = useRef<HTMLUListElement>(null);
const isControlled = typeof propValue !== 'undefined';
const isRequired = !!props.required;
Expand Down Expand Up @@ -381,6 +385,14 @@ const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
</Listbox>
);

const errorId = `${id}-error`;
const inputProps = {
...props,
'aria-describedby': error
? tokenList(errorId, ariaDescribedby)
: ariaDescribedby
};

return (
<div
id={id}
Expand Down Expand Up @@ -423,7 +435,7 @@ const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
aria-activedescendant={
open && activeDescendant ? activeDescendant.element.id : undefined
}
{...props}
{...inputProps}
onChange={handleChange}
onKeyDown={handleKeyDown}
onFocus={handleFocus}
Expand Down Expand Up @@ -452,7 +464,7 @@ const Combobox = forwardRef<HTMLInputElement, ComboboxProps>(
: comboboxListbox}
</ComboboxProvider>
{hasError && (
<div className="Error" id={`${id}-error`}>
<div className="Error" id={errorId}>
{error}
</div>
)}
Expand Down

0 comments on commit 6dd5a34

Please sign in to comment.