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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Comment thread
smhigley marked this conversation as resolved.
"type": "patch",
"comment": "fix: Escape in an open Combobox or Dropdown does not trigger tabster actions",
"packageName": "@fluentui/react-combobox",
"email": "sarah.higley@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isConformant } from '../../testing/isConformant';
import { resetIdsForTests } from '@fluentui/react-utilities';
import { comboboxClassNames } from './useComboboxStyles.styles';
import type { ComboboxProps } from '@fluentui/react-combobox';
import { getTabsterAttribute } from 'tabster';

describe('Combobox', () => {
beforeEach(() => {
Expand Down Expand Up @@ -1026,6 +1027,41 @@ describe('Combobox', () => {
expect(listbox.getAttribute('aria-labelledby')).toEqual(null);
});

it('should update the tabster escape ignore attribute based on open state', () => {
const tabsterOpenAttr = getTabsterAttribute(
{
focusable: {
ignoreKeydown: { Escape: true },
},
},
true,
);
const tabsterClosedAttr = getTabsterAttribute(
{
focusable: {
ignoreKeydown: { Escape: false },
},
},
true,
);

const { getByRole } = render(
<Combobox>
<Option>Red</Option>
<Option>Green</Option>
<Option>Blue</Option>
</Combobox>,
);
const combobox = getByRole('combobox');

expect(combobox.getAttribute('data-tabster')).toMatch(tabsterClosedAttr);

// open
userEvent.click(getByRole('combobox'));

expect(combobox.getAttribute('data-tabster')).toMatch(tabsterOpenAttr);
});

describe('clearable', () => {
it('clears the selection on a button click', () => {
const { getByText, getByRole } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ exports[`Combobox renders a default state 1`] = `
<input
aria-expanded="false"
class="fui-Combobox__input"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":false}}}"
role="combobox"
type="text"
value=""
Expand Down Expand Up @@ -66,6 +67,7 @@ exports[`Combobox renders an open listbox 1`] = `
aria-controls="fluent-listbox_r_j_"
aria-expanded="true"
class="fui-Combobox__input"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":true}}}"
role="combobox"
type="text"
value=""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ exports[`Dropdown renders a default state 1`] = `
<button
aria-expanded="false"
class="fui-Dropdown__button"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":false}}}"
role="combobox"
tabindex="0"
type="button"
Expand Down Expand Up @@ -65,6 +66,7 @@ exports[`Dropdown renders an open listbox 1`] = `
aria-controls="fluent-listbox_r_d_"
aria-expanded="true"
class="fui-Dropdown__button"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":true}}}"
role="combobox"
tabindex="0"
type="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
'use client';

import * as React from 'react';
import { useSetKeyboardNavigation } from '@fluentui/react-tabster';
import {
useSetKeyboardNavigation,
useTabsterAttributes,
useMergedTabsterAttributes_unstable,
} from '@fluentui/react-tabster';
import type { ActiveDescendantImperativeRef } from '@fluentui/react-aria';
import { mergeCallbacks, slot, useEventCallback, useMergedRefs } from '@fluentui/react-utilities';
import type { ExtractSlotProps, Slot, SlotComponentType } from '@fluentui/react-utilities';
Expand Down Expand Up @@ -48,12 +52,26 @@ export function useTriggerSlot(
activeDescendantController,
} = options;

// need to prevent tabster from also handling escape when the dropdown is open
// event.stopPropagation() isn't enough here, since tabster uses the capture phase
const ignoreEscapeKeyAttribute = useTabsterAttributes({
focusable: {
ignoreKeydown: { Escape: open },
},
});

const tabsterOverrides = useMergedTabsterAttributes_unstable(
ignoreEscapeKeyAttribute,
typeof defaultProps === 'object' ? defaultProps : {},
);

const trigger = slot.always(triggerSlotFromProp, {
defaultProps: {
type: 'text',
'aria-expanded': open,
role: 'combobox',
...(typeof defaultProps === 'object' && defaultProps),
...tabsterOverrides,
},
elementType,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`TagPickerButton renders a default state 1`] = `
<button
aria-expanded="false"
class="fui-TagPickerButton"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":false}}}"
role="combobox"
tabindex="0"
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`TagPickerInput renders a default state 1`] = `
<input
aria-expanded="false"
class="fui-TagPickerInput"
data-tabster="{\\"focusable\\":{\\"ignoreKeydown\\":{\\"Escape\\":false}}}"
role="combobox"
type="text"
value=""
Expand Down
Loading