-
Notifications
You must be signed in to change notification settings - Fork 933
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
Activating the toggle menu button while the listbox popup is expanded doesn't collapse the list using TalkBack on Mobile Chrome #1492
Comments
i faced the same issue in web also while using multiple selection can not use toggle button to close the list after selection.Seems when close the dropdown, the list stay as a focused and this line prevent it to be close <input
ref={ref}
placeholder={!hasSelectedItems ? placeholder : undefined}
className={twMerge(
cn('w-full bg-gray-50 font-normal outline-none', {
'bg-red-50': errorText,
})
)}
{...getInputProps(
getDropdownProps({ preventKeyAction: isOpen })
)}
/> solution i found ; {...omit(
getInputProps(getDropdownProps({ preventKeyAction: isOpen })),
['onFocus', 'onBlur']
)} Screen.Recording.2023-08-16.at.20.19.17.mov |
@sayinmehmet47 what is happening there is:
Our multiple selection example works, so I'm curious why yours does not. If you perform the same testing on https://www.downshift-js.com/use-multiple-selection#usage-with-combobox, you will not get a selection when clicking the toggle button. Are you using Also, your workaround does not seem to be quite right. You are preventing opening the combobox on click and selection on blur, which is what users expect from an a11y point of view. You need a proper fix for your scenario, but I cannot help you without a code usage or a sandbox. |
@silviuaavram thanks for answer. Actually my multiselect component works perfectly when i try it in storybook. I used this component with use hook form library, to get the inputs from the components. I want to share some part of my code that i did. I actually sticked to the documentation here function MultiSelectAutocompleteInner<T>(
{
defaultValue = [],
getFilteredItems,
getOptionLabel,
label,
labelTestId,
placeholder,
emptyOptionsText,
selectedItems: controlledSelectedItems,
isLoading = false,
onInputChange,
onSelectionChange,
selectionOptions,
successText,
successTextTestId,
errorText,
errorTextTestId,
required = false,
testId,
}: MultiSelectAutocompleteProps<T>,
ref: ForwardedRef<HTMLInputElement>
) {
const scrollRef = useRef<HTMLDivElement>(null);
const [inputValue, setInputValue] = useState<string>('');
const [uncontrolledItems, setSelectedItems] = useState<T[]>(defaultValue);
const selectedItems = controlledSelectedItems ?? uncontrolledItems;
const hasSelectedItems = selectedItems?.length > 0;
const filteredItems = getFilteredItems
? getFilteredItems(inputValue, selectedItems)
: selectionOptions ?? [];
const {
getSelectedItemProps,
getDropdownProps,
removeSelectedItem,
addSelectedItem,
} = useMultipleSelection<T>({
selectedItems,
onStateChange({ selectedItems: newSelectedItems, type }) {
switch (type) {
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
if (onSelectionChange) {
onSelectionChange(newSelectedItems ?? []);
}
setSelectedItems(newSelectedItems ?? []);
setInputValue('');
break;
default:
break;
}
},
});
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
openMenu,
toggleMenu,
} = useCombobox({
items: filteredItems,
itemToString(item) {
return item ? getOptionLabel(item) : '';
},
defaultHighlightedIndex: 0, // after selection, highlight the first item.
selectedItem: null,
stateReducer(state, actionAndChanges) {
const { changes, type } = actionAndChanges;
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
highlightedIndex: 0, // with the first option highlighted.
inputValue: '', // also, clear the inputValue to
};
default:
return changes;
}
},
onStateChange({
inputValue: newInputValue,
type,
selectedItem: newSelectedItem,
}) {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
case useCombobox.stateChangeTypes.InputBlur:
if (newSelectedItem && Array.isArray(selectedItems)) {
setSelectedItems([...selectedItems, newSelectedItem]);
if (onSelectionChange) {
onSelectionChange([...selectedItems, newSelectedItem]);
}
} else if (newSelectedItem) {
setSelectedItems([newSelectedItem]);
if (onSelectionChange) {
onSelectionChange([newSelectedItem]);
}
}
scrollRef.current?.scroll({
top: scrollRef.current?.scrollHeight,
behavior: 'smooth',
});
break;
case useCombobox.stateChangeTypes.InputChange:
setInputValue(newInputValue ?? '');
if (onInputChange) {
onInputChange(newInputValue ?? '');
}
break;
default:
break;
}
},
});
return (
<div className="relative w-full">
<div className="flex flex-col gap-1 font-light">
{label && (
<LegacyLabel
{...getLabelProps()}
value={label + (required ? ' *' : '')}
testId={labelTestId}
/>
)}
{}
<div className="relative w-full">
<button
aria-label="toggle menu"
className={cn(
'absolute right-0 top-0 px-2 py-3 transition-transform',
{
'rotate-180': isOpen,
}
)}
type="button"
{...getToggleButtonProps()}
onClick={(e) => {
e.stopPropagation();
toggleMenu();
}}
>
<ChevronDownIcon className="h-4 w-4" />
</button>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events */}
<div
ref={scrollRef}
className={twMerge(
cn(
'inline-flex max-h-36 w-full flex-wrap items-center gap-2 overflow-auto rounded-lg border border-gray-300 bg-gray-50 py-2 pl-3 pr-6 shadow-sm',
'focus-within:border-blue-500 focus-within:outline-none focus-within:ring-1 focus-within:ring-blue-500 dark:border-blue-400 dark:bg-blue-100 dark:focus-within:border-blue-500 dark:focus-within:ring-blue-500',
{
'border-red-500 bg-red-50': errorText,
'focus-within:border-red-500 focus-within:ring-red-500':
errorText,
}
)
)}
data-testid={testId}
onClick={() => {
openMenu();
}}
>
{selectedItems?.map(function renderSelectedItem(
selectedItemForRender,
index
) {
return (
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events */
<span
style={{ overflowAnchor: 'none' }}
className="flex rounded-md border border-gray-500 bg-white p-1 px-1 font-normal focus:bg-blue-100"
key={`selected-item-${index}`}
{...getSelectedItemProps({
selectedItem: selectedItemForRender,
index,
})}
onClick={(e) => {
e.stopPropagation();
addSelectedItem(selectedItemForRender);
}}
>
{getOptionLabel(selectedItemForRender)}
<button
type="button"
className="cursor-pointer"
onClick={(e) => {
e.stopPropagation();
removeSelectedItem(selectedItemForRender);
}}
>
<XMarkIcon className="ml-1 h-4 w-4" />
</button>
</span>
);
})}
<div
style={{ overflowAnchor: 'auto' }}
className="flex grow gap-0.5"
>
<input
ref={ref}
placeholder={!hasSelectedItems ? placeholder : undefined}
className={twMerge(
cn('w-full bg-gray-50 font-normal outline-none', {
'bg-red-50': errorText,
})
)}
{...getInputProps(
getDropdownProps({ preventKeyAction: isOpen })
)}
/>
</div>
</div>
<ul
className={cn(
'w-inherit absolute mt-1 max-h-80 w-full overflow-auto rounded-b-md border border-gray-200 bg-white p-0 text-gray-500 shadow',
{
hidden:
(!isOpen && !isLoading) ||
(isOpen &&
!isLoading &&
!filteredItems.length &&
!emptyOptionsText),
}
)}
{...getMenuProps()}
>
{isLoading && (
<li className="flex flex-col px-3 py-2">
<Loading size="sm" />
</li>
)}
{isOpen &&
!isLoading &&
filteredItems.map((item, index) => (
<li
className={cn(
highlightedIndex === index && 'bg-gray-100',
selectedItem === item && 'font-bold',
'flex cursor-pointer flex-col px-3 py-2'
)}
key={index}
{...getItemProps({ item, index })}
>
<span className="text-sm text-gray-700">
{getOptionLabel(item)}
</span>
</li>
))}
{isOpen &&
emptyOptionsText &&
!isLoading &&
filteredItems.length === 0 && (
<li className="flex flex-col px-3 py-2">{emptyOptionsText}</li>
)}
</ul>
</div>
{successText && (
<LegacyHelperText.Success testId={successTextTestId}>
{successText}
</LegacyHelperText.Success>
)}
{errorText && (
<LegacyHelperText.Error testId={errorTextTestId}>
{errorText}
</LegacyHelperText.Error>
)}
</div>
</div>
);
} |
<button
aria-label="toggle menu"
className={cn(
'absolute right-0 top-0 px-2 py-3 transition-transform',
{
'rotate-180': isOpen,
}
)}
type="button"
{...getToggleButtonProps()}
onClick={(e) => {
e.stopPropagation();
toggleMenu();
}}
> Why do you need that onClick for? |
downshift
version: 6.1.7node
version: 14.20.0npm
(oryarn
) version: 6.14.17Environment:
Browser(s): Mobile Chrome;
OS: Android 12;
Device(s): Redmi 10C;
Screen Reader(s): TalkBack;
What you did:
I'm currently working on a project that uses the
useCombobox
anduseMultipleSelection
hooks from this library to create a combobox/autocomplete dropdown menu.What happened:
I've encountered a problem with the accessibility of the combobox/autocomplete dropdown menu when activating the menu toggle button while the listbox popup is expanded doesn't collapse the list using TalkBack on Mobile Chrome. Unfortunately, this issue also appears in the examples provided in the library documentation. To illustrate the issue, I've included a video below demonstrating how this issue manifests in an example from the documentation (the same behavior is present in the component on my side). In addition, I've also included a video of the correct behavior on iOS devices.
Android | Chrome | TalkBack:
https://user-images.githubusercontent.com/110240086/227996701-706cf002-099a-45f0-8709-de342cd23107.mov
iOS | Safari | VoiceOver:
https://user-images.githubusercontent.com/110240086/227997091-81a0b145-494e-4690-ae3e-4f168411dd5d.mov
Reproduction repository:
Unfortunately, I'm unable to provide a reproduction repo at this point.
Problem description:
In an autocomplete/combobox dropdown menu when using Talkback in mobile Chrome, pressing the toggle menu button while the listbox is expanding does not collapse the listbox. If the text field is empty, pressing the toggle menu button opens the listbox and moves the focus to the input field. However, after that, the user cannot close the list menu by pressing the toggle menu button again.
This problem is reproduced not only in the project component I am working on but also appears in the examples provided in the library's documentation. Specifically, this issue is only present on Android devices when using the TalkBack screen reader. In contrast, everything works correctly on iOS devices when using the VoiceOver screen reader.
As this is a critical accessibility issue, I would like to report it and get feedback from the developers. I assume this problem is related to the library hooks, as it is not only present in my project but also in the examples provided in the documentation. Perhaps there are options in the hooks that can help fix this problem on my end, so I'd appreciate it if you could tell me about them.
Suggested solution:
The toggle menu button should expand and collapse the list popup using TalkBack on Mobile Chrome.
The text was updated successfully, but these errors were encountered: