Skip to content

Commit 07153e7

Browse files
authored
Navigation mode: polish enabling when no block is selected (WordPress#19298)
* Navigation mode: polish enabling when no block is selected * Account for title * Add e2e tests
1 parent 3ca05a7 commit 07153e7

File tree

2 files changed

+108
-48
lines changed

2 files changed

+108
-48
lines changed

packages/block-editor/src/components/writing-flow/index.js

+43-48
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { overEvery, find, findLast, reverse, first, last } from 'lodash';
66
/**
77
* WordPress dependencies
88
*/
9-
import { Component, createRef, useEffect } from '@wordpress/element';
9+
import { Component, createRef } from '@wordpress/element';
1010
import {
1111
computeCaretRect,
1212
focus,
@@ -18,7 +18,7 @@ import {
1818
isEntirelySelected,
1919
} from '@wordpress/dom';
2020
import { UP, DOWN, LEFT, RIGHT, TAB, isKeyboardEvent } from '@wordpress/keycodes';
21-
import { withSelect, withDispatch, useDispatch } from '@wordpress/data';
21+
import { withSelect, withDispatch, useSelect, useDispatch } from '@wordpress/data';
2222
import { compose } from '@wordpress/compose';
2323

2424
/**
@@ -77,9 +77,30 @@ export function isNavigationCandidate( element, keyCode, hasModifier ) {
7777
* Renders focus capturing areas to redirect focus to the selected block if not
7878
* in Navigation mode.
7979
*/
80-
function FocusCapture( { isReverse, clientId, isNavigationMode } ) {
80+
function FocusCapture( { selectedClientId, isReverse, containerRef } ) {
81+
const isNavigationMode = useSelect( ( select ) =>
82+
select( 'core/block-editor' ).isNavigationMode()
83+
);
84+
const { setNavigationMode } = useDispatch( 'core/block-editor' );
85+
8186
function onFocus() {
82-
const wrapper = getBlockFocusableWrapper( clientId );
87+
if ( ! selectedClientId ) {
88+
setNavigationMode( true );
89+
90+
const tabbables = focus.tabbable.find( containerRef.current );
91+
92+
if ( tabbables.length ) {
93+
if ( isReverse ) {
94+
last( tabbables ).focus();
95+
} else {
96+
first( tabbables ).focus();
97+
}
98+
}
99+
100+
return;
101+
}
102+
103+
const wrapper = getBlockFocusableWrapper( selectedClientId );
83104

84105
if ( isReverse ) {
85106
const tabbables = focus.tabbable.find( wrapper );
@@ -91,7 +112,7 @@ function FocusCapture( { isReverse, clientId, isNavigationMode } ) {
91112

92113
return (
93114
<div
94-
tabIndex={ clientId && ! isNavigationMode ? '0' : undefined }
115+
tabIndex={ ! isNavigationMode ? '0' : undefined }
95116
onFocus={ onFocus }
96117
// Needs to be positioned within the viewport, so focus to this
97118
// element does not scroll the page.
@@ -100,33 +121,11 @@ function FocusCapture( { isReverse, clientId, isNavigationMode } ) {
100121
);
101122
}
102123

103-
/**
104-
* Enables navigation mode as soon as tab is pressed.
105-
* Meant to be rendered if there is no block selected.
106-
*/
107-
function EnableNavigationModeOnTab() {
108-
const { setNavigationMode } = useDispatch( 'core/block-editor' );
109-
useEffect( () => {
110-
function handleTab( event ) {
111-
if ( event.keyCode === TAB ) {
112-
setNavigationMode( true );
113-
}
114-
}
115-
116-
window.addEventListener( 'keydown', handleTab );
117-
return () => {
118-
window.removeEventListener( 'keydown', handleTab );
119-
};
120-
}, [ setNavigationMode ] );
121-
return null;
122-
}
123-
124124
class WritingFlow extends Component {
125125
constructor() {
126126
super( ...arguments );
127127

128128
this.onKeyDown = this.onKeyDown.bind( this );
129-
this.bindContainer = this.bindContainer.bind( this );
130129
this.onMouseDown = this.onMouseDown.bind( this );
131130
this.focusLastTextField = this.focusLastTextField.bind( this );
132131

@@ -139,6 +138,8 @@ class WritingFlow extends Component {
139138
*/
140139
this.verticalRect = null;
141140

141+
this.container = createRef();
142+
142143
/**
143144
* Reference of the writing flow appender element.
144145
* The reference is used to focus the first tabbable element after the block list
@@ -147,10 +148,6 @@ class WritingFlow extends Component {
147148
this.appender = createRef();
148149
}
149150

150-
bindContainer( ref ) {
151-
this.container = ref;
152-
}
153-
154151
onMouseDown() {
155152
this.verticalRect = null;
156153
}
@@ -168,7 +165,7 @@ class WritingFlow extends Component {
168165
getClosestTabbable( target, isReverse ) {
169166
// Since the current focus target is not guaranteed to be a text field,
170167
// find all focusables. Tabbability is considered later.
171-
let focusableNodes = focus.focusable.find( this.container );
168+
let focusableNodes = focus.focusable.find( this.container.current );
172169

173170
if ( isReverse ) {
174171
focusableNodes = reverse( focusableNodes );
@@ -336,7 +333,7 @@ class WritingFlow extends Component {
336333

337334
if ( isShift ) {
338335
if ( target === wrapper ) {
339-
const focusableParent = this.container.closest( '[tabindex]' );
336+
const focusableParent = this.container.current.closest( '[tabindex]' );
340337
const beforeEditorElement = focus.tabbable.findPrevious( focusableParent );
341338
beforeEditorElement.focus();
342339
event.preventDefault();
@@ -346,7 +343,7 @@ class WritingFlow extends Component {
346343
const tabbables = focus.tabbable.find( wrapper );
347344

348345
if ( target === last( tabbables ) ) {
349-
const focusableParent = this.container.closest( '[tabindex]' );
346+
const focusableParent = this.container.current.closest( '[tabindex]' );
350347
const afterEditorElement = focus.tabbable.findNext( focusableParent );
351348
afterEditorElement.focus();
352349
event.preventDefault();
@@ -452,7 +449,7 @@ class WritingFlow extends Component {
452449
* Sets focus to the end of the last tabbable text field, if one exists.
453450
*/
454451
focusLastTextField() {
455-
const focusableNodes = focus.focusable.find( this.container );
452+
const focusableNodes = focus.focusable.find( this.container.current );
456453
const target = findLast( focusableNodes, isTabbableTextField );
457454
if ( target ) {
458455
placeCaretAtHorizontalEdge( target, true );
@@ -462,41 +459,39 @@ class WritingFlow extends Component {
462459
render() {
463460
const {
464461
children,
465-
isNavigationMode,
466462
selectedBlockClientId,
467463
selectionStartClientId,
468464
} = this.props;
469-
const clientId = selectedBlockClientId || selectionStartClientId;
465+
const selectedClientId = selectedBlockClientId || selectionStartClientId;
470466

471467
// Disable reason: Wrapper itself is non-interactive, but must capture
472468
// bubbling events from children to determine focus transition intents.
473469
/* eslint-disable jsx-a11y/no-static-element-interactions */
474470
return (
475471
<div className="block-editor-writing-flow">
472+
<FocusCapture
473+
selectedClientId={ selectedClientId }
474+
containerRef={ this.container }
475+
/>
476476
<div
477-
ref={ this.bindContainer }
477+
ref={ this.container }
478478
onKeyDown={ this.onKeyDown }
479479
onMouseDown={ this.onMouseDown }
480480
>
481-
<FocusCapture
482-
clientId={ clientId }
483-
isNavigationMode={ isNavigationMode }
484-
/>
485481
{ children }
486-
<FocusCapture
487-
clientId={ clientId }
488-
isNavigationMode={ isNavigationMode }
489-
isReverse
490-
/>
491482
</div>
483+
<FocusCapture
484+
selectedClientId={ selectedClientId }
485+
containerRef={ this.container }
486+
isReverse
487+
/>
492488
<div
493489
ref={ this.appender }
494490
aria-hidden
495491
tabIndex={ -1 }
496492
onClick={ this.focusLastTextField }
497493
className="block-editor-writing-flow__click-redirect"
498494
/>
499-
{ ! clientId && <EnableNavigationModeOnTab /> }
500495
</div>
501496
);
502497
/* eslint-enable jsx-a11y/no-static-element-interactions */

packages/e2e-tests/specs/editor/various/keyboard-navigable-blocks.test.js

+65
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,69 @@ describe( 'Order of block keyboard navigation', () => {
101101
await navigateToContentEditorTop();
102102
await tabThroughParagraphBlock( 'Paragraph 1' );
103103
} );
104+
105+
it( 'allows tabbing in navigation mode if no block is selected', async () => {
106+
const paragraphBlocks = [ '0', '1' ];
107+
108+
// Create 2 paragraphs blocks with some content.
109+
for ( const paragraphBlock of paragraphBlocks ) {
110+
await insertBlock( 'Paragraph' );
111+
await page.keyboard.type( paragraphBlock );
112+
}
113+
114+
// Clear the selected block and put focus in front of the block list.
115+
await page.evaluate( () => {
116+
document.querySelector( '.edit-post-visual-editor' ).focus();
117+
} );
118+
119+
await page.keyboard.press( 'Tab' );
120+
await expect( await page.evaluate( () => {
121+
return document.activeElement.placeholder;
122+
} ) ).toBe( 'Add title' );
123+
124+
await page.keyboard.press( 'Tab' );
125+
await expect( await getActiveLabel() ).toBe( 'Paragraph' );
126+
127+
await page.keyboard.press( 'Tab' );
128+
await expect( await getActiveLabel() ).toBe( 'Paragraph' );
129+
130+
await page.keyboard.press( 'Tab' );
131+
await expect( await getActiveLabel() ).toBe( 'Open publish panel' );
132+
} );
133+
134+
it( 'allows tabbing in navigation mode if no block is selected (reverse)', async () => {
135+
const paragraphBlocks = [ '0', '1' ];
136+
137+
// Create 2 paragraphs blocks with some content.
138+
for ( const paragraphBlock of paragraphBlocks ) {
139+
await insertBlock( 'Paragraph' );
140+
await page.keyboard.type( paragraphBlock );
141+
}
142+
143+
// Clear the selected block and put focus behind the block list.
144+
await page.evaluate( () => {
145+
document.querySelector( '.edit-post-visual-editor' ).focus();
146+
document.querySelector( '.edit-post-editor-regions__sidebar' ).focus();
147+
} );
148+
149+
await pressKeyWithModifier( 'shift', 'Tab' );
150+
await expect( await getActiveLabel() ).toBe( 'Open publish panel' );
151+
152+
await pressKeyWithModifier( 'shift', 'Tab' );
153+
await expect( await getActiveLabel() ).toBe( 'Paragraph' );
154+
155+
await pressKeyWithModifier( 'shift', 'Tab' );
156+
await expect( await getActiveLabel() ).toBe( 'Paragraph' );
157+
158+
await pressKeyWithModifier( 'shift', 'Tab' );
159+
await expect( await getActiveLabel() ).toBe( 'Add block' );
160+
161+
await pressKeyWithModifier( 'shift', 'Tab' );
162+
await expect( await getActiveLabel() ).toBe( 'Block: Paragraph' );
163+
164+
await pressKeyWithModifier( 'shift', 'Tab' );
165+
await expect( await page.evaluate( () => {
166+
return document.activeElement.placeholder;
167+
} ) ).toBe( 'Add title' );
168+
} );
104169
} );

0 commit comments

Comments
 (0)