Skip to content

Commit

Permalink
NavigableContailer: do not trap focus in TabbableContainer (#49846)
Browse files Browse the repository at this point in the history
* Add before / after container buttons

* Update text with new expectations

* Move `isTab` prevent default only when the component is actually handling the focus

* CHANGELOG
  • Loading branch information
ciampo committed May 8, 2023
1 parent 023631c commit d770f97
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 27 deletions.
3 changes: 3 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

- `onDragStart` in `<Draggable>` is now a synchronous function to allow setting additional data for `event.dataTransfer` ([#49673](https://github.com/WordPress/gutenberg/pull/49673)).

### Bug Fix

- `NavigableContainer`: do not trap focus in `TabbableContainer` ([#49846](https://github.com/WordPress/gutenberg/pull/49846)).

### Internal

Expand Down
13 changes: 8 additions & 5 deletions packages/components/src/navigable-container/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,7 @@ class NavigableContainer extends Component< NavigableContainerProps > {
const targetHasMenuItemRole =
!! targetRole && MENU_ITEM_ROLES.includes( targetRole );

// `preventDefault()` on tab to avoid having the browser move the focus
// after this component has already moved it.
const isTab = event.code === 'Tab';

if ( targetHasMenuItemRole || isTab ) {
if ( targetHasMenuItemRole ) {
event.preventDefault();
}
}
Expand All @@ -150,9 +146,16 @@ class NavigableContainer extends Component< NavigableContainerProps > {
const nextIndex = cycle
? cycleValue( index, focusables.length, offset )
: index + offset;

if ( nextIndex >= 0 && nextIndex < focusables.length ) {
focusables[ nextIndex ].focus();
onNavigate( nextIndex, focusables[ nextIndex ] );

// `preventDefault()` on tab to avoid having the browser move the focus
// after this component has already moved it.
if ( event.code === 'Tab' ) {
event.preventDefault();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ import { TabbableContainer } from '../tabbable';
import type { TabbableContainerProps } from '../types';

const TabbableContainerTestCase = ( props: TabbableContainerProps ) => (
<TabbableContainer { ...props }>
<button>Item 1</button>
<span>
<span tabIndex={ -1 }>Item 2 (not tabbable)</span>
</span>
<span>
<span tabIndex={ 0 }>Item 3</span>
</span>
<p>I can not be tabbed</p>
<input type="text" disabled name="disabled-input" />
<a href="https://example.com">Item 4</a>
</TabbableContainer>
<>
<button>Before container</button>
<TabbableContainer { ...props }>
<button>Item 1</button>
<span>
<span tabIndex={ -1 }>Item 2 (not tabbable)</span>
</span>
<span>
<span tabIndex={ 0 }>Item 3</span>
</span>
<p>I can not be tabbed</p>
<input type="text" disabled name="disabled-input" />
<a href="https://example.com">Item 4</a>
</TabbableContainer>
<button>After container</button>
</>
);

const getTabbableContainerTabbables = () => [
Expand Down Expand Up @@ -57,7 +61,11 @@ describe( 'TabbableContainer', () => {

const tabbables = getTabbableContainerTabbables();

// Move focus to first item.
await user.tab();
expect(
screen.getByRole( 'button', { name: 'Before container' } )
).toHaveFocus();

await user.tab();
expect( tabbables[ 0 ] ).toHaveFocus();

Expand Down Expand Up @@ -91,7 +99,11 @@ describe( 'TabbableContainer', () => {
const lastTabbableIndex = tabbables.length - 1;
const lastTabbable = tabbables[ lastTabbableIndex ];

// Move focus to first item.
await user.tab();
expect(
screen.getByRole( 'button', { name: 'Before container' } )
).toHaveFocus();

await user.tab();
expect( firstTabbable ).toHaveFocus();

Expand All @@ -116,12 +128,17 @@ describe( 'TabbableContainer', () => {
/>
);

// With the `cycle` prop set to `false`, cycling is not allowed.
// By default, cycling from first to last and from last to first is allowed.
// With the `cycle` prop set to `false`, cycling is not allowed.
// Therefore, focus will escape the `TabbableContainer` and continue its
// natural path in the page.
await user.tab( { shift: true } );
expect( firstTabbable ).toHaveFocus();
expect(
screen.getByRole( 'button', { name: 'Before container' } )
).toHaveFocus();
expect( onNavigateSpy ).toHaveBeenCalledTimes( 2 );

await user.tab();
await user.tab();
await user.tab();
expect( lastTabbable ).toHaveFocus();
Expand All @@ -131,8 +148,12 @@ describe( 'TabbableContainer', () => {
lastTabbable
);

// Focus will move to the next natively focusable elements after
// `TabbableContainer`
await user.tab();
expect( lastTabbable ).toHaveFocus();
expect(
screen.getByRole( 'button', { name: 'After container' } )
).toHaveFocus();
expect( onNavigateSpy ).toHaveBeenCalledTimes( 4 );
} );

Expand All @@ -151,21 +172,27 @@ describe( 'TabbableContainer', () => {

const tabbables = getTabbableContainerTabbables();

// Move focus to first item
await user.tab();
expect(
screen.getByRole( 'button', { name: 'Before container' } )
).toHaveFocus();
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 0 );

await user.tab();
expect( tabbables[ 0 ] ).toHaveFocus();
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 1 );

await user.keyboard( '[Space]' );
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 1 );
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 2 );

await user.tab();
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 1 );
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 2 );
await user.tab( { shift: true } );
// This extra call is caused by the "shift" key being pressed
// on its own before "tab"
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 2 );
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 3 );

await user.keyboard( '[Escape]' );
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 3 );
expect( externalWrapperOnKeyDownSpy ).toHaveBeenCalledTimes( 4 );
} );
} );

1 comment on commit d770f97

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in d770f97.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4916566590
📝 Reported issues:

Please sign in to comment.