Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(combobox): controlled autocomplete cursor placement onInputValueChange bug #2647

Merged
merged 1 commit into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/old-paws-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/combobox': patch
'@twilio-paste/core': patch
---

[Combobox] Minor fix to controlled Comboboxes, where the input cursor would always jump to the end of the input string in autocomplete examples, even when you want to amend the beginning or middle. Cursor position should now remain in place as you type or modify the input value.
4 changes: 4 additions & 0 deletions packages/paste-core/components/combobox/src/Combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
groupItemsBy,
groupLabelTemplate,
variant = 'default',
getA11yStatusMessage,
getA11ySelectionMessage,
state,
...props
},
Expand Down Expand Up @@ -99,6 +101,8 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
initialSelectedItem,
items,
state,
getA11yStatusMessage,
getA11ySelectionMessage,
});

React.useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type DefaultStateProps = {
selectedItem: ComboboxProps['selectedItem'];
initialSelectedItem: ComboboxProps['initialSelectedItem'];
items: ComboboxProps['items'];
getA11yStatusMessage: ComboboxProps['getA11yStatusMessage'];
getA11ySelectionMessage: ComboboxProps['getA11ySelectionMessage'];
};

const getDefaultState = ({
Expand All @@ -29,6 +31,8 @@ const getDefaultState = ({
selectedItem,
initialSelectedItem,
items,
getA11yStatusMessage,
getA11ySelectionMessage,
}: DefaultStateProps): Partial<UseComboboxPrimitiveReturnValue<any>> => {
return useComboboxPrimitive({
initialSelectedItem,
Expand All @@ -46,8 +50,11 @@ const getDefaultState = ({
onSelectedItemChange,
...(itemToString != null && {itemToString}),
...(initialIsOpen != null && {initialIsOpen}),
...(inputValue != null && {inputValue}),
// We remap inputValue to defaultInputValue because we want downshift to manage the state of controlled inputs
...(inputValue != null && {defaultInputValue: inputValue}),
...(selectedItem != null && {selectedItem}),
...(getA11yStatusMessage != null && {getA11yStatusMessage}),
...(getA11ySelectionMessage != null && {getA11ySelectionMessage}),
});
};

Expand Down
16 changes: 11 additions & 5 deletions packages/paste-core/components/combobox/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,27 @@ export type HighlightedIndexChanges = {
export interface ComboboxProps extends Omit<InputProps, 'id' | 'type' | 'value'>, Pick<BoxProps, 'element'> {
autocomplete?: boolean;
helpText?: string | React.ReactNode;
labelText: string | NonNullable<React.ReactNode>;
optionTemplate?: OptionTemplateFn<any>;
groupLabelTemplate?: (groupName: string) => React.ReactNode;
groupItemsBy?: string;
variant?: InputVariants;
SiTaggart marked this conversation as resolved.
Show resolved Hide resolved

// Downshift useCombobox Hook Props. Thes are mainly covered in https://github.com/downshift-js/downshift/blob/master/src/hooks/useCombobox/README.md#advanced-props docs
initialIsOpen?: UseComboboxPrimitiveProps<any>['initialIsOpen'];
initialSelectedItem?: UseComboboxPrimitiveProps<any>['initialSelectedItem'];
items: UseComboboxPrimitiveProps<any>['items'];
itemToString?: UseComboboxPrimitiveProps<any>['itemToString'];
labelText: string | NonNullable<React.ReactNode>;
onHighlightedIndexChange?: UseComboboxPrimitiveProps<any>['onHighlightedIndexChange'];
onInputValueChange?: UseComboboxPrimitiveProps<any>['onInputValueChange'];
onIsOpenChange?: UseComboboxPrimitiveProps<any>['onIsOpenChange'];
onSelectedItemChange?: UseComboboxPrimitiveProps<any>['onSelectedItemChange'];
optionTemplate?: OptionTemplateFn<any>;
groupLabelTemplate?: (groupName: string) => React.ReactNode;
selectedItem?: UseComboboxPrimitiveProps<any>['selectedItem'];
inputValue?: UseComboboxPrimitiveProps<any>['inputValue'];
groupItemsBy?: string;
variant?: InputVariants;
getA11yStatusMessage?: UseComboboxPrimitiveProps<any>['getA11yStatusMessage'];
getA11ySelectionMessage?: UseComboboxPrimitiveProps<any>['getA11ySelectionMessage'];

// Downshift useCombobox Hook return props for when you are using the hook outside of the component
state?: Partial<UseComboboxPrimitiveReturnValue<any>>;
/**
* Use `onInputValueChange` instead.
Expand Down
57 changes: 33 additions & 24 deletions packages/paste-core/components/combobox/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,21 +508,30 @@ ComboboxOverflowLongValue.story = {
};

export const ComboboxControlled = (): React.ReactNode => {
const [value, setValue] = React.useState('');
const [selectedItem, setSelectedItem] = React.useState({});
const [value, setValue] = React.useState('United Arab Emirates');
const [selectedItem, setSelectedItem] = React.useState({code: 'AE', label: 'United Arab Emirates', phone: '971'});
const [inputItems, setInputItems] = React.useState(objectItems);
return (
<>
<Box paddingBottom="space70">
Input value state: {JSON.stringify(value)}
<br />
Selected item state: {JSON.stringify(selectedItem)}
</Box>

<Combobox
autocomplete
items={inputItems}
labelText="Choose a country:"
helpText="This is the help text"
optionTemplate={(item: ObjectItem) => (
<div>
{item.code} | {item.label} | {item.phone}
</div>
)}
optionTemplate={(item: ObjectItem) => {
SiTaggart marked this conversation as resolved.
Show resolved Hide resolved
return (
<Box>
{item.code} | {item.label} | {item.phone}{' '}
{item && selectedItem && item.label === selectedItem.label ? '✅' : null}
</Box>
);
}}
onInputValueChange={({inputValue}) => {
if (inputValue !== undefined) {
setInputItems(
Expand All @@ -538,11 +547,6 @@ export const ComboboxControlled = (): React.ReactNode => {
}}
inputValue={value}
/>
<Box paddingTop="space70">
Input value state: {JSON.stringify(value)}
<br />
Selected item state: {JSON.stringify(selectedItem)}
</Box>
</>
);
};
Expand All @@ -552,10 +556,15 @@ ComboboxControlled.story = {
};

export const ComboboxControlledUsingState: Story = () => {
const [value, setValue] = React.useState('');
const [selectedItem, setSelectedItem] = React.useState<ObjectItem>({} as ObjectItem);
const [value, setValue] = React.useState('United Arab Emirates');
const [selectedItem, setSelectedItem] = React.useState<ObjectItem>({
code: 'AE',
label: 'United Arab Emirates',
phone: '971',
} as ObjectItem);
const [inputItems, setInputItems] = React.useState<ObjectItem[] | never[]>(objectItems as ObjectItem[]);
const {reset, ...state} = useCombobox<ObjectItem>({
initialInputValue: value,
items: inputItems,
itemToString: (item) => (item ? item.label : ''),
onSelectedItemChange: (changes) => {
Expand All @@ -571,11 +580,15 @@ export const ComboboxControlledUsingState: Story = () => {
setValue(inputValue);
}
},
inputValue: value,
selectedItem,
initialSelectedItem: selectedItem,
});
return (
<>
<Box paddingBottom="space70">
Input value state: {JSON.stringify(value)}
<br />
Selected item state: {JSON.stringify(selectedItem)}
</Box>
<Combobox
state={{...state, reset}}
items={inputItems}
Expand All @@ -584,9 +597,10 @@ export const ComboboxControlledUsingState: Story = () => {
labelText="Choose a country:"
helpText="This is the help text"
optionTemplate={(item: ObjectItem) => (
<div>
{item.code} | {item.label} | {item.phone}
</div>
<Box>
{item.code} | {item.label} | {item.phone}{' '}
{item && selectedItem && item.label === selectedItem.label ? '✅' : null}
</Box>
)}
insertAfter={
<Button
Expand All @@ -606,11 +620,6 @@ export const ComboboxControlledUsingState: Story = () => {
</Button>
}
/>
<Box paddingTop="space70">
Input value state: {JSON.stringify(value)}
<br />
Selected item state: {JSON.stringify(selectedItem)}
</Box>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,8 @@ const ComboboxControlledUsingState = () => {
setValue(inputValue);
}
},
inputValue: value,
selectedItem,
initialInputValue: value,
initialSelectedItem: selectedItem,
});
return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@ This function allows you to use your own `jsx` template for the items in the dro

The variant of the Combobox. Available variants are `default` or `inverse`.

##### `getA11yStatusMessage?: () => void`

Useful to compose accessible status messages to assistive technology users, including translations.

##### `getA11ySelectionMessage?: () => void`

Useful to compose accessible selection messages to assistive technology users, including translations.

#### State props

These props are used when want to create a Controlled Combobox. They control the state of the Combobox.
Expand Down