diff --git a/docs/manifest.json b/docs/manifest.json index 8bb99966f29b5..fbd0a960597ba 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 5e7c7287b903b..491382e0bc4ae 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 cf4ba7068ef36..e2d4a628634d3 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 0000000000000..f31b8e793c8a2 --- /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 0000000000000..e12bf981bd0de --- /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 0000000000000..7c0fca3f28534 --- /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 3b3b5839772d1..58e24890f79c9 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 0000000000000..e21075d25d0f2 --- /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 0000000000000..58b7144bd6dd8 --- /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 ( +