Skip to content

Commit aa8ec5b

Browse files
authored
List View: Try directing focus to the list view toggle button when closing the list view (#54175)
* List View: Try directing focus to the list view toggle button when closing the list view * Only focus if no blocks are selected * Also use focus logic for when the list view is closed via the keyboard shortcut * Try implementing in the site editor, too * Always return focus to the toggle whenever the list view is closed, roll out to widgets editor * Update e2e tests
1 parent 8f3f218 commit aa8ec5b

File tree

14 files changed

+148
-100
lines changed

14 files changed

+148
-100
lines changed

packages/edit-post/src/components/header/header-toolbar/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const preventDefault = ( event ) => {
3333
event.preventDefault();
3434
};
3535

36-
function HeaderToolbar() {
36+
function HeaderToolbar( { setListViewToggleElement } ) {
3737
const inserterButton = useRef();
3838
const { setIsInserterOpened, setIsListViewOpened } =
3939
useDispatch( editPostStore );
@@ -108,6 +108,7 @@ function HeaderToolbar() {
108108
showTooltip={ ! showIconLabels }
109109
variant={ showIconLabels ? 'tertiary' : undefined }
110110
aria-expanded={ isListViewOpen }
111+
ref={ setListViewToggleElement }
111112
/>
112113
</>
113114
);

packages/edit-post/src/components/header/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ const slideX = {
3232
hover: { x: 0, transition: { type: 'tween', delay: 0.2 } },
3333
};
3434

35-
function Header( { setEntitiesSavedStatesCallback } ) {
35+
function Header( {
36+
setEntitiesSavedStatesCallback,
37+
setListViewToggleElement,
38+
} ) {
3639
const isLargeViewport = useViewportMatch( 'large' );
3740
const { hasActiveMetaboxes, isPublishSidebarOpened, showIconLabels } =
3841
useSelect(
@@ -61,7 +64,9 @@ function Header( { setEntitiesSavedStatesCallback } ) {
6164
transition={ { type: 'tween', delay: 0.8 } }
6265
className="edit-post-header__toolbar"
6366
>
64-
<HeaderToolbar />
67+
<HeaderToolbar
68+
setListViewToggleElement={ setListViewToggleElement }
69+
/>
6570
<div className="edit-post-header__center">
6671
<DocumentActions />
6772
</div>

packages/edit-post/src/components/layout/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ function Layout() {
210210
// Note 'truthy' callback implies an open panel.
211211
const [ entitiesSavedStatesCallback, setEntitiesSavedStatesCallback ] =
212212
useState( false );
213+
214+
const [ listViewToggleElement, setListViewToggleElement ] =
215+
useState( null );
216+
213217
const closeEntitiesSavedStates = useCallback(
214218
( arg ) => {
215219
if ( typeof entitiesSavedStatesCallback === 'function' ) {
@@ -244,7 +248,11 @@ function Layout() {
244248
return <InserterSidebar />;
245249
}
246250
if ( mode === 'visual' && isListViewOpened ) {
247-
return <ListViewSidebar />;
251+
return (
252+
<ListViewSidebar
253+
listViewToggleElement={ listViewToggleElement }
254+
/>
255+
);
248256
}
249257

250258
return null;
@@ -285,6 +293,7 @@ function Layout() {
285293
setEntitiesSavedStatesCallback={
286294
setEntitiesSavedStatesCallback
287295
}
296+
setListViewToggleElement={ setListViewToggleElement }
288297
/>
289298
}
290299
editorNotices={ <EditorNotices /> }

packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@
33
*/
44
import { __experimentalListView as ListView } from '@wordpress/block-editor';
55
import { Button, TabPanel } from '@wordpress/components';
6-
import {
7-
useFocusOnMount,
8-
useFocusReturn,
9-
useMergeRefs,
10-
} from '@wordpress/compose';
6+
import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
117
import { useDispatch } from '@wordpress/data';
128
import { focus } from '@wordpress/dom';
13-
import { useRef, useState } from '@wordpress/element';
9+
import { useCallback, useRef, useState } from '@wordpress/element';
1410
import { __, _x } from '@wordpress/i18n';
1511
import { closeSmall } from '@wordpress/icons';
1612
import { useShortcut } from '@wordpress/keyboard-shortcuts';
@@ -22,21 +18,27 @@ import { ESCAPE } from '@wordpress/keycodes';
2218
import { store as editPostStore } from '../../store';
2319
import ListViewOutline from './list-view-outline';
2420

25-
export default function ListViewSidebar() {
21+
export default function ListViewSidebar( { listViewToggleElement } ) {
2622
const { setIsListViewOpened } = useDispatch( editPostStore );
2723

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

34-
function closeOnEscape( event ) {
35-
if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
36-
event.preventDefault();
37-
setIsListViewOpened( false );
38-
}
39-
}
27+
// When closing the list view, focus should return to the toggle button.
28+
const closeListView = useCallback( () => {
29+
setIsListViewOpened( false );
30+
listViewToggleElement?.focus();
31+
}, [ listViewToggleElement, setIsListViewOpened ] );
32+
33+
const closeOnEscape = useCallback(
34+
( event ) => {
35+
if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
36+
event.preventDefault();
37+
closeListView();
38+
}
39+
},
40+
[ closeListView ]
41+
);
4042

4143
// Use internal state instead of a ref to make sure that the component
4244
// re-renders when the dropZoneElement updates.
@@ -53,7 +55,6 @@ export default function ListViewSidebar() {
5355

5456
// Must merge the refs together so focus can be handled properly in the next function.
5557
const listViewContainerRef = useMergeRefs( [
56-
contentFocusReturnRef,
5758
focusOnMountRef,
5859
listViewRef,
5960
setDropZoneElement,
@@ -87,20 +88,26 @@ export default function ListViewSidebar() {
8788
}
8889
}
8990

90-
// 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.
91-
useShortcut( 'core/edit-post/toggle-list-view', () => {
91+
const handleToggleListViewShortcut = useCallback( () => {
9292
// If the sidebar has focus, it is safe to close.
9393
if (
9494
sidebarRef.current.contains(
9595
sidebarRef.current.ownerDocument.activeElement
9696
)
9797
) {
98-
setIsListViewOpened( false );
99-
// If the list view or outline does not have focus, focus should be moved to it.
98+
closeListView();
10099
} else {
100+
// If the list view or outline does not have focus, focus should be moved to it.
101101
handleSidebarFocus( tab );
102102
}
103-
} );
103+
}, [ closeListView, tab ] );
104+
105+
// This only fires when the sidebar is open because of the conditional rendering.
106+
// It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed.
107+
useShortcut(
108+
'core/edit-post/toggle-list-view',
109+
handleToggleListViewShortcut
110+
);
104111

105112
/**
106113
* Render tab content for a given tab name.
@@ -127,10 +134,9 @@ export default function ListViewSidebar() {
127134
>
128135
<Button
129136
className="edit-post-editor__document-overview-panel__close-button"
130-
ref={ headerFocusReturnRef }
131137
icon={ closeSmall }
132138
label={ __( 'Close' ) }
133-
onClick={ () => setIsListViewOpened( false ) }
139+
onClick={ closeListView }
134140
/>
135141
<TabPanel
136142
className="edit-post-editor__document-overview-panel__tab-panel"

packages/edit-site/src/components/editor/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const blockRemovalRules = {
7171
),
7272
};
7373

74-
export default function Editor( { isLoading } ) {
74+
export default function Editor( { listViewToggleElement, isLoading } ) {
7575
const {
7676
record: editedPost,
7777
getTitle,
@@ -247,7 +247,11 @@ export default function Editor( { isLoading } ) {
247247
<InserterSidebar />
248248
) ) ||
249249
( shouldShowListView && (
250-
<ListViewSidebar />
250+
<ListViewSidebar
251+
listViewToggleElement={
252+
listViewToggleElement
253+
}
254+
/>
251255
) ) )
252256
}
253257
sidebar={

packages/edit-site/src/components/header-edit-mode/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const preventDefault = ( event ) => {
5353
event.preventDefault();
5454
};
5555

56-
export default function HeaderEditMode() {
56+
export default function HeaderEditMode( { setListViewToggleElement } ) {
5757
const inserterButton = useRef();
5858
const {
5959
deviceType,
@@ -259,6 +259,7 @@ export default function HeaderEditMode() {
259259
/* translators: button label text should, if possible, be under 16 characters. */
260260
label={ __( 'List View' ) }
261261
onClick={ toggleListView }
262+
ref={ setListViewToggleElement }
262263
shortcut={ listViewShortcut }
263264
showTooltip={ ! showIconLabels }
264265
variant={

packages/edit-site/src/components/layout/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export default function Layout() {
127127
const isEditorLoading = useIsSiteEditorLoading();
128128
const [ isResizableFrameOversized, setIsResizableFrameOversized ] =
129129
useState( false );
130+
const [ listViewToggleElement, setListViewToggleElement ] =
131+
useState( null );
130132

131133
// This determines which animation variant should apply to the header.
132134
// There is also a `isDistractionFreeHovering` state that gets priority
@@ -256,7 +258,11 @@ export default function Layout() {
256258
ease: 'easeOut',
257259
} }
258260
>
259-
<Header />
261+
<Header
262+
setListViewToggleElement={
263+
setListViewToggleElement
264+
}
265+
/>
260266
</NavigableRegion>
261267
) }
262268
</AnimatePresence>
@@ -369,6 +375,9 @@ export default function Layout() {
369375
} }
370376
>
371377
<Editor
378+
listViewToggleElement={
379+
listViewToggleElement
380+
}
372381
isLoading={
373382
isEditorLoading
374383
}

packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@
33
*/
44
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
55
import { Button } from '@wordpress/components';
6-
import {
7-
useFocusOnMount,
8-
useFocusReturn,
9-
useMergeRefs,
10-
} from '@wordpress/compose';
6+
import { useFocusOnMount, useMergeRefs } from '@wordpress/compose';
117
import { useDispatch } from '@wordpress/data';
12-
import { useRef, useState } from '@wordpress/element';
8+
import { useCallback, useRef, useState } from '@wordpress/element';
139
import { __ } from '@wordpress/i18n';
1410
import { closeSmall } from '@wordpress/icons';
1511
import { ESCAPE } from '@wordpress/keycodes';
@@ -24,20 +20,27 @@ import { unlock } from '../../lock-unlock';
2420

2521
const { PrivateListView } = unlock( blockEditorPrivateApis );
2622

27-
export default function ListViewSidebar() {
23+
export default function ListViewSidebar( { listViewToggleElement } ) {
2824
const { setIsListViewOpened } = useDispatch( editSiteStore );
2925

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

36-
function closeOnEscape( event ) {
37-
if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
38-
setIsListViewOpened( false );
39-
}
40-
}
29+
// When closing the list view, focus should return to the toggle button.
30+
const closeListView = useCallback( () => {
31+
setIsListViewOpened( false );
32+
listViewToggleElement?.focus();
33+
}, [ listViewToggleElement, setIsListViewOpened ] );
34+
35+
const closeOnEscape = useCallback(
36+
( event ) => {
37+
if ( event.keyCode === ESCAPE && ! event.defaultPrevented ) {
38+
event.preventDefault();
39+
closeListView();
40+
}
41+
},
42+
[ closeListView ]
43+
);
4144

4245
// Use internal state instead of a ref to make sure that the component
4346
// re-renders when the dropZoneElement updates.
@@ -68,20 +71,26 @@ export default function ListViewSidebar() {
6871
listViewFocusArea.focus();
6972
}
7073

71-
// 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.
72-
useShortcut( 'core/edit-site/toggle-list-view', () => {
74+
const handleToggleListViewShortcut = useCallback( () => {
7375
// If the sidebar has focus, it is safe to close.
7476
if (
7577
sidebarRef.current.contains(
7678
sidebarRef.current.ownerDocument.activeElement
7779
)
7880
) {
79-
setIsListViewOpened( false );
80-
// If the list view or close button does not have focus, focus should be moved to it.
81+
closeListView();
8182
} else {
83+
// If the list view or close button does not have focus, focus should be moved to it.
8284
handleSidebarFocus();
8385
}
84-
} );
86+
}, [ closeListView ] );
87+
88+
// This only fires when the sidebar is open because of the conditional rendering.
89+
// It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed.
90+
useShortcut(
91+
'core/edit-site/toggle-list-view',
92+
handleToggleListViewShortcut
93+
);
8594

8695
return (
8796
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
@@ -90,22 +99,18 @@ export default function ListViewSidebar() {
9099
onKeyDown={ closeOnEscape }
91100
ref={ sidebarRef }
92101
>
93-
<div
94-
className="edit-site-editor__list-view-panel-header"
95-
ref={ headerFocusReturnRef }
96-
>
102+
<div className="edit-site-editor__list-view-panel-header">
97103
<strong>{ __( 'List View' ) }</strong>
98104
<Button
99105
icon={ closeSmall }
100106
label={ __( 'Close' ) }
101-
onClick={ () => setIsListViewOpened( false ) }
107+
onClick={ closeListView }
102108
ref={ sidebarCloseButtonRef }
103109
/>
104110
</div>
105111
<div
106112
className="edit-site-editor__list-view-panel-content"
107113
ref={ useMergeRefs( [
108-
contentFocusReturnRef,
109114
focusOnMountRef,
110115
setDropZoneElement,
111116
listViewRef,

packages/edit-widgets/src/components/header/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { unlock } from '../../lock-unlock';
2727

2828
const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis );
2929

30-
function Header() {
30+
function Header( { setListViewToggleElement } ) {
3131
const isMediumViewport = useViewportMatch( 'medium' );
3232
const inserterButton = useRef();
3333
const widgetAreaClientId = useLastSelectedWidgetArea();
@@ -140,6 +140,7 @@ function Header() {
140140
/* translators: button label text should, if possible, be under 16 characters. */
141141
label={ __( 'List View' ) }
142142
onClick={ toggleListView }
143+
ref={ setListViewToggleElement }
143144
/>
144145
</>
145146
) }

0 commit comments

Comments
 (0)