Skip to content

Combobox VoiceOver announcement not respecting aria-label of ListBoxItem #8907

@SimonSelg

Description

@SimonSelg

Provide a general summary of the issue here

The ComboBox announces the currently selected item when you interact with it in the open state (for example with up-down arrow keys). For each item (ListBoxItem), an aria-label prop can be used to set an accessible name. On Mac OSX this announcement does not respect the aria-label prop of the ListBoxItem and announces the regular name.

🤔 Expected Behavior?

When focusing an Option given by a ListBoxItem that has an aria-label text set, the VoiceOver announcement should use the text set via aria-label as title of the option, not the text provided via children/textContent

😯 Current Behavior

When focusing an Option given by a ListBoxItem that has an aria-label text set, the VoiceOver announcement does not take aria-label of the ListBoxItem into account and hence uses the wrong title.

🔦 Context

The ComboBox component/hook has a special codepath for MacOSX VoiceOver which manually generates feedback for focus changes in the option list:

// VoiceOver has issues with announcing aria-activedescendant properly on change
// (especially on iOS). We use a live region announcer to announce focus changes
// manually. In addition, section titles are announced when navigating into a new section.
let focusedItem = state.selectionManager.focusedKey != null && state.isOpen
? state.collection.getItem(state.selectionManager.focusedKey)
: undefined;
let sectionKey = focusedItem?.parentKey ?? null;
let itemKey = state.selectionManager.focusedKey ?? null;
let lastSection = useRef(sectionKey);
let lastItem = useRef(itemKey);
useEffect(() => {
if (isAppleDevice() && focusedItem != null && itemKey != null && itemKey !== lastItem.current) {
let isSelected = state.selectionManager.isSelected(itemKey);
let section = sectionKey != null ? state.collection.getItem(sectionKey) : null;
let sectionTitle = section?.['aria-label'] || (typeof section?.rendered === 'string' ? section.rendered : '') || '';
let announcement = stringFormatter.format('focusAnnouncement', {
isGroupChange: (section && sectionKey !== lastSection.current) ?? false,
groupTitle: sectionTitle,
groupCount: section ? [...getChildNodes(section, state.collection)].length : 0,
optionText: focusedItem['aria-label'] || focusedItem.textValue || '',
isSelected
});
announce(announcement);
}

This codepath attempt to take a possible existingaria-label of each item into account:

optionText: focusedItem['aria-label'] || focusedItem.textValue || '',


focusedItem comes from a collection of the available items, which is build by a CollectionBuilder from the JSX inside the ComboBox:

<CollectionBuilder content={content}>
{collection => <ComboBoxInner props={props} collection={collection} comboBoxRef={ref} />}
</CollectionBuilder>

However, this CollectionBuilder never populates the aria-label attribute of the collection nodes:

setProps<E extends Element>(obj: {[key: string]: any}, ref: ForwardedRef<E>, CollectionNodeClass: CollectionNodeClass<any>, rendered?: ReactNode, render?: (node: Node<T>) => ReactElement): void {
let node;
let {value, textValue, id, ...props} = obj;
if (this.node == null) {
node = new CollectionNodeClass(id ?? `react-aria-${++this.ownerDocument.nodeId}`);
this.node = node;
} else {
node = this.getMutableNode();
}
props.ref = ref;
node.props = props;
node.rendered = rendered;
node.render = render;
node.value = value;
node.textValue = textValue || (typeof props.children === 'string' ? props.children : '') || obj['aria-label'] || '';
if (id != null && id !== node.key) {
throw new Error('Cannot change the id of an item');
}
if (props.colSpan != null) {
node.colSpan = props.colSpan;
}
if (this.isConnected) {
this.ownerDocument.queueUpdate();
}
}

Hence the announcement does not change if a aria-label is used on an ListBoxItem and continues to use the existing label/ticket.

🖥️ Steps to Reproduce

Codesandbox: https://codesandbox.io/p/sandbox/funny-wu-ryrjtl

Create a ComboBox in the following way

<ComboBox>
  <Label>Preferred fruit or vegetable</Label>
  <Input />
  <Button />
  <Popover>
    <ListBox>
      <ListBoxItem id="apple" textValue="Entry Apple" aria-label="Apple">
        Entry: <b>Apple</b>
      </ListBoxItem>
      <ListBoxItem id="banana" textValue="Entry Banana"  aria-label="Banana">
        Entry: <b>Banana</b>
      </ListBoxItem>
    </ListBox>
  </Popover>
</ComboBox>

Activate VoiceOver, then open the combobox via Keyboard. Navigate over the items via arrow keys. You will hear that VoiceOver will read "Entry Apple" / "Entry Banana", despite the aria-label being "Apple" / "Banana".

💁 Possible Solution

I see two ways to fix this:

  1. modify CollectionBuilder implementation to correctly initialize the aria-label attributed from the ListBoxItem. I created a PR with this solution strategy: fix: Combobox VoiceOver announcement not respecting aria-label of ListBoxItem #8908

  2. Modify the custom ComboBox VoiceOver codepath to read focusedItem.props.['aria-label'] instead of focusedItem['aria-label']. There seeem to be some inconsistencies whether aria-label is read from the prop or the aria-label attribute from the collection item.

Version

1.12.2

What browsers are you seeing the problem on?

Chrome, Safari, Firefox

What operating system are you using?

Mac OSX 15.6.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions