Skip to content

Commit

Permalink
fix(combobox): provide a way to use the state hook
Browse files Browse the repository at this point in the history
Provide a way to pass state to the combobox by using the useCombobox
hook, and passing the return props to the component via the state prop.
  • Loading branch information
SiTaggart committed Aug 21, 2020
1 parent 13c2c9d commit 8c4cb81
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 80 deletions.
97 changes: 96 additions & 1 deletion packages/paste-core/components/combobox/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import * as React from 'react';
import _ from 'lodash';
import {axe} from 'jest-axe';
import {uid} from 'react-uid';
import {render, screen} from '@testing-library/react';
import {render, screen, fireEvent} from '@testing-library/react';
import {FormLabel, FormHelpText, InputElement} from '@twilio-paste/form';
import {Button} from '@twilio-paste/button';
import {CloseIcon} from '@twilio-paste/icons/esm/CloseIcon';
import {Box} from '@twilio-paste/box';
import {useCombobox, Combobox, ComboboxInputWrapper, ComboboxListbox, ComboboxListboxOption} from '../src';
import {ComboboxProps} from '../src/types';

const items = ['Alert', 'Anchor', 'Button', 'Card', 'Heading', 'List', 'Modal', 'Paragraph'];

const objectItems = [
{code: 'AD', label: 'Andorra', phone: '376'},
{code: 'AE', label: 'United Arab Emirates', phone: '971'},
{code: 'AF', label: 'Afghanistan', phone: '93'},
{code: 'AG', label: 'Antigua and Barbuda', phone: '1-268'},
{code: 'AI', label: 'Anguilla', phone: '1-264'},
{code: 'AL', label: 'Albania', phone: '355'},
{code: 'AM', label: 'Armenia', phone: '374'},
{code: 'AO', label: 'Angola', phone: '244'},
{code: 'AQ', label: 'Antarctica', phone: '672'},
{code: 'AR', label: 'Argentina', phone: '54'},
{code: 'AS', label: 'American Samoa', phone: '1-684'},
{code: 'AT', label: 'Austria', phone: '43'},
];

const groupedItems = [
{group: 'Components', label: 'Alert'},
{group: 'Components', label: 'Anchor'},
Expand Down Expand Up @@ -107,6 +126,60 @@ const GroupedMockCombobox: React.FC<{groupLabelTemplate?: ComboboxProps['groupLa
);
};

const ControlledCombobox: React.FC = () => {
const [value, setValue] = React.useState('');
const [selectedItem, setSelectedItem] = React.useState({});
const [inputItems, setInputItems] = React.useState(objectItems);
const {reset, ...state} = useCombobox({
items: inputItems,
itemToString: item => (item ? item.label : null),
onSelectedItemChange: changes => {
setSelectedItem(changes.selectedItem);
},
onInputValueChange: ({inputValue}) => {
if (inputValue !== undefined) {
setInputItems(
_.filter(objectItems, (item: any) => item.label.toLowerCase().startsWith(inputValue.toLowerCase()))
);
setValue(inputValue);
}
},
inputValue: value,
});
return (
<>
<Combobox
state={state}
items={inputItems}
autocomplete
labelText="Choose a country:"
helpText="This is the help text"
optionTemplate={(item: any) => (
<div>
{item.code} | {item.label} | {item.phone}
</div>
)}
insertAfter={
<Button
variant="link"
size="reset"
onClick={() => {
reset();
}}
>
<CloseIcon decorative={false} title="Clear" />
</Button>
}
/>
<Box paddingTop="space70">
Input value state: <span data-testid="input-value-span">{JSON.stringify(value)}</span>
<br />
Selected item state: <span data-testid="selected-item-span">{JSON.stringify(selectedItem)}</span>
</Box>
</>
);
};

describe('Combobox ', () => {
describe('Render', () => {
it('should render', () => {
Expand Down Expand Up @@ -182,6 +255,28 @@ describe('Combobox ', () => {
});
});

describe('Controlled Combobox', () => {
it('should be able to clear a Combobox', () => {
render(<ControlledCombobox />);
// open the combobox
fireEvent.click(screen.getByRole('textbox'));
// select the first item
fireEvent.click(screen.getAllByRole('option')[0]);
// @ts-ignore Property 'value' does not exist on type 'HTMLElement' (I get it, but this is right)
expect(screen.getByRole('textbox').value).toEqual('Andorra');
expect(screen.getByTestId('input-value-span').textContent).toEqual('"Andorra"');
expect(screen.getByTestId('selected-item-span').textContent).toEqual(
'{"code":"AD","label":"Andorra","phone":"376"}'
);
// click the clear button
fireEvent.click(screen.getByText('Clear'));
// @ts-ignore Property 'value' does not exist on type 'HTMLElement' (I get it, but this is right)
expect(screen.getByRole('textbox').value).toEqual('');
expect(screen.getByTestId('input-value-span').textContent).toEqual('""');
expect(screen.getByTestId('selected-item-span').textContent).toEqual('null');
});
});

describe('Accessibility', () => {
it('Should have no accessibility violations', async () => {
const {container} = render(<ComboboxMock />);
Expand Down
43 changes: 31 additions & 12 deletions packages/paste-core/components/combobox/src/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
groupItemsBy,
groupLabelTemplate,
variant = 'default',
state,
...props
},
ref
Expand All @@ -178,18 +179,36 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
getToggleButtonProps,
highlightedIndex,
isOpen,
} = useComboboxPrimitive({
initialSelectedItem,
items,
onHighlightedIndexChange,
onInputValueChange,
onIsOpenChange,
onSelectedItemChange,
...(itemToString && {itemToString}),
...(initialIsOpen && {initialIsOpen}),
...(inputValue && {inputValue}),
...(selectedItem && {selectedItem}),
});
} =
state ||
useComboboxPrimitive({
initialSelectedItem,
items,
onHighlightedIndexChange,
onInputValueChange,
onIsOpenChange,
onSelectedItemChange,
...(itemToString && {itemToString}),
...(initialIsOpen && {initialIsOpen}),
...(inputValue && {inputValue}),
...(selectedItem && {selectedItem}),
});

if (
getComboboxProps === undefined ||
getInputProps === undefined ||
getItemProps === undefined ||
getLabelProps === undefined ||
getMenuProps === undefined ||
getToggleButtonProps === undefined ||
highlightedIndex === undefined ||
isOpen === undefined
) {
throw new Error(
'[Combobox]: One of getComboboxProps, getInputProps, getItemProps, getLabelProps, getMenuProps, getToggleButtonProps, highlightedIndex or isOpen is missing from the state object. Please make sure this is provided.'
);
}

const helpTextId = useUID();
const groupUID = useUIDSeed();
const optionUID = useUIDSeed();
Expand Down
1 change: 1 addition & 0 deletions packages/paste-core/components/combobox/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './ComboboxInputWrapper';
export * from './ComboboxListbox';
export * from './ComboboxListboxOption';
export * from './ComboboxListboxGroup';
export {ComboboxProps} from './types';
7 changes: 6 additions & 1 deletion packages/paste-core/components/combobox/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {UseComboboxPrimitiveProps, UseComboboxPrimitiveState} from '@twilio-paste/combobox-primitive';
import {
UseComboboxPrimitiveProps,
UseComboboxPrimitiveState,
UseComboboxPrimitiveReturnValue,
} from '@twilio-paste/combobox-primitive';
import {FieldVariants, FormInputProps} from '@twilio-paste/form';

type Item = string | {[key: string]: any};
Expand All @@ -21,6 +25,7 @@ export interface ComboboxProps extends Omit<FormInputProps, 'id' | 'type' | 'val
inputValue?: UseComboboxPrimitiveProps<Item>['inputValue'];
groupItemsBy?: string;
variant?: FieldVariants;
state?: Partial<UseComboboxPrimitiveReturnValue<Item>>;
}

export interface RenderItemProps extends Pick<ComboboxProps, 'optionTemplate'> {
Expand Down
Loading

0 comments on commit 8c4cb81

Please sign in to comment.