Skip to content
Open
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
23 changes: 14 additions & 9 deletions packages/@adobe/react-spectrum/test/combobox/ComboBox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5135,25 +5135,27 @@ describe('ComboBox', function () {
});

describe('keyboard navigating', function () {
it('should announce items when navigating with the arrow keys', async function () {
it('should not announce items when navigating with the arrow keys', async function () {
renderComboBox();
await user.tab();
await user.keyboard('{ArrowDown}');
act(() => {
jest.runAllTimers();
});

expect(announce).toHaveBeenLastCalledWith('One');
// VoiceOver announces per-item details natively within a section, so we
// should not interrupt it with our own live region announcement.
expect(announce).not.toHaveBeenCalledWith('One');

await user.keyboard('{ArrowDown}');
act(() => {
jest.runAllTimers();
});

expect(announce).toHaveBeenLastCalledWith('Two');
expect(announce).not.toHaveBeenCalledWith('Two');
});

it('should announce when navigating to the selected item', async function () {
it('should not announce when navigating to the selected item', async function () {
let {getByRole} = renderComboBox({selectedKey: '2'});
let combobox = getByRole('combobox');
act(() => {
Expand All @@ -5164,7 +5166,8 @@ describe('ComboBox', function () {
jest.runAllTimers();
});

expect(announce).toHaveBeenLastCalledWith('Two, selected');
// Same as above: VoiceOver handles per-item announcement natively.
expect(announce).not.toHaveBeenCalledWith('Two, selected');
});

it('should announce when navigating into a section with multiple items', async function () {
Expand All @@ -5183,12 +5186,14 @@ describe('ComboBox', function () {
'Entered group Section One, with 3 options. One'
);

// Moving within the same section should not announce again, VoiceOver
// handles the per-item announcement natively.
await user.keyboard('{ArrowDown}');
act(() => {
jest.runAllTimers();
});

expect(announce).toHaveBeenLastCalledWith('Two');
expect(announce).not.toHaveBeenLastCalledWith('Two');
});

it('should announce when navigating into a section with a single item', async function () {
Expand Down Expand Up @@ -5290,7 +5295,8 @@ describe('ComboBox', function () {
jest.runAllTimers();
});

expect(announce).toHaveBeenLastCalledWith('One');
// No per-item announcement when arrowing; VoiceOver handles that natively.
expect(announce).not.toHaveBeenCalledWith('One');

await user.keyboard('{Enter}');
act(() => {
Expand Down Expand Up @@ -5396,9 +5402,8 @@ describe('ComboBox', function () {

let listbox = getByRole('listbox');
expect(listbox).toBeVisible();
expect(announce).toHaveBeenCalledTimes(2);
expect(announce).toHaveBeenCalledTimes(1);
expect(announce).toHaveBeenNthCalledWith(1, '3 options available.');
expect(announce).toHaveBeenNthCalledWith(2, 'One');
platformMock.mockRestore();
});

Expand Down
46 changes: 28 additions & 18 deletions packages/react-aria/src/combobox/useComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,11 @@ export function useComboBox<T, M extends SelectionMode = 'single'>(
}
};

// 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.
// VoiceOver used to have issues with announcing aria-activedescendant properly on
// change (especially on iOS), so we announced focus changes manually via a live
// region. Current VoiceOver versions announce per-item details (selected state,
// role, item count) natively within a section, so we only announce when navigating
// into a new section, where VoiceOver is silent.
let focusedItem =
state.selectionManager.focusedKey != null && state.isOpen
? state.collection.getItem(state.selectionManager.focusedKey)
Expand All @@ -404,22 +406,30 @@ export function useComboBox<T, M extends SelectionMode = 'single'>(
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);
let isGroupChange = (section && sectionKey !== lastSection.current) ?? false;

// VoiceOver now announces per-item details natively (selected state, role,
// item count) when arrowing within a section, so announcing every focused
// item here interrupts that richer announcement. Only announce manually when
// navigating into a new section, where VoiceOver is silent.
if (isGroupChange) {
let isSelected = state.selectionManager.isSelected(itemKey);
let sectionTitle =
section?.['aria-label'] ||
(typeof section?.rendered === 'string' ? section.rendered : '') ||
'';

let announcement = stringFormatter.format('focusAnnouncement', {
isGroupChange: true,
groupTitle: sectionTitle,
groupCount: section ? [...getChildNodes(section, state.collection)].length : 0,
optionText: focusedItem['aria-label'] || focusedItem.textValue || '',
isSelected
});

announce(announcement);
}
}

lastSection.current = sectionKey;
Expand Down