Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix inline block toolbar keyboard navigation #28420

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions packages/block-editor/src/components/navigable-toolbar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,35 @@ function useIsAccessibleToolbar( ref ) {
return isAccessibleToolbar;
}

function useToolbarFocus(
function useToolbarFocus( {
ref,
focusOnMount,
isAccessibleToolbar,
defaultIndex,
onIndexChange
) {
onIndexChange,
shortcutTarget,
} ) {
// Make sure we don't use modified versions of this prop
const [ initialFocusOnMount ] = useState( focusOnMount );
const [ initialIndex ] = useState( defaultIndex );

const focusToolbar = useCallback( () => {
focusFirstTabbableIn( ref.current );
}, [] );
const performFocus = () => focusFirstTabbableIn( ref.current );
// If shortcutTarget is passed (for example, on the nested image caption
// toolbar), we must ensure this toolbar gets focused. Without this delay,
// the block toolbar would have been focused instead.
if ( shortcutTarget ) {
window.requestAnimationFrame( performFocus );
} else {
performFocus();
}
}, [ shortcutTarget ] );

// Focus on toolbar when pressing alt+F10 when the toolbar is visible
useShortcut( 'core/block-editor/focus-toolbar', focusToolbar, {
bindGlobal: true,
bindGlobal: ! shortcutTarget,
eventName: 'keydown',
target: shortcutTarget,
} );

useEffect( () => {
Expand Down Expand Up @@ -141,18 +151,20 @@ function NavigableToolbar( {
focusOnMount,
__experimentalInitialIndex: initialIndex,
__experimentalOnIndexChange: onIndexChange,
__unstableShortcutTarget: shortcutTarget,
...props
} ) {
const ref = useRef();
const isAccessibleToolbar = useIsAccessibleToolbar( ref );

useToolbarFocus(
useToolbarFocus( {
ref,
focusOnMount,
isAccessibleToolbar,
initialIndex,
onIndexChange
);
defaultIndex: initialIndex,
onIndexChange,
shortcutTarget,
} );

if ( isAccessibleToolbar ) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@
* WordPress dependencies
*/
import { Popover } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import NavigableToolbar from '../navigable-toolbar';
import BlockFormatControls from '../block-format-controls';
import FormatToolbar from './format-toolbar';

const FormatToolbarContainer = ( { inline, anchorRef } ) => {
const FormatToolbarContainer = ( {
inline,
anchorRef,
label = __( 'Format' ),
} ) => {
if ( inline ) {
// Render in popover
return (
<Popover
noArrow
position="top center"
focusOnMount={ false }
anchorRef={ anchorRef }
anchorRef={ anchorRef.current }
className="block-editor-rich-text__inline-format-toolbar"
__unstableSlotName="block-toolbar"
// Render inline
__unstableSlotName={ null }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm passing null to the slot name so it renders inline in the React tree, and not on a separate slot as it won't find any slot:

if ( slot.ref ) {
content = <Fill name={ __unstableSlotName }>{ content }</Fill>;
}
if ( anchorRef || anchorRect ) {
return content;
}
return <span ref={ anchorRefFallback }>{ content }</span>;

I don't know if there's a better way to achieve this, but I think we could have an inline prop or something on Popover.

>
<FormatToolbar />
<NavigableToolbar
label={ label }
__unstableShortcutTarget={ anchorRef }
>
<FormatToolbar />
</NavigableToolbar>
</Popover>
);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/block-editor/src/components/rich-text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function RichTextWrapper(
isSelected: originalIsSelected,
multiline,
inlineToolbar,
__experimentalInlineToolbarLabel: inlineToolbarLabel,
wrapperClassName,
autocompleters,
onReplace,
Expand Down Expand Up @@ -628,8 +629,9 @@ function RichTextWrapper(
{ children && children( { value, onChange, onFocus } ) }
{ nestedIsSelected && hasFormats && (
<FormatToolbarContainer
label={ inlineToolbarLabel }
inline={ inlineToolbar }
anchorRef={ ref.current }
anchorRef={ ref }
/>
) }
{ nestedIsSelected && <RemoveBrowserShortcuts /> }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ figcaption.block-editor-rich-text__editable [data-rich-text-placeholder]::before
background-color: $white;
}

.components-accessible-toolbar,
.components-toolbar-group,
.components-toolbar {
// The popover already provides a border.
Expand Down
3 changes: 3 additions & 0 deletions packages/block-library/src/audio/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@ function AudioEdit( {
setAttributes( { caption: value } )
}
inlineToolbar
__experimentalInlineToolbarLabel={ __(
'Audio caption format'
) }
__unstableOnSplitAtEnd={ () =>
insertBlocksAfter( createBlock( 'core/paragraph' ) )
}
Expand Down
3 changes: 3 additions & 0 deletions packages/block-library/src/embed/embed-preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ class EmbedPreview extends Component {
value={ caption }
onChange={ onCaptionChange }
inlineToolbar
__experimentalInlineToolbarLabel={ __(
'Caption format'
) }
__unstableOnSplitAtEnd={ () =>
insertBlocksAfter( createBlock( 'core/paragraph' ) )
}
Expand Down
3 changes: 3 additions & 0 deletions packages/block-library/src/gallery/gallery-image.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ class GalleryImage extends Component {
}
unstableOnFocus={ this.onSelectCaption }
inlineToolbar
__experimentalInlineToolbarLabel={ __(
'Image caption format'
) }
/>
) }
</figure>
Expand Down
3 changes: 3 additions & 0 deletions packages/block-library/src/gallery/gallery.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export const Gallery = ( props ) => {
unstableOnFocus={ onFocusGalleryCaption }
onChange={ ( value ) => setAttributes( { caption: value } ) }
inlineToolbar
__experimentalInlineToolbarLabel={ __(
'Gallery caption format'
) }
__unstableOnSplitAtEnd={ () =>
insertBlocksAfter( createBlock( 'core/paragraph' ) )
}
Expand Down
3 changes: 3 additions & 0 deletions packages/block-library/src/image/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,9 @@ export default function Image( {
}
isSelected={ captionFocused }
inlineToolbar
__experimentalInlineToolbarLabel={ __(
'Image caption format'
) }
__unstableOnSplitAtEnd={ () =>
insertBlocksAfter( createBlock( 'core/paragraph' ) )
}
Expand Down
3 changes: 3 additions & 0 deletions packages/block-library/src/video/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ function VideoEdit( {
setAttributes( { caption: value } )
}
inlineToolbar
__experimentalInlineToolbarLabel={ __(
'Video caption format'
) }
__unstableOnSplitAtEnd={ () =>
insertBlocksAfter( createBlock( 'core/paragraph' ) )
}
Expand Down
35 changes: 35 additions & 0 deletions packages/e2e-tests/specs/editor/blocks/image.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async function waitForImage( filename ) {
async function getSrc( elementHandle ) {
return elementHandle.evaluate( ( node ) => node.src );
}

async function getDataURL( elementHandle ) {
return elementHandle.evaluate( ( node ) => {
const canvas = document.createElement( 'canvas' );
Expand All @@ -58,6 +59,14 @@ async function getDataURL( elementHandle ) {
} );
}

async function expectAriaLabelToHaveFocus( label ) {
await expect(
await page.evaluate( () =>
document.activeElement.getAttribute( 'aria-label' )
)
).toBe( label );
}

describe( 'Image', () => {
beforeEach( async () => {
await createNewPost();
Expand Down Expand Up @@ -288,4 +297,30 @@ describe( 'Image', () => {
expect( initialImageDataURL ).not.toEqual( updatedImageDataURL );
expect( updatedImageDataURL ).toMatchSnapshot();
} );

it( 'allows navigating through inline and block toolbars with keyboard', async () => {
await insertBlock( 'Image' );
const fileName = await upload( '.wp-block-image input[type="file"]' );
await waitForImage( fileName );

await pressKeyWithModifier( 'shift', 'Tab' );
// First image caption toolbar item
await expectAriaLabelToHaveFocus( 'Bold' );

await pressKeyWithModifier( 'shift', 'Tab' );
await expectAriaLabelToHaveFocus( 'Block: Image' );

await pressKeyWithModifier( 'shift', 'Tab' );
// First image block toolbar item
await expectAriaLabelToHaveFocus( 'Image' );

await page.keyboard.press( 'Tab' );
await page.keyboard.press( 'Tab' );
await page.keyboard.press( 'ArrowRight' );
// Second image caption toolbar item
await expectAriaLabelToHaveFocus( 'Italic' );

await page.keyboard.press( 'Tab' );
await expectAriaLabelToHaveFocus( 'Image caption text' );
} );
} );