/
use-focus-first-element.js
121 lines (103 loc) · 3.09 KB
/
use-focus-first-element.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* WordPress dependencies
*/
import { useEffect, useRef } from '@wordpress/element';
import {
focus,
isFormElement,
isTextField,
placeCaretAtHorizontalEdge,
} from '@wordpress/dom';
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { isInsideRootBlock } from '../../../utils/dom';
import { store as blockEditorStore } from '../../../store';
/** @typedef {import('@wordpress/element').RefObject} RefObject */
/**
* Returns the initial position if the block needs to be focussed, `undefined`
* otherwise. The initial position is either 0 (start) or -1 (end).
*
* @param {string} clientId Block client ID.
*
* @return {number} The initial position, either 0 (start) or -1 (end).
*/
function useInitialPosition( clientId ) {
return useSelect(
( select ) => {
const {
getSelectedBlocksInitialCaretPosition,
isNavigationMode,
isBlockSelected,
} = select( blockEditorStore );
if ( ! isBlockSelected( clientId ) ) {
return;
}
if ( isNavigationMode() ) {
return;
}
// If there's no initial position, return 0 to focus the start.
return getSelectedBlocksInitialCaretPosition();
},
[ clientId ]
);
}
/**
* Transitions focus to the block or inner tabbable when the block becomes
* selected and an initial position is set.
*
* @param {string} clientId Block client ID.
*
* @return {RefObject} React ref with the block element.
*/
export function useFocusFirstElement( clientId ) {
const ref = useRef();
const initialPosition = useInitialPosition( clientId );
const { isBlockSelected, isMultiSelecting } = useSelect( blockEditorStore );
useEffect( () => {
// Check if the block is still selected at the time this effect runs.
if ( ! isBlockSelected( clientId ) || isMultiSelecting() ) {
return;
}
if ( initialPosition === undefined || initialPosition === null ) {
return;
}
if ( ! ref.current ) {
return;
}
const { ownerDocument } = ref.current;
// Do not focus the block if it already contains the active element.
if ( isInsideRootBlock( ref.current, ownerDocument.activeElement ) ) {
return;
}
// Find all tabbables within node.
const textInputs = focus.tabbable
.find( ref.current )
.filter( ( node ) => isTextField( node ) );
// If reversed (e.g. merge via backspace), use the last in the set of
// tabbables.
const isReverse = -1 === initialPosition;
const target =
textInputs[ isReverse ? textInputs.length - 1 : 0 ] || ref.current;
if ( ! isInsideRootBlock( ref.current, target ) ) {
ref.current.focus();
return;
}
// Check to see if element is focussable before a generic caret insert.
if ( ! ref.current.getAttribute( 'contenteditable' ) ) {
const focusElement = focus.tabbable.findNext( ref.current );
// Make sure focusElement is valid, contained in the same block, and a form field.
if (
focusElement &&
isInsideRootBlock( ref.current, focusElement ) &&
isFormElement( focusElement )
) {
focusElement.focus();
return;
}
}
placeCaretAtHorizontalEdge( target, isReverse );
}, [ initialPosition, clientId ] );
return ref;
}