Skip to content

Commit c44f369

Browse files
committed
Make region navigation more robust
1 parent acb176e commit c44f369

File tree

3 files changed

+112
-68
lines changed
  • packages
    • components/src/higher-order/navigate-regions
    • edit-post/src/components/layout
    • edit-site/src/components/layout

3 files changed

+112
-68
lines changed

packages/components/src/higher-order/navigate-regions/index.tsx

Lines changed: 104 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
/**
22
* WordPress dependencies
33
*/
4-
import { useState, useRef } from '@wordpress/element';
4+
import { useState } from '@wordpress/element';
55
import {
66
createHigherOrderComponent,
77
useRefEffect,
8-
useMergeRefs,
8+
useEvent,
99
} from '@wordpress/compose';
1010
import { isKeyboardEvent } from '@wordpress/keycodes';
1111
import type { WPKeycodeModifier } from '@wordpress/keycodes';
1212

13+
type Shortcut = { modifier: WPKeycodeModifier; character: string };
14+
type Shortcuts = { previous: readonly Shortcut[]; next: readonly Shortcut[] };
15+
1316
const defaultShortcuts = {
1417
previous: [
1518
{
@@ -37,43 +40,63 @@ const defaultShortcuts = {
3740
] as const,
3841
};
3942

40-
type Shortcuts = {
41-
previous: readonly { modifier: WPKeycodeModifier; character: string }[];
42-
next: readonly { modifier: WPKeycodeModifier; character: string }[];
43+
const getShortcutSign = (
44+
event: React.KeyboardEvent< HTMLElement > | KeyboardEvent,
45+
shortcuts: Shortcuts
46+
) => {
47+
const isMatch = ( { modifier, character }: Shortcut ) =>
48+
isKeyboardEvent[ modifier ]( event, character );
49+
if ( shortcuts.previous.some( isMatch ) ) {
50+
return -1;
51+
} else if ( shortcuts.next.some( isMatch ) ) {
52+
return 1;
53+
}
54+
return 0;
4355
};
4456

45-
export function useNavigateRegions( shortcuts: Shortcuts = defaultShortcuts ) {
46-
const ref = useRef< HTMLDivElement >( null );
47-
const [ isFocusingRegions, setIsFocusingRegions ] = useState( false );
57+
const regionsSelector = '[role="region"][tabindex="-1"]';
4858

49-
function focusRegion( offset: number ) {
50-
const regions = Array.from(
51-
ref.current?.querySelectorAll< HTMLElement >(
52-
'[role="region"][tabindex="-1"]'
53-
) ?? []
54-
);
55-
if ( ! regions.length ) {
56-
return;
57-
}
58-
let nextRegion = regions[ 0 ];
59-
// Based off the current element, use closest to determine the wrapping region since this operates up the DOM. Also, match tabindex to avoid edge cases with regions we do not want.
60-
const wrappingRegion =
61-
ref.current?.ownerDocument?.activeElement?.closest< HTMLElement >(
62-
'[role="region"][tabindex="-1"]'
63-
);
64-
const selectedIndex = wrappingRegion
65-
? regions.indexOf( wrappingRegion )
66-
: -1;
67-
if ( selectedIndex !== -1 ) {
68-
let nextIndex = selectedIndex + offset;
69-
nextIndex = nextIndex === -1 ? regions.length - 1 : nextIndex;
70-
nextIndex = nextIndex === regions.length ? 0 : nextIndex;
71-
nextRegion = regions[ nextIndex ];
72-
}
59+
const focusRegion = ( root: HTMLElement, offset: number ) => {
60+
const regions = root.querySelectorAll< HTMLElement >( regionsSelector );
61+
if ( ! regions.length ) {
62+
return;
63+
}
64+
let nextRegion = regions[ 0 ];
65+
const { activeElement } = root.ownerDocument;
66+
// Based off the current element, use closest to determine the wrapping region since this operates up the DOM. Also, match tabindex to avoid edge cases with regions we do not want.
67+
const wrappingRegion =
68+
activeElement?.closest< HTMLElement >( regionsSelector );
69+
const selectedIndex = wrappingRegion
70+
? [ ...regions ].indexOf( wrappingRegion )
71+
: -1;
72+
if ( selectedIndex !== -1 ) {
73+
let nextIndex = selectedIndex + offset;
74+
nextIndex = nextIndex === -1 ? regions.length - 1 : nextIndex;
75+
nextIndex = nextIndex === regions.length ? 0 : nextIndex;
76+
nextRegion = regions[ nextIndex ];
77+
}
78+
79+
nextRegion.focus();
80+
};
7381

74-
nextRegion.focus();
75-
setIsFocusingRegions( true );
82+
export function useNavigateRegions(
83+
options: Shortcuts | { shortcuts: Shortcuts; isGlobal: boolean }
84+
) {
85+
let shortcuts: Shortcuts = defaultShortcuts;
86+
let isGlobal = false;
87+
if ( options ) {
88+
if ( 'previous' in options && 'next' in options ) {
89+
shortcuts = options;
90+
} else {
91+
if ( 'shortcuts' in options ) {
92+
( { shortcuts } = options );
93+
}
94+
if ( 'isGlobal' in options ) {
95+
( { isGlobal } = options );
96+
}
97+
}
7698
}
99+
const [ isFocusingRegions, setIsFocusingRegions ] = useState( false );
77100

78101
const clickRef = useRefEffect(
79102
( element ) => {
@@ -90,25 +113,55 @@ export function useNavigateRegions( shortcuts: Shortcuts = defaultShortcuts ) {
90113
[ setIsFocusingRegions ]
91114
);
92115

93-
return {
94-
ref: useMergeRefs( [ ref, clickRef ] ),
95-
className: isFocusingRegions ? 'is-focusing-regions' : '',
96-
onKeyDown( event: React.KeyboardEvent< HTMLDivElement > ) {
97-
if (
98-
shortcuts.previous.some( ( { modifier, character } ) => {
99-
return isKeyboardEvent[ modifier ]( event, character );
100-
} )
101-
) {
102-
focusRegion( -1 );
103-
} else if (
104-
shortcuts.next.some( ( { modifier, character } ) => {
105-
return isKeyboardEvent[ modifier ]( event, character );
106-
} )
107-
) {
108-
focusRegion( 1 );
116+
const navigate = useEvent(
117+
(
118+
event: KeyboardEvent | React.KeyboardEvent< HTMLElement >,
119+
root: HTMLElement
120+
) => {
121+
const sign = getShortcutSign( event, shortcuts );
122+
if ( sign ) {
123+
focusRegion( root, sign );
124+
if ( isGlobal ) {
125+
root.classList.add( 'is-focusing-regions' );
126+
} else {
127+
setIsFocusingRegions( true );
128+
}
129+
}
130+
}
131+
);
132+
133+
const globalEffect = useRefEffect< HTMLElement >(
134+
( node ) => {
135+
const { ownerDocument } = node;
136+
if ( ! ownerDocument ) {
137+
return;
109138
}
139+
const onKeyDown = ( event: KeyboardEvent ) =>
140+
navigate( event, node );
141+
const onPointerDown = () => {
142+
node.classList.remove( 'is-focusing-regions' );
143+
};
144+
ownerDocument.addEventListener( 'keydown', onKeyDown );
145+
ownerDocument.addEventListener( 'pointerdown', onPointerDown );
146+
return () => {
147+
ownerDocument.removeEventListener( 'keydown', onKeyDown );
148+
ownerDocument.removeEventListener(
149+
'pointerdown',
150+
onPointerDown
151+
);
152+
};
110153
},
111-
};
154+
[ navigate ]
155+
);
156+
157+
return isGlobal
158+
? globalEffect
159+
: {
160+
ref: clickRef,
161+
className: isFocusingRegions ? 'is-focusing-regions' : '',
162+
onKeyDown: ( event: React.KeyboardEvent< HTMLElement > ) =>
163+
navigate( event, event.currentTarget ),
164+
};
112165
}
113166

114167
/**

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ function Layout( {
481481
document.body.classList.remove( 'show-icon-labels' );
482482
}
483483

484-
const navigateRegionsProps = useNavigateRegions();
484+
const navigateRegionsRef = useNavigateRegions( { isGlobal: true } );
485485

486486
const className = clsx( 'edit-post-layout', 'is-mode-' + mode, {
487487
'has-metaboxes': hasActiveMetaboxes,
@@ -568,11 +568,7 @@ function Layout( {
568568
<ErrorBoundary canCopyContent>
569569
<CommandMenu />
570570
<WelcomeGuide postType={ currentPostType } />
571-
<div
572-
className={ navigateRegionsProps.className }
573-
{ ...navigateRegionsProps }
574-
ref={ navigateRegionsProps.ref }
575-
>
571+
<div ref={ navigateRegionsRef }>
576572
<Editor
577573
settings={ editorSettings }
578574
initialEdits={ initialEdits }

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

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ function Layout() {
6161
useCommands();
6262
const isMobileViewport = useViewportMatch( 'medium', '<' );
6363
const toggleRef = useRef();
64-
const navigateRegionsProps = useNavigateRegions();
64+
const navigateRegionsRef = useNavigateRegions( { isGlobal: true } );
6565
const disableMotion = useReducedMotion();
6666
const [ canvasResizer, canvasSize ] = useResizeObserver();
6767
const isEditorLoading = useIsSiteEditorLoading();
@@ -96,16 +96,11 @@ function Layout() {
9696
<CommandMenu />
9797
{ canvas === 'view' && <SaveKeyboardShortcut /> }
9898
<div
99-
{ ...navigateRegionsProps }
100-
ref={ navigateRegionsProps.ref }
101-
className={ clsx(
102-
'edit-site-layout',
103-
navigateRegionsProps.className,
104-
{
105-
'is-full-canvas': canvas === 'edit',
106-
'show-icon-labels': showIconLabels,
107-
}
108-
) }
99+
ref={ navigateRegionsRef }
100+
className={ clsx( 'edit-site-layout', {
101+
'is-full-canvas': canvas === 'edit',
102+
'show-icon-labels': showIconLabels,
103+
} ) }
109104
>
110105
<div className="edit-site-layout__content">
111106
{ /*

0 commit comments

Comments
 (0)