diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 25de5483f5192..6b9de943ea0bf 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -38,6 +38,8 @@ function ListViewBlockSelectButton( className, block: { clientId }, onClick, + onContextMenu, + onMouseDown, onToggleExpanded, tabIndex, onFocus, @@ -237,7 +239,9 @@ function ListViewBlockSelectButton( className ) } onClick={ onClick } + onContextMenu={ onContextMenu } onKeyDown={ onKeyDownHandler } + onMouseDown={ onMouseDown } ref={ ref } tabIndex={ tabIndex } onFocus={ onFocus } diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 4957f79fa0d48..a90bf116e1d08 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -13,7 +13,13 @@ import { } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { moreVertical } from '@wordpress/icons'; -import { useState, useRef, useCallback, memo } from '@wordpress/element'; +import { + useCallback, + useMemo, + useState, + useRef, + memo, +} from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { sprintf, __ } from '@wordpress/i18n'; import { ESCAPE } from '@wordpress/keycodes'; @@ -53,7 +59,9 @@ function ListViewBlock( { } ) { const cellRef = useRef( null ); const rowRef = useRef( null ); + const settingsRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); + const [ settingsAnchorRect, setSettingsAnchorRect ] = useState(); const { isLocked, canEdit } = useBlockLock( clientId ); @@ -82,6 +90,11 @@ function ListViewBlock( { }, [ clientId ] ); + const allowRightClickOverrides = useSelect( + ( select ) => + select( blockEditorStore ).getSettings().allowRightClickOverrides, + [] + ); const showBlockActions = // When a block hides its toolbar it also hides the block settings menu, @@ -190,6 +203,56 @@ function ListViewBlock( { [ clientId, expand, collapse, isExpanded ] ); + // Allow right-clicking an item in the List View to open up the block settings dropdown. + const onContextMenu = useCallback( + ( event ) => { + if ( showBlockActions && allowRightClickOverrides ) { + settingsRef.current?.click(); + // Ensure the position of the settings dropdown is at the cursor. + setSettingsAnchorRect( + new window.DOMRect( event.clientX, event.clientY, 0, 0 ) + ); + event.preventDefault(); + } + }, + [ allowRightClickOverrides, settingsRef, showBlockActions ] + ); + + const onMouseDown = useCallback( + ( event ) => { + // Prevent right-click from focusing the block, + // because focus will be handled when opening the block settings dropdown. + if ( allowRightClickOverrides && event.button === 2 ) { + event.preventDefault(); + } + }, + [ allowRightClickOverrides ] + ); + + const settingsPopoverAnchor = useMemo( () => { + const { ownerDocument } = rowRef?.current || {}; + + // If no custom position is set, the settings dropdown will be anchored to the + // DropdownMenu toggle button. + if ( ! settingsAnchorRect || ! ownerDocument ) { + return undefined; + } + + // Position the settings dropdown at the cursor when right-clicking a block. + return { + ownerDocument, + getBoundingClientRect() { + return settingsAnchorRect; + }, + }; + }, [ settingsAnchorRect ] ); + + const clearSettingsAnchorRect = useCallback( () => { + // Clear the custom position for the settings dropdown so that it is restored back + // to being anchored to the DropdownMenu toggle button. + setSettingsAnchorRect( undefined ); + }, [ setSettingsAnchorRect ] ); + let colSpan; if ( hasRenderedMovers ) { colSpan = 2; @@ -257,6 +320,8 @@ function ListViewBlock( { { ( { ref, tabIndex, onFocus } ) => ( ) } + + ), }, diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 82014ad06eb49..9dcdb2ce99b5b 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -52,6 +52,7 @@ export function initializeEditor( id, settings ) { // We dispatch actions and update the store synchronously before rendering // so that we won't trigger unnecessary re-renders with useEffect. dispatch( preferencesStore ).setDefaults( 'core/edit-site', { + allowRightClickOverrides: true, editorMode: 'visual', fixedToolbar: false, focusMode: false, diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index de5d9cf43437d..34aa472a9921d 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -29,6 +29,7 @@ const BLOCK_EDITOR_SETTINGS = [ '__unstableGalleryWithImageBlocks', 'alignWide', 'allowedBlockTypes', + 'allowRightClickOverrides', 'blockInspectorTabs', 'allowedMimeTypes', 'bodyPlaceholder', diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js index 05c4ea3b8e97e..3ec7318ab89e7 100644 --- a/test/e2e/specs/editor/various/a11y.spec.js +++ b/test/e2e/specs/editor/various/a11y.spec.js @@ -124,6 +124,13 @@ test.describe( 'a11y (@firefox, @webkit)', () => { page, pageUtils, } ) => { + // Note: this test depends on a particular viewport height to determine whether or not + // the modal content is scrollable. If this tests fails and needs to be debugged locally, + // double-check the viewport height when running locally versus in CI. Additionally, + // when adding or removing items from the preference menu, this test may need to be updated + // if the height of panels has changed. It would be good to find a more robust way to test + // this behavior. + // Open the top bar Options menu. await page.click( 'role=region[name="Editor top bar"i] >> role=button[name="Options"i]' @@ -145,6 +152,9 @@ test.describe( 'a11y (@firefox, @webkit)', () => { const generalTab = preferencesModal.locator( 'role=tab[name="General"i]' ); + const accessibilityTab = preferencesModal.locator( + 'role=tab[name="Accessibility"i]' + ); const blocksTab = preferencesModal.locator( 'role=tab[name="Blocks"i]' ); @@ -165,9 +175,20 @@ test.describe( 'a11y (@firefox, @webkit)', () => { await tab.focus(); } - // The General tab panel content is short and not scrollable. - // Check it's not focusable. + // The Accessibility tab is currently short and not scrollable. + // Check that it cannot be focused by tabbing. Note: this test depends + // on a particular viewport height to determine whether or not the + // modal content is scrollable. If additional Accessibility options are + // added, then eventually this test will fail. + // TODO: find a more robust way to test this behavior. await clickAndFocusTab( generalTab ); + // Navigate down to the Accessibility tab. + await pageUtils.pressKeys( 'ArrowDown', { times: 2 } ); + // Check the Accessibility tab panel is visible. + await expect( + preferencesModal.locator( 'role=tabpanel[name="Accessibility"i]' ) + ).toBeVisible(); + await expect( accessibilityTab ).toBeFocused(); await pageUtils.pressKeys( 'Shift+Tab' ); await expect( closeButton ).toBeFocused(); await pageUtils.pressKeys( 'Shift+Tab' );