From 3de8bd14bdcd07b981b26dde2e8137e759691489 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Mon, 3 Feb 2020 17:54:34 +0800 Subject: [PATCH] Add movers for block navigator Fix doc examples Make components experimental Simplify creation of RovingTabIndex provider value Add storybook snapshots Add tests for RovingTabIndex, RovingTabIndexItem Add tests for TreeGrid components minor variable name change Format JS Import TreeGrid components from @wordpress/components Update styling for block navigator Try adding descender line styles Add some further finesse to styling Extract animated row to fix animation and use change detection that includes a full path so that all children animate Patch up code after rebase Use colSpan instead of colspan Fix row animation when moving a row quickly multiple times Remove descender lines from the accessibility tree Use useCallback for callback Use aria-hidden on purely presentational descender indicator in tree grid Update example Fix arrow key navigation when using Voiceover Only handle arrow key navigation when no modifiers are pressed Fix regressions in BlockMoverButton caused by rebase. Rename file to just button Name buttons in a consistent way Add screenreader description for focused block in navigator Add aria-describedby for appender button Fix selected block movers appearing with darker background Remove writing flow hack Fix typo Allow aria-setsize and aria-posinset on role=row in axe e2e tests Fix translator comment and use Add as the verb instead of Insert Add aria-label for navigation structure tree grid Fix typos Try to fix tests More typo fixes Update snapshot to reflect renaming of test description Update more e2e test classnames Rename components to more closely reflect `master` --- docs/manifest.json | 12 ++ .../components/block-list/block-wrapper.js | 2 +- .../block-mobile-toolbar/style.scss | 4 +- .../components/block-navigation/appender.js | 68 +++++++ .../block-navigation/block-contents.js | 185 ++++++++++++++++++ .../src/components/block-navigation/block.js | 110 +++++++++++ .../src/components/block-navigation/branch.js | 131 ++++++++----- .../components/block-navigation/context.js | 11 ++ .../block-navigation/descender-lines.js | 31 +++ .../src/components/block-navigation/index.js | 14 +- .../src/components/block-navigation/leaf.js | 58 ++++++ .../src/components/block-navigation/list.js | 98 ---------- .../components/block-navigation/style.scss | 157 ++++++++++----- .../src/components/block-navigation/tree.js | 41 ++++ .../components/button-block-appender/index.js | 21 +- packages/block-editor/src/components/index.js | 5 +- .../index.js} | 0 .../src/navigation/block-navigation-list.js | 5 +- packages/components/src/index.js | 9 + .../components/src/roving-tab-index/README.md | 97 +++++++++ .../src/roving-tab-index/context.js | 9 + .../components/src/roving-tab-index/index.js | 32 +++ .../components/src/roving-tab-index/item.js | 35 ++++ .../src/roving-tab-index/stories/index.js | 64 ++++++ .../test/__snapshots__/index.js.snap | 7 + .../test/__snapshots__/item.js.snap | 25 +++ .../src/roving-tab-index/test/index.js | 21 ++ .../src/roving-tab-index/test/item.js | 66 +++++++ packages/components/src/tree-grid/README.md | 129 ++++++++++++ packages/components/src/tree-grid/cell.js | 12 ++ packages/components/src/tree-grid/index.js | 157 +++++++++++++++ packages/components/src/tree-grid/row.js | 31 +++ .../components/src/tree-grid/stories/index.js | 103 ++++++++++ .../tree-grid/test/__snapshots__/cell.js.snap | 23 +++ .../test/__snapshots__/index.js.snap | 16 ++ .../tree-grid/test/__snapshots__/row.js.snap | 32 +++ .../components/src/tree-grid/test/cell.js | 54 +++++ .../components/src/tree-grid/test/index.js | 23 +++ packages/components/src/tree-grid/test/row.js | 40 ++++ .../e2e-tests/config/setup-test-framework.js | 6 + .../specs/editor/blocks/columns.test.js | 2 +- .../specs/editor/plugins/block-icons.test.js | 2 +- .../block-hierarchy-navigation.test.js | 12 +- .../menu-editor/navigation-structure-panel.js | 5 +- 44 files changed, 1741 insertions(+), 224 deletions(-) create mode 100644 packages/block-editor/src/components/block-navigation/appender.js create mode 100644 packages/block-editor/src/components/block-navigation/block-contents.js create mode 100644 packages/block-editor/src/components/block-navigation/block.js create mode 100644 packages/block-editor/src/components/block-navigation/context.js create mode 100644 packages/block-editor/src/components/block-navigation/descender-lines.js create mode 100644 packages/block-editor/src/components/block-navigation/leaf.js delete mode 100644 packages/block-editor/src/components/block-navigation/list.js create mode 100644 packages/block-editor/src/components/block-navigation/tree.js rename packages/block-editor/src/components/{block-list/moving-animation.js => use-moving-animation/index.js} (100%) create mode 100644 packages/components/src/roving-tab-index/README.md create mode 100644 packages/components/src/roving-tab-index/context.js create mode 100644 packages/components/src/roving-tab-index/index.js create mode 100644 packages/components/src/roving-tab-index/item.js create mode 100644 packages/components/src/roving-tab-index/stories/index.js create mode 100644 packages/components/src/roving-tab-index/test/__snapshots__/index.js.snap create mode 100644 packages/components/src/roving-tab-index/test/__snapshots__/item.js.snap create mode 100644 packages/components/src/roving-tab-index/test/index.js create mode 100644 packages/components/src/roving-tab-index/test/item.js create mode 100644 packages/components/src/tree-grid/README.md create mode 100644 packages/components/src/tree-grid/cell.js create mode 100644 packages/components/src/tree-grid/index.js create mode 100644 packages/components/src/tree-grid/row.js create mode 100644 packages/components/src/tree-grid/stories/index.js create mode 100644 packages/components/src/tree-grid/test/__snapshots__/cell.js.snap create mode 100644 packages/components/src/tree-grid/test/__snapshots__/index.js.snap create mode 100644 packages/components/src/tree-grid/test/__snapshots__/row.js.snap create mode 100644 packages/components/src/tree-grid/test/cell.js create mode 100644 packages/components/src/tree-grid/test/index.js create mode 100644 packages/components/src/tree-grid/test/row.js diff --git a/docs/manifest.json b/docs/manifest.json index 8bb99966f29b52..fbd0a960597ba4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -971,6 +971,12 @@ "markdown_source": "../packages/components/src/responsive-wrapper/README.md", "parent": "components" }, + { + "title": "RovingTabIndex", + "slug": "roving-tab-index", + "markdown_source": "../packages/components/src/roving-tab-index/README.md", + "parent": "components" + }, { "title": "Sandbox", "slug": "sandbox", @@ -1055,6 +1061,12 @@ "markdown_source": "../packages/components/src/tooltip/README.md", "parent": "components" }, + { + "title": "TreeGrid", + "slug": "tree-grid", + "markdown_source": "../packages/components/src/tree-grid/README.md", + "parent": "components" + }, { "title": "TreeSelect", "slug": "tree-select", diff --git a/packages/block-editor/src/components/block-list/block-wrapper.js b/packages/block-editor/src/components/block-list/block-wrapper.js index 5e7c7287b903bb..491382e0bc4ae9 100644 --- a/packages/block-editor/src/components/block-list/block-wrapper.js +++ b/packages/block-editor/src/components/block-list/block-wrapper.js @@ -18,7 +18,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; * Internal dependencies */ import { isInsideRootBlock } from '../../utils/dom'; -import useMovingAnimation from './moving-animation'; +import useMovingAnimation from '../use-moving-animation'; import { Context, SetBlockNodes } from './root-container'; import { BlockListBlockContext } from './block'; import ELEMENTS from './block-elements'; diff --git a/packages/block-editor/src/components/block-mobile-toolbar/style.scss b/packages/block-editor/src/components/block-mobile-toolbar/style.scss index cf4ba7068ef368..e2d4a628634d37 100644 --- a/packages/block-editor/src/components/block-mobile-toolbar/style.scss +++ b/packages/block-editor/src/components/block-mobile-toolbar/style.scss @@ -3,7 +3,7 @@ flex-direction: row; border-right: $border-width solid $light-gray-500; - .block-editor-block-mover__control { + .block-editor-block-mover-button { width: $button-size; height: $button-size; border-radius: $radius-round-rectangle; @@ -22,7 +22,7 @@ display: flex; margin-right: auto; - .block-editor-block-mover__control { + .block-editor-block-mover-button { float: left; } } diff --git a/packages/block-editor/src/components/block-navigation/appender.js b/packages/block-editor/src/components/block-navigation/appender.js new file mode 100644 index 00000000000000..f31b8e793c8a29 --- /dev/null +++ b/packages/block-editor/src/components/block-navigation/appender.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies + */ +import { __experimentalTreeGridCell as TreeGridCell } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BlockNavigationLeaf from './leaf'; +import ButtonBlockAppender from '../button-block-appender'; +import DescenderLines from './descender-lines'; + +export default function BlockNavigationAppender( { + parentBlockClientId, + position, + level, + rowCount, + terminatedLevels, + path, +} ) { + const instanceId = useInstanceId( BlockNavigationAppender ); + const descriptionId = `block-navigation-appender-row__description_${ instanceId }`; + + const appenderPositionDescription = sprintf( + /* translators: 1: The numerical position of the block that will be inserted. 2: The level of nesting for the block that will be inserted. */ + __( 'Add block at position %1$d, Level %2$d' ), + position, + level + ); + + return ( + + + { ( props ) => ( +
+ + +
+ { appenderPositionDescription } +
+
+ ) } +
+
+ ); +} diff --git a/packages/block-editor/src/components/block-navigation/block-contents.js b/packages/block-editor/src/components/block-navigation/block-contents.js new file mode 100644 index 00000000000000..e12bf981bd0de9 --- /dev/null +++ b/packages/block-editor/src/components/block-navigation/block-contents.js @@ -0,0 +1,185 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + __experimentalGetBlockLabel as getBlockLabel, + getBlockType, +} from '@wordpress/blocks'; +import { Button, Fill, Slot, VisuallyHidden } from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { + Children, + cloneElement, + forwardRef, + useContext, +} from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BlockIcon from '../block-icon'; +import { BlockListBlockContext } from '../block-list/block'; +import { useBlockNavigationContext } from './context'; + +export const BlockNavigationBlockContentWrapper = forwardRef( function( + { + as: WrapperComponent, + className, + block, + isSelected, + onClick, + position, + siblingCount, + level, + children, + ...props + }, + ref +) { + const { name, attributes } = block; + const instanceId = useInstanceId( BlockNavigationBlockContentWrapper ); + const descriptionId = `block-navigation-block-select-button_${ instanceId }`; + const blockType = getBlockType( name ); + const blockDisplayName = getBlockLabel( blockType, attributes ); + const blockPositionDescription = sprintf( + /* translators: 1: The numerical position of the block. 2: The total number of blocks. 3. The level of nesting for the block. */ + __( 'Block %1$d of %2$d, Level %3$d' ), + position, + siblingCount, + level + ); + + return ( + <> + + + { children ? children : blockDisplayName } + { isSelected && ( + + { __( '(selected block)' ) } + + ) } + +
+ { blockPositionDescription } +
+ + ); +} ); + +const BlockNavigationBlockSelectButton = forwardRef( ( props, ref ) => ( + +) ); + +const getSlotName = ( clientId ) => `BlockNavigationBlock-${ clientId }`; + +const BlockNavigationBlockSlot = forwardRef( + ( + { block, isSelected, onClick, position, siblingCount, level, ...props }, + ref + ) => { + const { clientId } = block; + + return ( + + { ( fills ) => { + if ( ! fills.length ) { + return ( + + ); + } + + return ( + + { Children.map( fills, ( fill ) => + cloneElement( fill, { + ...{ + block, + isSelected, + onClick, + ...props, + }, + ...fill.props, + } ) + ) } + + ); + } } + + ); + } +); + +export const BlockNavigationBlockFill = ( props ) => { + const { clientId } = useContext( BlockListBlockContext ); + return ; +}; + +const BlockNavigationBlockContents = forwardRef( + ( + { onClick, block, isSelected, position, siblingCount, level, ...props }, + ref + ) => { + const { + __experimentalWithBlockNavigationSlots: withBlockNavigationSlots, + } = useBlockNavigationContext(); + + return withBlockNavigationSlots ? ( + + ) : ( + + ); + } +); + +export default BlockNavigationBlockContents; diff --git a/packages/block-editor/src/components/block-navigation/block.js b/packages/block-editor/src/components/block-navigation/block.js new file mode 100644 index 00000000000000..7c0fca3f285344 --- /dev/null +++ b/packages/block-editor/src/components/block-navigation/block.js @@ -0,0 +1,110 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __experimentalTreeGridCell as TreeGridCell } from '@wordpress/components'; + +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import BlockNavigationLeaf from './leaf'; +import { + BlockMoverUpButton, + BlockMoverDownButton, +} from '../block-mover/button'; +import DescenderLines from './descender-lines'; +import BlockNavigationBlockContents from './block-contents'; + +export default function BlockNavigationBlock( { + block, + onClick, + isSelected, + position, + level, + rowCount, + showBlockMovers, + terminatedLevels, + path, +} ) { + const [ isHovered, setIsHovered ] = useState( false ); + const [ isFocused, setIsFocused ] = useState( false ); + const { clientId } = block; + + // Subtract 1 from rowCount, as it includes the block appender. + const siblingCount = rowCount - 1; + const hasSiblings = siblingCount > 1; + const hasRenderedMovers = showBlockMovers && hasSiblings; + const hasVisibleMovers = isHovered || isSelected || isFocused; + const moverCellClassName = classnames( + 'block-editor-block-navigation-block__mover-cell', + { 'is-visible': hasVisibleMovers } + ); + + return ( + setIsHovered( true ) } + onMouseLeave={ () => setIsHovered( false ) } + onFocus={ () => setIsFocused( true ) } + onBlur={ () => setIsFocused( false ) } + level={ level } + position={ position } + rowCount={ rowCount } + path={ path } + > + + { ( props ) => ( +
+ + +
+ ) } +
+ { hasRenderedMovers && ( + <> + + { ( props ) => ( + + ) } + + + { ( props ) => ( + + ) } + + + ) } +
+ ); +} diff --git a/packages/block-editor/src/components/block-navigation/branch.js b/packages/block-editor/src/components/block-navigation/branch.js index 3b3b5839772d1f..58e24890f79c9f 100644 --- a/packages/block-editor/src/components/block-navigation/branch.js +++ b/packages/block-editor/src/components/block-navigation/branch.js @@ -1,59 +1,100 @@ +/** + * External dependencies + */ +import { map, compact } from 'lodash'; + /** * WordPress dependencies */ -import { Children, cloneElement, useContext } from '@wordpress/element'; -import { Fill, Slot } from '@wordpress/components'; +import { Fragment } from '@wordpress/element'; /** * Internal dependencies */ -import BlockNavigationListItem from './list-item'; -import { BlockNavigationContext } from './list'; -import { BlockListBlockContext } from '../block-list/block'; +import BlockNavigationBlock from './block'; +import BlockNavigationAppender from './appender'; -const BlockNavigationBranch = ( { children, ...props } ) => { - const { __experimentalWithBlockNavigationSlots } = useContext( - BlockNavigationContext - ); - if ( ! __experimentalWithBlockNavigationSlots ) { - return ( -
  • - - { children } -
  • - ); - } +export default function BlockNavigationBranch( props ) { + const { + blocks, + selectBlock, + selectedBlockClientId, + showAppender, + showBlockMovers, + showNestedBlocks, + parentBlockClientId, + level = 1, + terminatedLevels = [], + path = [], + } = props; + + const isTreeRoot = ! parentBlockClientId; + const filteredBlocks = compact( blocks ); + // Add +1 to the rowCount to take the block appender into account. + const rowCount = showAppender + ? filteredBlocks.length + 1 + : filteredBlocks.length; + const hasAppender = + showAppender && filteredBlocks.length > 0 && ! isTreeRoot; + const appenderPosition = rowCount; return ( -
  • - - { ( fills ) => { - if ( ! fills.length ) { - return ; - } + <> + { map( filteredBlocks, ( block, index ) => { + const { clientId, innerBlocks } = block; + const hasNestedBlocks = + showNestedBlocks && !! innerBlocks && !! innerBlocks.length; + const position = index + 1; + const isLastRowAtLevel = rowCount === position; + const updatedTerminatedLevels = isLastRowAtLevel + ? [ ...terminatedLevels, level ] + : terminatedLevels; + const updatedPath = [ ...path, position ]; - return Children.map( fills, ( fill ) => - cloneElement( fill, { - ...props, - ...fill.props, - } ) - ); - } } - - { children } -
  • + return ( + + selectBlock( clientId ) } + isSelected={ selectedBlockClientId === clientId } + level={ level } + position={ position } + rowCount={ rowCount } + showBlockMovers={ showBlockMovers } + terminatedLevels={ terminatedLevels } + path={ updatedPath } + /> + { hasNestedBlocks && ( + + ) } + + ); + } ) } + { hasAppender && ( + + ) } + ); -}; - -export default BlockNavigationBranch; - -const listItemSlotName = ( blockId ) => `BlockNavigationList-item-${ blockId }`; - -export const BlockNavigationListItemSlot = ( { blockId, ...props } ) => ( - -); +} -export const BlockNavigationListItemFill = ( props ) => { - const { clientId } = useContext( BlockListBlockContext ); - return ; +BlockNavigationBranch.defaultProps = { + selectBlock: () => {}, }; diff --git a/packages/block-editor/src/components/block-navigation/context.js b/packages/block-editor/src/components/block-navigation/context.js new file mode 100644 index 00000000000000..e21075d25d0f20 --- /dev/null +++ b/packages/block-editor/src/components/block-navigation/context.js @@ -0,0 +1,11 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +export const BlockNavigationContext = createContext( { + __experimentalWithBlockNavigationSlots: false, +} ); + +export const useBlockNavigationContext = () => + useContext( BlockNavigationContext ); diff --git a/packages/block-editor/src/components/block-navigation/descender-lines.js b/packages/block-editor/src/components/block-navigation/descender-lines.js new file mode 100644 index 00000000000000..58b7144bd6dd85 --- /dev/null +++ b/packages/block-editor/src/components/block-navigation/descender-lines.js @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { times } from 'lodash'; +import classnames from 'classnames'; + +const lineClassName = 'block-editor-block-navigator-descender-line'; + +export default function DescenderLines( { + level, + isLastRow, + terminatedLevels, +} ) { + return times( level - 1, ( index ) => { + // The first 'level' that has a descender line is level 2. + // Add 2 to the zero-based index below to reflect that. + const currentLevel = index + 2; + const hasItem = currentLevel === level; + return ( +