From 5b54f871214003320da8d7b50d6edd1707873398 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 11 Oct 2021 14:39:55 +1100 Subject: [PATCH] Show ellipsis menu in the List View (#35170) --- .../block-settings-menu-controls/index.js | 15 ++- packages/block-editor/src/components/index.js | 2 - .../components/list-view/block-contents.js | 55 +++----- .../src/components/list-view/block-slot.js | 120 ------------------ .../src/components/list-view/block.js | 57 +-------- .../src/components/list-view/editor.js | 29 ----- .../src/components/list-view/style.scss | 85 ++++++++----- packages/components/src/tree-grid/index.js | 4 +- packages/dom/src/focusable.js | 69 +++++----- .../secondary-sidebar/list-view-sidebar.js | 1 + .../edit-template-part-menu-button/index.js | 69 ++++++---- .../secondary-sidebar/list-view-sidebar.js | 1 + 12 files changed, 171 insertions(+), 336 deletions(-) delete mode 100644 packages/block-editor/src/components/list-view/block-slot.js delete mode 100644 packages/block-editor/src/components/list-view/editor.js diff --git a/packages/block-editor/src/components/block-settings-menu-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js index 81246c583d28d..d5011f8e57313 100644 --- a/packages/block-editor/src/components/block-settings-menu-controls/index.js +++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js @@ -25,17 +25,20 @@ import { store as blockEditorStore } from '../../store'; const { Fill, Slot } = createSlotFill( 'BlockSettingsMenuControls' ); const BlockSettingsMenuControlsSlot = ( { fillProps, clientIds = null } ) => { - const selectedBlocks = useSelect( + const { selectedBlocks, selectedClientIds } = useSelect( ( select ) => { const { getBlocksByClientId, getSelectedBlockClientIds } = select( blockEditorStore ); const ids = clientIds !== null ? clientIds : getSelectedBlockClientIds(); - return map( - compact( getBlocksByClientId( ids ) ), - ( block ) => block.name - ); + return { + selectedBlocks: map( + compact( getBlocksByClientId( ids ) ), + ( block ) => block.name + ), + selectedClientIds: ids, + }; }, [ clientIds ] ); @@ -46,7 +49,7 @@ const BlockSettingsMenuControlsSlot = ( { fillProps, clientIds = null } ) => { const { isGroupable, isUngroupable } = convertToGroupButtonProps; const showConvertToGroupButton = isGroupable || isUngroupable; return ( - + { ( fills ) => { if ( fills?.length > 0 || showConvertToGroupButton ) { return ( diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 34f5a7aaff262..528108cd558eb 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -70,8 +70,6 @@ export { default as __experimentalLinkControlSearchResults } from './link-contro export { default as __experimentalLinkControlSearchItem } from './link-control/search-item'; export { default as LineHeightControl } from './line-height-control'; export { default as __experimentalListView } from './list-view'; -export { ListViewBlockFill as __experimentalListViewBlockFill } from './list-view/block-slot'; -export { default as __experimentalListViewEditor } from './list-view/editor'; export { default as MediaReplaceFlow } from './media-replace-flow'; export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload } from './media-upload'; diff --git a/packages/block-editor/src/components/list-view/block-contents.js b/packages/block-editor/src/components/list-view/block-contents.js index d3bfd824cfef2..61e221b77f106 100644 --- a/packages/block-editor/src/components/list-view/block-contents.js +++ b/packages/block-editor/src/components/list-view/block-contents.js @@ -12,8 +12,6 @@ import { forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import { useListViewContext } from './context'; -import ListViewBlockSlot from './block-slot'; import ListViewBlockSelectButton from './block-select-button'; import BlockDraggable from '../block-draggable'; import { store as blockEditorStore } from '../../store'; @@ -32,8 +30,6 @@ const ListViewBlockContents = forwardRef( }, ref ) => { - const { __experimentalFeatures } = useListViewContext(); - const { clientId } = block; const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect( @@ -61,40 +57,23 @@ const ListViewBlockContents = forwardRef( return ( - { ( { draggable, onDragStart, onDragEnd } ) => - __experimentalFeatures ? ( - - ) : ( - - ) - } + { ( { draggable, onDragStart, onDragEnd } ) => ( + + ) } ); } diff --git a/packages/block-editor/src/components/list-view/block-slot.js b/packages/block-editor/src/components/list-view/block-slot.js deleted file mode 100644 index 95d30b9a0e42a..0000000000000 --- a/packages/block-editor/src/components/list-view/block-slot.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { getBlockType } from '@wordpress/blocks'; -import { Fill, Slot, VisuallyHidden } from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; -import { - Children, - cloneElement, - forwardRef, - useContext, -} from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import BlockIcon from '../block-icon'; -import { BlockListBlockContext } from '../block-list/block'; -import ListViewBlockSelectButton from './block-select-button'; -import { getBlockPositionDescription } from './utils'; -import { store as blockEditorStore } from '../../store'; -import ListViewExpander from './expander'; - -const getSlotName = ( clientId ) => `ListViewBlock-${ clientId }`; - -function ListViewBlockSlot( props, ref ) { - const { clientId } = props.block; - const { name } = useSelect( - ( select ) => select( blockEditorStore ).getBlockName( clientId ), - [ clientId ] - ); - const instanceId = useInstanceId( ListViewBlockSlot ); - - return ( - - { ( fills ) => { - if ( ! fills.length ) { - return ( - - ); - } - - const { - className, - isSelected, - position, - siblingBlockCount, - level, - tabIndex, - onFocus, - onToggleExpanded, - } = props; - - const blockType = getBlockType( name ); - const descriptionId = `list-view-block-slot__${ instanceId }`; - const blockPositionDescription = getBlockPositionDescription( - position, - siblingBlockCount, - level - ); - - const forwardedFillProps = { - // Ensure that the component in the slot can receive - // keyboard navigation. - tabIndex, - onFocus, - ref, - // Give the element rendered in the slot a description - // that describes its position. - 'aria-describedby': descriptionId, - }; - - return ( - <> -
- - - { Children.map( fills, ( fill ) => - cloneElement( fill, { - ...fill.props, - ...forwardedFillProps, - } ) - ) } - { isSelected && ( - - { __( '(selected block)' ) } - - ) } -
- { blockPositionDescription } -
-
- - ); - } } -
- ); -} - -export default forwardRef( ListViewBlockSlot ); - -export const ListViewBlockFill = ( props ) => { - const { clientId } = useContext( BlockListBlockContext ); - return ; -}; diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index f0a5eeda36b96..f9b7bc8bbc973 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -9,13 +9,10 @@ import classnames from 'classnames'; import { __experimentalTreeGridCell as TreeGridCell, __experimentalTreeGridItem as TreeGridItem, - MenuGroup, - MenuItem, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; import { moreVertical } from '@wordpress/icons'; import { useState, useRef, useEffect } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -49,23 +46,14 @@ export default function ListViewBlock( { const cellRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); const { clientId } = block; - const blockParents = useSelect( - ( select ) => { - return select( blockEditorStore ).getBlockParents( clientId ); - }, - [ clientId ] - ); - const { - selectBlock: selectEditorBlock, - toggleBlockHighlight, - } = useDispatch( blockEditorStore ); + const { toggleBlockHighlight } = useDispatch( blockEditorStore ); const hasSiblings = siblingBlockCount > 0; const hasRenderedMovers = showBlockMovers && hasSiblings; const moverCellClassName = classnames( 'block-editor-list-view-block__mover-cell', - { 'is-visible': isHovered } + { 'is-visible': isHovered || isSelected } ); const { __experimentalFeatures: withExperimentalFeatures, @@ -74,7 +62,7 @@ export default function ListViewBlock( { } = useListViewContext(); const listViewBlockSettingsClassName = classnames( 'block-editor-list-view-block__menu-cell', - { 'is-visible': isHovered } + { 'is-visible': isHovered || isSelected } ); // If ListView has experimental features related to the Persistent List View, @@ -90,14 +78,6 @@ export default function ListViewBlock( { } }, [] ); - // If ListView has experimental features (such as drag and drop) enabled, - // leave the focus handling as it was before, to avoid accidental regressions. - useEffect( () => { - if ( withExperimentalFeatures && isSelected ) { - cellRef.current.focus(); - } - }, [ withExperimentalFeatures, isSelected ] ); - const highlightBlock = withExperimentalPersistentListViewFeatures ? toggleBlockHighlight : () => {}; @@ -198,38 +178,13 @@ export default function ListViewBlock( { icon={ moreVertical } toggleProps={ { ref, + className: 'block-editor-list-view-block__menu', tabIndex, onFocus, } } disableOpenOnArrowDown __experimentalSelectBlock={ onClick } - > - { ( { onClose } ) => ( - - { - if ( blockParents.length ) { - // If the block to select is inside a dropdown, we need to open the dropdown. - // Otherwise focus won't transfer to the block. - for ( const parent of blockParents ) { - await selectEditorBlock( - parent - ); - } - } else { - // If clientId is already selected, it won't be focused (see block-wrapper.js) - // This removes the selection first to ensure the focus will always switch. - await selectEditorBlock( null ); - } - await selectEditorBlock( clientId ); - onClose(); - } } - > - { __( 'Go to block' ) } - - - ) } - + /> ) } ) } diff --git a/packages/block-editor/src/components/list-view/editor.js b/packages/block-editor/src/components/list-view/editor.js deleted file mode 100644 index 1d3df28a61083..0000000000000 --- a/packages/block-editor/src/components/list-view/editor.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { RichText } from '../'; -import { ListViewBlockFill } from './block-slot'; - -export default function ListViewEditor( { value, onChange } ) { - return ( - - - - ); -} diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index a8f766af5bae4..8f8555f44ca35 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -15,10 +15,14 @@ // Use position relative for row animation. position: relative; - &.is-selected .block-editor-list-view-block-contents { + &.is-selected { background: var(--wp-admin-theme-color); + } + &.is-selected .block-editor-list-view-block-contents, + &.is-selected .components-button.has-icon { color: $white; - + } + &.is-selected .block-editor-list-view-block-contents { // Hide selection styles while a user is dragging blocks/files etc. .is-dragging-components-draggable & { background: none; @@ -26,30 +30,31 @@ } } &.is-selected .block-editor-list-view-block-contents:focus { - box-shadow: - inset 0 0 0 1px $white, - 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - - // Hide focus styles while a user is dragging blocks/files etc. - .is-dragging-components-draggable & { - box-shadow: none; + &::after { + box-shadow: + inset 0 0 0 1px $white, + 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } } + &.is-selected .block-editor-list-view-block__menu:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $white; + } + &.is-branch-selected:not(.is-selected) { + // Lighten a CSS variable without introducing a new SASS variable + background: + linear-gradient(transparentize($white, 0.1), transparentize($white, 0.1)), + linear-gradient(var(--wp-admin-theme-color), var(--wp-admin-theme-color)); + } &.is-branch-selected.is-selected .block-editor-list-view-block-contents { border-radius: 2px 2px 0 0; } - &[aria-expanded="false"] { &.is-branch-selected.is-selected .block-editor-list-view-block-contents { border-radius: 2px; } } &.is-branch-selected:not(.is-selected) .block-editor-list-view-block-contents { - // Lighten a CSS variable without introducing a new SASS variable - background: - linear-gradient(transparentize($white, 0.1), transparentize($white, 0.1)), - linear-gradient(var(--wp-admin-theme-color), var(--wp-admin-theme-color)); border-radius: 0; } &.is-branch-selected.is-last-of-selected-branch .block-editor-list-view-block-contents { @@ -72,19 +77,6 @@ position: relative; white-space: nowrap; - &:hover, - &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - - // Hide hover styles while a user is dragging blocks/files etc. - .is-dragging-components-draggable & { - box-shadow: none; - } - } - &:focus { - z-index: 1; - } - &.is-dropping-before::before { content: ""; position: absolute; @@ -102,6 +94,36 @@ } } + .block-editor-list-view-block-contents:focus { + box-shadow: none; + + &::after { + content: ""; + position: absolute; + top: 0; + right: -(24px + 5px); // Icon size + padding. + bottom: 0; + left: 0; + border-radius: inherit; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + z-index: 1; + + // Hide focus styles while a user is dragging blocks/files etc. + .is-dragging-components-draggable & { + box-shadow: none; + } + } + } + .block-editor-list-view-block__menu:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + z-index: 1; + + // Hide focus styles while a user is dragging blocks/files etc. + .is-dragging-components-draggable & { + box-shadow: none; + } + } + &.is-visible .block-editor-list-view-block-contents { opacity: 1; @include edit-post__fade-in-animation; @@ -125,7 +147,7 @@ line-height: 0; width: $button-size; opacity: 0; - vertical-align: top; + vertical-align: middle; @include reduce-motion("transition"); // Show on hover, visible, and show above to keep the hit area size. @@ -146,7 +168,11 @@ } .block-editor-list-view-block__menu-cell { - padding-top: $grid-unit-10; + padding-right: 5px; + + .components-button.has-icon { + height: 24px; + } } .block-editor-list-view-block__mover-cell-alignment-wrapper { @@ -248,7 +274,6 @@ } } -.block-editor-list-view-block-slot__description, .block-editor-list-view-block-select-button__description, .block-editor-list-view-appender__description { display: none; diff --git a/packages/components/src/tree-grid/index.js b/packages/components/src/tree-grid/index.js index 3f99e90e9cb89..29637a9626125 100644 --- a/packages/components/src/tree-grid/index.js +++ b/packages/components/src/tree-grid/index.js @@ -24,7 +24,9 @@ import RovingTabIndexContainer from './roving-tab-index'; * @return {?Array} The array of focusables in the row. */ function getRowFocusables( rowElement ) { - const focusablesInRow = focus.focusable.find( rowElement ); + const focusablesInRow = focus.focusable.find( rowElement, { + sequential: true, + } ); if ( ! focusablesInRow || ! focusablesInRow.length ) { return; diff --git a/packages/dom/src/focusable.js b/packages/dom/src/focusable.js index 80e9dcb496831..75e4f72f0c1f7 100644 --- a/packages/dom/src/focusable.js +++ b/packages/dom/src/focusable.js @@ -17,19 +17,32 @@ * - https://w3c.github.io/html/editing.html#data-model */ -const SELECTOR = [ - '[tabindex]', - 'a[href]', - 'button:not([disabled])', - 'input:not([type="hidden"]):not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - 'iframe', - 'object', - 'embed', - 'area[href]', - '[contenteditable]:not([contenteditable=false])', -].join( ',' ); +/** + * Returns a CSS selector used to query for focusable elements. + * + * @param {boolean} sequential If set, only query elements that are sequentially + * focusable. Non-interactive elements with a + * negative `tabindex` are focusable but not + * sequentially focusable. + * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute + * + * @return {string} CSS selector. + */ +function buildSelector( sequential ) { + return [ + sequential ? '[tabindex]:not([tabindex^="-"])' : '[tabindex]', + 'a[href]', + 'button:not([disabled])', + 'input:not([type="hidden"]):not([disabled])', + 'select:not([disabled])', + 'textarea:not([disabled])', + 'iframe:not([tabindex^="-"])', + 'object', + 'embed', + 'area[href]', + '[contenteditable]:not([contenteditable=false])', + ].join( ',' ); +} /** * Returns true if the specified element is visible (i.e. neither display: none @@ -47,21 +60,6 @@ function isVisible( element ) { ); } -/** - * Returns true if the specified element should be skipped from focusable elements. - * For now it rather specific for `iframes` and if tabindex attribute is set to -1. - * - * @param {Element} element DOM element to test. - * - * @return {boolean} Whether element should be skipped from focusable elements. - */ -function skipFocus( element ) { - return ( - element.nodeName.toLowerCase() === 'iframe' && - element.getAttribute( 'tabindex' ) === '-1' - ); -} - /** * Returns true if the specified area element is a valid focusable element, or * false otherwise. Area is only focusable if within a map where a named map @@ -88,18 +86,25 @@ function isValidFocusableArea( element ) { /** * Returns all focusable elements within a given context. * - * @param {Element} context Element in which to search. + * @param {Element} context Element in which to search. + * @param {Object} [options] + * @param {boolean} [options.sequential] If set, only return elements that are + * sequentially focusable. + * Non-interactive elements with a + * negative `tabindex` are focusable but + * not sequentially focusable. + * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute * * @return {Element[]} Focusable elements. */ -export function find( context ) { +export function find( context, { sequential = false } = {} ) { /* eslint-disable jsdoc/no-undefined-types */ /** @type {NodeListOf} */ /* eslint-enable jsdoc/no-undefined-types */ - const elements = context.querySelectorAll( SELECTOR ); + const elements = context.querySelectorAll( buildSelector( sequential ) ); return Array.from( elements ).filter( ( element ) => { - if ( ! isVisible( element ) || skipFocus( element ) ) { + if ( ! isVisible( element ) ) { return false; } diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js index 617a22f9b7632..3f4f6107b3613 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js @@ -65,6 +65,7 @@ export default function ListViewSidebar() { diff --git a/packages/edit-site/src/components/edit-template-part-menu-button/index.js b/packages/edit-site/src/components/edit-template-part-menu-button/index.js index b79a08b8a76d2..8c08a8c1764be 100644 --- a/packages/edit-site/src/components/edit-template-part-menu-button/index.js +++ b/packages/edit-site/src/components/edit-template-part-menu-button/index.js @@ -17,20 +17,39 @@ import { __, sprintf } from '@wordpress/i18n'; import { store as editSiteStore } from '../../store'; export default function EditTemplatePartMenuButton() { - const selectedTemplatePart = useSelect( ( select ) => { - const block = select( blockEditorStore ).getSelectedBlock(); - - if ( block && isTemplatePart( block ) ) { - const { theme, slug } = block.attributes; + return ( + + { ( { selectedClientIds, onClose } ) => ( + + ) } + + ); +} - return select( coreStore ).getEntityRecord( - 'postType', - 'wp_template_part', - // Ideally this should be an official public API. - `${ theme }//${ slug }` +function EditTemplatePartMenuItem( { selectedClientId, onClose } ) { + const selectedTemplatePart = useSelect( + ( select ) => { + const block = select( blockEditorStore ).getBlock( + selectedClientId ); - } - }, [] ); + + if ( block && isTemplatePart( block ) ) { + const { theme, slug } = block.attributes; + + return select( coreStore ).getEntityRecord( + 'postType', + 'wp_template_part', + // Ideally this should be an official public API. + `${ theme }//${ slug }` + ); + } + }, + [ selectedClientId ] + ); + const { pushTemplatePart } = useDispatch( editSiteStore ); if ( ! selectedTemplatePart ) { @@ -38,20 +57,16 @@ export default function EditTemplatePartMenuButton() { } return ( - - { ( { onClose } ) => ( - { - pushTemplatePart( selectedTemplatePart.id ); - onClose(); - } } - > - { - /* translators: %s: template part title */ - sprintf( __( 'Edit %s' ), selectedTemplatePart.slug ) - } - - ) } - + { + pushTemplatePart( selectedTemplatePart.id ); + onClose(); + } } + > + { + /* translators: %s: template part title */ + sprintf( __( 'Edit %s' ), selectedTemplatePart.slug ) + } + ); } diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index 469dabaff5619..bf9641ca0c90b 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -64,6 +64,7 @@ export default function ListViewSidebar() {