11/**
22 * WordPress dependencies
33 */
4- import { useState , useRef } from '@wordpress/element' ;
4+ import { useState } from '@wordpress/element' ;
55import {
66 createHigherOrderComponent ,
77 useRefEffect ,
8- useMergeRefs ,
8+ useEvent ,
99} from '@wordpress/compose' ;
1010import { isKeyboardEvent } from '@wordpress/keycodes' ;
1111import type { WPKeycodeModifier } from '@wordpress/keycodes' ;
1212
13+ type Shortcut = { modifier : WPKeycodeModifier ; character : string } ;
14+ type Shortcuts = { previous : readonly Shortcut [ ] ; next : readonly Shortcut [ ] } ;
15+
1316const 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/**
0 commit comments