diff --git a/packages/block-editor/src/components/block-breadcrumb/index.js b/packages/block-editor/src/components/block-breadcrumb/index.js index b1fd13dbf3475a..70f038181237b4 100644 --- a/packages/block-editor/src/components/block-breadcrumb/index.js +++ b/packages/block-editor/src/components/block-breadcrumb/index.js @@ -12,6 +12,8 @@ import { chevronRightSmall, Icon } from '@wordpress/icons'; import BlockTitle from '../block-title'; import { store as blockEditorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs'; +import getEditorRegion from '../../utils/get-editor-region'; /** * Block breadcrumb component, displaying the hierarchy of the current block selection as a breadcrumb. @@ -37,6 +39,10 @@ function BlockBreadcrumb( { rootLabelText } ) { }, [] ); const rootLabel = rootLabelText || __( 'Document' ); + // We don't care about this specific ref, but this is a way + // to get a ref within the editor canvas so we can focus it later. + const blockRef = useBlockRef( clientId ); + /* * Disable reason: The `list` ARIA role is redundant but * Safari+VoiceOver won't announce the list otherwise. @@ -60,7 +66,16 @@ function BlockBreadcrumb( { rootLabelText } ) { diff --git a/packages/block-editor/src/components/block-patterns-paging/style.scss b/packages/block-editor/src/components/block-patterns-paging/style.scss index ce57f96cd327a5..383d4d72a8e38a 100644 --- a/packages/block-editor/src/components/block-patterns-paging/style.scss +++ b/packages/block-editor/src/components/block-patterns-paging/style.scss @@ -42,4 +42,22 @@ } } } + + @media screen and (min-width: $break-large) { + .block-editor-patterns__grid-pagination { + flex-direction: row; + .block-editor-patterns__grid-pagination-previous, + .block-editor-patterns__grid-pagination-next { + flex-direction: row; + } + } + } +} + +.block-editor-block-patterns-list .block-editor-patterns__grid-pagination { + flex-direction: column; + .block-editor-patterns__grid-pagination-previous, + .block-editor-patterns__grid-pagination-next { + flex-direction: column; + } } diff --git a/packages/block-editor/src/components/block-tools/block-selection-button.js b/packages/block-editor/src/components/block-tools/block-selection-button.js index d4ec0f8cf79fb6..805e41c580f950 100644 --- a/packages/block-editor/src/components/block-tools/block-selection-button.js +++ b/packages/block-editor/src/components/block-tools/block-selection-button.js @@ -9,7 +9,7 @@ import clsx from 'clsx'; import { dragHandle, trash } from '@wordpress/icons'; import { Button, Flex, FlexItem, ToolbarButton } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef } from '@wordpress/element'; +import { forwardRef, useEffect } from '@wordpress/element'; import { BACKSPACE, DELETE, @@ -48,10 +48,11 @@ import Shuffle from '../block-toolbar/shuffle'; * * @param {string} props Component props. * @param {string} props.clientId Client ID of block. + * @param {Object} ref Reference to the component. * * @return {Component} The component to be rendered. */ -function BlockSelectionButton( { clientId, rootClientId } ) { +function BlockSelectionButton( { clientId, rootClientId }, ref ) { const selected = useSelect( ( select ) => { const { @@ -125,7 +126,6 @@ function BlockSelectionButton( { clientId, rootClientId } ) { canMove, } = selected; const { setNavigationMode, removeBlock } = useDispatch( blockEditorStore ); - const ref = useRef(); // Focus the breadcrumb in navigation mode. useEffect( () => { @@ -164,11 +164,6 @@ function BlockSelectionButton( { clientId, rootClientId } ) { const isEnter = keyCode === ENTER; const isSpace = keyCode === SPACE; const isShift = event.shiftKey; - if ( isEscape && editorMode === 'navigation' ) { - setNavigationMode( false ); - event.preventDefault(); - return; - } if ( keyCode === BACKSPACE || keyCode === DELETE ) { removeBlock( clientId ); @@ -368,4 +363,4 @@ function BlockSelectionButton( { clientId, rootClientId } ) { ); } -export default BlockSelectionButton; +export default forwardRef( BlockSelectionButton ); diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js index 0ae67e1be0001e..ae03bdb4f51647 100644 --- a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js +++ b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js @@ -3,6 +3,11 @@ */ import clsx from 'clsx'; +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + /** * Internal dependencies */ @@ -11,10 +16,7 @@ import { PrivateBlockPopover } from '../block-popover'; import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; import useSelectedBlockToolProps from './use-selected-block-tool-props'; -export default function BlockToolbarBreadcrumb( { - clientId, - __unstableContentRef, -} ) { +function BlockToolbarBreadcrumb( { clientId, __unstableContentRef }, ref ) { const { capturingClientId, isInsertionPointVisible, @@ -38,9 +40,12 @@ export default function BlockToolbarBreadcrumb( { { ...popoverProps } > ); } + +export default forwardRef( BlockToolbarBreadcrumb ); diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index ad744a81cca623..5cde3cccaf57e5 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -25,6 +25,7 @@ import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; import { useShowBlockTools } from './use-show-block-tools'; import { unlock } from '../../lock-unlock'; +import getEditorRegion from '../../utils/get-editor-region'; function selector( select ) { const { @@ -81,6 +82,7 @@ export default function BlockTools( { } = useShowBlockTools(); const { + clearSelectedBlock, duplicateBlocks, removeBlocks, replaceBlocks, @@ -92,6 +94,8 @@ export default function BlockTools( { expandBlock, } = unlock( useDispatch( blockEditorStore ) ); + const blockSelectionButtonRef = useRef(); + function onKeyDown( event ) { if ( event.defaultPrevented ) { return; @@ -152,6 +156,13 @@ export default function BlockTools( { // block so that focus is directed back to the beginning of the selection. // In effect, to the user this feels like deselecting the multi-selection. selectBlock( clientIds[ 0 ] ); + } else if ( + clientIds.length === 1 && + event.target === blockSelectionButtonRef?.current + ) { + event.preventDefault(); + clearSelectedBlock(); + getEditorRegion( __unstableContentRef.current ).focus(); } } else if ( isMatch( 'core/block-editor/collapse-list-view', event ) ) { // If focus is currently within a text field, such as a rich text block or other editable field, @@ -182,7 +193,6 @@ export default function BlockTools( { } } } - const blockToolbarRef = usePopoverScroll( __unstableContentRef ); const blockToolbarAfterRef = usePopoverScroll( __unstableContentRef ); @@ -213,6 +223,7 @@ export default function BlockTools( { { showBreadcrumb && ( diff --git a/packages/block-editor/src/utils/get-editor-region.js b/packages/block-editor/src/utils/get-editor-region.js new file mode 100644 index 00000000000000..7edc57d1157fb4 --- /dev/null +++ b/packages/block-editor/src/utils/get-editor-region.js @@ -0,0 +1,31 @@ +/** + * Gets the editor region for a given editor canvas element or + * returns the passed element if no region is found + * + * @param { Object } editor The editor canvas element. + * @return { Object } The editor region or given editor element + */ +export default function getEditorRegion( editor ) { + if ( ! editor ) { + return null; + } + + // If there are multiple editors, we need to find the iframe that contains our contentRef to make sure + // we're focusing the region that contains this editor. + const editorCanvas = + Array.from( + document.querySelectorAll( 'iframe[name="editor-canvas"]' ).values() + ).find( ( iframe ) => { + // Find the iframe that contains our contentRef + const iframeDocument = + iframe.contentDocument || iframe.contentWindow.document; + + return iframeDocument === editor.ownerDocument; + } ) ?? editor; + + // The region is provivided by the editor, not the block-editor. + // We should send focus to the region if one is available to reuse the + // same interface for navigating landmarks. If no region is available, + // use the canvas instead. + return editorCanvas?.closest( '[role="region"]' ) ?? editorCanvas; +} diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index cbf32c9eab93ba..728151987c9ef4 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Internal + +- `CustomSelectControlV2`: add root element wrapper. ([#62803](https://github.com/WordPress/gutenberg/pull/62803)) +- `CustomSelectControlV2`: fix popover styles. ([#62821](https://github.com/WordPress/gutenberg/pull/62821)) +- `CustomSelectControlV2`: fix trigger text alignment in RTL languages ([#62869](https://github.com/WordPress/gutenberg/pull/62869)). + ## 28.2.0 (2024-06-26) ### Enhancements diff --git a/packages/components/src/custom-select-control-v2/custom-select.tsx b/packages/components/src/custom-select-control-v2/custom-select.tsx index 414a805eccfb1a..f76c7f67ea77b4 100644 --- a/packages/components/src/custom-select-control-v2/custom-select.tsx +++ b/packages/components/src/custom-select-control-v2/custom-select.tsx @@ -88,11 +88,13 @@ function _CustomSelect( label, size, store, + className, ...restProps } = props; return ( - <> + // Where should `restProps` be forwarded to? +
{ hideLabelFromVision ? ( // TODO: Replace with BaseControl { label } ) : ( @@ -116,7 +118,7 @@ function _CustomSelect( - +
); } diff --git a/packages/components/src/custom-select-control-v2/legacy-component/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/index.tsx index e2b9a8a7471e5f..209483775db9e4 100644 --- a/packages/components/src/custom-select-control-v2/legacy-component/index.tsx +++ b/packages/components/src/custom-select-control-v2/legacy-component/index.tsx @@ -3,6 +3,7 @@ */ // eslint-disable-next-line no-restricted-imports import * as Ariakit from '@ariakit/react'; +import clsx from 'clsx'; /** * Internal dependencies @@ -21,6 +22,7 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) { onChange, size = 'default', value, + className: classNameProp, ...restProps } = props; @@ -122,6 +124,10 @@ function CustomSelectControl( props: LegacyCustomSelectProps ) { } size={ translatedSize } store={ store } + className={ clsx( + 'components-custom-select-control', + classNameProp + ) } { ...restProps } > { children } diff --git a/packages/components/src/custom-select-control-v2/styles.ts b/packages/components/src/custom-select-control-v2/styles.ts index c75a9a79c71c5f..c806bbee794d1a 100644 --- a/packages/components/src/custom-select-control-v2/styles.ts +++ b/packages/components/src/custom-select-control-v2/styles.ts @@ -92,7 +92,7 @@ export const Select = styled( Ariakit.Select, { cursor: pointer; font-family: inherit; font-size: ${ CONFIG.fontSize }; - text-align: left; + text-align: start; width: 100%; &[data-focus-visible] { @@ -105,10 +105,20 @@ export const Select = styled( Ariakit.Select, { } ); export const SelectPopover = styled( Ariakit.SelectPopover )` + display: flex; + flex-direction: column; + + background-color: ${ COLORS.theme.background }; border-radius: 2px; - background: ${ COLORS.theme.background }; border: 1px solid ${ COLORS.theme.foreground }; + /* z-index(".components-popover") */ + z-index: 1000000; + + max-height: min( var( --popover-available-height, 400px ), 400px ); + overflow: auto; + overscroll-behavior: contain; + &[data-focus-visible] { outline: none; // outline will be on the trigger, rather than the popover } diff --git a/packages/components/src/custom-select-control-v2/types.ts b/packages/components/src/custom-select-control-v2/types.ts index 12b41ba54f4a20..3c192cfa56711f 100644 --- a/packages/components/src/custom-select-control-v2/types.ts +++ b/packages/components/src/custom-select-control-v2/types.ts @@ -50,6 +50,10 @@ export type CustomSelectButtonProps = { }; export type _CustomSelectProps = CustomSelectButtonProps & { + /** + * Additional className added to the root wrapper element. + */ + className?: string; /** * The child elements. This should be composed of `CustomSelectItem` components. */ diff --git a/packages/components/src/higher-order/navigate-regions/style.scss b/packages/components/src/higher-order/navigate-regions/style.scss index b3a4a0c1a9d1b5..5c3767e310b8f4 100644 --- a/packages/components/src/higher-order/navigate-regions/style.scss +++ b/packages/components/src/higher-order/navigate-regions/style.scss @@ -1,22 +1,35 @@ // Allow the position to be easily overridden to e.g. fixed. + +@mixin region-selection-outline { + outline: 4px solid $components-color-accent; + outline-offset: -4px; +} + +@mixin region-selection-focus { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + content: ""; + pointer-events: none; + @include region-selection-outline; + z-index: z-index(".is-focusing-regions {region} :focus::after"); +} + [role="region"] { position: relative; + + // Handles the focus when we programatically send focus to this region + &.interface-interface-skeleton__content:focus-visible::after { + @include region-selection-focus; + } } .is-focusing-regions { [role="region"]:focus::after { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - content: ""; - pointer-events: none; - outline: 4px solid $components-color-accent; - outline-offset: -4px; - z-index: z-index(".is-focusing-regions {region} :focus::after"); + @include region-selection-focus; } - // Fixes for edge cases. // Some of the regions are currently used for layout purposes as 'interface skeleton' // items. When they're absolutely positioned or when they contain absolutely @@ -33,7 +46,6 @@ .interface-interface-skeleton__actions .editor-layout__toggle-publish-panel, .interface-interface-skeleton__actions .editor-layout__toggle-entities-saved-states-panel, .editor-post-publish-panel { - outline: 4px solid $components-color-accent; - outline-offset: -4px; + @include region-selection-outline; } } diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 5844e0c9133369..20190623a3da81 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -759,7 +759,7 @@ padding: 0 $grid-unit-15; height: $grid-unit-40; background: $gray-100; - color: $gray-700; + color: $gray-800; position: relative; display: flex; align-items: center; diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 8c1819b3a7c674..b01394c7f846a1 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -216,7 +216,7 @@ interface ViewBase { /** * The hidden fields. */ - hiddenFields: string[]; + hiddenFields?: string[]; } export interface ViewTable extends ViewBase { diff --git a/packages/dataviews/src/view-grid.tsx b/packages/dataviews/src/view-grid.tsx index 77ac3c92738523..4538ab145d2134 100644 --- a/packages/dataviews/src/view-grid.tsx +++ b/packages/dataviews/src/view-grid.tsx @@ -206,7 +206,7 @@ export default function ViewGrid< Item extends AnyItem >( { const { visibleFields, badgeFields } = fields.reduce( ( accumulator: Record< string, NormalizedField< Item >[] >, field ) => { if ( - view.hiddenFields.includes( field.id ) || + view.hiddenFields?.includes( field.id ) || [ view.layout.mediaField, view.layout.primaryField ].includes( field.id ) diff --git a/packages/dataviews/src/view-list.tsx b/packages/dataviews/src/view-list.tsx index 295c3d28856ebe..eb2b9c6c077a6a 100644 --- a/packages/dataviews/src/view-list.tsx +++ b/packages/dataviews/src/view-list.tsx @@ -329,7 +329,7 @@ export default function ViewList< Item extends AnyItem >( ); const visibleFields = fields.filter( ( field ) => - ! view.hiddenFields.includes( field.id ) && + ! view.hiddenFields?.includes( field.id ) && ! [ view.layout.primaryField, view.layout.mediaField ].includes( field.id ) diff --git a/packages/dataviews/src/view-table.tsx b/packages/dataviews/src/view-table.tsx index 2ddb09e2640a01..66e59a8ebb4230 100644 --- a/packages/dataviews/src/view-table.tsx +++ b/packages/dataviews/src/view-table.tsx @@ -223,9 +223,9 @@ const _HeaderMenu = forwardRef( function HeaderMenu< Item extends AnyItem >( onHide( field ); onChangeView( { ...view, - hiddenFields: view.hiddenFields.concat( - field.id - ), + hiddenFields: ( + view.hiddenFields ?? [] + ).concat( field.id ), } ); } } > @@ -473,7 +473,7 @@ function ViewTable< Item extends AnyItem >( { }; const visibleFields = fields.filter( ( field ) => - ! view.hiddenFields.includes( field.id ) && + ! view.hiddenFields?.includes( field.id ) && ! [ view.layout.mediaField ].includes( field.id ) ); const hasData = !! data?.length; diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 7080ff8f185f4f..0a6a4469cef4e7 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -76,7 +76,6 @@ const DEFAULT_VIEW = { search: '', page: 1, perPage: 20, - hiddenFields: [], layout: { ...defaultConfigPerViewType[ LAYOUT_GRID ], }, diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index edf67bb1da9244..0783478126a1eb 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -223,7 +223,11 @@ const trashPostAction = { } else if ( items[ 0 ].type === 'page' ) { successMessage = sprintf( /* translators: The number of items. */ - __( '%s items moved to trash.' ), + _n( + '%s item moved to trash.', + '%s items moved to trash.', + items.length + ), items.length ); } else { diff --git a/packages/editor/src/components/post-featured-image/style.scss b/packages/editor/src/components/post-featured-image/style.scss index 052f6943012595..d3a4fbbcaef689 100644 --- a/packages/editor/src/components/post-featured-image/style.scss +++ b/packages/editor/src/components/post-featured-image/style.scss @@ -20,6 +20,21 @@ opacity: 1; } } + + .components-drop-zone__content { + border-radius: $radius-block-ui; + } + + // Align text and icons horizontally to avoid clipping when the featured image is not set. + &:has(.editor-post-featured-image__toggle) .components-drop-zone .components-drop-zone__content-inner { + display: flex; + align-items: center; + gap: $grid-unit-10; + + .components-drop-zone__content-icon { + margin: 0; + } + } } .editor-post-featured-image__toggle, diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js index 1af46a80896f07..bd1552ad4cb66a 100644 --- a/test/e2e/specs/editor/various/writing-flow.spec.js +++ b/test/e2e/specs/editor/various/writing-flow.spec.js @@ -958,7 +958,7 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { ` ); } ); - test( 'escape should toggle between edit and navigation modes', async ( { + test( 'escape should set select mode and then focus the canvas', async ( { page, writingFlowUtils, } ) => { @@ -975,15 +975,13 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { .poll( writingFlowUtils.getActiveBlockName ) .toBe( 'core/paragraph' ); - // Second escape Toggles back to Edit Mode + // Second escape should send focus to the canvas await page.keyboard.press( 'Escape' ); + // The navigation button should be hidden. await expect( navigationButton ).toBeHidden(); - const blockToolbar = page.getByLabel( 'Block tools' ); - - await expect( blockToolbar ).toBeVisible(); - await expect - .poll( writingFlowUtils.getActiveBlockName ) - .toBe( 'core/paragraph' ); + await expect( + page.getByRole( 'region', { name: 'Editor content' } ) + ).toBeFocused(); } ); // Checks for regressions of https://github.com/WordPress/gutenberg/issues/40091.