Skip to content

fix(accessibility): Improve screen reader support for combobox and listbox components#3432

Draft
Diaan wants to merge 8 commits into
mainfrom
fix/3256-combobox-information-about-options-being-selected-and-not-selected-are-not-announced-by-screen-readers
Draft

fix(accessibility): Improve screen reader support for combobox and listbox components#3432
Diaan wants to merge 8 commits into
mainfrom
fix/3256-combobox-information-about-options-being-selected-and-not-selected-are-not-announced-by-screen-readers

Conversation

@Diaan

@Diaan Diaan commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

This pull request focuses on improving accessibility for grouped options in sl-combobox, sl-listbox, and sl-select components, specifically targeting better compatibility with Safari/VoiceOver. The main changes remove problematic ARIA roles, ensure correct ARIA attributes for all options, and enhance accessible names to include group context. Comprehensive tests were added to verify these behaviors.

Accessibility improvements for grouped options:

  • Removed role="group" from sl-option-group elements and their headers, as it caused issues with Safari/VoiceOver, and stopped setting aria-label on group elements. Group context is now conveyed through option labels instead. [1] [2] [3] [4]
  • Ensured all group headers (sl-option-group-header) are hidden from assistive technology by setting aria-hidden="true". [1] [2]

ARIA attribute management and accessible names:

  • All options now have aria-selected, aria-posinset, and aria-setsize attributes set, reflecting their flattened position in the list, for improved screen reader support. [1] [2] [3] [4]
  • Option accessible names now include group context (e.g., "Group 1, Option 1") for better clarity in assistive technology, especially Safari/VoiceOver. [1] [2] [3] [4]

Test coverage:

  • Added and updated tests for sl-combobox, sl-listbox, and sl-select to verify the absence of role="group", presence of aria-hidden="true" on headers, correct ARIA attributes on options, and inclusion of group context in accessible names. [1] [2] [3] [4]

These changes collectively ensure that grouped options are presented in a way that is accessible and compatible with major screen readers, with a particular focus on Safari/VoiceOver.

Copilot AI review requested due to automatic review settings June 12, 2026 12:49
@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 149fed3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves accessibility of grouped options across sl-select, sl-combobox, and the underlying listbox components, aiming to resolve Safari/VoiceOver issues by flattening semantics, hiding group headers from AT, and ensuring options expose consistent ARIA state/positioning.

Changes:

  • Removed role="group" / group-level labeling from sl-option-group, and made sl-option-group-header consistently aria-hidden="true".
  • Added flattened aria-selected, aria-posinset, and aria-setsize to options and augmented option accessible names with group context.
  • Added/updated tests for grouped-option accessibility behaviors.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
packages/components/select/src/select.ts Sets flattened ARIA attributes and group-context accessible names for select options.
packages/components/select/src/select.spec.ts Adds assertions for grouped-options ARIA behavior in sl-select.
packages/components/listbox/src/option-group.ts Removes role="group"/group labeling logic from option groups.
packages/components/listbox/src/option-group.spec.ts Updates expectations for removed group role and hidden headers.
packages/components/listbox/src/option-group-header.ts Ensures group headers are hidden from assistive technology.
packages/components/listbox/src/listbox.ts Sets flattened ARIA attributes for virtualized listbox items and adds group context labels.
packages/components/combobox/src/combobox.ts Sets flattened ARIA attributes and group-context accessible names in both light DOM and virtual list modes.
packages/components/combobox/src/combobox.spec.ts Adds grouped-options accessibility tests for sl-combobox.

Comment thread packages/components/select/src/select.ts Outdated
Comment thread packages/components/combobox/src/combobox.ts Outdated
Comment thread packages/components/listbox/src/listbox.ts Outdated
Comment thread packages/components/listbox/src/listbox.ts Outdated
Comment thread packages/components/listbox/src/option-group.ts Outdated
Comment thread packages/components/combobox/src/combobox.spec.ts
Comment thread packages/components/combobox/src/combobox.spec.ts
Comment thread packages/components/combobox/src/combobox.spec.ts Outdated
Comment thread packages/components/combobox/src/combobox.spec.ts Outdated
Comment thread packages/components/combobox/src/combobox.ts Outdated

@a11ymiko a11ymiko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow! I'm huge fan of how change between 'selected'/'not selected' in Combobox is announced right now on Safari 😍

Right now the only small issue or improvement in that announcement would be to announce 'OPTION NAME, removed from selection X items selected' after user expand listbox again and deselect one option.

Right now when user expand listbox again and deselect one of options the only announcements they get is 'X items selected' without information about option removed from selection. (This is clearly visible without screen capture turned on, but when screen is being recorded VoiceOver behaves differently, which is VO issue more than Listbox issue)

Untitled.mov

Copilot AI review requested due to automatic review settings June 15, 2026 10:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Comment on lines +281 to +285
const metadata = options.map(option => ({
group: option.closest<OptionGroup>('sl-option-group')?.label,
label: option.textContent?.trim() || '',
option
}));
Comment on lines +348 to +355
option.setAttribute('aria-posinset', position.toString());
option.setAttribute('aria-setsize', setSize.toString());
option.setAttribute('aria-selected', Boolean(selected).toString());

if (group) {
option.setAttribute('aria-label', `${label} (${group})`);
}
}
Comment thread packages/components/combobox/src/combobox.spec.ts Outdated
Comment thread packages/components/combobox/src/combobox.ts Outdated
Diaan added 2 commits June 15, 2026 17:00
…stbox options by optimizing flattened position caching
…ormation-about-options-being-selected-and-not-selected-are-not-announced-by-screen-readers
Copilot AI review requested due to automatic review settings June 15, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Comment on lines +411 to +417
option.setAttribute('aria-posinset', position.toString());
option.setAttribute('aria-setsize', setSize.toString());
option.setAttribute('aria-selected', Boolean(selected).toString());

if (group) {
option.setAttribute('aria-label', `${label} (${group})`);
}
Comment on lines +1343 to 1346
// Add group context to accessible name for Safari/VoiceOver compatibility
if (item.group) {
el.setAttribute('aria-label', `${item.group}, ${item.label}`);
}
Comment on lines 1474 to 1477
it('should group the selected options', () => {
expect(selectedGroup).to.exist;
expect(selectedGroup).to.have.attribute('aria-label', 'Selected');
// Note: aria-label is no longer set since we removed role="group" for Safari/VoiceOver compatibility

Comment on lines +1517 to +1522
it('should not have role="group" on group wrappers', () => {
const listbox = el.querySelector('sl-listbox');
const groups = listbox?.shadowRoot?.querySelectorAll('[role="group"]');

expect(groups).to.have.lengthOf(0);
});
Comment on lines +1566 to +1576
it('should include group context in option accessible names', () => {
// Verify that items have group information
const options = el.items.filter(item => 'option' in item);

expect(options[0].group).to.equal('Fruits');
expect(options[1].group).to.equal('Fruits');
expect(options[2].group).to.equal('Vegetables');
expect(options[3].group).to.equal('Vegetables');

// The aria-label with group context is set in #renderItem when virtualizer renders
});
Diaan added 3 commits June 16, 2026 10:35
…n listbox and clear stale labels when options move out of groups
…stbox by improving element filtering and aria-label handling
Copilot AI review requested due to automatic review settings June 16, 2026 15:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.

Comment on lines +80 to +90
/** Cache mapping each option item to its 0-based flattened position (excludes group headers). */
#flattenedPositionCache?: WeakMap<ListboxItem<T, U>, number>;

/** Cache version matching the items version when the cache was last built. */
#flattenedPositionCacheVersion = -1;

/** Monotonically increasing version, incremented whenever `items` changes. */
#itemsVersion = 0;

/** Total number of option items in the last built cache. */
#flattenedSetSize = 0;
Comment on lines +355 to +366
getFlattenedPosition(item: ListboxItem<T, U>): number {
if (!this.items || !('option' in item)) return -1;

if (
this.#flattenedPositionCacheVersion !== this.#itemsVersion ||
!this.#flattenedPositionCache
) {
this.#buildFlattenedPositionCache();
}

return this.#flattenedPositionCache!.get(item) ?? -1;
}
Comment on lines +373 to +384
getFlattenedSetSize(): number {
if (!this.items) return 0;

if (
this.#flattenedPositionCacheVersion !== this.#itemsVersion ||
!this.#flattenedPositionCache
) {
this.#buildFlattenedPositionCache();
}

return this.#flattenedSetSize;
}
Comment on lines +386 to +398
#buildFlattenedPositionCache(): void {
this.#flattenedPositionCache = new WeakMap<ListboxItem<T, U>, number>();
this.#flattenedPositionCacheVersion = this.#itemsVersion;

let position = 0;
(this.items ?? []).forEach(i => {
if ('option' in i) {
this.#flattenedPositionCache!.set(i, position++);
}
});

this.#flattenedSetSize = position;
}
Comment on lines +485 to +494
const currentGroup = option.closest<OptionGroup>('sl-option-group')?.label;
const hasGeneratedLabel =
option.hasAttribute('aria-label') &&
option.getAttribute('aria-label')!.includes('(') &&
option.getAttribute('aria-label')!.includes(')');

// If option has a generated aria-label but no current group, it's stale
if (hasGeneratedLabel && !currentGroup) {
option.removeAttribute('aria-label');
}
Comment on lines +1352 to 1355
// Add group context to accessible name for Safari/VoiceOver compatibility
if (item.group) {
el.setAttribute('aria-label', `${item.group}, ${item.label}`);
}
Comment on lines +1588 to +1598
it('should include group context in option accessible names', () => {
// Verify that items have group information
const options = el.items.filter(item => 'option' in item);

expect(options[0].group).to.equal('Fruits');
expect(options[1].group).to.equal('Fruits');
expect(options[2].group).to.equal('Vegetables');
expect(options[3].group).to.equal('Vegetables');

// The aria-label with group context is set in #renderItem when virtualizer renders
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Combobox] Information about options being 'selected' and 'not selected' are not announced by screen readers

3 participants