Skip to content

Commit

Permalink
List View: A11Y focus enhancements for edit-site based on modificatio…
Browse files Browse the repository at this point in the history
…ns to edit-post (#51404)

* Bring A11Y changes to site editor list view from the post editor. Add E2E testing.

* Add missing comment to explain usage of open-only keyboard shortcut usage.

* Reviewer feedback.
  • Loading branch information
alexstine authored Jun 13, 2023
1 parent 97379fd commit 05a229b
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ function KeyboardShortcutsEditMode() {
event.preventDefault();
} );

// Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar.
useShortcut( 'core/edit-site/toggle-list-view', () => {
setIsListViewOpened( ! isListViewOpen );
if ( isListViewOpen ) {
return;
}
setIsListViewOpened( true );
} );

useShortcut( 'core/edit-site/toggle-block-settings-sidebar', ( event ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ function KeyboardShortcuts() {
event.preventDefault();
} );

// Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar.
useShortcut( 'core/edit-site/toggle-list-view', () => {
setIsListViewOpened( ! isListViewOpen );
if ( ! isListViewOpen ) {
return;
}
setIsListViewOpened( true );
} );

useShortcut( 'core/edit-site/toggle-block-settings-sidebar', ( event ) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
useMergeRefs,
} from '@wordpress/compose';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { ESCAPE } from '@wordpress/keycodes';
import { focus } from '@wordpress/dom';
import { useShortcut } from '@wordpress/keyboard-shortcuts';

/**
* Internal dependencies
Expand All @@ -25,11 +27,9 @@ const { PrivateListView } = unlock( blockEditorPrivateApis );
export default function ListViewSidebar() {
const { setIsListViewOpened } = useDispatch( editSiteStore );

// Use internal state instead of a ref to make sure that the component
// re-renders when the dropZoneElement updates.
const [ dropZoneElement, setDropZoneElement ] = useState( null );

// This hook handles focus when the sidebar first renders.
const focusOnMountRef = useFocusOnMount( 'firstElement' );
// The next 2 hooks handle focus for when the sidebar closes and returning focus to the element that had focus before sidebar opened.
const headerFocusReturnRef = useFocusReturn();
const contentFocusReturnRef = useFocusReturn();

Expand All @@ -39,11 +39,56 @@ export default function ListViewSidebar() {
}
}

// Use internal state instead of a ref to make sure that the component
// re-renders when the dropZoneElement updates.
const [ dropZoneElement, setDropZoneElement ] = useState( null );

// This ref refers to the sidebar as a whole.
const sidebarRef = useRef();
// This ref refers to the close button.
const sidebarCloseButtonRef = useRef();
// This ref refers to the list view application area.
const listViewRef = useRef();

/*
* Callback function to handle list view or close button focus.
*
* @return void
*/
function handleSidebarFocus() {
// Either focus the list view or the sidebar close button. Must have a fallback because the list view does not render when there are no blocks.
const listViewApplicationFocus = focus.tabbable.find(
listViewRef.current
)[ 0 ];
const listViewFocusArea = sidebarRef.current.contains(
listViewApplicationFocus
)
? listViewApplicationFocus
: sidebarCloseButtonRef.current;
listViewFocusArea.focus();
}

// This only fires when the sidebar is open because of the conditional rendering. It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed.
useShortcut( 'core/edit-site/toggle-list-view', () => {
// If the sidebar has focus, it is safe to close.
if (
sidebarRef.current.contains(
sidebarRef.current.ownerDocument.activeElement
)
) {
setIsListViewOpened( false );
// If the list view or close button does not have focus, focus should be moved to it.
} else {
handleSidebarFocus();
}
} );

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="edit-site-editor__list-view-panel"
onKeyDown={ closeOnEscape }
ref={ sidebarRef }
>
<div
className="edit-site-editor__list-view-panel-header"
Expand All @@ -54,6 +99,7 @@ export default function ListViewSidebar() {
icon={ closeSmall }
label={ __( 'Close' ) }
onClick={ () => setIsListViewOpened( false ) }
ref={ sidebarCloseButtonRef }
/>
</div>
<div
Expand All @@ -62,6 +108,7 @@ export default function ListViewSidebar() {
contentFocusReturnRef,
focusOnMountRef,
setDropZoneElement,
listViewRef,
] ) }
>
<PrivateListView dropZoneElement={ dropZoneElement } />
Expand Down
108 changes: 108 additions & 0 deletions test/e2e/specs/site-editor/list-view.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

test.describe( 'Site Editor List View', () => {
test.beforeAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'emptytheme' );
} );

test.afterAll( async ( { requestUtils } ) => {
await requestUtils.activateTheme( 'twentytwentyone' );
} );

test.beforeEach( async ( { admin, editor } ) => {
// Select a template part with a few blocks.
await admin.visitSiteEditor( {
postId: 'emptytheme//header',
postType: 'wp_template_part',
} );
await editor.canvas.click( 'body' );
} );

// If list view sidebar is open and focus is not inside the sidebar, move
// focus to the sidebar when using the shortcut. If focus is inside the
// sidebar, shortcut should close the sidebar.
test( 'ensures List View global shortcut works properly', async ( {
editor,
page,
pageUtils,
} ) => {
// Current starting focus should be at Open Navigation button.
const openNavigationButton = page.getByRole( 'button', {
name: 'Open Navigation',
exact: true,
} );
await openNavigationButton.focus();
await expect( openNavigationButton ).toBeFocused();

// Open List View.
await pageUtils.pressKeys( 'access+o' );
const listView = page.getByRole( 'treegrid', {
name: 'Block navigation structure',
} );
await expect( listView ).toBeVisible();

// The site title block should have focus.
await expect(
listView.getByRole( 'link', {
name: 'Site Title',
exact: true,
} )
).toBeFocused();

// Navigate to the site tagline block.
await page.keyboard.press( 'ArrowDown' );
const siteTaglineItem = listView.getByRole( 'link', {
name: 'Site Tagline',
exact: true,
} );
await expect( siteTaglineItem ).toBeFocused();

// Hit enter to focus the site tagline block in the canvas.
await page.keyboard.press( 'Enter' );
await expect(
editor.canvas.getByRole( 'document', {
name: 'Block: Site Tagline',
} )
).toBeFocused();

// Since focus is now at the site tagline block in the canvas,
// pressing the list view shortcut should bring focus back to the site tagline
// block in the list view.
await pageUtils.pressKeys( 'access+o' );
await expect( siteTaglineItem ).toBeFocused();

// Since focus is now inside the list view, the shortcut should close
// the sidebar.
await pageUtils.pressKeys( 'access+o' );
await expect( listView ).not.toBeVisible();

// Focus should now be on the Open Navigation button since that is
// where we opened the list view sidebar. This is not a perfect
// solution, but current functionality prevents a better way at
// the moment.
await expect( openNavigationButton ).toBeFocused();

// Open List View.
await pageUtils.pressKeys( 'access+o' );
await expect( listView ).toBeVisible();

// Focus the list view close button and make sure the shortcut will
// close the list view. This is to catch a bug where elements could be
// out of range of the sidebar region. Must shift+tab 1 time to reach
// close button before list view area.
await pageUtils.pressKeys( 'shift+Tab' );
await expect(
page
.getByRole( 'region', { name: 'List View' } )
.getByRole( 'button', {
name: 'Close',
} )
).toBeFocused();
await pageUtils.pressKeys( 'access+o' );
await expect( listView ).not.toBeVisible();
await expect( openNavigationButton ).toBeFocused();
} );
} );

1 comment on commit 05a229b

@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 05a229b.
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/5260376416
📝 Reported issues:

Please sign in to comment.