Skip to content

Commit 424e5bb

Browse files
committed
fix: Escape in an open Combobox or Dropdown does not trigger tabster actions
1 parent 1d15557 commit 424e5bb

4 files changed

Lines changed: 59 additions & 1 deletion

File tree

packages/react-components/react-combobox/library/src/components/Combobox/Combobox.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { isConformant } from '../../testing/isConformant';
88
import { resetIdsForTests } from '@fluentui/react-utilities';
99
import { comboboxClassNames } from './useComboboxStyles.styles';
1010
import type { ComboboxProps } from '@fluentui/react-combobox';
11+
import { getTabsterAttribute } from 'tabster';
1112

1213
describe('Combobox', () => {
1314
beforeEach(() => {
@@ -1026,6 +1027,41 @@ describe('Combobox', () => {
10261027
expect(listbox.getAttribute('aria-labelledby')).toEqual(null);
10271028
});
10281029

1030+
it('should update the tabster escape ignore attribute based on open state', () => {
1031+
const tabsterOpenAttr = getTabsterAttribute(
1032+
{
1033+
focusable: {
1034+
ignoreKeydown: { Escape: true },
1035+
},
1036+
},
1037+
true,
1038+
);
1039+
const tabsterClosedAttr = getTabsterAttribute(
1040+
{
1041+
focusable: {
1042+
ignoreKeydown: { Escape: false },
1043+
},
1044+
},
1045+
true,
1046+
);
1047+
1048+
const { getByRole } = render(
1049+
<Combobox>
1050+
<Option>Red</Option>
1051+
<Option>Green</Option>
1052+
<Option>Blue</Option>
1053+
</Combobox>,
1054+
);
1055+
const combobox = getByRole('combobox');
1056+
1057+
expect(combobox.getAttribute('data-tabster')).toMatch(tabsterClosedAttr);
1058+
1059+
// open
1060+
userEvent.click(getByRole('combobox'));
1061+
1062+
expect(combobox.getAttribute('data-tabster')).toMatch(tabsterOpenAttr);
1063+
});
1064+
10291065
describe('clearable', () => {
10301066
it('clears the selection on a button click', () => {
10311067
const { getByText, getByRole } = render(

packages/react-components/react-combobox/library/src/components/Combobox/__snapshots__/Combobox.test.tsx.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ exports[`Combobox renders a default state 1`] = `
88
<input
99
aria-expanded="false"
1010
class="fui-Combobox__input"
11+
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":false}}}"
1112
role="combobox"
1213
type="text"
1314
value=""
@@ -66,6 +67,7 @@ exports[`Combobox renders an open listbox 1`] = `
6667
aria-controls="fluent-listbox_r_j_"
6768
aria-expanded="true"
6869
class="fui-Combobox__input"
70+
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":true}}}"
6971
role="combobox"
7072
type="text"
7173
value=""

packages/react-components/react-combobox/library/src/components/Dropdown/__snapshots__/Dropdown.test.tsx.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ exports[`Dropdown renders a default state 1`] = `
88
<button
99
aria-expanded="false"
1010
class="fui-Dropdown__button"
11+
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":false}}}"
1112
role="combobox"
1213
tabindex="0"
1314
type="button"
@@ -65,6 +66,7 @@ exports[`Dropdown renders an open listbox 1`] = `
6566
aria-controls="fluent-listbox_r_d_"
6667
aria-expanded="true"
6768
class="fui-Dropdown__button"
69+
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":true}}}"
6870
role="combobox"
6971
tabindex="0"
7072
type="button"

packages/react-components/react-combobox/library/src/utils/useTriggerSlot.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
'use client';
22

33
import * as React from 'react';
4-
import { useSetKeyboardNavigation } from '@fluentui/react-tabster';
4+
import {
5+
useSetKeyboardNavigation,
6+
useTabsterAttributes,
7+
useMergedTabsterAttributes_unstable,
8+
} from '@fluentui/react-tabster';
59
import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
610
import { mergeCallbacks, slot, useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
711
import type { ExtractSlotProps, Slot, SlotComponentType } from '@fluentui/react-utilities';
@@ -48,12 +52,26 @@ export function useTriggerSlot(
4852
activeDescendantController,
4953
} = options;
5054

55+
// need to prevent tabster from also handling escape when the dropdown is open
56+
// event.stopPropagation() isn't enough here, since tabster uses the capture phase
57+
const ignoreEscapeKeyAttribute = useTabsterAttributes({
58+
focusable: {
59+
ignoreKeydown: { Escape: open },
60+
},
61+
});
62+
63+
const tabsterOverrides = useMergedTabsterAttributes_unstable(
64+
ignoreEscapeKeyAttribute,
65+
typeof defaultProps === 'object' ? defaultProps : {},
66+
);
67+
5168
const trigger = slot.always(triggerSlotFromProp, {
5269
defaultProps: {
5370
type: 'text',
5471
'aria-expanded': open,
5572
role: 'combobox',
5673
...(typeof defaultProps === 'object' && defaultProps),
74+
...tabsterOverrides,
5775
},
5876
elementType,
5977
});

0 commit comments

Comments
 (0)