From e63ef38ef7585d33e4b67810bb368dbae04ee992 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:28:19 +0200 Subject: [PATCH 01/38] Revert format types hook refactor (#56859) --- .../components/rich-text/use-format-types.js | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/use-format-types.js b/packages/block-editor/src/components/rich-text/use-format-types.js index e30880422634c..9d26d61943249 100644 --- a/packages/block-editor/src/components/rich-text/use-format-types.js +++ b/packages/block-editor/src/components/rich-text/use-format-types.js @@ -1,9 +1,14 @@ /** * WordPress dependencies */ +import { useMemo } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as richTextStore } from '@wordpress/rich-text'; +function formatTypesSelector( select ) { + return select( richTextStore ).getFormatTypes(); +} + /** * Set of all interactive content tags. * @@ -59,50 +64,45 @@ export function useFormatTypes( { withoutInteractiveFormatting, allowedFormats, } ) { - const { formatTypes, ...keyedSelected } = useSelect( - ( select ) => { - const _formatTypes = select( richTextStore ) - .getFormatTypes() - .filter( ( { name, interactive, tagName } ) => { - if ( allowedFormats && ! allowedFormats.includes( name ) ) { - return false; - } + const allFormatTypes = useSelect( formatTypesSelector, [] ); + const formatTypes = useMemo( () => { + return allFormatTypes.filter( ( { name, interactive, tagName } ) => { + if ( allowedFormats && ! allowedFormats.includes( name ) ) { + return false; + } - if ( - withoutInteractiveFormatting && - ( interactive || interactiveContentTags.has( tagName ) ) - ) { - return false; - } + if ( + withoutInteractiveFormatting && + ( interactive || interactiveContentTags.has( tagName ) ) + ) { + return false; + } - return true; - } ); - return _formatTypes.reduce( - ( accumulator, type ) => { - if ( - ! type.__experimentalGetPropsForEditableTreePreparation - ) { - return accumulator; - } + return true; + } ); + }, [ allFormatTypes, allowedFormats, withoutInteractiveFormatting ] ); + const keyedSelected = useSelect( + ( select ) => + formatTypes.reduce( ( accumulator, type ) => { + if ( ! type.__experimentalGetPropsForEditableTreePreparation ) { + return accumulator; + } - return { - ...accumulator, - ...prefixSelectKeys( - type.__experimentalGetPropsForEditableTreePreparation( - select, - { - richTextIdentifier: identifier, - blockClientId: clientId, - } - ), - type.name + return { + ...accumulator, + ...prefixSelectKeys( + type.__experimentalGetPropsForEditableTreePreparation( + select, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } ), - }; - }, - { formatTypes: _formatTypes } - ); - }, - [ clientId, identifier, allowedFormats, withoutInteractiveFormatting ] + type.name + ), + }; + }, {} ), + [ formatTypes, clientId, identifier ] ); const dispatch = useDispatch(); const prepareHandlers = []; From 5067361dc000cb3ab2b0a180b6a4eae19cd7e7a2 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 7 Dec 2023 12:42:36 +0000 Subject: [PATCH 02/38] Adding `aria-sort` to table view headers (#56860) Ensures sorting semantics are not just communicated visually --- packages/dataviews/src/view-table.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index dece8a55107c0..0e51bd85cdee4 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -545,6 +545,9 @@ function ViewTable( { // TODO:Add spinner or progress bar.. return

{ __( 'Loading' ) }

; } + + const sortValues = { asc: 'ascending', desc: 'descending' }; + return (
{ hasRows && ( @@ -568,6 +571,11 @@ function ViewTable( { .maxWidth || undefined, } } data-field-id={ header.id } + aria-sort={ + sortValues[ + header.column.getIsSorted() + ] + } > Date: Thu, 7 Dec 2023 13:04:56 +0000 Subject: [PATCH 03/38] Update data view layout (#56786) Co-authored-by: ntsekouras --- packages/dataviews/src/dataviews.js | 7 ++- packages/dataviews/src/style.scss | 46 ++++++++++++++++--- packages/dataviews/src/view-table.js | 13 +++++- .../src/components/page-pages/style.scss | 4 +- .../edit-site/src/components/page/header.js | 3 +- .../edit-site/src/components/page/style.scss | 4 +- 6 files changed, 62 insertions(+), 15 deletions(-) diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 13397242953b7..9e7b45d04ef87 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -49,8 +49,11 @@ export default function DataViews( { }, [ fields ] ); return (
- - + + { search && ( div { @@ -10,13 +9,18 @@ } } +.dataviews__filters-view-actions { + padding: $grid-unit-15 $grid-unit-40; +} + .dataviews-pagination { margin-top: auto; position: sticky; bottom: 0; background-color: $white; - padding: $grid-unit-20 0; - border-top: $border-width solid $gray-200; + padding: $grid-unit-15 $grid-unit-40; + border-top: $border-width solid $gray-100; + color: $gray-700; } .dataviews-filters-options { @@ -29,9 +33,12 @@ border-color: inherit; border-collapse: collapse; position: relative; + color: $gray-700; a { text-decoration: none; + color: $gray-900; + font-weight: 500; } th { text-align: left; @@ -42,6 +49,7 @@ td, th { padding: $grid-unit-15; + min-width: 160px; &[data-field-id="actions"] { text-align: right; } @@ -49,6 +57,16 @@ tr { border-bottom: 1px solid $gray-100; + td:first-child, + th:first-child { + padding-left: $grid-unit-40; + } + + td:last-child, + th:last-child { + padding-right: $grid-unit-40; + } + &:last-child { border-bottom: 0; } @@ -59,9 +77,12 @@ } th { position: sticky; - top: - #{$grid-unit-40}; // Offset the container padding - background-color: $white; - box-shadow: inset 0 -#{$border-width} 0 $gray-200; + top: -1px; + background-color: lighten($gray-100, 4%); + box-shadow: inset 0 -#{$border-width} 0 $gray-100; + border-top: 1px solid $gray-100; + padding-top: $grid-unit-05; + padding-bottom: $grid-unit-05; } } } @@ -69,6 +90,7 @@ .dataviews-grid-view { margin-bottom: $grid-unit-30; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + padding: 0 $grid-unit-40; @include break-xlarge() { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; // Todo: eliminate !important dependency @@ -128,9 +150,14 @@ } .dataviews-list-view { + margin: 0; + li { border-bottom: $border-width solid $gray-100; margin: 0; + &:first-child { + border-top: $border-width solid $gray-100; + } &:last-child { border-bottom: 0; } @@ -210,3 +237,8 @@ .dataviews-action-modal { z-index: z-index(".dataviews-action-modal"); } + +.dataviews-no-results, +.dataviews-loading { + padding: 0 $grid-unit-40; +} diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 0e51bd85cdee4..209b9e443dc2a 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -122,6 +122,7 @@ function HeaderMenu( { dataView, header } ) { iconPosition="right" text={ text } style={ { padding: 0 } } + size="compact" /> } > @@ -543,7 +544,11 @@ function ViewTable( { const hasRows = !! rows?.length; if ( isLoading ) { // TODO:Add spinner or progress bar.. - return

{ __( 'Loading' ) }

; + return ( +
+

{ __( 'Loading' ) }

+
+ ); } const sortValues = { asc: 'ascending', desc: 'descending' }; @@ -615,7 +620,11 @@ function ViewTable( { ) } - { ! hasRows &&

{ __( 'no results' ) }

} + { ! hasRows && ( +
+

{ __( 'No results' ) }

+
+ ) }
); } diff --git a/packages/edit-site/src/components/page-pages/style.scss b/packages/edit-site/src/components/page-pages/style.scss index fde960ca1a72c..933fdadb8d070 100644 --- a/packages/edit-site/src/components/page-pages/style.scss +++ b/packages/edit-site/src/components/page-pages/style.scss @@ -1,3 +1,5 @@ .edit-site-page-pages__featured-image { - border-radius: $radius-block-ui; + border-radius: $grid-unit-05; + width: $grid-unit-40; + height: $grid-unit-40; } diff --git a/packages/edit-site/src/components/page/header.js b/packages/edit-site/src/components/page/header.js index 06de80c25685b..274fd395a16f1 100644 --- a/packages/edit-site/src/components/page/header.js +++ b/packages/edit-site/src/components/page/header.js @@ -19,7 +19,8 @@ export default function Header( { title, subTitle, actions } ) { { title } diff --git a/packages/edit-site/src/components/page/style.scss b/packages/edit-site/src/components/page/style.scss index 8da7df8e0385b..72ecbb4e2b7d7 100644 --- a/packages/edit-site/src/components/page/style.scss +++ b/packages/edit-site/src/components/page/style.scss @@ -12,8 +12,8 @@ } .edit-site-page-header { - padding: 0 $grid-unit-40; - min-height: $header-height; + padding: $grid-unit-20 $grid-unit-40; + min-height: $grid-unit * 9; border-bottom: 1px solid $gray-100; background: $white; position: sticky; From c97e586e1025a68b026552e8b2fff7ed476351f3 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 7 Dec 2023 15:32:29 +0100 Subject: [PATCH 04/38] Editor: Move the device type state to the editor package (#56866) --- .../reference-guides/data/data-core-editor.md | 24 +++++++++++++++++++ .../src/components/preview-options/README.md | 2 +- .../components/use-resize-canvas/README.md | 4 ++-- .../src/components/device-preview/index.js | 8 +++---- .../src/components/visual-editor/index.js | 8 +++---- packages/edit-post/src/editor.native.js | 13 ++++------ packages/edit-post/src/store/actions.js | 21 +++++++++++----- packages/edit-post/src/store/reducer.js | 18 -------------- packages/edit-post/src/store/selectors.js | 18 +++++++++++--- .../components/block-editor/editor-canvas.js | 16 +++++++------ .../header-edit-mode/document-tools/index.js | 22 +++++++---------- .../src/components/header-edit-mode/index.js | 20 ++++++---------- .../src/components/site-hub/index.js | 9 ++++--- packages/edit-site/src/store/actions.js | 20 ++++++++++++---- packages/edit-site/src/store/reducer.js | 18 -------------- packages/edit-site/src/store/selectors.js | 18 +++++++++++--- packages/editor/src/store/actions.js | 14 +++++++++++ packages/editor/src/store/reducer.js | 18 ++++++++++++++ packages/editor/src/store/selectors.js | 11 +++++++++ 19 files changed, 171 insertions(+), 111 deletions(-) diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index fae5b8a78e2cf..7b5a6dbeeb909 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -268,6 +268,18 @@ _Returns_ - `string?`: Template ID. +### getDeviceType + +Returns the current editing canvas device type. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `string`: Device type. + ### getEditedPostAttribute Returns a single attribute of the post being edited, preferring the unsaved edit if one exists, but falling back to the attribute for the last known saved state of the post. @@ -1261,6 +1273,18 @@ _Related_ - selectBlock in core/block-editor store. +### setDeviceType + +Action that changes the width of the editing canvas. + +_Parameters_ + +- _deviceType_ `string`: + +_Returns_ + +- `Object`: Action object. + ### setRenderingMode Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: diff --git a/packages/block-editor/src/components/preview-options/README.md b/packages/block-editor/src/components/preview-options/README.md index baf886c71bd65..80182f18d243d 100644 --- a/packages/block-editor/src/components/preview-options/README.md +++ b/packages/block-editor/src/components/preview-options/README.md @@ -27,7 +27,7 @@ const MyPreviewOptions = () => ( isEnabled={ true } className="edit-post-post-preview-dropdown" deviceType={ deviceType } - setDeviceType={ setPreviewDeviceType } + setDeviceType={ setDeviceType } > { ( { onClose } ) => (
diff --git a/packages/block-editor/src/components/use-resize-canvas/README.md b/packages/block-editor/src/components/use-resize-canvas/README.md index 18d28df7b3e3a..51e583f8def47 100644 --- a/packages/block-editor/src/components/use-resize-canvas/README.md +++ b/packages/block-editor/src/components/use-resize-canvas/README.md @@ -14,14 +14,14 @@ Note that this is currently experimental, and is available as `__experimentalUse ### Usage -The hook returns a style object which can be applied to a container. It is passed the current device type, which can be obtained from `__experimentalGetPreviewDeviceType`. +The hook returns a style object which can be applied to a container. It is passed the current device type, which can be obtained from `getDeviceType`. ```jsx import { __experimentalUseResizeCanvas as useResizeCanvas } from '@wordpress/block-editor'; function ResizedContainer() { const deviceType = useSelect( ( select ) => { - return select( 'core/edit-post' ).__experimentalGetPreviewDeviceType(); + return select( 'core/editor' ).getDeviceType(); }, [] ); const inlineStyles = useResizeCanvas( deviceType ); diff --git a/packages/edit-post/src/components/device-preview/index.js b/packages/edit-post/src/components/device-preview/index.js index a10688d185023..9fc95b943609d 100644 --- a/packages/edit-post/src/components/device-preview/index.js +++ b/packages/edit-post/src/components/device-preview/index.js @@ -30,21 +30,19 @@ export default function DevicePreview() { hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), isPostSaveable: select( editorStore ).isEditedPostSaveable(), isViewable: postType?.viewable ?? false, - deviceType: - select( editPostStore ).__experimentalGetPreviewDeviceType(), + deviceType: select( editorStore ).getDeviceType(), showIconLabels: select( editPostStore ).isFeatureActive( 'showIconLabels' ), }; }, [] ); - const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = - useDispatch( editPostStore ); + const { setDeviceType } = useDispatch( editorStore ); return ( diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 9b975414510c5..5d309947a37c2 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -37,14 +37,14 @@ export default function VisualEditor( { styles } ) { isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { - const { isFeatureActive, __experimentalGetPreviewDeviceType } = - select( editPostStore ); - const { getEditorSettings, getRenderingMode } = select( editorStore ); + const { isFeatureActive } = select( editPostStore ); + const { getEditorSettings, getRenderingMode, getDeviceType } = + select( editorStore ); const { getBlockTypes } = select( blocksStore ); const editorSettings = getEditorSettings(); return { - deviceType: __experimentalGetPreviewDeviceType(), + deviceType: getDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), renderingMode: getRenderingMode(), isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index b031601186c72..49b6022d6c05b 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -9,7 +9,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { EditorProvider } from '@wordpress/editor'; +import { EditorProvider, store as editorStore } from '@wordpress/editor'; import { parse, serialize, store as blocksStore } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -192,18 +192,15 @@ class Editor extends Component { export default compose( [ withSelect( ( select ) => { - const { - isFeatureActive, - getEditorMode, - __experimentalGetPreviewDeviceType, - getHiddenBlockTypes, - } = select( editPostStore ); + const { isFeatureActive, getEditorMode, getHiddenBlockTypes } = + select( editPostStore ); const { getBlockTypes } = select( blocksStore ); + const { getDeviceType } = select( editorStore ); return { hasFixedToolbar: isFeatureActive( 'fixedToolbar' ) || - __experimentalGetPreviewDeviceType() !== 'Desktop', + getDeviceType() !== 'Desktop', focusMode: isFeatureActive( 'focusMode' ), mode: getEditorMode(), hiddenBlockTypes: getHiddenBlockTypes(), diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 2659b7ad33398..eae1030fad024 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -456,18 +456,27 @@ export function metaBoxUpdatesFailure() { } /** - * Returns an action object used to toggle the width of the editing canvas. + * Action that changes the width of the editing canvas. + * + * @deprecated * * @param {string} deviceType * * @return {Object} Action object. */ -export function __experimentalSetPreviewDeviceType( deviceType ) { - return { - type: 'SET_PREVIEW_DEVICE_TYPE', - deviceType, +export const __experimentalSetPreviewDeviceType = + ( deviceType ) => + ( { registry } ) => { + deprecated( + "dispatch( 'core/edit-post' ).__experimentalSetPreviewDeviceType", + { + since: '6.5', + version: '6.7', + hint: 'registry.dispatch( editorStore ).setDeviceType', + } + ); + registry.dispatch( editorStore ).setDeviceType( deviceType ); }; -} /** * Returns an action object used to open/close the inserter. diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js index 4748c4fff4972..1072919d388db 100644 --- a/packages/edit-post/src/store/reducer.js +++ b/packages/edit-post/src/store/reducer.js @@ -98,23 +98,6 @@ export function metaBoxLocations( state = {}, action ) { return state; } -/** - * Reducer returning the editing canvas device type. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function deviceType( state = 'Desktop', action ) { - switch ( action.type ) { - case 'SET_PREVIEW_DEVICE_TYPE': - return action.deviceType; - } - - return state; -} - /** * Reducer to set the block inserter panel open or closed. * @@ -179,7 +162,6 @@ export default combineReducers( { metaBoxes, publishSidebarActive, removedPanels, - deviceType, blockInserterPanel, listViewPanel, } ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index f7b8f91d380dc..115dcd9bcd78e 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -450,13 +450,25 @@ export function isSavingMetaBoxes( state ) { /** * Returns the current editing canvas device type. * + * @deprecated + * * @param {Object} state Global application state. * * @return {string} Device type. */ -export function __experimentalGetPreviewDeviceType( state ) { - return state.deviceType; -} +export const __experimentalGetPreviewDeviceType = createRegistrySelector( + ( select ) => () => { + deprecated( + `select( 'core/edit-site' ).__experimentalGetPreviewDeviceType`, + { + since: '6.5', + version: '6.7', + alternative: `select( 'core/editor' ).getDeviceType`, + } + ); + return select( editorStore ).getDeviceType(); + } +); /** * Returns true if the inserter is opened. diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index fa20a4abae1ad..bf40c655b4477 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -14,7 +14,10 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { useState, useEffect, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { privateApis as editorPrivateApis } from '@wordpress/editor'; +import { + privateApis as editorPrivateApis, + store as editorStore, +} from '@wordpress/editor'; /** * Internal dependencies @@ -45,17 +48,16 @@ function EditorCanvas( { } = useSelect( ( select ) => { const { getBlockCount, __unstableGetEditorMode } = select( blockEditorStore ); - const { - getEditedPostType, - __experimentalGetPreviewDeviceType, - getCanvasMode, - } = unlock( select( editSiteStore ) ); + const { getEditedPostType, getCanvasMode } = unlock( + select( editSiteStore ) + ); + const { getDeviceType } = select( editorStore ); const _templateType = getEditedPostType(); return { templateType: _templateType, isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), - deviceType: __experimentalGetPreviewDeviceType(), + deviceType: getDeviceType(), isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', canvasMode: getCanvasMode(), hasBlocks: !! getBlockCount(), diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index f0231e1e62116..eefc6668b5b95 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -14,6 +14,7 @@ import { _x, __ } from '@wordpress/i18n'; import { listView, plus, chevronUpDown } from '@wordpress/icons'; import { Button, ToolbarItem } from '@wordpress/components'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -39,18 +40,15 @@ export default function DocumentTools( { const inserterButton = useRef(); const { isInserterOpen, isListViewOpen, listViewShortcut, isVisualMode } = useSelect( ( select ) => { - const { - __experimentalGetPreviewDeviceType, - isInserterOpened, - isListViewOpened, - getEditorMode, - } = select( editSiteStore ); + const { isInserterOpened, isListViewOpened, getEditorMode } = + select( editSiteStore ); + const { getDeviceType } = select( editorStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); return { - deviceType: __experimentalGetPreviewDeviceType(), + deviceType: getDeviceType(), isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), listViewShortcut: getShortcutRepresentation( @@ -60,12 +58,10 @@ export default function DocumentTools( { }; }, [] ); - const { - __experimentalSetPreviewDeviceType: setPreviewDeviceType, - setIsInserterOpened, - setIsListViewOpened, - } = useDispatch( editSiteStore ); + const { setIsInserterOpened, setIsListViewOpened } = + useDispatch( editSiteStore ); const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { setDeviceType } = useDispatch( editorStore ); const isLargeViewport = useViewportMatch( 'medium' ); @@ -189,7 +185,7 @@ export default function DocumentTools( { /* translators: button label text should, if possible, be under 16 characters. */ label={ __( 'Zoom-out View' ) } onClick={ () => { - setPreviewDeviceType( 'Desktop' ); + setDeviceType( 'Desktop' ); __unstableSetEditorMode( isZoomedOutView ? 'edit' diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index f8a9d9d4e892b..c6dbe4b6b9144 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -27,7 +27,7 @@ import { VisuallyHidden, } from '@wordpress/components'; import { store as preferencesStore } from '@wordpress/preferences'; -import { DocumentBar } from '@wordpress/editor'; +import { DocumentBar, store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -58,22 +58,18 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { hasFixedToolbar, isZoomOutMode, } = useSelect( ( select ) => { - const { __experimentalGetPreviewDeviceType, getEditedPostType } = - select( editSiteStore ); + const { getEditedPostType } = select( editSiteStore ); const { getBlockSelectionStart, __unstableGetEditorMode } = select( blockEditorStore ); - - const postType = getEditedPostType(); - const { getUnstableBase, // Site index. } = select( coreStore ); - const { get: getPreference } = select( preferencesStore ); + const { getDeviceType } = select( editorStore ); return { - deviceType: __experimentalGetPreviewDeviceType(), - templateType: postType, + deviceType: getDeviceType(), + templateType: getEditedPostType(), blockEditorMode: __unstableGetEditorMode(), blockSelectionStart: getBlockSelectionStart(), homeUrl: getUnstableBase()?.home, @@ -99,9 +95,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { const isLargeViewport = useViewportMatch( 'medium' ); const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; const blockToolbarRef = useRef(); - - const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = - useDispatch( editSiteStore ); + const { setDeviceType } = useDispatch( editorStore ); const disableMotion = useReducedMotion(); const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); @@ -225,7 +219,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { > { const { open: openCommandCenter } = useDispatch( commandsStore ); const disableMotion = useReducedMotion(); - const { - setCanvasMode, - __experimentalSetPreviewDeviceType: setPreviewDeviceType, - } = unlock( useDispatch( editSiteStore ) ); + const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); + const { setDeviceType } = useDispatch( editorStore ); const isBackToDashboardButton = canvasMode === 'view'; const siteIconButtonProps = isBackToDashboardButton ? { @@ -76,7 +75,7 @@ const SiteHub = memo( ( { isTransparent, className } ) => { event.preventDefault(); if ( canvasMode === 'edit' ) { clearSelectedBlock(); - setPreviewDeviceType( 'Desktop' ); + setDeviceType( 'Desktop' ); setCanvasMode( 'view' ); } }, diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 2dd7aacd38401..6397a31af120b 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -10,6 +10,7 @@ import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as editorStore } from '@wordpress/editor'; import { speak } from '@wordpress/a11y'; import { store as preferencesStore } from '@wordpress/preferences'; import { decodeEntities } from '@wordpress/html-entities'; @@ -49,16 +50,25 @@ export function toggleFeature( featureName ) { /** * Action that changes the width of the editing canvas. * + * @deprecated + * * @param {string} deviceType * * @return {Object} Action object. */ -export function __experimentalSetPreviewDeviceType( deviceType ) { - return { - type: 'SET_PREVIEW_DEVICE_TYPE', - deviceType, +export const __experimentalSetPreviewDeviceType = + ( deviceType ) => + ( { registry } ) => { + deprecated( + "dispatch( 'core/edit-site' ).__experimentalSetPreviewDeviceType", + { + since: '6.5', + version: '6.7', + hint: 'registry.dispatch( editorStore ).setDeviceType', + } + ); + registry.dispatch( editorStore ).setDeviceType( deviceType ); }; -} /** * Action that sets a template, optionally fetching it from REST API. diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index a46d215f90507..b55acbffd626e 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -3,23 +3,6 @@ */ import { combineReducers } from '@wordpress/data'; -/** - * Reducer returning the editing canvas device type. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function deviceType( state = 'Desktop', action ) { - switch ( action.type ) { - case 'SET_PREVIEW_DEVICE_TYPE': - return action.deviceType; - } - - return state; -} - /** * Reducer returning the settings. * @@ -158,7 +141,6 @@ function editorCanvasContainerView( state = undefined, action ) { } export default combineReducers( { - deviceType, settings, editedPost, blockInserterPanel, diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index 9d00e141270c4..ebaee12dfdc5e 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -44,13 +44,25 @@ export const isFeatureActive = createRegistrySelector( /** * Returns the current editing canvas device type. * + * @deprecated + * * @param {Object} state Global application state. * * @return {string} Device type. */ -export function __experimentalGetPreviewDeviceType( state ) { - return state.deviceType; -} +export const __experimentalGetPreviewDeviceType = createRegistrySelector( + ( select ) => () => { + deprecated( + `select( 'core/edit-site' ).__experimentalGetPreviewDeviceType`, + { + since: '6.5', + version: '6.7', + alternative: `select( 'core/editor' ).getDeviceType`, + } + ); + return select( editorStore ).getDeviceType(); + } +); /** * Returns whether the current user can create media or not. diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 4c1170b064202..5d8335382b5db 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -573,6 +573,20 @@ export const setRenderingMode = } ); }; +/** + * Action that changes the width of the editing canvas. + * + * @param {string} deviceType + * + * @return {Object} Action object. + */ +export function setDeviceType( deviceType ) { + return { + type: 'SET_DEVICE_TYPE', + deviceType, + }; +} + /** * Backward compatibility */ diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 7821baf5cdc06..23fa84edba032 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -297,6 +297,23 @@ export function renderingMode( state = 'all', action ) { return state; } +/** + * Reducer returning the editing canvas device type. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function deviceType( state = 'Desktop', action ) { + switch ( action.type ) { + case 'SET_DEVICE_TYPE': + return action.deviceType; + } + + return state; +} + export default combineReducers( { postId, postType, @@ -310,4 +327,5 @@ export default combineReducers( { editorSettings, postAutosavingLock, renderingMode, + deviceType, } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index c47a80e96735f..0b82bb797584e 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1210,6 +1210,17 @@ export function getRenderingMode( state ) { return state.renderingMode; } +/** + * Returns the current editing canvas device type. + * + * @param {Object} state Global application state. + * + * @return {string} Device type. + */ +export function getDeviceType( state ) { + return state.deviceType; +} + /* * Backward compatibility */ From be7d5189651d9b966e925f98277a46190943b059 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Thu, 7 Dec 2023 15:42:31 +0000 Subject: [PATCH 05/38] Update changelog files --- packages/create-block-interactive-template/CHANGELOG.md | 2 ++ packages/create-block-interactive-template/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index fbe3a8c8c857c..72ed6677e0b4e 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.10.1 (2023-12-07) + - Update template to use modules instead of scripts. [#56694](https://github.com/WordPress/gutenberg/pull/56694) ## 1.10.0 (2023-11-29) diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 48723b3b756d1..7da853a532852 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.10.0", + "version": "1.10.1-prerelease", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From eae6a91b16363f0db48938c54bf1555ff7dbe40c Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Thu, 7 Dec 2023 15:44:15 +0000 Subject: [PATCH 06/38] chore(release): publish - @wordpress/block-directory@4.24.1 - @wordpress/block-library@8.24.1 - @wordpress/create-block-interactive-template@1.10.1 - @wordpress/customize-widgets@4.24.1 - @wordpress/e2e-tests@7.18.1 - @wordpress/edit-post@7.24.1 - @wordpress/edit-site@5.24.1 - @wordpress/edit-widgets@5.24.1 - @wordpress/editor@13.24.1 - @wordpress/interactivity@3.0.1 --- package-lock.json | 18 +++++++++--------- packages/block-directory/package.json | 2 +- packages/block-library/package.json | 2 +- .../package.json | 2 +- packages/customize-widgets/package.json | 2 +- packages/e2e-tests/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/edit-site/package.json | 2 +- packages/edit-widgets/package.json | 2 +- packages/editor/package.json | 2 +- packages/interactivity/package.json | 2 +- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b27980b6b40f..e59b7b849ebd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54440,7 +54440,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.24.0", + "version": "4.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54572,7 +54572,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.24.0", + "version": "8.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54973,7 +54973,7 @@ }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.24.0", + "version": "4.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55208,7 +55208,7 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "7.18.0", + "version": "7.18.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55247,7 +55247,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.24.0", + "version": "7.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55295,7 +55295,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.24.0", + "version": "5.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55360,7 +55360,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.24.0", + "version": "5.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55402,7 +55402,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.24.0", + "version": "13.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55739,7 +55739,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "3.0.0", + "version": "3.0.1", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.1.3", diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 147f58dd719e1..04bcd1c50d8b3 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.24.0", + "version": "4.24.1", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index f989e586ec7b8..bcb7a843b3232 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.24.0", + "version": "8.24.1", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 7da853a532852..3bc6b1f646c26 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.10.1-prerelease", + "version": "1.10.1", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 65a64d7826a7f..14aff02afb016 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.24.0", + "version": "4.24.1", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 7327336b19b9b..103daf0498b53 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "7.18.0", + "version": "7.18.1", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 0bc4376cedec9..eea3306a2665f 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.24.0", + "version": "7.24.1", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 92e9fd8bebe59..eba0a06012da7 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.24.0", + "version": "5.24.1", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index eb318c962c1c3..a983c1893ed12 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.24.0", + "version": "5.24.1", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/package.json b/packages/editor/package.json index 81d24961a775e..70344e9dc3e72 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.24.0", + "version": "13.24.1", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index da118762e3c02..bf8576fd67ae7 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "3.0.0", + "version": "3.0.1", "description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From ee9baf2baa00d7e762c5baab863e5436ebd3e2ec Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:01:19 -0500 Subject: [PATCH 07/38] Tabs: Implement `ariakit/test` in unit tests (#56835) --- packages/components/src/tabs/test/index.tsx | 127 +++++++------------- 1 file changed, 46 insertions(+), 81 deletions(-) diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index fac8127c4cc0d..f923dc455fd7b 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, click } from '@ariakit/test'; /** * WordPress dependencies @@ -184,26 +184,22 @@ describe( 'Tabs', () => { } ); describe( 'Focus Behavior', () => { it( 'should focus on the related TabPanel when pressing the Tab key', async () => { - const user = userEvent.setup(); - render( ); const selectedTabPanel = await screen.findByRole( 'tabpanel' ); // Tab should initially focus the first tab in the tablist, which // is Alpha. - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); // By default the tabpanel should receive focus - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( selectedTabPanel ).toHaveFocus(); } ); it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { - const user = userEvent.setup(); - const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => tabObj.id === 'alpha' ? { @@ -229,13 +225,13 @@ describe( 'Tabs', () => { // Tab should initially focus the first tab in the tablist, which // is Alpha. - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); // Because the alpha tabpanel is set to `focusable: false`, pressing // the Tab key should focus the button, not the tabpanel - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( alphaButton ).toHaveFocus(); } ); } ); @@ -258,7 +254,6 @@ describe( 'Tabs', () => { describe( 'Tab Activation', () => { it( 'defaults to automatic tab activation (pointer clicks)', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -273,7 +268,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); // Click on Beta, make sure beta is the selected tab - await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) ); + await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( @@ -282,7 +277,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); // Click on Alpha, make sure beta is the selected tab - await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( @@ -292,7 +287,6 @@ describe( 'Tabs', () => { } ); it( 'defaults to automatic tab activation (arrow keys)', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -307,12 +301,12 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Navigate forward with arrow keys and make sure the Beta tab is // selected automatically. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); @@ -320,7 +314,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -328,7 +322,6 @@ describe( 'Tabs', () => { } ); it( 'wraps around the last/first tab when using arrow keys', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -341,12 +334,12 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure Alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Navigate backwards with arrow keys and make sure that the Gamma tab // (the last tab) is selected automatically. - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); @@ -354,7 +347,7 @@ describe( 'Tabs', () => { // Navigate forward with arrow keys. Make sure alpha (the first tab) is // selected automatically. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -362,7 +355,6 @@ describe( 'Tabs', () => { } ); it( 'should not move tab selection when pressing the up/down arrow keys, unless the orientation is changed to `vertical`', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const { rerender } = render( @@ -377,18 +369,18 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Press the arrow up key, nothing happens. - await user.keyboard( '[ArrowUp]' ); + await press.ArrowUp(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); // Press the arrow down key, nothing happens - await user.keyboard( '[ArrowDown]' ); + await press.ArrowDown(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); @@ -415,7 +407,7 @@ describe( 'Tabs', () => { // Navigate forward with arrow keys and make sure the Beta tab is // selected automatically. - await user.keyboard( '[ArrowDown]' ); + await press.ArrowDown(); expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); @@ -423,7 +415,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowUp]' ); + await press.ArrowUp(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -431,7 +423,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowUp]' ); + await press.ArrowUp(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); @@ -439,7 +431,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowDown]' ); + await press.ArrowDown(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 5 ); @@ -447,7 +439,6 @@ describe( 'Tabs', () => { } ); it( 'should move focus on a tab even if disabled with arrow key, but not with pointer clicks', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => @@ -477,7 +468,7 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure Alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Confirm onSelect has not been re-called expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); @@ -487,7 +478,9 @@ describe( 'Tabs', () => { // it was the tab that was last selected before delta. Therefore, the // `mockOnSelect` function gets called only twice (and not three times) // - it will receive focus, when using arrow keys - await user.keyboard( '[ArrowRight][ArrowRight][ArrowRight]' ); + await press.ArrowRight(); + await press.ArrowRight(); + await press.ArrowRight(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( screen.getByRole( 'tab', { name: 'Delta' } ) @@ -498,7 +491,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. The gamma tab receives focus. // The `mockOnSelect` callback doesn't fire, since the gamma tab was // already selected. - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -506,37 +499,26 @@ describe( 'Tabs', () => { // Click on the disabled tab. Compared to using arrow keys to move the // focus, disabled tabs ignore pointer clicks — and therefore, they don't // receive focus, nor they cause the `mockOnSelect` function to fire. - await user.click( screen.getByRole( 'tab', { name: 'Delta' } ) ); + await click( screen.getByRole( 'tab', { name: 'Delta' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); } ); it( 'should not focus the next tab when the Tab key is pressed', async () => { - const user = userEvent.setup(); - render( ); // Tab should initially focus the first tab in the tablist, which // is Alpha. - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); - // This assertion ensures the component has had time to fully - // render, preventing flakiness. - // see https://github.com/WordPress/gutenberg/pull/55950 - await waitFor( () => - expect( - screen.getByRole( 'tab', { name: 'Beta' } ) - ).toHaveAttribute( 'tabindex', '-1' ) - ); - // Because all other tabs should have `tabindex=-1`, pressing Tab // should NOT move the focus to the next tab, which is Beta. // Instead, focus should go to the currently selected tabpanel (alpha). - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tabpanel', { name: 'Alpha', @@ -545,7 +527,6 @@ describe( 'Tabs', () => { } ); it( 'switches to manual tab activation when the `selectOnMove` prop is set to `false`', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -563,7 +544,7 @@ describe( 'Tabs', () => { // Click on Alpha and make sure it is selected. // onSelect shouldn't fire since the selected tab didn't change. - await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); @@ -574,13 +555,13 @@ describe( 'Tabs', () => { // that the tab selection happens only when pressing the spacebar // or enter key. onSelect shouldn't fire since the selected tab // didn't change. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await screen.findByRole( 'tab', { name: 'Beta' } ) ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - await user.keyboard( '[Enter]' ); + await press.Enter(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); @@ -588,7 +569,7 @@ describe( 'Tabs', () => { // focused, but that tab selection happens only when pressing the // spacebar or enter key. onSelect shouldn't fire since the selected // tab didn't change. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await screen.findByRole( 'tab', { name: 'Gamma' } ) ).toHaveFocus(); @@ -597,7 +578,7 @@ describe( 'Tabs', () => { screen.getByRole( 'tab', { name: 'Gamma' } ) ).toHaveFocus(); - await user.keyboard( '[Space]' ); + await press.Space(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); } ); @@ -700,7 +681,6 @@ describe( 'Tabs', () => { } ); it( 'should fall back to the tab associated to `initialTabId` if the currently active tab is removed', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const { rerender } = render( @@ -713,9 +693,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - await user.click( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); @@ -731,7 +709,6 @@ describe( 'Tabs', () => { } ); it( 'should fall back to the tab associated to `initialTabId` if the currently active tab becomes disabled', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const { rerender } = render( @@ -744,9 +721,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - await user.click( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); @@ -822,7 +797,6 @@ describe( 'Tabs', () => { describe( 'Disabled tab', () => { it( 'should disable the tab when `disabled` is `true`', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( @@ -853,20 +827,15 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Move focus to the tablist, make sure alpha is focused. + await press.Tab(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + // onSelect should not be called since the disabled tab is // highlighted, but not selected. - await user.keyboard( '[Tab]' ); - - // This assertion ensures focus has time to move to the first - // tab before the test proceeds, preventing flakiness. - // see https://github.com/WordPress/gutenberg/pull/55950 - await waitFor( () => - expect( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus() - ); - - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); // Delta (which is disabled) has focus @@ -1067,14 +1036,10 @@ describe( 'Tabs', () => { /> ); - // No tab should be selected i.e. it doesn't fall back to first tab. - // `waitFor` is needed here to prevent testing library from - // throwing a 'not wrapped in `act()`' error. - await waitFor( () => - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + // No tabpanel should be rendered either expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument(); } ); From 9d13cf09f06dc8e4dadbcdb31265a0f49bd42f0e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 7 Dec 2023 17:40:07 +0100 Subject: [PATCH 08/38] Site Editor: Fix active edited post (#56863) --- .../reference-guides/data/data-core-editor.md | 21 +++++++--- .../src/components/document-bar/index.js | 8 ++-- .../editor/src/components/provider/index.js | 13 ++++--- packages/editor/src/store/actions.js | 38 +++++++++++++++---- packages/editor/src/store/reducer.js | 31 ++------------- packages/editor/src/store/reducer.native.js | 2 - packages/editor/src/store/selectors.js | 2 +- 7 files changed, 63 insertions(+), 52 deletions(-) diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 7b5a6dbeeb909..f6086090f9b54 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -1285,6 +1285,19 @@ _Returns_ - `Object`: Action object. +### setEditedPost + +Returns an action that sets the current post Type and post ID. + +_Parameters_ + +- _postType_ `string`: Post Type. +- _postId_ `string`: Post ID. + +_Returns_ + +- `Object`: Action object. + ### setRenderingMode Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: @@ -1316,16 +1329,14 @@ _Parameters_ ### setupEditorState -Returns an action object used to setup the editor state when first opening an editor. +> **Deprecated** + +Setup the editor state. _Parameters_ - _post_ `Object`: Post object. -_Returns_ - -- `Object`: Action object. - ### showInsertionPoint _Related_ diff --git a/packages/editor/src/components/document-bar/index.js b/packages/editor/src/components/document-bar/index.js index ffb2be3307456..da43533bfa5bc 100644 --- a/packages/editor/src/components/document-bar/index.js +++ b/packages/editor/src/components/document-bar/index.js @@ -88,7 +88,7 @@ export default function DocumentBar() { function BaseDocumentActions( { postType, postId, onBack } ) { const { open: openCommandCenter } = useDispatch( commandsStore ); - const { editedRecord: document, isResolving } = useEntityRecord( + const { editedRecord: doc, isResolving } = useEntityRecord( 'postType', postType, postId @@ -96,13 +96,13 @@ function BaseDocumentActions( { postType, postId, onBack } ) { const { templateIcon, templateTitle } = useSelect( ( select ) => { const { __experimentalGetTemplateInfo: getTemplateInfo } = select( editorStore ); - const templateInfo = getTemplateInfo( document ); + const templateInfo = getTemplateInfo( doc ); return { templateIcon: templateInfo.icon, templateTitle: templateInfo.title, }; } ); - const isNotFound = ! document && ! isResolving; + const isNotFound = ! doc && ! isResolving; const icon = icons[ postType ] ?? pageIcon; const [ isAnimated, setIsAnimated ] = useState( false ); const isMounting = useRef( true ); @@ -123,7 +123,7 @@ function BaseDocumentActions( { postType, postId, onBack } ) { isMounting.current = false; }, [ postType, postId ] ); - const title = isTemplate ? templateTitle : document.title; + const title = isTemplate ? templateTitle : doc.title; return (
{ // Assume that we don't need to initialize in the case of an error recovery. @@ -196,17 +195,19 @@ export const ExperimentalEditorProvider = withRegistryProvider( } ); } - - return () => { - __experimentalTearDownEditor(); - }; }, [] ); + // Synchronizes the active post with the state + useEffect( () => { + setEditedPost( post.type, post.id ); + }, [ post.type, post.id ] ); + // Synchronize the editor settings as they change. useEffect( () => { updateEditorSettings( settings ); }, [ settings, updateEditorSettings ] ); + // Synchronizes the active template with the state. useEffect( () => { setCurrentTemplateId( template?.id ); }, [ template?.id, setCurrentTemplateId ] ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 5d8335382b5db..8fe0822e6a016 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -36,7 +36,7 @@ import { export const setupEditor = ( post, edits, template ) => ( { dispatch } ) => { - dispatch.setupEditorState( post ); + dispatch.setEditedPost( post.type, post.id ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; if ( isNewPost && template ) { @@ -70,10 +70,18 @@ export const setupEditor = * Returns an action object signalling that the editor is being destroyed and * that any necessary state or side-effect cleanup should occur. * + * @deprecated + * * @return {Object} Action object. */ export function __experimentalTearDownEditor() { - return { type: 'TEAR_DOWN_EDITOR' }; + deprecated( + "wp.data.dispatch( 'core/editor' ).__experimentalTearDownEditor", + { + since: '6.5', + } + ); + return { type: 'DO_NOTHING' }; } /** @@ -109,17 +117,33 @@ export function updatePost() { } /** - * Returns an action object used to setup the editor state when first opening - * an editor. + * Setup the editor state. + * + * @deprecated * * @param {Object} post Post object. + */ +export function setupEditorState( post ) { + deprecated( "wp.data.dispatch( 'core/editor' ).setupEditorState", { + since: '6.5', + alternative: "wp.data.dispatch( 'core/editor' ).setEditedPost", + } ); + return setEditedPost( post.type, post.id ); +} + +/** + * Returns an action that sets the current post Type and post ID. + * + * @param {string} postType Post Type. + * @param {string} postId Post ID. * * @return {Object} Action object. */ -export function setupEditorState( post ) { +export function setEditedPost( postType, postId ) { return { - type: 'SETUP_EDITOR_STATE', - post, + type: 'SET_EDITED_POST', + postType, + postId, }; } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 23fa84edba032..a4323b5967956 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -83,8 +83,8 @@ export function shouldOverwriteState( action, previousAction ) { export function postId( state = null, action ) { switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return action.post.id; + case 'SET_EDITED_POST': + return action.postId; } return state; @@ -101,8 +101,8 @@ export function templateId( state = null, action ) { export function postType( state = null, action ) { switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return action.post.type; + case 'SET_EDITED_POST': + return action.postType; } return state; @@ -246,28 +246,6 @@ export function postAutosavingLock( state = {}, action ) { return state; } -/** - * Reducer returning whether the editor is ready to be rendered. - * The editor is considered ready to be rendered once - * the post object is loaded properly and the initial blocks parsed. - * - * @param {boolean} state - * @param {Object} action - * - * @return {boolean} Updated state. - */ -export function isReady( state = false, action ) { - switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return true; - - case 'TEAR_DOWN_EDITOR': - return false; - } - - return state; -} - /** * Reducer returning the post editor setting. * @@ -323,7 +301,6 @@ export default combineReducers( { postLock, template, postSavingLock, - isReady, editorSettings, postAutosavingLock, renderingMode, diff --git a/packages/editor/src/store/reducer.native.js b/packages/editor/src/store/reducer.native.js index 991addd88620b..7566dfc5dfd03 100644 --- a/packages/editor/src/store/reducer.native.js +++ b/packages/editor/src/store/reducer.native.js @@ -13,7 +13,6 @@ import { postLock, postSavingLock, template, - isReady, editorSettings, } from './reducer.js'; @@ -87,7 +86,6 @@ export default combineReducers( { postLock, postSavingLock, template, - isReady, editorSettings, clipboard, notices, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 0b82bb797584e..3b3f315812430 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1185,7 +1185,7 @@ export function getEditorSelection( state ) { * @return {boolean} is Ready. */ export function __unstableIsEditorReady( state ) { - return state.isReady; + return !! state.postId; } /** From 0fa652ac4f078ef11a83f3383fce8e3288f2a798 Mon Sep 17 00:00:00 2001 From: Damon Cook Date: Thu, 7 Dec 2023 12:59:47 -0500 Subject: [PATCH 09/38] Update InnerBlocks defaultblock doc usage (#56728) Co-authored-by: Riad Benguella --- .../src/components/inner-blocks/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/inner-blocks/README.md b/packages/block-editor/src/components/inner-blocks/README.md index 0f5d303b8c791..92d4fdb5739ce 100644 --- a/packages/block-editor/src/components/inner-blocks/README.md +++ b/packages/block-editor/src/components/inner-blocks/README.md @@ -188,8 +188,19 @@ For example, a button block, deeply nested in several levels of block `X` that u ### `defaultBlock` -- **Type:** `Array` -- **Default:** - `undefined`. Determines which block type should be inserted by default and any attributes that should be set by default when the block is inserted. Takes an array in the form of `[ blockname, {blockAttributes} ]`. +- **Type:** `Object` +- **Default:** - `undefined` + +Determines which block type should be inserted by default and any attributes that should be set by default when the block is inserted. Takes an object in the form of `{ name: blockname, attributes: {blockAttributes} }`. + +```jsx +const DEFAULT_BLOCK = { name: 'core/paragraph', attributes: { content: 'Lorem ipsum...' } }; +... + +``` ### `directInsert` From a9cbc06d5e55ebf1b5255edd832809bd684b2c9f Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:44:24 +0200 Subject: [PATCH 10/38] One hook to rule them all: preparation for a block supports API (#56862) --- .../src/components/block-controls/hook.js | 30 +------ .../components/block-info-slot-fill/index.js | 2 +- .../src/components/inspector-controls/fill.js | 2 +- .../inspector-controls/fill.native.js | 2 +- .../use-display-block-controls/index.js | 28 ++++--- .../index.native.js | 6 +- packages/block-editor/src/hooks/align.js | 54 +++---------- .../block-editor/src/hooks/align.native.js | 1 + packages/block-editor/src/hooks/anchor.js | 51 +++--------- .../block-editor/src/hooks/block-hooks.js | 78 ++++++------------- .../block-editor/src/hooks/block-renaming.js | 45 ++--------- .../src/hooks/custom-class-name.js | 51 ++---------- .../block-editor/src/hooks/custom-fields.js | 60 +++----------- packages/block-editor/src/hooks/duotone.js | 61 +++------------ packages/block-editor/src/hooks/index.js | 36 ++++++--- .../block-editor/src/hooks/index.native.js | 9 ++- packages/block-editor/src/hooks/layout.js | 50 +++--------- packages/block-editor/src/hooks/position.js | 58 +++----------- packages/block-editor/src/hooks/style.js | 43 ++-------- packages/block-editor/src/hooks/test/align.js | 72 +---------------- packages/block-editor/src/hooks/utils.js | 68 ++++++++++++++++ 21 files changed, 229 insertions(+), 578 deletions(-) diff --git a/packages/block-editor/src/components/block-controls/hook.js b/packages/block-editor/src/components/block-controls/hook.js index 18a38e245e58a..e3f69c8bec3b2 100644 --- a/packages/block-editor/src/components/block-controls/hook.js +++ b/packages/block-editor/src/components/block-controls/hook.js @@ -1,45 +1,19 @@ /** * WordPress dependencies */ -import { store as blocksStore } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import groups from './groups'; -import { store as blockEditorStore } from '../../store'; -import { useBlockEditContext } from '../block-edit/context'; import useDisplayBlockControls from '../use-display-block-controls'; export default function useBlockControlsFill( group, shareWithChildBlocks ) { - const isDisplayed = useDisplayBlockControls(); - const { clientId } = useBlockEditContext(); - const isParentDisplayed = useSelect( - ( select ) => { - if ( ! shareWithChildBlocks ) { - return false; - } - - const { getBlockName, hasSelectedInnerBlock } = - select( blockEditorStore ); - const { hasBlockSupport } = select( blocksStore ); - - return ( - hasBlockSupport( - getBlockName( clientId ), - '__experimentalExposeControlsToChildren', - false - ) && hasSelectedInnerBlock( clientId ) - ); - }, - [ shareWithChildBlocks, clientId ] - ); - + const { isDisplayed, isParentDisplayed } = useDisplayBlockControls(); if ( isDisplayed ) { return groups[ group ]?.Fill; } - if ( isParentDisplayed ) { + if ( isParentDisplayed && shareWithChildBlocks ) { return groups.parent.Fill; } return null; diff --git a/packages/block-editor/src/components/block-info-slot-fill/index.js b/packages/block-editor/src/components/block-info-slot-fill/index.js index db7919b6ef5ea..8e16757f3ebbc 100644 --- a/packages/block-editor/src/components/block-info-slot-fill/index.js +++ b/packages/block-editor/src/components/block-info-slot-fill/index.js @@ -13,7 +13,7 @@ const { createPrivateSlotFill } = unlock( componentsPrivateApis ); const { Fill, Slot } = createPrivateSlotFill( 'BlockInformation' ); const BlockInfo = ( props ) => { - const isDisplayed = useDisplayBlockControls(); + const { isDisplayed } = useDisplayBlockControls(); if ( ! isDisplayed ) { return null; } diff --git a/packages/block-editor/src/components/inspector-controls/fill.js b/packages/block-editor/src/components/inspector-controls/fill.js index f0640a9d31ddc..fdb0d44f0eccb 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.js +++ b/packages/block-editor/src/components/inspector-controls/fill.js @@ -33,7 +33,7 @@ export default function InspectorControlsFill( { group = __experimentalGroup; } - const isDisplayed = useDisplayBlockControls(); + const { isDisplayed } = useDisplayBlockControls(); const Fill = groups[ group ]?.Fill; if ( ! Fill ) { warning( `Unknown InspectorControls group "${ group }" provided.` ); diff --git a/packages/block-editor/src/components/inspector-controls/fill.native.js b/packages/block-editor/src/components/inspector-controls/fill.native.js index d38d865cd15cc..f1ee5a14cd18e 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.native.js +++ b/packages/block-editor/src/components/inspector-controls/fill.native.js @@ -35,7 +35,7 @@ export default function InspectorControlsFill( { ); group = __experimentalGroup; } - const isDisplayed = useDisplayBlockControls(); + const { isDisplayed } = useDisplayBlockControls(); const Fill = groups[ group ]?.Fill; if ( ! Fill ) { diff --git a/packages/block-editor/src/components/use-display-block-controls/index.js b/packages/block-editor/src/components/use-display-block-controls/index.js index 605556f295b96..ef27479593a73 100644 --- a/packages/block-editor/src/components/use-display-block-controls/index.js +++ b/packages/block-editor/src/components/use-display-block-controls/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies @@ -13,23 +14,28 @@ export default function useDisplayBlockControls() { const { isSelected, clientId, name } = useBlockEditContext(); return useSelect( ( select ) => { - if ( isSelected ) { - return true; - } - const { getBlockName, isFirstMultiSelectedBlock, getMultiSelectedBlockClientIds, + hasSelectedInnerBlock, } = select( blockEditorStore ); + const { hasBlockSupport } = select( blocksStore ); - if ( isFirstMultiSelectedBlock( clientId ) ) { - return getMultiSelectedBlockClientIds().every( - ( id ) => getBlockName( id ) === name - ); - } - - return false; + return { + isDisplayed: + isSelected || + ( isFirstMultiSelectedBlock( clientId ) && + getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === name + ) ), + isParentDisplayed: + hasBlockSupport( + getBlockName( clientId ), + '__experimentalExposeControlsToChildren', + false + ) && hasSelectedInnerBlock( clientId ), + }; }, [ clientId, isSelected, name ] ); diff --git a/packages/block-editor/src/components/use-display-block-controls/index.native.js b/packages/block-editor/src/components/use-display-block-controls/index.native.js index e8a198e1592e8..d865ed6d9d7b2 100644 --- a/packages/block-editor/src/components/use-display-block-controls/index.native.js +++ b/packages/block-editor/src/components/use-display-block-controls/index.native.js @@ -26,11 +26,7 @@ export default function useDisplayBlockControls() { false ); - if ( ! hideControls && isSelected ) { - return true; - } - - return false; + return { isDisplayed: ! hideControls && isSelected }; }, [ clientId, isSelected, name ] ); diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 3b916d9577f1a..2019228cf2d3e 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, @@ -109,7 +109,7 @@ export function addAttribute( settings ) { } function BlockEditAlignmentToolbarControlsPure( { - blockName, + name: blockName, align, setAttributes, } ) { @@ -152,45 +152,14 @@ function BlockEditAlignmentToolbarControlsPure( { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockEditAlignmentToolbarControls = pure( - BlockEditAlignmentToolbarControlsPure -); - -/** - * Override the default edit UI to include new toolbar controls for block - * alignment, if block defines support. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withAlignmentControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const hasAlignmentSupport = hasBlockSupport( - props.name, - 'align', - false - ); - - return ( - <> - { hasAlignmentSupport && ( - - ) } - - - ); +export default { + shareWithChildBlocks: true, + edit: BlockEditAlignmentToolbarControlsPure, + attributeKeys: [ 'align' ], + hasSupport( name ) { + return hasBlockSupport( name, 'align', false ); }, - 'withAlignmentControls' -); +}; function BlockListBlockWithDataAlign( { block: BlockListBlock, props } ) { const { name, attributes } = props; @@ -273,11 +242,6 @@ addFilter( 'core/editor/align/with-data-align', withDataAlign ); -addFilter( - 'editor.BlockEdit', - 'core/editor/align/with-toolbar-controls', - withAlignmentControls -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/align/addAssignedAlign', diff --git a/packages/block-editor/src/hooks/align.native.js b/packages/block-editor/src/hooks/align.native.js index 1bf375b654ad4..7a229e79870a8 100644 --- a/packages/block-editor/src/hooks/align.native.js +++ b/packages/block-editor/src/hooks/align.native.js @@ -8,6 +8,7 @@ import { WIDE_ALIGNMENTS } from '@wordpress/components'; const ALIGNMENTS = [ 'left', 'center', 'right' ]; export * from './align.js'; +export { default } from './align.js'; // Used to filter out blocks that don't support wide/full alignment on mobile addFilter( diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 9902ed479531c..882820678aa87 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -5,7 +5,6 @@ import { addFilter } from '@wordpress/hooks'; import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { Platform } from '@wordpress/element'; /** @@ -52,7 +51,11 @@ export function addAttribute( settings ) { return settings; } -function BlockEditAnchorControlPure( { blockName, anchor, setAttributes } ) { +function BlockEditAnchorControlPure( { + name: blockName, + anchor, + setAttributes, +} ) { const blockEditingMode = useBlockEditingMode(); const isWeb = Platform.OS === 'web'; @@ -116,38 +119,13 @@ function BlockEditAnchorControlPure( { blockName, anchor, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockEditAnchorControl = pure( BlockEditAnchorControlPure ); - -/** - * Override the default edit UI to include a new block inspector control for - * assigning the anchor ID, if block supports anchor. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -export const withAnchorControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - return ( - <> - - { props.isSelected && - hasBlockSupport( props.name, 'anchor' ) && ( - - ) } - - ); - }; -}, 'withAnchorControls' ); +export default { + edit: BlockEditAnchorControlPure, + attributeKeys: [ 'anchor' ], + hasSupport( name ) { + return hasBlockSupport( name, 'anchor' ); + }, +}; /** * Override props assigned to save component to inject anchor ID, if block @@ -169,11 +147,6 @@ export function addSaveProps( extraProps, blockType, attributes ) { } addFilter( 'blocks.registerBlockType', 'core/anchor/attribute', addAttribute ); -addFilter( - 'editor.BlockEdit', - 'core/editor/anchor/with-inspector-controls', - withAnchorControls -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/anchor/save-props', diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 59c0e3c85486f..a1640c72f4b2b 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -2,14 +2,12 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { addFilter } from '@wordpress/hooks'; import { Fragment, useMemo } from '@wordpress/element'; import { __experimentalHStack as HStack, PanelBody, ToggleControl, } from '@wordpress/components'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { createBlock, store as blocksStore } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -21,7 +19,7 @@ import { store as blockEditorStore } from '../store'; const EMPTY_OBJECT = {}; -function BlockHooksControlPure( props ) { +function BlockHooksControlPure( { name, clientId } ) { const blockTypes = useSelect( ( select ) => select( blocksStore ).getBlockTypes(), [] @@ -30,10 +28,9 @@ function BlockHooksControlPure( props ) { const hookedBlocksForCurrentBlock = useMemo( () => blockTypes?.filter( - ( { blockHooks } ) => - blockHooks && props.blockName in blockHooks + ( { blockHooks } ) => blockHooks && name in blockHooks ), - [ blockTypes, props.blockName ] + [ blockTypes, name ] ); const { blockIndex, rootClientId, innerBlocksLength } = useSelect( @@ -42,13 +39,12 @@ function BlockHooksControlPure( props ) { select( blockEditorStore ); return { - blockIndex: getBlockIndex( props.clientId ), - innerBlocksLength: getBlock( props.clientId )?.innerBlocks - ?.length, - rootClientId: getBlockRootClientId( props.clientId ), + blockIndex: getBlockIndex( clientId ), + innerBlocksLength: getBlock( clientId )?.innerBlocks?.length, + rootClientId: getBlockRootClientId( clientId ), }; }, - [ props.clientId ] + [ clientId ] ); const hookedBlockClientIds = useSelect( @@ -65,8 +61,7 @@ function BlockHooksControlPure( props ) { return clientIds; } - const relativePosition = - block?.blockHooks?.[ props.blockName ]; + const relativePosition = block?.blockHooks?.[ name ]; let candidates; switch ( relativePosition ) { @@ -83,12 +78,12 @@ function BlockHooksControlPure( props ) { // Any of the current block's child blocks (with the right block type) qualifies // as a hooked first or last child block, as the block might've been automatically // inserted and then moved around a bit by the user. - candidates = getBlock( props.clientId ).innerBlocks; + candidates = getBlock( clientId ).innerBlocks; break; } const hookedBlock = candidates?.find( - ( { name } ) => name === block.name + ( candidate ) => name === candidate.name ); // If the block exists in the designated location, we consider it hooked @@ -118,12 +113,7 @@ function BlockHooksControlPure( props ) { return EMPTY_OBJECT; }, - [ - hookedBlocksForCurrentBlock, - props.blockName, - props.clientId, - rootClientId, - ] + [ hookedBlocksForCurrentBlock, name, clientId, rootClientId ] ); const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); @@ -169,7 +159,7 @@ function BlockHooksControlPure( props ) { block, // TODO: It'd be great if insertBlock() would accept negative indices for insertion. relativePosition === 'first_child' ? 0 : innerBlocksLength, - props.clientId, // Insert as a child of the current block. + clientId, // Insert as a child of the current block. false ); break; @@ -207,9 +197,7 @@ function BlockHooksControlPure( props ) { if ( ! checked ) { // Create and insert block. const relativePosition = - block.blockHooks[ - props.blockName - ]; + block.blockHooks[ name ]; insertBlockIntoDesignatedLocation( createBlock( block.name ), relativePosition @@ -218,11 +206,12 @@ function BlockHooksControlPure( props ) { } // Remove block. - const clientId = + removeBlock( hookedBlockClientIds[ block.name - ]; - removeBlock( clientId, false ); + ], + false + ); } } /> ); @@ -235,32 +224,9 @@ function BlockHooksControlPure( props ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockHooksControl = pure( BlockHooksControlPure ); - -export const withBlockHooksControls = createHigherOrderComponent( - ( BlockEdit ) => { - return ( props ) => { - return ( - <> - - { props.isSelected && ( - - ) } - - ); - }; +export default { + edit: BlockHooksControlPure, + hasSupport() { + return true; }, - 'withBlockHooksControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/editor/block-hooks/with-inspector-controls', - withBlockHooksControls -); +}; diff --git a/packages/block-editor/src/hooks/block-renaming.js b/packages/block-editor/src/hooks/block-renaming.js index 452be6e686dbf..26ada6ba73281 100644 --- a/packages/block-editor/src/hooks/block-renaming.js +++ b/packages/block-editor/src/hooks/block-renaming.js @@ -3,7 +3,6 @@ */ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { TextControl } from '@wordpress/components'; @@ -11,7 +10,6 @@ import { TextControl } from '@wordpress/components'; * Internal dependencies */ import { InspectorControls } from '../components'; -import { useBlockRename } from '../components/block-rename'; /** * Filters registered block settings, adding an `__experimentalLabel` callback if one does not already exist. @@ -47,13 +45,7 @@ export function addLabelCallback( settings ) { return settings; } -function BlockRenameControlPure( { name, metadata, setAttributes } ) { - const { canRename } = useBlockRename( name ); - - if ( ! canRename ) { - return null; - } - +function BlockRenameControlPure( { metadata, setAttributes } ) { return ( ( props ) => { - const { name, attributes, setAttributes, isSelected } = props; - return ( - <> - { isSelected && ( - - ) } - - - ); +export default { + edit: BlockRenameControlPure, + attributeKeys: [ 'metadata' ], + hasSupport( name ) { + return hasBlockSupport( name, 'renaming', true ); }, - 'withToolbarControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/block-rename-ui/with-block-rename-control', - withBlockRenameControl -); +}; addFilter( 'blocks.registerBlockType', diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index 8c0f58ddda682..331edd9ef214a 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -10,7 +10,6 @@ import { addFilter } from '@wordpress/hooks'; import { TextControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; /** * Internal dependencies @@ -64,46 +63,13 @@ function CustomClassNameControlsPure( { className, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const CustomClassNameControls = pure( CustomClassNameControlsPure ); - -/** - * Override the default edit UI to include a new block inspector control for - * assigning the custom class name, if block supports custom class name. - * The control is displayed within the Advanced panel in the block inspector. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -export const withCustomClassNameControls = createHigherOrderComponent( - ( BlockEdit ) => { - return ( props ) => { - const hasCustomClassName = hasBlockSupport( - props.name, - 'customClassName', - true - ); - - return ( - <> - - { hasCustomClassName && props.isSelected && ( - - ) } - - ); - }; +export default { + edit: CustomClassNameControlsPure, + attributeKeys: [ 'className' ], + hasSupport( name ) { + return hasBlockSupport( name, 'customClassName', true ); }, - 'withCustomClassNameControls' -); +}; /** * Override props assigned to save component to inject the className, if block @@ -174,11 +140,6 @@ addFilter( 'core/editor/custom-class-name/attribute', addAttribute ); -addFilter( - 'editor.BlockEdit', - 'core/editor/custom-class-name/with-inspector-controls', - withCustomClassNameControls -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/custom-class-name/save-props', diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 19729d00ad61a..9b677933adc13 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -5,7 +5,6 @@ import { addFilter } from '@wordpress/hooks'; import { PanelBody, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; /** * Internal dependencies @@ -91,50 +90,18 @@ function CustomFieldsControlPure( { name, connections, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const CustomFieldsControl = pure( CustomFieldsControlPure ); - -/** - * Override the default edit UI to include a new block inspector control for - * assigning a connection to blocks that has support for connections. - * Currently, only the `core/paragraph` block is supported and there is only a relation - * between paragraph content and a custom field. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const hasCustomFieldsSupport = hasBlockSupport( - props.name, - '__experimentalConnections', - false - ); - - // Check if the current block is a paragraph or image block. - // Currently, only these two blocks are supported. - if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) { - return ; - } - +export default { + edit: CustomFieldsControlPure, + attributeKeys: [ 'connections' ], + hasSupport( name ) { return ( - <> - - { hasCustomFieldsSupport && props.isSelected && ( - - ) } - + hasBlockSupport( name, '__experimentalConnections', false ) && + // Check if the current block is a paragraph or image block. + // Currently, only these two blocks are supported. + [ 'core/paragraph', 'core/image' ].includes( name ) ); - }; -}, 'withCustomFieldsControls' ); + }, +}; if ( window.__experimentalConnections || @@ -146,10 +113,3 @@ if ( addAttribute ); } -if ( window.__experimentalConnections ) { - addFilter( - 'editor.BlockEdit', - 'core/editor/connections/with-inspector-controls', - withCustomFieldsControls - ); -} diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index 6e18b44cef163..c0b76d12cb370 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -13,11 +13,7 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { - createHigherOrderComponent, - useInstanceId, - pure, -} from '@wordpress/compose'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; @@ -179,10 +175,14 @@ function DuotonePanelPure( { style, setAttributes, name } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const DuotonePanel = pure( DuotonePanelPure ); +export default { + shareWithChildBlocks: true, + edit: DuotonePanelPure, + attributeKeys: [ 'style' ], + hasSupport( name ) { + return hasBlockSupport( name, 'filter.duotone' ); + }, +}; /** * Filters registered block settings, extending attributes to include @@ -212,44 +212,6 @@ function addDuotoneAttributes( settings ) { return settings; } -/** - * Override the default edit UI to include toolbar controls for duotone if the - * block supports duotone. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -const withDuotoneControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - // Previous `color.__experimentalDuotone` support flag is migrated via - // block_type_metadata_settings filter in `lib/block-supports/duotone.php`. - const hasDuotoneSupport = hasBlockSupport( - props.name, - 'filter.duotone' - ); - - // CAUTION: code added before this line will be executed - // for all blocks, not just those that support duotone. Code added - // above this line should be carefully evaluated for its impact on - // performance. - return ( - <> - { hasDuotoneSupport && ( - - ) } - - - ); - }, - 'withDuotoneControls' -); - function DuotoneStyles( { clientId, id: filterId, @@ -438,11 +400,6 @@ addFilter( 'core/editor/duotone/add-attributes', addDuotoneAttributes ); -addFilter( - 'editor.BlockEdit', - 'core/editor/duotone/with-editor-controls', - withDuotoneControls -); addFilter( 'editor.BlockListBlock', 'core/editor/duotone/with-styles', diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index c088216c0645c..6ae589dd672bf 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,27 +1,43 @@ /** * Internal dependencies */ +import { createBlockEditFilter } from './utils'; import './compat'; -import './align'; +import align from './align'; import './lock'; -import './anchor'; +import anchor from './anchor'; import './aria-label'; -import './custom-class-name'; +import customClassName from './custom-class-name'; import './generated-class-name'; -import './style'; +import style from './style'; import './settings'; import './color'; -import './duotone'; +import duotone from './duotone'; import './font-family'; import './font-size'; import './border'; -import './position'; -import './layout'; +import position from './position'; +import layout from './layout'; import './content-lock-ui'; import './metadata'; -import './custom-fields'; -import './block-hooks'; -import './block-renaming'; +import customFields from './custom-fields'; +import blockHooks from './block-hooks'; +import blockRenaming from './block-renaming'; + +createBlockEditFilter( + [ + align, + anchor, + customClassName, + style, + duotone, + position, + layout, + window.__experimentalConnections ? customFields : null, + blockHooks, + blockRenaming, + ].filter( Boolean ) +); export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index 42bda25bfe1cc..3f1a4473c1389 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -1,16 +1,19 @@ /** * Internal dependencies */ +import { createBlockEditFilter } from './utils'; import './compat'; -import './align'; -import './anchor'; +import align from './align'; +import anchor from './anchor'; import './custom-class-name'; import './generated-class-name'; -import './style'; +import style from './style'; import './color'; import './font-size'; import './layout'; +createBlockEditFilter( [ align, anchor, style ] ); + export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 3ea5c56da8e77..46239e1de0703 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -6,11 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - createHigherOrderComponent, - pure, - useInstanceId, -} from '@wordpress/compose'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; @@ -290,10 +286,14 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const LayoutPanel = pure( LayoutPanelPure ); +export default { + shareWithChildBlocks: true, + edit: LayoutPanelPure, + attributeKeys: [ 'layout' ], + hasSupport( name ) { + return hasLayoutBlockSupport( name ); + }, +}; function LayoutTypeSwitcher( { type, onChange } ) { return ( @@ -336,33 +336,6 @@ export function addAttribute( settings ) { return settings; } -/** - * Override the default edit UI to include layout controls - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withLayoutControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const supportLayout = hasLayoutBlockSupport( props.name ); - - return [ - supportLayout && ( - - ), - , - ]; - }, - 'withLayoutControls' -); - function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { const { name, attributes } = props; const id = useInstanceId( BlockListBlock ); @@ -516,8 +489,3 @@ addFilter( 'core/editor/layout/with-child-layout-styles', withChildLayoutStyles ); -addFilter( - 'editor.BlockEdit', - 'core/editor/layout/with-inspector-controls', - withLayoutControls -); diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index 32d4f6582969e..cdeb90822f0ac 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -12,11 +12,7 @@ import { BaseControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { - createHigherOrderComponent, - pure, - useInstanceId, -} from '@wordpress/compose'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo, Platform } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; @@ -318,44 +314,19 @@ export function PositionPanelPure( { } ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const PositionPanel = pure( PositionPanelPure ); - -/** - * Override the default edit UI to include position controls. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withPositionControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { name: blockName } = props; - const positionSupport = hasBlockSupport( - blockName, - POSITION_SUPPORT_KEY - ); +export default { + edit: function Edit( props ) { const isPositionDisabled = useIsPositionDisabled( props ); - const showPositionControls = positionSupport && ! isPositionDisabled; - - return [ - showPositionControls && ( - - ), - , - ]; + if ( isPositionDisabled ) { + return null; + } + return ; }, - 'withPositionControls' -); + attributeKeys: [ 'style' ], + hasSupport( name ) { + return hasBlockSupport( name, POSITION_SUPPORT_KEY ); + }, +}; /** * Override the default block element to add the position styles. @@ -411,8 +382,3 @@ addFilter( 'core/editor/position/with-position-styles', withPositionStyles ); -addFilter( - 'editor.BlockEdit', - 'core/editor/position/with-inspector-controls', - withPositionControls -); diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 1acb2cda3ac01..4036342316887 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -32,7 +32,6 @@ import { SPACING_SUPPORT_KEY, DimensionsPanel, } from './dimensions'; -import useDisplayBlockControls from '../components/use-display-block-controls'; import { shouldSkipSerialization, useStyleOverride, @@ -356,12 +355,16 @@ function BlockStyleControls( { __unstableParentLayout, } ) { const settings = useBlockSettings( name, __unstableParentLayout ); + const blockEditingMode = useBlockEditingMode(); const passedProps = { clientId, name, setAttributes, settings, }; + if ( blockEditingMode !== 'default' ) { + return null; + } return ( <> @@ -373,34 +376,10 @@ function BlockStyleControls( { ); } -/** - * Override the default edit UI to include new inspector controls for - * all the custom styles configs. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withBlockStyleControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - if ( ! hasStyleSupport( props.name ) ) { - return ; - } - - const shouldDisplayControls = useDisplayBlockControls(); - const blockEditingMode = useBlockEditingMode(); - - return ( - <> - { shouldDisplayControls && blockEditingMode === 'default' && ( - - ) } - - - ); - }, - 'withBlockStyleControls' -); +export default { + edit: BlockStyleControls, + hasSupport: hasStyleSupport, +}; // Defines which element types are supported, including their hover styles or // any other elements that have been included under a single element type @@ -542,12 +521,6 @@ addFilter( addEditProps ); -addFilter( - 'editor.BlockEdit', - 'core/style/with-block-controls', - withBlockStyleControls -); - addFilter( 'editor.BlockListBlock', 'core/editor/with-elements-styles', diff --git a/packages/block-editor/src/hooks/test/align.js b/packages/block-editor/src/hooks/test/align.js index b928e6eafc8b2..c695399e993b0 100644 --- a/packages/block-editor/src/hooks/test/align.js +++ b/packages/block-editor/src/hooks/test/align.js @@ -12,20 +12,12 @@ import { registerBlockType, unregisterBlockType, } from '@wordpress/blocks'; -import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies */ -import BlockControls from '../../components/block-controls'; -import BlockEdit from '../../components/block-edit'; import BlockEditorProvider from '../../components/provider'; -import { - getValidAlignments, - withAlignmentControls, - withDataAlign, - addAssignedAlign, -} from '../align'; +import { getValidAlignments, withDataAlign, addAssignedAlign } from '../align'; const noop = () => {}; @@ -157,68 +149,6 @@ describe( 'align', () => { } ); } ); - describe( 'withAlignControls', () => { - const componentProps = { - name: 'core/foo', - attributes: {}, - isSelected: true, - }; - - it( 'should do nothing if no valid alignments', () => { - registerBlockType( 'core/foo', blockSettings ); - - const EnhancedComponent = withAlignmentControls( - ( { wrapperProps } ) =>
- ); - - render( - - - - - - - ); - - expect( - screen.queryByRole( 'button', { - name: 'Align', - expanded: false, - } ) - ).not.toBeInTheDocument(); - } ); - - it( 'should render toolbar controls if valid alignments', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: false, - }, - } ); - - const EnhancedComponent = withAlignmentControls( - ( { wrapperProps } ) =>
- ); - - render( - - - - - - - ); - - expect( - screen.getAllByRole( 'button', { - name: 'Align', - expanded: false, - } ) - ).toHaveLength( 2 ); - } ); - } ); - describe( 'withDataAlign', () => { it( 'should render with wrapper props', () => { registerBlockType( 'core/foo', { diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 5985b821d00a8..98638ae5dabf5 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -4,10 +4,13 @@ import { getBlockSupport } from '@wordpress/blocks'; import { useMemo, useEffect, useId } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ +import useDisplayBlockControls from '../components/use-display-block-controls'; import { useSettings } from '../components'; import { useSettingsForBlockElement } from '../components/global-styles/hooks'; import { getValueFromObjectPath, setImmutably } from '../utils/object'; @@ -362,3 +365,68 @@ export function useBlockSettings( name, parentLayout ) { return useSettingsForBlockElement( rawSettings, name ); } + +export function createBlockEditFilter( features ) { + // We don't want block controls to re-render when typing inside a block. + // `pure` will prevent re-renders unless props change, so only pass the + // needed props and not the whole attributes object. + features = features.map( ( settings ) => { + return { ...settings, Edit: pure( settings.edit ) }; + } ); + const withBlockEditHooks = createHigherOrderComponent( + ( OriginalBlockEdit ) => ( props ) => { + const { isDisplayed, isParentDisplayed } = + useDisplayBlockControls(); + // CAUTION: code added before this line will be executed for all + // blocks, not just those that support the feature! Code added + // above this line should be carefully evaluated for its impact on + // performance. + return [ + ...features.map( ( feature, i ) => { + const { + Edit, + hasSupport, + attributeKeys = [], + shareWithChildBlocks, + } = feature; + const shouldDisplayControls = + isDisplayed || + ( isParentDisplayed && shareWithChildBlocks ); + + if ( + ! shouldDisplayControls || + ! hasSupport( props.name ) + ) { + return null; + } + + const neededProps = {}; + for ( const key of attributeKeys ) { + if ( props.attributes[ key ] ) { + neededProps[ key ] = props.attributes[ key ]; + } + } + return ( + + ); + } ), + , + ]; + }, + 'withBlockEditHooks' + ); + addFilter( 'editor.BlockEdit', 'core/editor/hooks', withBlockEditHooks ); +} From 6a42225124e69276a2deec4597a855bb504b37cc Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:41:11 +0200 Subject: [PATCH 11/38] RichText: pass value to store (#43204) --- package-lock.json | 2 + .../src/components/rich-text/content.js | 47 ++++--- .../rich-text/get-rich-text-values.js | 7 +- .../components/rich-text/use-input-rules.js | 7 +- packages/block-editor/src/utils/selection.js | 11 +- packages/block-library/src/audio/block.json | 4 +- packages/block-library/src/button/block.json | 4 +- packages/block-library/src/code/block.json | 4 +- packages/block-library/src/code/save.js | 5 +- packages/block-library/src/details/block.json | 4 +- packages/block-library/src/embed/block.json | 4 +- packages/block-library/src/file/block.json | 8 +- packages/block-library/src/file/save.js | 6 +- .../block-library/src/form-input/block.json | 4 +- packages/block-library/src/gallery/block.json | 8 +- packages/block-library/src/heading/block.json | 5 +- packages/block-library/src/image/block.json | 4 +- .../block-library/src/list-item/block.json | 5 +- .../block-library/src/paragraph/block.json | 5 +- .../block-library/src/preformatted/block.json | 5 +- .../block-library/src/pullquote/block.json | 9 +- packages/block-library/src/quote/block.json | 5 +- packages/block-library/src/table/block.json | 19 ++- .../src/utils/remove-anchor-tag.js | 3 +- packages/block-library/src/verse/block.json | 5 +- packages/block-library/src/video/block.json | 4 +- packages/blocks/package.json | 1 + packages/blocks/src/api/matchers.js | 12 ++ .../src/api/parser/get-block-attributes.js | 25 +++- .../api/raw-handling/test/paste-handler.js | 4 +- packages/blocks/src/api/utils.js | 42 +++++- .../src/footnotes/get-footnotes-order.js | 17 +-- packages/core-data/src/footnotes/index.js | 22 +-- packages/rich-text/README.md | 13 ++ packages/rich-text/src/component/index.js | 38 ++++-- packages/rich-text/src/create.js | 129 +++++++++++++++++- packages/rich-text/src/index.ts | 2 +- schemas/json/block.json | 2 + .../blocks/core__gallery__deprecated-1.json | 2 + .../documents/ms-word-online-out.html | 14 +- .../non-matched-tags-handling.test.js | 8 +- 41 files changed, 380 insertions(+), 145 deletions(-) diff --git a/package-lock.json b/package-lock.json index e59b7b849ebd9..86cf832a8e41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54674,6 +54674,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", @@ -70141,6 +70142,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index 9762582f86f14..92e150fb174ed 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -5,36 +5,43 @@ import { RawHTML } from '@wordpress/element'; import { children as childrenSource } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; +/** + * Internal dependencies + */ +import RichText from './'; + /** * Internal dependencies */ import { getMultilineTag } from './utils'; -export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { - // Handle deprecated `children` and `node` sources. - if ( Array.isArray( value ) ) { +export function Content( { + value, + tagName: Tag, + multiline, + format, + ...props +} ) { + if ( RichText.isEmpty( value ) ) { + const MultilineTag = getMultilineTag( multiline ); + value = MultilineTag ? : null; + } else if ( Array.isArray( value ) ) { deprecated( 'wp.blockEditor.RichText value prop as children type', { since: '6.1', version: '6.3', alternative: 'value prop as string', link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/', } ); - - value = childrenSource.toHTML( value ); - } - - const MultilineTag = getMultilineTag( multiline ); - - if ( ! value && MultilineTag ) { - value = `<${ MultilineTag }>`; - } - - const content = { value }; - - if ( Tag ) { - const { format, ...restProps } = props; - return { content }; + value = { childrenSource.toHTML( value ) }; + } else if ( typeof value === 'string' ) { + // To do: deprecate. + value = { value }; + } else { + // To do: create a toReactComponent method on RichTextData, which we + // might in the future also use for the editable tree. See + // https://github.com/WordPress/gutenberg/pull/41655. + value = { value.toHTMLString() }; } - return content; -}; + return Tag ? { value } : value; +} diff --git a/packages/block-editor/src/components/rich-text/get-rich-text-values.js b/packages/block-editor/src/components/rich-text/get-rich-text-values.js index bd1c62ea5e6f6..ee2bc63826930 100644 --- a/packages/block-editor/src/components/rich-text/get-rich-text-values.js +++ b/packages/block-editor/src/components/rich-text/get-rich-text-values.js @@ -6,6 +6,7 @@ import { getSaveElement, __unstableGetBlockProps as getBlockProps, } from '@wordpress/blocks'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies @@ -95,5 +96,9 @@ export function getRichTextValues( blocks = [] ) { const values = []; addValuesForBlocks( values, blocks ); getBlockProps.skipFilters = false; - return values; + return values.map( ( value ) => + value instanceof RichTextData + ? value + : RichTextData.fromHTMLString( value ) + ); } diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 5aa47e7c7b4d7..5640a85f5f269 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -28,7 +28,12 @@ function findSelection( blocks ) { if ( attributeKey ) { blocks[ i ].attributes[ attributeKey ] = blocks[ i ].attributes[ attributeKey - ].replace( START_OF_SELECTED_AREA, '' ); + ] + // To do: refactor this to use rich text's selection instead, so + // we no longer have to use on this hack inserting a special + // character. + .toString() + .replace( START_OF_SELECTED_AREA, '' ); return [ blocks[ i ].clientId, attributeKey, 0, 0 ]; } diff --git a/packages/block-editor/src/utils/selection.js b/packages/block-editor/src/utils/selection.js index 68c634d591c5e..4e97148583879 100644 --- a/packages/block-editor/src/utils/selection.js +++ b/packages/block-editor/src/utils/selection.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { RichTextData } from '@wordpress/rich-text'; + /** * A robust way to retain selection position through various * transforms is to insert a special character at the position and @@ -19,8 +24,10 @@ export function retrieveSelectedAttribute( blockAttributes ) { return Object.keys( blockAttributes ).find( ( name ) => { const value = blockAttributes[ name ]; return ( - typeof value === 'string' && - value.indexOf( START_OF_SELECTED_AREA ) !== -1 + ( typeof value === 'string' || value instanceof RichTextData ) && + // To do: refactor this to use rich text's selection instead, so we + // no longer have to use on this hack inserting a special character. + value.toString().indexOf( START_OF_SELECTED_AREA ) !== -1 ); } ); } diff --git a/packages/block-library/src/audio/block.json b/packages/block-library/src/audio/block.json index a4740e304451c..04df268a74a63 100644 --- a/packages/block-library/src/audio/block.json +++ b/packages/block-library/src/audio/block.json @@ -16,8 +16,8 @@ "__experimentalRole": "content" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index eec327b4ca48e..3c232700a876e 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -36,8 +36,8 @@ "__experimentalRole": "content" }, "text": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "a,button", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/code/block.json b/packages/block-library/src/code/block.json index 80df74b5062b5..bd5db3c918b96 100644 --- a/packages/block-library/src/code/block.json +++ b/packages/block-library/src/code/block.json @@ -8,8 +8,8 @@ "textdomain": "default", "attributes": { "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "code", "__unstablePreserveWhiteSpace": true } diff --git a/packages/block-library/src/code/save.js b/packages/block-library/src/code/save.js index 7dd355f3855a8..5bb9f68767b5e 100644 --- a/packages/block-library/src/code/save.js +++ b/packages/block-library/src/code/save.js @@ -13,7 +13,10 @@ export default function save( { attributes } ) {
 			
 		
); diff --git a/packages/block-library/src/details/block.json b/packages/block-library/src/details/block.json index d449d42e1e10c..a71d3af2a5ed3 100644 --- a/packages/block-library/src/details/block.json +++ b/packages/block-library/src/details/block.json @@ -13,8 +13,8 @@ "default": false }, "summary": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "summary" } }, diff --git a/packages/block-library/src/embed/block.json b/packages/block-library/src/embed/block.json index 9ca54db871db1..5aac8bbd6b8ca 100644 --- a/packages/block-library/src/embed/block.json +++ b/packages/block-library/src/embed/block.json @@ -12,8 +12,8 @@ "__experimentalRole": "content" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/file/block.json b/packages/block-library/src/file/block.json index 0cc20b3f501e9..9dc6677e4adce 100644 --- a/packages/block-library/src/file/block.json +++ b/packages/block-library/src/file/block.json @@ -21,8 +21,8 @@ "attribute": "id" }, "fileName": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "a:not([download])" }, "textLinkHref": { @@ -42,8 +42,8 @@ "default": true }, "downloadButtonText": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "a[download]" }, "displayPreview": { diff --git a/packages/block-library/src/file/save.js b/packages/block-library/src/file/save.js index 6d0684ac76b8e..f5eb1ce3c2b14 100644 --- a/packages/block-library/src/file/save.js +++ b/packages/block-library/src/file/save.js @@ -25,7 +25,11 @@ export default function save( { attributes } ) { previewHeight, } = attributes; - const pdfEmbedLabel = RichText.isEmpty( fileName ) ? 'PDF embed' : fileName; + const pdfEmbedLabel = RichText.isEmpty( fileName ) + ? 'PDF embed' + : // To do: use toPlainText, but we need ensure it's RichTextData. See + // https://github.com/WordPress/gutenberg/pull/56710. + fileName.toString(); const hasFilename = ! RichText.isEmpty( fileName ); diff --git a/packages/block-library/src/form-input/block.json b/packages/block-library/src/form-input/block.json index 067b7ac69430c..53aa0be6744cb 100644 --- a/packages/block-library/src/form-input/block.json +++ b/packages/block-library/src/form-input/block.json @@ -19,10 +19,10 @@ "type": "string" }, "label": { - "type": "string", + "type": "rich-text", "default": "Label", "selector": ".wp-block-form-input__label-content", - "source": "html", + "source": "rich-text", "__experimentalRole": "content" }, "inlineLabel": { diff --git a/packages/block-library/src/gallery/block.json b/packages/block-library/src/gallery/block.json index 0867989af4ec7..fad92aed59bf7 100644 --- a/packages/block-library/src/gallery/block.json +++ b/packages/block-library/src/gallery/block.json @@ -46,8 +46,8 @@ "attribute": "data-id" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": ".blocks-gallery-item__caption" } } @@ -72,8 +72,8 @@ "maximum": 8 }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": ".blocks-gallery-caption" }, "imageCrop": { diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index dfd5bb72b6331..72cc67caddd9e 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -12,10 +12,9 @@ "type": "string" }, "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "h1,h2,h3,h4,h5,h6", - "default": "", "__experimentalRole": "content" }, "level": { diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index cfe91a71ff4f9..c5191e3dd8654 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -25,8 +25,8 @@ "__experimentalRole": "content" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/list-item/block.json b/packages/block-library/src/list-item/block.json index 07797be8623a5..06997c2ac23f8 100644 --- a/packages/block-library/src/list-item/block.json +++ b/packages/block-library/src/list-item/block.json @@ -12,10 +12,9 @@ "type": "string" }, "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "li", - "default": "", "__experimentalRole": "content" } }, diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 85f56f4a838f5..3fe4fbb34e102 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -13,10 +13,9 @@ "type": "string" }, "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "p", - "default": "", "__experimentalRole": "content" }, "dropCap": { diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index ec6ea839385eb..def870e7ad2fb 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -8,10 +8,9 @@ "textdomain": "default", "attributes": { "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "pre", - "default": "", "__unstablePreserveWhiteSpace": true, "__experimentalRole": "content" } diff --git a/packages/block-library/src/pullquote/block.json b/packages/block-library/src/pullquote/block.json index 1d6c74dbc4ae0..7fc81d5683bd1 100644 --- a/packages/block-library/src/pullquote/block.json +++ b/packages/block-library/src/pullquote/block.json @@ -8,16 +8,15 @@ "textdomain": "default", "attributes": { "value": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "p", "__experimentalRole": "content" }, "citation": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "cite", - "default": "", "__experimentalRole": "content" }, "textAlign": { diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index 7ed406c0d2096..9deca000efe06 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -17,10 +17,9 @@ "__experimentalRole": "content" }, "citation": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "cite", - "default": "", "__experimentalRole": "content" }, "align": { diff --git a/packages/block-library/src/table/block.json b/packages/block-library/src/table/block.json index d1139d6c55add..470886a1247fe 100644 --- a/packages/block-library/src/table/block.json +++ b/packages/block-library/src/table/block.json @@ -12,10 +12,9 @@ "default": false }, "caption": { - "type": "string", - "source": "html", - "selector": "figcaption", - "default": "" + "type": "rich-text", + "source": "rich-text", + "selector": "figcaption" }, "head": { "type": "array", @@ -30,8 +29,8 @@ "selector": "td,th", "query": { "content": { - "type": "string", - "source": "html" + "type": "rich-text", + "source": "rich-text" }, "tag": { "type": "string", @@ -75,8 +74,8 @@ "selector": "td,th", "query": { "content": { - "type": "string", - "source": "html" + "type": "rich-text", + "source": "rich-text" }, "tag": { "type": "string", @@ -120,8 +119,8 @@ "selector": "td,th", "query": { "content": { - "type": "string", - "source": "html" + "type": "rich-text", + "source": "rich-text" }, "tag": { "type": "string", diff --git a/packages/block-library/src/utils/remove-anchor-tag.js b/packages/block-library/src/utils/remove-anchor-tag.js index 31d1877082f50..82e7b03423648 100644 --- a/packages/block-library/src/utils/remove-anchor-tag.js +++ b/packages/block-library/src/utils/remove-anchor-tag.js @@ -6,5 +6,6 @@ * @return {string} The value with anchor tags removed. */ export default function removeAnchorTag( value ) { - return value.replace( /<\/?a[^>]*>/g, '' ); + // To do: Refactor this to use rich text's removeFormat instead. + return value.toString().replace( /<\/?a[^>]*>/g, '' ); } diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index fa0bc30798212..846a1dc99caaf 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -9,10 +9,9 @@ "textdomain": "default", "attributes": { "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "pre", - "default": "", "__unstablePreserveWhiteSpace": true, "__experimentalRole": "content" }, diff --git a/packages/block-library/src/video/block.json b/packages/block-library/src/video/block.json index debe6f20fe53f..5d4680f39e79a 100644 --- a/packages/block-library/src/video/block.json +++ b/packages/block-library/src/video/block.json @@ -15,8 +15,8 @@ "attribute": "autoplay" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 961cb338d7337..928d9d94740b4 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -42,6 +42,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", diff --git a/packages/blocks/src/api/matchers.js b/packages/blocks/src/api/matchers.js index 7a6ac84891658..950f1539440a0 100644 --- a/packages/blocks/src/api/matchers.js +++ b/packages/blocks/src/api/matchers.js @@ -3,6 +3,11 @@ */ export { attr, prop, text, query } from 'hpq'; +/** + * WordPress dependencies + */ +import { RichTextData } from '@wordpress/rich-text'; + /** * Internal dependencies */ @@ -41,3 +46,10 @@ export function html( selector, multilineTag ) { return match.innerHTML; }; } + +export const richText = ( selector, preserveWhiteSpace ) => ( el ) => { + const target = selector ? el.querySelector( selector ) : el; + return target + ? RichTextData.fromHTMLElement( target, { preserveWhiteSpace } ) + : RichTextData.empty(); +}; diff --git a/packages/blocks/src/api/parser/get-block-attributes.js b/packages/blocks/src/api/parser/get-block-attributes.js index cc81c10800552..24faae7370463 100644 --- a/packages/blocks/src/api/parser/get-block-attributes.js +++ b/packages/blocks/src/api/parser/get-block-attributes.js @@ -9,12 +9,22 @@ import memoize from 'memize'; */ import { pipe } from '@wordpress/compose'; import { applyFilters } from '@wordpress/hooks'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies */ -import { attr, html, text, query, node, children, prop } from '../matchers'; -import { normalizeBlockType } from '../utils'; +import { + attr, + html, + text, + query, + node, + children, + prop, + richText, +} from '../matchers'; +import { normalizeBlockType, getDefault } from '../utils'; /** * Higher-order hpq matcher which enhances an attribute matcher to return true @@ -58,6 +68,9 @@ export const toBooleanAttributeMatcher = ( matcher ) => */ export function isOfType( value, type ) { switch ( type ) { + case 'rich-text': + return value instanceof RichTextData; + case 'string': return typeof value === 'string'; @@ -134,6 +147,7 @@ export function getBlockAttribute( case 'property': case 'html': case 'text': + case 'rich-text': case 'children': case 'node': case 'query': @@ -152,7 +166,7 @@ export function getBlockAttribute( } if ( value === undefined ) { - value = attributeSchema.default; + value = getDefault( attributeSchema ); } return value; @@ -211,6 +225,11 @@ export const matcherFromSource = memoize( ( sourceConfig ) => { return html( sourceConfig.selector, sourceConfig.multiline ); case 'text': return text( sourceConfig.selector ); + case 'rich-text': + return richText( + sourceConfig.selector, + sourceConfig.__unstablePreserveWhiteSpace + ); case 'children': return children( sourceConfig.selector ); case 'node': diff --git a/packages/blocks/src/api/raw-handling/test/paste-handler.js b/packages/blocks/src/api/raw-handling/test/paste-handler.js index 6938ad0d9c408..9b3dad39a0a5b 100644 --- a/packages/blocks/src/api/raw-handling/test/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/test/paste-handler.js @@ -73,9 +73,9 @@ describe( 'pasteHandler', () => { expect( console ).toHaveLogged(); + delete result.attributes.caption; expect( result.attributes ).toEqual( { hasFixedLayout: false, - caption: '', head: [ { cells: [ @@ -113,9 +113,9 @@ describe( 'pasteHandler', () => { expect( console ).toHaveLogged(); + delete result.attributes.caption; expect( result.attributes ).toEqual( { hasFixedLayout: false, - caption: '', head: [ { cells: [ diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index c43445c627226..60a94117b36e2 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -11,6 +11,7 @@ import a11yPlugin from 'colord/plugins/a11y'; import { Component, isValidElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies @@ -47,8 +48,12 @@ export function isUnmodifiedBlock( block ) { const newBlock = isUnmodifiedBlock[ block.name ]; const blockType = getBlockType( block.name ); - return Object.keys( blockType?.attributes ?? {} ).every( - ( key ) => newBlock.attributes[ key ] === block.attributes[ key ] + function isEqual( a, b ) { + return ( a?.valueOf() ?? a ) === ( b?.valueOf() ?? b ); + } + + return Object.keys( blockType?.attributes ?? {} ).every( ( key ) => + isEqual( newBlock.attributes[ key ], block.attributes[ key ] ) ); } @@ -243,6 +248,16 @@ export function getAccessibleBlockLabel( ); } +export function getDefault( attributeSchema ) { + if ( attributeSchema.default !== undefined ) { + return attributeSchema.default; + } + + if ( attributeSchema.type === 'rich-text' ) { + return new RichTextData(); + } +} + /** * Ensure attributes contains only values defined by block type, and merge * default values for missing attributes. @@ -264,9 +279,26 @@ export function __experimentalSanitizeBlockAttributes( name, attributes ) { const value = attributes[ key ]; if ( undefined !== value ) { - accumulator[ key ] = value; - } else if ( schema.hasOwnProperty( 'default' ) ) { - accumulator[ key ] = schema.default; + if ( schema.type === 'rich-text' ) { + if ( value instanceof RichTextData ) { + accumulator[ key ] = value; + } else if ( typeof value === 'string' ) { + accumulator[ key ] = + RichTextData.fromHTMLString( value ); + } + } else if ( + schema.type === 'string' && + value instanceof RichTextData + ) { + accumulator[ key ] = value.toHTMLString(); + } else { + accumulator[ key ] = value; + } + } else { + const _default = getDefault( schema ); + if ( undefined !== _default ) { + accumulator[ key ] = _default; + } } if ( [ 'node', 'children' ].indexOf( schema.source ) !== -1 ) { diff --git a/packages/core-data/src/footnotes/get-footnotes-order.js b/packages/core-data/src/footnotes/get-footnotes-order.js index 42adeed7621e8..fcaeae660ec1a 100644 --- a/packages/core-data/src/footnotes/get-footnotes-order.js +++ b/packages/core-data/src/footnotes/get-footnotes-order.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { create } from '@wordpress/rich-text'; - /** * Internal dependencies */ @@ -14,18 +9,16 @@ function getBlockFootnotesOrder( block ) { if ( ! cache.has( block ) ) { const order = []; for ( const value of getRichTextValuesCached( block ) ) { - if ( ! value || ! value.includes( 'data-fn' ) ) { + if ( ! value ) { continue; } // replacements is a sparse array, use forEach to skip empty slots. - create( { html: value } ).replacements.forEach( - ( { type, attributes } ) => { - if ( type === 'core/footnote' ) { - order.push( attributes[ 'data-fn' ] ); - } + value.replacements.forEach( ( { type, attributes } ) => { + if ( type === 'core/footnote' ) { + order.push( attributes[ 'data-fn' ] ); } - ); + } ); } cache.set( block, order ); } diff --git a/packages/core-data/src/footnotes/index.js b/packages/core-data/src/footnotes/index.js index fa1c5fad5c7e7..9458290f9cb40 100644 --- a/packages/core-data/src/footnotes/index.js +++ b/packages/core-data/src/footnotes/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { create, toHTMLString } from '@wordpress/rich-text'; +import { RichTextData, create, toHTMLString } from '@wordpress/rich-text'; /** * Internal dependencies @@ -53,15 +53,18 @@ export function updateFootnotesFromMeta( blocks, meta ) { continue; } - if ( typeof value !== 'string' ) { + // To do, remove support for string values? + if ( + typeof value !== 'string' && + ! ( value instanceof RichTextData ) + ) { continue; } - if ( value.indexOf( 'data-fn' ) === -1 ) { - continue; - } - - const richTextValue = create( { html: value } ); + const richTextValue = + typeof value === 'string' + ? RichTextData.fromHTMLString( value ) + : value; richTextValue.replacements.forEach( ( replacement ) => { if ( replacement.type === 'core/footnote' ) { @@ -78,7 +81,10 @@ export function updateFootnotesFromMeta( blocks, meta ) { } } ); - attributes[ key ] = toHTMLString( { value: richTextValue } ); + attributes[ key ] = + typeof value === 'string' + ? richTextValue.toHTMLString() + : richTextValue; } return attributes; diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 90726ff238c1b..90fd15e1c905c 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -355,6 +355,19 @@ _Returns_ - `RichTextValue`: A new value with replacements applied. +### RichTextData + +The RichTextData class is used to instantiate a wrapper around rich text values, with methods that can be used to transform or manipulate the data. + +- Create an emtpy instance: `new RichTextData()`. +- Create one from an html string: `RichTextData.fromHTMLString( +'hello' )`. +- Create one from a wrapper HTMLElement: `RichTextData.fromHTMLElement( +document.querySelector( 'p' ) )`. +- Create one from plain text: `RichTextData.fromPlainText( '1\n2' )`. +- Create one from a rich text value: `new RichTextData( { text: '...', +formats: [ ... ] } )`. + ### RichTextValue An object which represents a formatted string. See main `@wordpress/rich-text` documentation for more information. diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 0e9291b7a5e03..a2b5734d5c204 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -8,7 +8,7 @@ import { useRegistry } from '@wordpress/data'; /** * Internal dependencies */ -import { collapseWhiteSpace, create } from '../create'; +import { create, RichTextData } from '../create'; import { apply } from '../to-dom'; import { toHTMLString } from '../to-html-string'; import { useDefaultStyle } from './use-default-style'; @@ -70,11 +70,18 @@ export function useRichText( { function setRecordFromProps() { _value.current = value; - record.current = create( { - html: preserveWhiteSpace - ? value - : collapseWhiteSpace( typeof value === 'string' ? value : '' ), - } ); + record.current = value; + if ( ! ( value instanceof RichTextData ) ) { + record.current = value + ? RichTextData.fromHTMLString( value, { preserveWhiteSpace } ) + : RichTextData.empty(); + } + // To do: make rich text internally work with RichTextData. + record.current = { + text: record.current.text, + formats: record.current.formats, + replacements: record.current.replacements, + }; if ( disableFormats ) { record.current.formats = Array( value.length ); record.current.replacements = Array( value.length ); @@ -117,17 +124,18 @@ export function useRichText( { if ( disableFormats ) { _value.current = newRecord.text; } else { - _value.current = toHTMLString( { - value: __unstableBeforeSerialize - ? { - ...newRecord, - formats: __unstableBeforeSerialize( newRecord ), - } - : newRecord, - } ); + const newFormats = __unstableBeforeSerialize + ? __unstableBeforeSerialize( newRecord ) + : newRecord.formats; + newRecord = { ...newRecord, formats: newFormats }; + if ( typeof value === 'string' ) { + _value.current = toHTMLString( { value: newRecord } ); + } else { + _value.current = new RichTextData( newRecord ); + } } - const { start, end, formats, text } = newRecord; + const { start, end, formats, text } = record.current; // Selection must be updated first, so it is recorded in history when // the content change happens. diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index a23baf70078bc..a35fabbd4e2fa 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -10,6 +10,8 @@ import { store as richTextStore } from './store'; import { createElement } from './create-element'; import { mergePair } from './concat'; import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters'; +import { toHTMLString } from './to-html-string'; +import { getTextContent } from './get-text-content'; /** @typedef {import('./types').RichTextValue} RichTextValue */ @@ -96,6 +98,86 @@ function toFormat( { tagName, attributes } ) { }; } +// Ideally we use a private property. +const RichTextInternalData = Symbol( 'RichTextInternalData' ); + +/** + * The RichTextData class is used to instantiate a wrapper around rich text + * values, with methods that can be used to transform or manipulate the data. + * + * - Create an emtpy instance: `new RichTextData()`. + * - Create one from an html string: `RichTextData.fromHTMLString( + * 'hello' )`. + * - Create one from a wrapper HTMLElement: `RichTextData.fromHTMLElement( + * document.querySelector( 'p' ) )`. + * - Create one from plain text: `RichTextData.fromPlainText( '1\n2' )`. + * - Create one from a rich text value: `new RichTextData( { text: '...', + * formats: [ ... ] } )`. + * + * @todo Add methods to manipulate the data, such as applyFormat, slice etc. + */ +export class RichTextData { + static empty() { + return new RichTextData(); + } + static fromPlainText( text ) { + return new RichTextData( create( { text } ) ); + } + static fromHTMLString( html ) { + return new RichTextData( create( { html } ) ); + } + static fromHTMLElement( htmlElement, options = {} ) { + const { preserveWhiteSpace = false } = options; + const element = preserveWhiteSpace + ? htmlElement + : collapseWhiteSpace( htmlElement ); + const richTextData = new RichTextData( create( { element } ) ); + Object.defineProperty( richTextData, 'originalHTML', { + value: htmlElement.innerHTML, + } ); + return richTextData; + } + constructor( init = createEmptyValue() ) { + // Setting text, formats, and replacements as enumerable properties + // unfortunately visualises these in the e2e tests. As long as the class + // instance doesn't have any enumerable properties, it will be + // visualised as a string. + Object.defineProperty( this, RichTextInternalData, { value: init } ); + } + toPlainText() { + return getTextContent( this[ RichTextInternalData ] ); + } + // We could expose `toHTMLElement` at some point as well, but we'd only use + // it internally. + toHTMLString() { + return ( + this.originalHTML || + toHTMLString( { value: this[ RichTextInternalData ] } ) + ); + } + valueOf() { + return this.toHTMLString(); + } + toString() { + return this.toHTMLString(); + } + toJSON() { + return this.toHTMLString(); + } + get length() { + return this.text.length; + } + get formats() { + return this[ RichTextInternalData ].formats; + } + get replacements() { + return this[ RichTextInternalData ].replacements; + } + get text() { + return this[ RichTextInternalData ].text; + } +} + /** * Create a RichText value from an `Element` tree (DOM), an HTML string or a * plain text string, with optionally a `Range` object to set the selection. If @@ -128,7 +210,6 @@ function toFormat( { tagName, attributes } ) { * @param {string} [$1.html] HTML to create value from. * @param {Range} [$1.range] Range to create value from. * @param {boolean} [$1.__unstableIsEditableTree] - * * @return {RichTextValue} A rich text value. */ export function create( { @@ -138,6 +219,14 @@ export function create( { range, __unstableIsEditableTree: isEditableTree, } = {} ) { + if ( html instanceof RichTextData ) { + return { + text: html.text, + formats: html.formats, + replacements: html.replacements, + }; + } + if ( typeof text === 'string' && text.length > 0 ) { return { formats: Array( text.length ), @@ -268,10 +357,42 @@ function filterRange( node, range, filter ) { * @see * https://developer.mozilla.org/en-US/docs/Web/CSS/white-space-collapse#collapsing_of_white_space * - * @param {string} string + * @param {HTMLElement} element + * @param {boolean} isRoot + * + * @return {HTMLElement} New element with collapsed whitespace. */ -export function collapseWhiteSpace( string ) { - return string.replace( /[\n\r\t]+/g, ' ' ); +function collapseWhiteSpace( element, isRoot = true ) { + const clone = element.cloneNode( true ); + clone.normalize(); + Array.from( clone.childNodes ).forEach( ( node, i, nodes ) => { + if ( node.nodeType === node.TEXT_NODE ) { + let newNodeValue = node.nodeValue; + + if ( /[\n\t\r\f]/.test( newNodeValue ) ) { + newNodeValue = newNodeValue.replace( /[\n\t\r\f]+/g, ' ' ); + } + + if ( newNodeValue.indexOf( ' ' ) !== -1 ) { + newNodeValue = newNodeValue.replace( / {2,}/g, ' ' ); + } + + if ( i === 0 && newNodeValue.startsWith( ' ' ) ) { + newNodeValue = newNodeValue.slice( 1 ); + } else if ( + isRoot && + i === nodes.length - 1 && + newNodeValue.endsWith( ' ' ) + ) { + newNodeValue = newNodeValue.slice( 0, -1 ); + } + + node.nodeValue = newNodeValue; + } else if ( node.nodeType === node.ELEMENT_NODE ) { + collapseWhiteSpace( node, false ); + } + } ); + return clone; } /** diff --git a/packages/rich-text/src/index.ts b/packages/rich-text/src/index.ts index 14d26cab8f7fb..f82317d81573d 100644 --- a/packages/rich-text/src/index.ts +++ b/packages/rich-text/src/index.ts @@ -1,7 +1,7 @@ export { store } from './store'; export { applyFormat } from './apply-format'; export { concat } from './concat'; -export { create } from './create'; +export { RichTextData, create } from './create'; export { getActiveFormat } from './get-active-format'; export { getActiveFormats } from './get-active-formats'; export { getActiveObject } from './get-active-object'; diff --git a/schemas/json/block.json b/schemas/json/block.json index d5b4a04452eaa..7e0c8715a4abd 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -114,6 +114,7 @@ "object", "array", "string", + "rich-text", "integer", "number" ] @@ -159,6 +160,7 @@ "enum": [ "attribute", "text", + "rich-text", "html", "raw", "query", diff --git a/test/integration/fixtures/blocks/core__gallery__deprecated-1.json b/test/integration/fixtures/blocks/core__gallery__deprecated-1.json index 9e15ee7f1c714..bd6108a97230a 100644 --- a/test/integration/fixtures/blocks/core__gallery__deprecated-1.json +++ b/test/integration/fixtures/blocks/core__gallery__deprecated-1.json @@ -16,6 +16,7 @@ "attributes": { "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==", "alt": "title", + "caption": "", "linkDestination": "none" }, "innerBlocks": [] @@ -26,6 +27,7 @@ "attributes": { "url": "data:image/jpeg;base64,/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=", "alt": "title", + "caption": "", "linkDestination": "none" }, "innerBlocks": [] diff --git a/test/integration/fixtures/documents/ms-word-online-out.html b/test/integration/fixtures/documents/ms-word-online-out.html index 398281520f254..8187b598f9a91 100644 --- a/test/integration/fixtures/documents/ms-word-online-out.html +++ b/test/integration/fixtures/documents/ms-word-online-out.html @@ -8,33 +8,33 @@
    -
  • +
  • -
  • Bulleted 
  • +
  • Bulleted 
  • -
  • Indented 
  • +
  • Indented 
  • -
  • List 
  • +
  • List 
    -
  1. One 
  2. +
  3. One 
  4. -
  5. Two 
  6. +
  7. Two 
  8. -
  9. Three 
  10. +
  11. Three 
diff --git a/test/integration/non-matched-tags-handling.test.js b/test/integration/non-matched-tags-handling.test.js index 67438192f1368..451a628c32977 100644 --- a/test/integration/non-matched-tags-handling.test.js +++ b/test/integration/non-matched-tags-handling.test.js @@ -19,9 +19,9 @@ describe( 'Handling of non matched tags in block transforms', () => { expect( simplePreformattedResult ).toHaveLength( 1 ); expect( simplePreformattedResult[ 0 ].name ).toBe( 'core/paragraph' ); - expect( simplePreformattedResult[ 0 ].attributes.content ).toBe( - 'Pre' - ); + expect( + simplePreformattedResult[ 0 ].attributes.content.valueOf() + ).toBe( 'Pre' ); const codeResult = pasteHandler( { HTML: '
code
', @@ -30,7 +30,7 @@ describe( 'Handling of non matched tags in block transforms', () => { expect( codeResult ).toHaveLength( 1 ); expect( codeResult[ 0 ].name ).toBe( 'core/code' ); - expect( codeResult[ 0 ].attributes.content ).toBe( 'code' ); + expect( codeResult[ 0 ].attributes.content.valueOf() ).toBe( 'code' ); expect( console ).toHaveLogged(); } ); } ); From f6adf1e98b7e49cea4c95281ad225128b94e4e13 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:13:22 -0800 Subject: [PATCH 12/38] `FontSizePicker`: Add opt-in prop for 40px default size (#56804) * `FontSizePicker`: Add opt-in prop for 40px default size * Update button when __next40pxDefaultSize is true * Update changelog --- packages/components/CHANGELOG.md | 1 + .../font-size-picker/font-size-picker-select.tsx | 2 ++ .../font-size-picker-toggle-group.tsx | 10 +++++++++- packages/components/src/font-size-picker/index.tsx | 14 +++++++++++--- packages/components/src/font-size-picker/types.ts | 9 ++++++++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index c4153503fd798..8a6d52e758283 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,7 @@ - `PaletteEdit`: Gradient pickers to use same width as color pickers ([#56801](https://github.com/WordPress/gutenberg/pull/56801)). - `FocalPointPicker`: Add opt-in prop for 40px default size ([#56021](https://github.com/WordPress/gutenberg/pull/56021)). - `DimensionControl`: Add opt-in prop for 40px default size ([#56805](https://github.com/WordPress/gutenberg/pull/56805)). +- `FontSizePicker`: Add opt-in prop for 40px default size ([#56804](https://github.com/WordPress/gutenberg/pull/56804)). ### Bug Fix diff --git a/packages/components/src/font-size-picker/font-size-picker-select.tsx b/packages/components/src/font-size-picker/font-size-picker-select.tsx index d3fc2ffe4a61f..32438cfab8115 100644 --- a/packages/components/src/font-size-picker/font-size-picker-select.tsx +++ b/packages/components/src/font-size-picker/font-size-picker-select.tsx @@ -27,6 +27,7 @@ const CUSTOM_OPTION: FontSizePickerSelectOption = { const FontSizePickerSelect = ( props: FontSizePickerSelectProps ) => { const { + __next40pxDefaultSize, fontSizes, value, disableCustomFontSizes, @@ -67,6 +68,7 @@ const FontSizePickerSelect = ( props: FontSizePickerSelectProps ) => { return ( { - const { fontSizes, value, __nextHasNoMarginBottom, size, onChange } = props; + const { + fontSizes, + value, + __nextHasNoMarginBottom, + __next40pxDefaultSize, + size, + onChange, + } = props; return ( { if ( newValue === undefined ) { @@ -214,6 +217,7 @@ const UnforwardedFontSizePicker = ( { __( 'Reset' ) } diff --git a/packages/components/src/font-size-picker/types.ts b/packages/components/src/font-size-picker/types.ts index f4d00c2d3ce67..9363417222458 100644 --- a/packages/components/src/font-size-picker/types.ts +++ b/packages/components/src/font-size-picker/types.ts @@ -57,6 +57,12 @@ export type FontSizePickerProps = { * @default false */ __nextHasNoMarginBottom?: boolean; + /** + * Start opting into the larger default height that will become the default size in a future version. + * + * @default false + */ + __next40pxDefaultSize?: boolean; /** * Size of the control. * @@ -93,6 +99,7 @@ export type FontSizePickerSelectProps = Pick< >; onChange: NonNullable< FontSizePickerProps[ 'onChange' ] >; onSelectCustom: () => void; + __next40pxDefaultSize: boolean; }; export type FontSizePickerSelectOption = { @@ -104,7 +111,7 @@ export type FontSizePickerSelectOption = { export type FontSizePickerToggleGroupProps = Pick< FontSizePickerProps, - 'value' | 'size' | '__nextHasNoMarginBottom' + 'value' | 'size' | '__nextHasNoMarginBottom' | '__next40pxDefaultSize' > & { fontSizes: NonNullable< FontSizePickerProps[ 'fontSizes' ] >; onChange: NonNullable< FontSizePickerProps[ 'onChange' ] >; From f48ac055e68bafb869314fa7a9055303a3a1ae8c Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:01:56 +0200 Subject: [PATCH 13/38] Keycodes: avoid regex for capital case (#56822) --- package-lock.json | 6 ++--- .../test/__snapshots__/index.js.snap | 2 +- packages/keycodes/package.json | 3 +-- packages/keycodes/src/index.js | 27 +++++++++---------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86cf832a8e41d..b47ce2e24a9e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55868,8 +55868,7 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "file:../i18n", - "change-case": "^4.1.2" + "@wordpress/i18n": "file:../i18n" }, "engines": { "node": ">=12" @@ -70963,8 +70962,7 @@ "version": "file:packages/keycodes", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "file:../i18n", - "change-case": "^4.1.2" + "@wordpress/i18n": "file:../i18n" } }, "@wordpress/lazy-import": { diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap index b98bd562f0a6a..79990664a2427 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap @@ -898,7 +898,7 @@ exports[`KeyboardShortcutHelpModal should match snapshot when the modal is activ class="edit-post-keyboard-shortcut-help-modal__shortcut-term" > - capitalCase( replacementKeyMap[ key ] ?? key ) + capitaliseFirstCharacter( replacementKeyMap[ key ] ?? key ) ) .join( isApple ? ' ' : ' + ' ); }; From 50571da12dbdf9075096e00fd21429b48fb2bca6 Mon Sep 17 00:00:00 2001 From: Derek Blank Date: Thu, 7 Dec 2023 17:54:31 +1000 Subject: [PATCH 14/38] Add mobile OfflineStatus component --- .../components/offline-status/index.native.js | 46 +++++++++++++++++++ .../offline-status/style.native.scss | 27 +++++++++++ packages/icons/src/index.js | 1 + packages/icons/src/library/offline.js | 20 ++++++++ 4 files changed, 94 insertions(+) create mode 100644 packages/block-editor/src/components/offline-status/index.native.js create mode 100644 packages/block-editor/src/components/offline-status/style.native.scss create mode 100644 packages/icons/src/library/offline.js diff --git a/packages/block-editor/src/components/offline-status/index.native.js b/packages/block-editor/src/components/offline-status/index.native.js new file mode 100644 index 0000000000000..a7d51192cac0d --- /dev/null +++ b/packages/block-editor/src/components/offline-status/index.native.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { Text, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; +import { Icon } from '@wordpress/components'; +import { offline as offlineIcon } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { useIsConnected } from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import styles from './style'; + +const OfflineStatus = () => { + const { isConnected } = useIsConnected(); + + const containerStyle = usePreferredColorSchemeStyle( + styles.container, + styles[ 'container--dark' ] + ); + + const textStyle = usePreferredColorSchemeStyle( + styles.text, + styles[ 'text--dark' ] + ); + + const iconStyle = usePreferredColorSchemeStyle( + styles.icon, + styles[ 'icon--dark' ] + ); + + return ! isConnected ? ( + + + { __( 'Working Offline' ) } + + ) : null; +}; + +export default OfflineStatus; diff --git a/packages/block-editor/src/components/offline-status/style.native.scss b/packages/block-editor/src/components/offline-status/style.native.scss new file mode 100644 index 0000000000000..2b6e808fdbb49 --- /dev/null +++ b/packages/block-editor/src/components/offline-status/style.native.scss @@ -0,0 +1,27 @@ +.container { + background-color: $light-ultra-dim; + padding: $grid-unit; + justify-content: center; + flex-direction: row; +} + +.container--dark { + background-color: $dark-dim; +} + +.text { + padding-left: 3; + padding-top: 2; +} + +.text--dark { + color: $white; +} + +.icon { + fill: $light-secondary; +} + +.icon--dark { + fill: $dark-tertiary; +} diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 36b2971423442..d743299d35a24 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -190,6 +190,7 @@ export { default as postList } from './library/post-list'; export { default as postTerms } from './library/post-terms'; export { default as previous } from './library/previous'; export { default as next } from './library/next'; +export { default as offline } from './library/offline'; export { default as preformatted } from './library/preformatted'; export { default as pullLeft } from './library/pull-left'; export { default as pullRight } from './library/pull-right'; diff --git a/packages/icons/src/library/offline.js b/packages/icons/src/library/offline.js new file mode 100644 index 0000000000000..2e397fb23745a --- /dev/null +++ b/packages/icons/src/library/offline.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const offline = ( + + { /* + "no internet" by Heztasia is licensed under CCBY3.0 + https://creativecommons.org/licenses/by/3.0/ + */ } + + + + + + +); + +export default offline; From b45f22c3e2d33367a43122e6e13ce5ac41cf1ebf Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 7 Dec 2023 16:16:08 -0600 Subject: [PATCH 15/38] Refactor : Add variant prop and wrapper (#56335) - Combine `` into ``. This brings all the necessary functionality for `` to be used, mainly by wrapping it in a ``. - Remove ``. This is no longer needed, and was originally intended for Contextual to mean Popover. - Replace usage of `` in edit-site, edit-post, edit-widget, etc headers with `` - Refactor `` to become `` to better reflect its purpose. - Add `` with new props of `__experimentalInitialIndex`, `__experimentalOnIndexChange`, and `focusOnMount`. - Export `` from `` with locked `__experimentalInitialIndex`, `__experimentalOnIndexChange`, and `focusOnMount` as `undefined`. Public API props are `hideDragHandles` and `variant` with a default of `unstyled`. - Remove concept of `isFixed` from the ``. The styles of the current small-screen fixed toolbar will become the default. The popover and top toolbars will have its own CSS overrides applied. - Split `` and `` into separate components for simplicity. - Top Toolbar now means. You have to implement the `` on your own. --- packages/block-editor/README.md | 8 + .../block-parent-selector/style.scss | 11 - .../src/components/block-toolbar/index.js | 275 ++++++++++++------ .../src/components/block-toolbar/style.scss | 116 ++++---- .../src/components/block-tools/back-compat.js | 4 +- .../block-tools/block-contextual-toolbar.js | 100 ------- .../block-tools/block-toolbar-breadcrumb.js | 46 +++ .../block-tools/block-toolbar-popover.js | 90 ++++++ .../src/components/block-tools/index.js | 71 +++-- .../block-tools/selected-block-tools.js | 127 -------- .../src/components/block-tools/style.scss | 232 ++++----------- .../components/navigable-toolbar/README.md | 2 + .../src/components/navigable-toolbar/index.js | 4 +- packages/block-editor/src/private-apis.js | 2 - packages/block-editor/src/style.scss | 1 - .../src/components/header/index.js | 27 +- .../src/components/header/style.scss | 2 +- .../components/sidebar-block-editor/index.js | 14 +- .../sidebar-block-editor/style.scss | 20 -- .../edit-post/src/components/header/index.js | 7 +- .../src/components/header/style.scss | 55 +++- .../edit-post/src/components/layout/index.js | 18 +- packages/edit-post/src/editor.js | 9 +- .../src/components/block-editor/style.scss | 11 - .../block-editor/use-site-editor-settings.js | 80 ++--- .../edit-site/src/components/editor/index.js | 8 +- .../src/components/header-edit-mode/index.js | 6 +- .../components/header-edit-mode/style.scss | 41 ++- .../src/components/header/index.js | 7 +- .../src/components/header/style.scss | 27 +- .../index.js | 4 + .../index.js | 5 +- storybook/stories/playground/box/index.js | 2 + .../playground/with-undo-redo/index.js | 2 + .../playground/with-undo-redo/style.css | 6 +- .../specs/editor/various/is-typing.spec.js | 40 ++- .../various/shortcut-focus-toolbar.spec.js | 4 + 37 files changed, 725 insertions(+), 759 deletions(-) delete mode 100644 packages/block-editor/src/components/block-parent-selector/style.scss delete mode 100644 packages/block-editor/src/components/block-tools/block-contextual-toolbar.js create mode 100644 packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js create mode 100644 packages/block-editor/src/components/block-tools/block-toolbar-popover.js delete mode 100644 packages/block-editor/src/components/block-tools/selected-block-tools.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 2d6a5627a52a4..56ab5f1bd94d9 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -280,10 +280,18 @@ _Returns_ ### BlockToolbar +Renders the block toolbar. + _Related_ - +_Parameters_ + +- _props_ `Object`: Components props. +- _props.hideDragHandle_ `boolean`: Show or hide the Drag Handle for drag and drop functionality. +- _props.variant_ `string`: Style variant of the toolbar, also passed to the Dropdowns rendered from Block Toolbar Buttons. + ### BlockTools Renders block tools (the block toolbar, select/navigation mode toolbar, the insertion point and a slot for the inline rich text toolbar). Must be wrapped around the block content and editor styles wrapper or iframe. diff --git a/packages/block-editor/src/components/block-parent-selector/style.scss b/packages/block-editor/src/components/block-parent-selector/style.scss deleted file mode 100644 index c5a1869835188..0000000000000 --- a/packages/block-editor/src/components/block-parent-selector/style.scss +++ /dev/null @@ -1,11 +0,0 @@ -.block-editor-block-parent-selector { - background: $white; - border-radius: $radius-block-ui; - - .block-editor-block-parent-selector__button { - width: $grid-unit-60; - height: $grid-unit-60; - border: $border-width solid $gray-900; - border-radius: $radius-block-ui; - } -} diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 963cd8a475328..7bb52a7e8f090 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -6,6 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { useRef } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; @@ -32,38 +33,82 @@ import BlockEditVisuallyButton from '../block-edit-visually-button'; import { useShowHoveredOrFocusedGestures } from './utils'; import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; +import NavigableToolbar from '../navigable-toolbar'; +import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; -const BlockToolbar = ( { hideDragHandle } ) => { - const { blockClientIds, blockType, isValid, isVisual, blockEditingMode } = - useSelect( ( select ) => { - const { - getBlockName, - getBlockMode, - getSelectedBlockClientIds, - isBlockValid, - getBlockRootClientId, - getBlockEditingMode, - } = select( blockEditorStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); - const selectedBlockClientId = selectedBlockClientIds[ 0 ]; - const blockRootClientId = getBlockRootClientId( - selectedBlockClientId - ); - return { - blockClientIds: selectedBlockClientIds, - blockType: - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ), - rootClientId: blockRootClientId, - isValid: selectedBlockClientIds.every( ( id ) => - isBlockValid( id ) - ), - isVisual: selectedBlockClientIds.every( - ( id ) => getBlockMode( id ) === 'visual' - ), - blockEditingMode: getBlockEditingMode( selectedBlockClientId ), - }; - }, [] ); +/** + * Renders the block toolbar. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-toolbar/README.md + * + * @param {Object} props Components props. + * @param {boolean} props.hideDragHandle Show or hide the Drag Handle for drag and drop functionality. + * @param {boolean} props.focusOnMount Focus the toolbar when mounted. + * @param {number} props.__experimentalInitialIndex The initial index of the toolbar item to focus. + * @param {Function} props.__experimentalOnIndexChange Callback function to be called when the index of the focused toolbar item changes. + * @param {string} props.variant Style variant of the toolbar, also passed to the Dropdowns rendered from Block Toolbar Buttons. + */ +export function PrivateBlockToolbar( { + hideDragHandle, + focusOnMount, + __experimentalInitialIndex, + __experimentalOnIndexChange, + variant = 'unstyled', +} ) { + const { + blockClientId, + blockClientIds, + isDefaultEditingMode, + blockType, + shouldShowVisualToolbar, + showParentSelector, + } = useSelect( ( select ) => { + const { + getBlockName, + getBlockMode, + getBlockParents, + getSelectedBlockClientIds, + isBlockValid, + getBlockRootClientId, + getBlockEditingMode, + } = select( blockEditorStore ); + const selectedBlockClientIds = getSelectedBlockClientIds(); + const selectedBlockClientId = selectedBlockClientIds[ 0 ]; + const blockRootClientId = getBlockRootClientId( selectedBlockClientId ); + const parents = getBlockParents( selectedBlockClientId ); + const firstParentClientId = parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( firstParentClientId ); + const parentBlockType = getBlockType( parentBlockName ); + const _isDefaultEditingMode = + getBlockEditingMode( selectedBlockClientId ) === 'default'; + const isValid = selectedBlockClientIds.every( ( id ) => + isBlockValid( id ) + ); + const isVisual = selectedBlockClientIds.every( + ( id ) => getBlockMode( id ) === 'visual' + ); + return { + blockClientId: selectedBlockClientId, + blockClientIds: selectedBlockClientIds, + isDefaultEditingMode: _isDefaultEditingMode, + blockType: + selectedBlockClientId && + getBlockType( getBlockName( selectedBlockClientId ) ), + + shouldShowVisualToolbar: isValid && isVisual, + rootClientId: blockRootClientId, + showParentSelector: + parentBlockType && + getBlockEditingMode( firstParentClientId ) === 'default' && + hasBlockSupport( + parentBlockType, + '__experimentalParentSelector', + true + ) && + selectedBlockClientIds.length === 1 && + _isDefaultEditingMode, + }; + }, [] ); const toolbarWrapperRef = useRef( null ); @@ -76,86 +121,126 @@ const BlockToolbar = ( { hideDragHandle } ) => { const isLargeViewport = ! useViewportMatch( 'medium', '<' ); - if ( blockType ) { - if ( ! hasBlockSupport( blockType, '__experimentalToolbar', true ) ) { - return null; - } - } + const isToolbarEnabled = + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ); + const hasAnyBlockControls = useHasAnyBlockControls(); - if ( blockClientIds.length === 0 ) { + if ( + ! isToolbarEnabled || + ( ! isDefaultEditingMode && ! hasAnyBlockControls ) + ) { return null; } - const shouldShowVisualToolbar = isValid && isVisual; const isMultiToolbar = blockClientIds.length > 1; const isSynced = isReusableBlock( blockType ) || isTemplatePart( blockType ); - const classes = classnames( 'block-editor-block-toolbar', { + // Shifts the toolbar to make room for the parent block selector. + const classes = classnames( 'block-editor-block-contextual-toolbar', { + 'has-parent': showParentSelector, + } ); + + const innerClasses = classnames( 'block-editor-block-toolbar', { 'is-synced': isSynced, } ); return ( -
- { ! isMultiToolbar && - isLargeViewport && - blockEditingMode === 'default' && } - { ( shouldShowVisualToolbar || isMultiToolbar ) && - blockEditingMode === 'default' && ( -
- - - { ! isMultiToolbar && ( - +
+ { ! isMultiToolbar && + isLargeViewport && + isDefaultEditingMode && } + { ( shouldShowVisualToolbar || isMultiToolbar ) && + isDefaultEditingMode && ( +
+ + + { ! isMultiToolbar && ( + + ) } + - ) } - - -
+ +
+ ) } + { shouldShowVisualToolbar && isMultiToolbar && ( + + ) } + { shouldShowVisualToolbar && ( + <> + + + + + + <__unstableBlockNameContext.Provider + value={ blockType?.name } + > + <__unstableBlockToolbarLastItem.Slot /> + + + ) } + + { isDefaultEditingMode && ( + ) } - { shouldShowVisualToolbar && isMultiToolbar && ( - - ) } - { shouldShowVisualToolbar && ( - <> - - - - - - <__unstableBlockNameContext.Provider - value={ blockType?.name } - > - <__unstableBlockToolbarLastItem.Slot /> - - - ) } - - { blockEditingMode === 'default' && ( - - ) } -
+
+
); -}; +} /** + * Renders the block toolbar. + * * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-toolbar/README.md + * + * @param {Object} props Components props. + * @param {boolean} props.hideDragHandle Show or hide the Drag Handle for drag and drop functionality. + * @param {string} props.variant Style variant of the toolbar, also passed to the Dropdowns rendered from Block Toolbar Buttons. */ -export default BlockToolbar; +export default function BlockToolbar( { hideDragHandle, variant } ) { + return ( + + ); +} diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 3f8a7057aef84..85020cea2aa23 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -56,57 +56,66 @@ } } -.block-editor-block-contextual-toolbar.is-fixed { +.block-editor-block-contextual-toolbar { position: sticky; top: 0; z-index: z-index(".block-editor-block-popover"); display: block; width: 100%; -} + // Block UI appearance. + background-color: $white; + flex-shrink: 3; + + // Raise the specificity. + &.components-accessible-toolbar { + border: none; + border-bottom: $border-width solid $gray-200; + border-radius: 0; + } -// on desktop browsers the fixed toolbar has tweaked borders -@include break-medium() { - .block-editor-block-contextual-toolbar.is-fixed { - .block-editor-block-toolbar { - .components-toolbar-group, - .components-toolbar { - border-right: none; - - &::after { - content: ""; - width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; - background-color: $gray-300; - margin-left: $grid-unit; - } - - & .components-toolbar-group.components-toolbar-group { - &::after { - display: none; - } - } - } + .block-editor-block-toolbar { + overflow: auto; + overflow-y: hidden; - > :last-child, - > :last-child .components-toolbar-group, - > :last-child .components-toolbar { - &::after { - display: none; - } + > :last-child, + > :last-child .components-toolbar-group, + > :last-child .components-toolbar { + &::after { + display: none; } } } -} -.block-editor-block-contextual-toolbar.has-parent:not(.is-fixed) { - margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); + .block-editor-block-toolbar .components-toolbar-group, + .block-editor-block-toolbar .components-toolbar { + border-right-color: $gray-200; + } - .show-icon-labels & { - margin-left: 0; + & > .block-editor-block-toolbar { + flex-grow: initial; + width: initial; + } + + .block-editor-block-parent-selector { + position: relative; + + // Parent selector dot divider + &::after { + content: "\00B7"; + position: absolute; + font-size: 16px; + right: 0; + bottom: $grid-unit-20; + } + } + + .block-editor-block-parent-selector__button { + position: relative; + top: -1px; } } + // Block controls. .block-editor-block-toolbar__block-controls { // Switcher. @@ -165,7 +174,6 @@ } // Padding overrides. - .components-accessible-toolbar .components-toolbar-group > div:first-child:last-child > .components-button.has-icon { padding-left: 6px; padding-right: 6px; @@ -181,10 +189,12 @@ } // Parent selector overrides - - .block-editor-block-parent-selector__button { + .block-editor-block-parent-selector .block-editor-block-parent-selector__button { border-top-right-radius: 0; border-bottom-right-radius: 0; + padding-left: $grid-unit-15; + padding-right: $grid-unit-15; + text-wrap: nowrap; .block-editor-block-icon { width: 0; @@ -210,25 +220,17 @@ // Mover overrides. .block-editor-block-toolbar__block-controls .block-editor-block-mover { - border-left: 1px solid $gray-900; + border-left: 1px solid $gray-300; margin-left: 6px; margin-right: -6px; white-space: nowrap; } - .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar__block-controls .block-editor-block-mover { - border-left-color: $gray-200; - } - .block-editor-block-mover .block-editor-block-mover__drag-handle.has-icon { padding-left: $grid-unit-15; padding-right: $grid-unit-15; } - .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-mover__move-button-container { - border-width: 0; - } - @include break-small() { // Specificity override for https://github.com/WordPress/gutenberg/blob/try/block-toolbar-labels/packages/block-editor/src/components/block-mover/style.scss#L69 .is-up-button.is-up-button.is-up-button { @@ -237,17 +239,9 @@ order: 1; } - .block-editor-block-mover__move-button-container { - border-left: 1px solid $gray-900; - } - .is-down-button.is-down-button.is-down-button { order: 2; } - - .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-mover__move-button-container::before { - background: $gray-300; - } } .block-editor-block-contextual-toolbar .block-editor-block-mover.is-horizontal .block-editor-block-mover-button.block-editor-block-mover-button { @@ -260,16 +254,6 @@ flex-shrink: 1; } - @include break-medium() { - .block-editor-block-contextual-toolbar.is-fixed { - .components-toolbar, - .components-toolbar-group { - flex-shrink: 0; - } - } - } - - .block-editor-rich-text__inline-format-toolbar-group { .components-button + .components-button { margin-left: 6px; diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js index 029419926e9ed..14027760be1e6 100644 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ b/packages/block-editor/src/components/block-tools/back-compat.js @@ -9,7 +9,7 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import InsertionPoint, { InsertionPointOpenRef } from './insertion-point'; -import BlockPopover from './selected-block-tools'; +import BlockToolbarPopover from './block-toolbar-popover'; export default function BlockToolsBackCompat( { children } ) { const openRef = useContext( InsertionPointOpenRef ); @@ -28,7 +28,7 @@ export default function BlockToolsBackCompat( { children } ) { return ( - + { children } ); diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js deleted file mode 100644 index b24a25ee60ed4..0000000000000 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import NavigableToolbar from '../navigable-toolbar'; -import BlockToolbar from '../block-toolbar'; -import { store as blockEditorStore } from '../../store'; -import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; - -export default function BlockContextualToolbar( { - focusOnMount, - isFixed, - ...props -} ) { - const { - blockType, - blockEditingMode, - hasParents, - showParentSelector, - selectedBlockClientId, - } = useSelect( ( select ) => { - const { - getBlockName, - getBlockParents, - getSelectedBlockClientIds, - getBlockEditingMode, - } = select( blockEditorStore ); - const { getBlockType } = select( blocksStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); - const _selectedBlockClientId = selectedBlockClientIds[ 0 ]; - const parents = getBlockParents( _selectedBlockClientId ); - const firstParentClientId = parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( firstParentClientId ); - const parentBlockType = getBlockType( parentBlockName ); - - return { - selectedBlockClientId: _selectedBlockClientId, - blockType: - _selectedBlockClientId && - getBlockType( getBlockName( _selectedBlockClientId ) ), - blockEditingMode: getBlockEditingMode( _selectedBlockClientId ), - hasParents: parents.length, - showParentSelector: - parentBlockType && - getBlockEditingMode( firstParentClientId ) === 'default' && - hasBlockSupport( - parentBlockType, - '__experimentalParentSelector', - true - ) && - selectedBlockClientIds.length <= 1 && - getBlockEditingMode( _selectedBlockClientId ) === 'default', - }; - }, [] ); - - const isToolbarEnabled = - blockType && - hasBlockSupport( blockType, '__experimentalToolbar', true ); - const hasAnyBlockControls = useHasAnyBlockControls(); - if ( - ! isToolbarEnabled || - ( blockEditingMode !== 'default' && ! hasAnyBlockControls ) - ) { - return null; - } - - // Shifts the toolbar to make room for the parent block selector. - const classes = classnames( 'block-editor-block-contextual-toolbar', { - 'has-parent': hasParents && showParentSelector, - 'is-fixed': isFixed, - } ); - - return ( - - - - ); -} 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 new file mode 100644 index 0000000000000..77afb824101d4 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import BlockSelectionButton from './block-selection-button'; +import BlockPopover 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, +} ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + return ( + + + + ); +} diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-popover.js b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js new file mode 100644 index 0000000000000..a50e5dc42b371 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useEffect, useRef } from '@wordpress/element'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; +/** + * Internal dependencies + */ +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; +import { store as blockEditorStore } from '../../store'; +import { PrivateBlockToolbar } from '../block-toolbar'; + +export default function BlockToolbarPopover( { + clientId, + isTyping, + __unstableContentRef, +} ) { + const { capturingClientId, isInsertionPointVisible, lastClientId } = + useSelectedBlockToolProps( clientId ); + + // Stores the active toolbar item index so the block toolbar can return focus + // to it when re-mounting. + const initialToolbarItemIndexRef = useRef(); + + useEffect( () => { + // Resets the index whenever the active block changes so this is not + // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + initialToolbarItemIndexRef.current = undefined; + }, [ clientId ] ); + + const { stopTyping } = useDispatch( blockEditorStore ); + const isToolbarForced = useRef( false ); + + useShortcut( + 'core/block-editor/focus-toolbar', + () => { + isToolbarForced.current = true; + stopTyping( true ); + }, + { + isDisabled: false, + } + ); + + useEffect( () => { + isToolbarForced.current = false; + } ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + return ( + ! isTyping && ( + + { + initialToolbarItemIndexRef.current = index; + } } + variant="toolbar" + /> + + ) + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index bc2729fbb1599..969f36d878b87 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; @@ -16,9 +15,9 @@ import { InsertionPointOpenRef, default as InsertionPoint, } from './insertion-point'; -import SelectedBlockTools from './selected-block-tools'; +import BlockToolbarPopover from './block-toolbar-popover'; +import BlockToolbarBreadcrumb from './block-toolbar-breadcrumb'; import { store as blockEditorStore } from '../../store'; -import BlockContextualToolbar from './block-contextual-toolbar'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; @@ -28,6 +27,7 @@ function selector( select ) { getFirstMultiSelectedBlockClientId, getBlock, getSettings, + hasMultiSelection, __unstableGetEditorMode, isTyping, } = select( blockEditorStore ); @@ -36,18 +36,35 @@ function selector( select ) { getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); const { name = '', attributes = {} } = getBlock( clientId ) || {}; + const editorMode = __unstableGetEditorMode(); + const hasSelectedBlock = clientId && name; + const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( { + name, + attributes, + } ); + const _showEmptyBlockSideInserter = + clientId && + ! isTyping() && + editorMode === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ); + const maybeShowBreadcrumb = + hasSelectedBlock && + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ); return { clientId, hasFixedToolbar: getSettings().hasFixedToolbar, - hasSelectedBlock: clientId && name, isTyping: isTyping(), - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', - showEmptyBlockSideInserter: - clientId && - ! isTyping() && - __unstableGetEditorMode() === 'edit' && - isUnmodifiedDefaultBlock( { name, attributes } ), + isZoomOutMode: editorMode === 'zoom-out', + showEmptyBlockSideInserter: _showEmptyBlockSideInserter, + showBreadcrumb: ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, + showBlockToolbar: + ! getSettings().hasFixedToolbar && + ! _showEmptyBlockSideInserter && + hasSelectedBlock && + ! isEmptyDefaultBlock && + ! maybeShowBreadcrumb, }; } @@ -65,14 +82,14 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const isLargeViewport = useViewportMatch( 'medium' ); const { clientId, hasFixedToolbar, - hasSelectedBlock, isTyping, isZoomOutMode, showEmptyBlockSideInserter, + showBreadcrumb, + showBlockToolbar, } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = @@ -162,12 +179,6 @@ export default function BlockTools( { const blockToolbarRef = usePopoverScroll( __unstableContentRef ); const blockToolbarAfterRef = usePopoverScroll( __unstableContentRef ); - // Conditions for fixed toolbar - // 1. Not zoom out mode - // 2. It's a large viewport. If it's a smaller viewport, let the floating toolbar handle it as it already has styles attached to make it render that way. - // 3. Fixed toolbar is enabled - const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; - return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
@@ -177,11 +188,6 @@ export default function BlockTools( { __unstableContentRef={ __unstableContentRef } /> ) } - { /* If there is no slot available, such as in the standalone block editor, render within the editor */ } - - { ! isLargeViewport && ( // Small viewports always get a fixed toolbar - - ) } { showEmptyBlockSideInserter && ( ) } - { /* Even if the toolbar is fixed, the block popover is still - needed for navigation and zoom-out mode. */ } - { ! showEmptyBlockSideInserter && hasSelectedBlock && ( - + ) } + + { showBreadcrumb && ( + ) } - { /* Used for the inline rich text toolbar. */ } - { ! isTopToolbar && ( + { /* Used for the inline rich text toolbar. Until this toolbar is combined into BlockToolbar, someone implementing their own BlockToolbar will also need to use this to see the image caption toolbar. */ } + { ! isZoomOutMode && ! hasFixedToolbar && ( { - const { hasMultiSelection, __unstableGetEditorMode } = - select( blockEditorStore ); - - const editorMode = __unstableGetEditorMode(); - - return { - shouldShowBreadcrumb: - ! hasMultiSelection() && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ), - }; - }, [] ); - - const isToolbarForced = useRef( false ); - const { shouldShowContextualToolbar, canFocusHiddenToolbar } = - useShouldContextualToolbarShow(); - - const { stopTyping } = useDispatch( blockEditorStore ); - - useShortcut( - 'core/block-editor/focus-toolbar', - () => { - isToolbarForced.current = true; - stopTyping( true ); - }, - { - isDisabled: ! canFocusHiddenToolbar, - } - ); - - useEffect( () => { - isToolbarForced.current = false; - } ); - - // Stores the active toolbar item index so the block toolbar can return focus - // to it when re-mounting. - const initialToolbarItemIndexRef = useRef(); - - useEffect( () => { - // Resets the index whenever the active block changes so this is not - // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - initialToolbarItemIndexRef.current = undefined; - }, [ clientId ] ); - - const popoverProps = useBlockToolbarPopoverProps( { - contentElement: __unstableContentRef?.current, - clientId, - } ); - - if ( showEmptyBlockSideInserter ) { - return null; - } - - if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { - return ( - - { shouldShowContextualToolbar && ( - { - initialToolbarItemIndexRef.current = index; - } } - /> - ) } - { shouldShowBreadcrumb && ( - - ) } - - ); - } - - return null; -} diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 07f22bb4946ea..3371d795e6c03 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -85,178 +85,6 @@ } } -/** - * Block Toolbar when contextual. - */ - -.block-editor-block-contextual-toolbar { - // Block UI appearance. - display: inline-flex; - border: $border-width solid $gray-900; - border-radius: $radius-block-ui; - background-color: $white; - - .block-editor-block-toolbar .components-toolbar-group, - .block-editor-block-toolbar .components-toolbar { - border-right-color: $gray-900; - } - - &.is-fixed { - overflow: hidden; - - .block-editor-block-toolbar { - overflow: auto; - overflow-y: hidden; - } - - border-bottom: $border-width solid $gray-200; - border-radius: 0; - - .block-editor-block-toolbar .components-toolbar-group, - .block-editor-block-toolbar .components-toolbar { - border-right-color: $gray-200; - } - } - - @include break-medium() { - &.is-fixed { - & > .block-editor-block-toolbar { - flex-grow: initial; - width: initial; - - // Add a border as separator in the block toolbar. - &::before { - content: ""; - width: $border-width; - height: 3 * $grid-unit; - margin-top: $grid-unit + $grid-unit-05; - margin-right: 0; - background-color: $gray-300; - position: relative; - left: math.div(-$grid-unit-05, 2); - top: -1px; - } - } - - & > .block-editor-block-toolbar__group-collapse-fixed-toolbar { - border: none; - - .show-icon-labels & { - .components-button.has-icon { - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - font-size: $helptext-font-size; - } - } - } - - // Add a border as separator in the block toolbar. - &::before { - content: ""; - width: $border-width; - height: 3 * $grid-unit; - margin-top: $grid-unit + $grid-unit-05; - margin-right: $grid-unit-10; - background-color: $gray-300; - position: relative; - left: 0; - top: -1px; - } - } - - & > .block-editor-block-toolbar__group-expand-fixed-toolbar { - border: none; - - .show-icon-labels & { - width: $grid-unit-80 * 4; - .components-button.has-icon { - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - font-size: $helptext-font-size; - } - } - } - - // Add a border as separator in the block toolbar. - &::before { - content: ""; - width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; - background-color: $gray-300; - position: relative; - left: -8px; - height: 3 * $grid-unit; - top: -1px; - } - } - - .show-icon-labels & { - .block-editor-block-parent-selector .block-editor-block-parent-selector__button::after { - left: 0; - } - - .block-editor-block-toolbar__block-controls .block-editor-block-mover { - border-left: none; - &::before { - content: ""; - width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; - background-color: $gray-300; - position: relative; - } - } - } - } - - &.is-fixed .block-editor-block-parent-selector { - - .block-editor-block-parent-selector__button { - position: relative; - top: -1px; - border: 0; - padding-right: 6px; - padding-left: 6px; - - &::after { - content: "\00B7"; - font-size: 16px; - line-height: $grid-unit-40 + $grid-unit-10; - position: absolute; - left: $grid-unit-40 + $grid-unit-15 + 2px; - bottom: $grid-unit-05; - } - } - } - - &:not(.is-fixed) .block-editor-block-parent-selector { - position: absolute; - top: -$border-width; - left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); - - .show-icon-labels & { - position: relative; - left: auto; - top: auto; - margin-top: -$border-width; - margin-left: -$border-width; - margin-bottom: -$border-width; - } - } - } -} - /** * Block Label for Navigation/Selection Mode */ @@ -349,6 +177,7 @@ } .components-popover.block-editor-block-list__block-popover { + // Position the block toolbar. .block-editor-block-list__block-selection-button, .block-editor-block-contextual-toolbar { @@ -357,6 +186,30 @@ margin-bottom: $grid-unit-15; } + .block-editor-block-contextual-toolbar { + border: $border-width solid $gray-900; + border-radius: $radius-block-ui; + overflow: visible; // allow the parent selector to be visible + position: static; + width: auto; + + &.has-parent { + margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); + .show-icon-labels & { + margin-left: 0; + } + } + } + + .block-editor-block-toolbar { + overflow: visible; + } + + .block-editor-block-toolbar .components-toolbar-group, + .block-editor-block-toolbar .components-toolbar { + border-right-color: $gray-900; + } + // Hide the block toolbar if the insertion point is shown. &.is-insertion-point-visible { visibility: hidden; @@ -368,6 +221,41 @@ // It's essential to hide the toolbar/popover so that `dragEnter` events can pass through them to the underlying elements. animation: hide-during-dragging 1ms linear forwards; } + + .block-editor-block-parent-selector { + position: absolute; + left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); + + &::before { + content: ""; + } + + .block-editor-block-parent-selector__button { + border: 1px solid $gray-900; + padding-right: 6px; + padding-left: 6px; + background-color: $white; + } + } + + // Show Icon Label Styles + .show-icon-labels & { + + .block-editor-block-parent-selector { + position: static; + margin-top: -$border-width; + margin-left: -$border-width; + margin-bottom: -$border-width; + + .block-editor-block-parent-selector__button { + position: static; + } + } + .block-editor-block-mover__move-button-container, + .block-editor-block-toolbar__block-controls .block-editor-block-mover { + border-left: 1px solid $gray-900; + } + } } .is-dragging-components-draggable .components-tooltip { diff --git a/packages/block-editor/src/components/navigable-toolbar/README.md b/packages/block-editor/src/components/navigable-toolbar/README.md index 30a4d100195f8..317be48f38faa 100644 --- a/packages/block-editor/src/components/navigable-toolbar/README.md +++ b/packages/block-editor/src/components/navigable-toolbar/README.md @@ -8,6 +8,8 @@ The component accepts the following props. Props not included in this set will b ## `focusOnMount` +_Note: this prop is deprecated._ + Whether to immediately focus when the component mounts. - Type: `Boolean` diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index fe216e1058f6f..8954f7e17f132 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -162,7 +162,7 @@ function useToolbarFocus( { const index = items.findIndex( ( item ) => item.tabIndex === 0 ); onIndexChange( index ); }; - }, [ initialIndex, initialFocusOnMount, toolbarRef ] ); + }, [ initialIndex, initialFocusOnMount, onIndexChange, toolbarRef ] ); const { lastFocus } = useSelect( ( select ) => { const { getLastFocus } = select( blockEditorStore ); @@ -210,9 +210,9 @@ export default function NavigableToolbar( { useToolbarFocus( { toolbarRef, focusOnMount, - isAccessibleToolbar, defaultIndex: initialIndex, onIndexChange, + isAccessibleToolbar, shouldUseKeyboardFocusShortcut, focusEditorOnEscape, } ); diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index b1d499ae09946..9837c206487be 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -10,7 +10,6 @@ import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; -import BlockContextualToolbar from './components/block-tools/block-contextual-toolbar'; import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show'; import { cleanEmptyObject, useStyleOverride } from './hooks/utils'; import BlockQuickNavigation from './components/block-quick-navigation'; @@ -42,7 +41,6 @@ lock( privateApis, { PrivateListView, ResizableBoxPopover, BlockInfo, - BlockContextualToolbar, useShouldContextualToolbarShow, cleanEmptyObject, useStyleOverride, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index a55756ae6f53d..16de2dfdb7114 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -10,7 +10,6 @@ @import "./components/block-draggable/style.scss"; @import "./components/block-mover/style.scss"; @import "./components/block-navigation/style.scss"; -@import "./components/block-parent-selector/style.scss"; @import "./components/block-patterns-list/style.scss"; @import "./components/block-patterns-paging/style.scss"; @import "./components/block-popover/style.scss"; diff --git a/packages/customize-widgets/src/components/header/index.js b/packages/customize-widgets/src/components/header/index.js index 34e4573c719dd..5bd0b2c2f4d47 100644 --- a/packages/customize-widgets/src/components/header/index.js +++ b/packages/customize-widgets/src/components/header/index.js @@ -6,13 +6,9 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Popover, ToolbarButton } from '@wordpress/components'; -import { useViewportMatch } from '@wordpress/compose'; -import { - NavigableToolbar, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { createPortal, useEffect, useRef, useState } from '@wordpress/element'; +import { ToolbarButton } from '@wordpress/components'; +import { NavigableToolbar } from '@wordpress/block-editor'; +import { createPortal, useEffect, useState } from '@wordpress/element'; import { displayShortcut, isAppleOS } from '@wordpress/keycodes'; import { __, _x, isRTL } from '@wordpress/i18n'; import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; @@ -22,9 +18,6 @@ import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; */ import Inserter from '../inserter'; import MoreMenu from '../more-menu'; -import { unlock } from '../../lock-unlock'; - -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); function Header( { sidebar, @@ -33,8 +26,6 @@ function Header( { setIsInserterOpened, isFixedToolbarActive, } ) { - const isLargeViewport = useViewportMatch( 'medium' ); - const blockToolbarRef = useRef(); const [ [ hasUndo, hasRedo ], setUndoRedo ] = useState( [ sidebar.hasUndo(), sidebar.hasRedo(), @@ -107,18 +98,6 @@ function Header( { , inserter.contentContainer[ 0 ] ) } - - { isFixedToolbarActive && isLargeViewport && ( - <> -
- -
- - - ) } ); } diff --git a/packages/customize-widgets/src/components/header/style.scss b/packages/customize-widgets/src/components/header/style.scss index d9d4a487e647c..27460a82e0ad1 100644 --- a/packages/customize-widgets/src/components/header/style.scss +++ b/packages/customize-widgets/src/components/header/style.scss @@ -1,5 +1,5 @@ .customize-widgets-header { - @include break-medium() { + @include break-small() { // Make space for the floating toolbar. margin-bottom: $grid-unit-20 + $default-block-margin; } diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js index ccb6fca871429..c2e10bca16ec0 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/index.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -1,11 +1,13 @@ /** * WordPress dependencies */ +import { useViewportMatch } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { useMemo, createPortal } from '@wordpress/element'; import { BlockList, + BlockToolbar, BlockTools, BlockInspector, privateApis as blockEditorPrivateApis, @@ -37,6 +39,7 @@ export default function SidebarBlockEditor( { inspector, } ) { const [ isInserterOpened, setIsInserterOpened ] = useInserter( inserter ); + const isMediumViewport = useViewportMatch( 'small' ); const { hasUploadPermissions, isFixedToolbarActive, @@ -77,7 +80,7 @@ export default function SidebarBlockEditor( { ...blockEditorSettings, __experimentalSetIsInserterOpened: setIsInserterOpened, mediaUpload: mediaUploadBlockEditor, - hasFixedToolbar: isFixedToolbarActive, + hasFixedToolbar: isFixedToolbarActive || ! isMediumViewport, keepCaretInsideBlock, __unstableHasCustomAppender: true, }; @@ -85,6 +88,7 @@ export default function SidebarBlockEditor( { hasUploadPermissions, blockEditorSettings, isFixedToolbarActive, + isMediumViewport, keepCaretInsideBlock, setIsInserterOpened, ] ); @@ -109,9 +113,13 @@ export default function SidebarBlockEditor( { inserter={ inserter } isInserterOpened={ isInserterOpened } setIsInserterOpened={ setIsInserterOpened } - isFixedToolbarActive={ isFixedToolbarActive } + isFixedToolbarActive={ + isFixedToolbarActive || ! isMediumViewport + } /> - + { ( isFixedToolbarActive || ! isMediumViewport ) && ( + + ) } .block-editor-block-toolbar__group-collapse-fixed-toolbar { - display: none; - } - - // Scroll sideways. - overflow-y: auto; - z-index: z-index(".customize-widgets__block-toolbar"); -} - .customize-control-sidebar_block_editor .block-editor-block-list__block-popover { // FloatingUI library used in Popover component forces us to have an "absolute" inline style. // We need to override this in the customizer. diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 8bfeb2d253ce1..b92c6c44fe49f 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { - privateApis as blockEditorPrivateApis, + BlockToolbar, store as blockEditorStore, } from '@wordpress/block-editor'; import { @@ -40,9 +40,6 @@ import { default as DevicePreview } from '../device-preview'; import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); const slideY = { hidden: { y: '-50px' }, @@ -130,7 +127,7 @@ function Header( { } ) } > - +
=' ); - const isLargeViewport = useViewportMatch( 'large' ); + const isWideViewport = useViewportMatch( 'large' ); + const isLargeViewport = useViewportMatch( 'medium' ); + const { openGeneralSidebar, closeGeneralSidebar, setIsInserterOpened } = useDispatch( editPostStore ); const { createErrorNotice } = useDispatch( noticesStore ); @@ -148,7 +151,6 @@ function Layout() { isRichEditingEnabled, sidebarIsOpened, hasActiveMetaboxes, - hasFixedToolbar, previousShortcut, nextShortcut, hasBlockSelected, @@ -167,8 +169,6 @@ function Layout() { return { showMetaBoxes: select( editorStore ).getRenderingMode() === 'post-only', - hasFixedToolbar: - select( editPostStore ).isFeatureActive( 'fixedToolbar' ), sidebarIsOpened: !! ( select( interfaceStore ).getActiveComplementaryArea( editPostStore.name @@ -219,12 +219,12 @@ function Layout() { if ( sidebarIsOpened && ! isHugeViewport ) { setIsInserterOpened( false ); } - }, [ sidebarIsOpened, isHugeViewport ] ); + }, [ isHugeViewport, setIsInserterOpened, sidebarIsOpened ] ); useEffect( () => { if ( isInserterOpened && ! isHugeViewport ) { closeGeneralSidebar(); } - }, [ isInserterOpened, isHugeViewport ] ); + }, [ closeGeneralSidebar, isInserterOpened, isHugeViewport ] ); // Local state for save panel. // Note 'truthy' callback implies an open panel. @@ -253,9 +253,8 @@ function Layout() { const className = classnames( 'edit-post-layout', 'is-mode-' + mode, { 'is-sidebar-opened': sidebarIsOpened, - 'has-fixed-toolbar': hasFixedToolbar, 'has-metaboxes': hasActiveMetaboxes, - 'is-distraction-free': isDistractionFree && isLargeViewport, + 'is-distraction-free': isDistractionFree && isWideViewport, 'is-entity-save-view-open': !! entitiesSavedStatesCallback, } ); @@ -302,7 +301,7 @@ function Layout() { ) } + { ! isLargeViewport && } { isRichEditingEnabled && mode === 'visual' && ( ) } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index cff867c3f7a2c..0abf3328635a8 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -14,6 +14,7 @@ import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { CommandMenu } from '@wordpress/commands'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -26,6 +27,8 @@ import { unlock } from './lock-unlock'; const { ExperimentalEditorProvider } = unlock( editorPrivateApis ); function Editor( { postId, postType, settings, initialEdits, ...props } ) { + const isLargeViewport = useViewportMatch( 'medium' ); + const { hasFixedToolbar, focusMode, @@ -66,9 +69,9 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { getEditorSettings().supportsTemplateMode; const isViewable = getPostType( postType )?.viewable ?? false; const canEditTemplate = canUser( 'create', 'templates' ); - return { - hasFixedToolbar: isFeatureActive( 'fixedToolbar' ), + hasFixedToolbar: + isFeatureActive( 'fixedToolbar' ) || ! isLargeViewport, focusMode: isFeatureActive( 'focusMode' ), isDistractionFree: isFeatureActive( 'distractionFree' ), hasInlineToolbar: isFeatureActive( 'inlineToolbar' ), @@ -86,7 +89,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { post: postObject, }; }, - [ postType, postId ] + [ postType, postId, isLargeViewport ] ); const { updatePreferredStyleVariations, setIsInserterOpened } = diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index e02240eb88099..1da43730d9575 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -69,17 +69,6 @@ &.is-view-mode { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.8), 0 8px 10px -6px rgba(0, 0, 0, 0.8); - - /* - Temporary to hide the contextual toolbar in view mode. - See: https://github.com/WordPress/gutenberg/pull/46298 - This rule can possibly be removed once the - contextual toolbar has been redesigned. - See: https://github.com/WordPress/gutenberg/issues/40450 - */ - .block-editor-block-contextual-toolbar.is-fixed { - display: none; - } } } diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 962cfe09afb72..2deb2d4cb5fa6 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { useViewportMatch } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; @@ -89,6 +90,7 @@ function useArchiveLabel( templateSlug ) { export function useSpecificEditorSettings() { const { setIsInserterOpened } = useDispatch( editSiteStore ); + const isLargeViewport = useViewportMatch( 'medium' ); const { templateSlug, focusMode, @@ -98,44 +100,46 @@ export function useSpecificEditorSettings() { canvasMode, settings, postWithTemplate, - } = useSelect( ( select ) => { - const { - getEditedPostType, - getEditedPostId, - getEditedPostContext, - getCanvasMode, - getSettings, - } = unlock( select( editSiteStore ) ); - const { get: getPreference } = select( preferencesStore ); - const { getEditedEntityRecord } = select( coreStore ); - const usedPostType = getEditedPostType(); - const usedPostId = getEditedPostId(); - const _record = getEditedEntityRecord( - 'postType', - usedPostType, - usedPostId - ); - const _context = getEditedPostContext(); - return { - templateSlug: _record.slug, - focusMode: !! getPreference( 'core/edit-site', 'focusMode' ), - isDistractionFree: !! getPreference( - 'core/edit-site', - 'distractionFree' - ), - hasFixedToolbar: !! getPreference( - 'core/edit-site', - 'fixedToolbar' - ), - keepCaretInsideBlock: !! getPreference( - 'core/edit-site', - 'keepCaretInsideBlock' - ), - canvasMode: getCanvasMode(), - settings: getSettings(), - postWithTemplate: _context?.postId, - }; - }, [] ); + } = useSelect( + ( select ) => { + const { + getEditedPostType, + getEditedPostId, + getEditedPostContext, + getCanvasMode, + getSettings, + } = unlock( select( editSiteStore ) ); + const { get: getPreference } = select( preferencesStore ); + const { getEditedEntityRecord } = select( coreStore ); + const usedPostType = getEditedPostType(); + const usedPostId = getEditedPostId(); + const _record = getEditedEntityRecord( + 'postType', + usedPostType, + usedPostId + ); + const _context = getEditedPostContext(); + return { + templateSlug: _record.slug, + focusMode: !! getPreference( 'core/edit-site', 'focusMode' ), + isDistractionFree: !! getPreference( + 'core/edit-site', + 'distractionFree' + ), + hasFixedToolbar: + !! getPreference( 'core/edit-site', 'fixedToolbar' ) || + ! isLargeViewport, + keepCaretInsideBlock: !! getPreference( + 'core/edit-site', + 'keepCaretInsideBlock' + ), + canvasMode: getCanvasMode(), + settings: getSettings(), + postWithTemplate: _context?.postId, + }; + }, + [ isLargeViewport ] + ); const archiveLabels = useArchiveLabel( templateSlug ); const defaultRenderingMode = postWithTemplate ? 'template-locked' : 'all'; const defaultEditorSettings = useMemo( () => { diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 5a2f1e2ec4d1a..295f4ec3cf5c6 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -8,10 +8,11 @@ import classnames from 'classnames'; */ import { useSelect } from '@wordpress/data'; import { Notice } from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; +import { useInstanceId, useViewportMatch } from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; import { BlockBreadcrumb, + BlockToolbar, store as blockEditorStore, privateApis as blockEditorPrivateApis, BlockInspector, @@ -92,6 +93,8 @@ export default function Editor( { listViewToggleElement, isLoading } ) { const { type: editedPostType } = editedPost; + const isLargeViewport = useViewportMatch( 'medium' ); + const { context, contextPost, @@ -232,6 +235,9 @@ export default function Editor( { listViewToggleElement, isLoading } ) { + { ! isLargeViewport && ( + + ) } - +
* + * { margin-left: $grid-unit-10; } + + .block-editor-block-mover { + border-left: none; + + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + } } .has-fixed-toolbar { .selected-block-tools-wrapper { overflow-x: scroll; + .block-editor-block-contextual-toolbar { + border-bottom: 0; + } + + // Modified group borders + .components-toolbar-group, + .components-toolbar { + border-right: none; + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + & .components-toolbar-group.components-toolbar-group { + &::after { + display: none; + } + } + } + &.is-collapsed { display: none; } diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js index 9251f528ca5ee..9d4cb4cb60103 100644 --- a/packages/edit-widgets/src/components/header/index.js +++ b/packages/edit-widgets/src/components/header/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { BlockToolbar } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -16,9 +16,6 @@ import { store as preferencesStore } from '@wordpress/preferences'; import DocumentTools from './document-tools'; import SaveButton from '../save-button'; import MoreMenu from '../more-menu'; -import { unlock } from '../../lock-unlock'; - -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); function Header( { setListViewToggleElement } ) { const isLargeViewport = useViewportMatch( 'medium' ); @@ -56,7 +53,7 @@ function Header( { setListViewToggleElement } ) { { hasFixedToolbar && isLargeViewport && ( <>
- +
{ return hasThemeStyles ? blockEditorSettings.styles : []; @@ -37,6 +40,7 @@ export default function WidgetAreasBlockEditorContent( { return (
+ { ! isLargeViewport && } + diff --git a/storybook/stories/playground/with-undo-redo/index.js b/storybook/stories/playground/with-undo-redo/index.js index a51d8624282a6..537ea16aade99 100644 --- a/storybook/stories/playground/with-undo-redo/index.js +++ b/storybook/stories/playground/with-undo-redo/index.js @@ -7,6 +7,7 @@ import { registerCoreBlocks } from '@wordpress/block-library'; import { BlockEditorProvider, BlockCanvas, + BlockToolbar, BlockTools, } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; @@ -58,6 +59,7 @@ export default function EditorWithUndoRedo() { icon={ redoIcon } label="Redo" /> +
diff --git a/storybook/stories/playground/with-undo-redo/style.css b/storybook/stories/playground/with-undo-redo/style.css index a3f0bd5d23deb..6ed082a1de719 100644 --- a/storybook/stories/playground/with-undo-redo/style.css +++ b/storybook/stories/playground/with-undo-redo/style.css @@ -6,5 +6,9 @@ display: flex; align-items: center; border-bottom: 1px solid #ddd; - height: 48px; + height: 46px; } + +.editor-with-undo-redo__toolbar .components-accessible-toolbar.block-editor-block-contextual-toolbar { + border-bottom: none; +} \ No newline at end of file diff --git a/test/e2e/specs/editor/various/is-typing.spec.js b/test/e2e/specs/editor/various/is-typing.spec.js index 0cd5e0d6f6495..8063f688409c4 100644 --- a/test/e2e/specs/editor/various/is-typing.spec.js +++ b/test/e2e/specs/editor/various/is-typing.spec.js @@ -14,24 +14,27 @@ test.describe( 'isTyping', () => { // Insert paragraph await page.keyboard.type( 'Type' ); - const blockToolbar = page.locator( - 'role=toolbar[name="Block tools"i]' + const blockToolbarPopover = page.locator( + '[data-wp-component="Popover"]', + { + has: page.locator( 'role=toolbar[name="Block tools"i]' ), + } ); - // Toolbar should not be showing - await expect( blockToolbar ).toBeHidden(); + // Toolbar Popover should not be showing + await expect( blockToolbarPopover ).toBeHidden(); // Moving the mouse shows the toolbar. await editor.showBlockToolbar(); - // Toolbar is visible. - await expect( blockToolbar ).toBeVisible(); + // Toolbar Popover is visible. + await expect( blockToolbarPopover ).toBeVisible(); // Typing again hides the toolbar await page.keyboard.type( ' and continue' ); - // Toolbar is hidden again - await expect( blockToolbar ).toBeHidden(); + // Toolbar Popover is hidden again + await expect( blockToolbarPopover ).toBeHidden(); } ); test( 'should not close the dropdown when typing in it', async ( { @@ -41,17 +44,22 @@ test.describe( 'isTyping', () => { // Add a block with a dropdown in the toolbar that contains an input. await editor.insertBlock( { name: 'core/query' } ); - // Tab to Start Blank Button - await page.keyboard.press( 'Tab' ); - // Select the Start Blank Button - await page.keyboard.press( 'Enter' ); - // Select the First variation - await page.keyboard.press( 'Enter' ); + await editor.canvas + .getByRole( 'document', { name: 'Block: Query Loop' } ) + .getByRole( 'button', { name: 'Start blank' } ) + .click(); + + await editor.canvas + .getByRole( 'button', { name: 'Title & Date' } ) + .click(); + // Moving the mouse shows the toolbar. await editor.showBlockToolbar(); // Open the dropdown. - await page.getByRole( 'button', { name: 'Display settings' } ).click(); - + const displaySettings = page.getByRole( 'button', { + name: 'Display settings', + } ); + await displaySettings.click(); const itemsPerPageInput = page.getByLabel( 'Items per Page' ); // Make sure we're where we think we are await expect( itemsPerPageInput ).toBeFocused(); diff --git a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js index 0223821613f55..a8e49f7a6b84d 100644 --- a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js +++ b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js @@ -98,6 +98,10 @@ test.describe( 'Focus toolbar shortcut (alt + F10)', () => { // Test: Focus the block toolbar from empty block await editor.insertBlock( { name: 'core/paragraph' } ); + // This fails if we don't wait for the block toolbar to show. + await expect( + toolbarUtils.blockToolbarParagraphButton + ).toBeVisible(); await toolbarUtils.moveToToolbarShortcut(); await expect( toolbarUtils.blockToolbarParagraphButton From d541afc710a01a6ac741fe0a03c65c4f4ddd8ca6 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:39:17 +0200 Subject: [PATCH 16/38] Mobile: remove rich text multiline (#56117) --- .../src/components/rich-text/index.js | 51 ++---------------- .../src/components/rich-text/index.native.js | 33 +++--------- .../rich-text/native/index.native.js | 53 +++---------------- .../components/rich-text/with-deprecations.js | 51 ++++++++++++++++++ .../src/components/post-title/index.native.js | 1 - 5 files changed, 70 insertions(+), 119 deletions(-) create mode 100644 packages/block-editor/src/components/rich-text/with-deprecations.js diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 1a6793ca9efe7..a3b7b44e214a5 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -13,14 +13,11 @@ import { createContext, } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { children as childrenSource } from '@wordpress/blocks'; -import { useInstanceId, useMergeRefs } from '@wordpress/compose'; +import { useMergeRefs } from '@wordpress/compose'; import { __unstableUseRichText as useRichText, - __unstableCreateElement, removeFormat, } from '@wordpress/rich-text'; -import deprecated from '@wordpress/deprecated'; import { Popover } from '@wordpress/components'; /** @@ -46,7 +43,7 @@ import { useFirefoxCompat } from './use-firefox-compat'; import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content } from './content'; -import RichTextMultiline from './multiline'; +import { withDeprecations } from './with-deprecations'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -387,47 +384,9 @@ export function RichTextWrapper( ); } -const ForwardedRichTextWrapper = forwardRef( RichTextWrapper ); - -function RichTextSwitcher( props, ref ) { - let value = props.value; - let onChange = props.onChange; - - // Handle deprecated format. - if ( Array.isArray( value ) ) { - deprecated( 'wp.blockEditor.RichText value prop as children type', { - since: '6.1', - version: '6.3', - alternative: 'value prop as string', - link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/', - } ); - - value = childrenSource.toHTML( props.value ); - onChange = ( newValue ) => - props.onChange( - childrenSource.fromDOM( - __unstableCreateElement( document, newValue ).childNodes - ) - ); - } - - const Component = props.multiline - ? RichTextMultiline - : ForwardedRichTextWrapper; - const instanceId = useInstanceId( RichTextSwitcher ); - - return ( - - ); -} - -const ForwardedRichTextContainer = forwardRef( RichTextSwitcher ); +const ForwardedRichTextContainer = withDeprecations( + forwardRef( RichTextWrapper ) +); ForwardedRichTextContainer.Content = Content; ForwardedRichTextContainer.isEmpty = ( value ) => { diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 9427962eced19..acadfb24a7221 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -39,17 +39,17 @@ import FormatToolbarContainer from './format-toolbar-container'; import { store as blockEditorStore } from '../../store'; import { addActiveFormats, - getMultilineTag, getAllowedFormats, createLinkInParagraph, } from './utils'; import EmbedHandlerPicker from './embed-handler-picker'; import { Content } from './content'; import RichText from './native'; +import { withDeprecations } from './with-deprecations'; const classes = 'block-editor-rich-text__editable'; -function RichTextWrapper( +export function RichTextWrapper( { children, tagName, @@ -58,7 +58,6 @@ function RichTextWrapper( value: originalValue, onChange: originalOnChange, isSelected: originalIsSelected, - multiline, inlineToolbar, wrapperClassName, autocompleters, @@ -80,7 +79,6 @@ function RichTextWrapper( disableLineBreaks, unstableOnFocus, __unstableAllowPrefixTransformations, - __unstableMultilineRootTag, // Native props. __unstableMobileNoFocusOnMount, deleteEnter, @@ -179,7 +177,6 @@ function RichTextWrapper( selectionChange, __unstableMarkAutomaticChange, } = useDispatch( blockEditorStore ); - const multilineTag = getMultilineTag( multiline ); const adjustedAllowedFormats = getAllowedFormats( { allowedFormats, disableFormats, @@ -261,10 +258,7 @@ function RichTextWrapper( if ( ! hasPastedBlocks || ! isEmpty( before ) ) { blocks.push( onSplit( - toHTMLString( { - value: before, - multilineTag, - } ), + toHTMLString( { value: before } ), ! isAfterOriginal ) ); @@ -288,13 +282,7 @@ function RichTextWrapper( : ! onSplitMiddle || ! isEmpty( after ) ) { blocks.push( - onSplit( - toHTMLString( { - value: after, - multilineTag, - } ), - isAfterOriginal - ) + onSplit( toHTMLString( { value: after } ), isAfterOriginal ) ); } @@ -308,7 +296,7 @@ function RichTextWrapper( onReplace( blocks, indexToSelect, initialPosition ); }, - [ onReplace, onSplit, multilineTag, onSplitMiddle ] + [ onReplace, onSplit, onSplitMiddle ] ); const onEnter = useCallback( @@ -370,7 +358,6 @@ function RichTextWrapper( onReplace, onSplit, __unstableMarkAutomaticChange, - multiline, splitValue, onSplitAtEnd, ] @@ -392,9 +379,6 @@ function RichTextWrapper( if ( isInternal ) { const pastedValue = create( { html, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, preserveWhiteSpace, } ); addActiveFormats( pastedValue, activeFormats ); @@ -496,7 +480,6 @@ function RichTextWrapper( onSplit, splitValue, __unstableEmbedURLOnPaste, - multilineTag, preserveWhiteSpace, pastePlainText, ] @@ -568,7 +551,6 @@ function RichTextWrapper( onPaste={ onPaste } __unstableIsSelected={ isSelected } __unstableInputRule={ inputRule } - __unstableMultilineTag={ multilineTag } __unstableOnEnterFormattedText={ enterFormattedText } __unstableOnExitFormattedText={ exitFormattedText } __unstableOnCreateUndoLevel={ __unstableMarkLastChangeAsPersistent } @@ -582,7 +564,6 @@ function RichTextWrapper( __unstableAllowPrefixTransformations={ __unstableAllowPrefixTransformations } - __unstableMultilineRootTag={ __unstableMultilineRootTag } // Native props. blockIsSelected={ originalIsSelected !== undefined @@ -675,7 +656,9 @@ function RichTextWrapper( ); } -const ForwardedRichTextContainer = forwardRef( RichTextWrapper ); +const ForwardedRichTextContainer = withDeprecations( + forwardRef( RichTextWrapper ) +); ForwardedRichTextContainer.Content = Content; diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 951d52ece6d69..165316fdbde76 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -105,27 +105,11 @@ const DEFAULT_FONT_SIZE = 16; const MIN_LINE_HEIGHT = 1; export class RichText extends Component { - constructor( { - value, - selectionStart, - selectionEnd, - __unstableMultilineTag: multiline, - } ) { + constructor( { value, selectionStart, selectionEnd } ) { super( ...arguments ); - this.isMultiline = false; - if ( multiline === true || multiline === 'p' || multiline === 'li' ) { - this.multilineTag = multiline === true ? 'p' : multiline; - this.isMultiline = true; - } - - if ( this.multilineTag === 'li' ) { - this.multilineWrapperTags = [ 'ul', 'ol' ]; - } - this.isIOS = Platform.OS === 'ios'; this.createRecord = this.createRecord.bind( this ); - this.restoreParagraphTags = this.restoreParagraphTags.bind( this ); this.onChangeFromAztec = this.onChangeFromAztec.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.handleEnter = this.handleEnter.bind( this ); @@ -223,8 +207,6 @@ export class RichText extends Component { ...create( { html: this.value, range: null, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, preserveWhiteSpace, } ), }; @@ -235,12 +217,7 @@ export class RichText extends Component { valueToFormat( value ) { // Remove the outer root tags. - return this.removeRootTagsProducedByAztec( - toHTMLString( { - value, - multilineTag: this.multilineTag, - } ) - ); + return this.removeRootTagsProducedByAztec( toHTMLString( { value } ) ); } getActiveFormatNames( record ) { @@ -357,29 +334,15 @@ export class RichText extends Component { const contentWithoutRootTag = this.removeRootTagsProducedByAztec( unescapeSpaces( event.nativeEvent.text ) ); - let formattedContent = contentWithoutRootTag; - if ( ! this.isIOS ) { - formattedContent = this.restoreParagraphTags( - contentWithoutRootTag, - this.multilineTag - ); - } this.debounceCreateUndoLevel(); - const refresh = this.value !== formattedContent; - this.value = formattedContent; + const refresh = this.value !== contentWithoutRootTag; + this.value = contentWithoutRootTag; // We don't want to refresh if our goal is just to create a record. if ( refresh ) { - this.props.onChange( formattedContent ); - } - } - - restoreParagraphTags( value, tag ) { - if ( tag === 'p' && ( ! value || ! value.startsWith( '

' ) ) ) { - return '

' + value + '

'; + this.props.onChange( contentWithoutRootTag ); } - return value; } /* @@ -739,8 +702,6 @@ export class RichText extends Component { if ( Array.isArray( value ) ) { return create( { html: childrenBlock.toHTML( value ), - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, preserveWhiteSpace, } ); } @@ -748,8 +709,6 @@ export class RichText extends Component { if ( this.props.format === 'string' ) { return create( { html: value, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, preserveWhiteSpace, } ); } @@ -1323,7 +1282,7 @@ export class RichText extends Component { fontWeight={ this.props.fontWeight } fontStyle={ this.props.fontStyle } disableEditingMenu={ disableEditingMenu } - isMultiline={ this.isMultiline } + isMultiline={ false } textAlign={ this.props.textAlign } { ...( this.isIOS ? { maxWidth } : {} ) } minWidth={ minWidth } diff --git a/packages/block-editor/src/components/rich-text/with-deprecations.js b/packages/block-editor/src/components/rich-text/with-deprecations.js new file mode 100644 index 0000000000000..8feab2206900a --- /dev/null +++ b/packages/block-editor/src/components/rich-text/with-deprecations.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; +import { children as childrenSource } from '@wordpress/blocks'; +import { useInstanceId } from '@wordpress/compose'; +import { __unstableCreateElement } from '@wordpress/rich-text'; +import deprecated from '@wordpress/deprecated'; + +/** + * Internal dependencies + */ +import RichTextMultiline from './multiline'; + +export function withDeprecations( Component ) { + return forwardRef( ( props, ref ) => { + let value = props.value; + let onChange = props.onChange; + + // Handle deprecated format. + if ( Array.isArray( value ) ) { + deprecated( 'wp.blockEditor.RichText value prop as children type', { + since: '6.1', + version: '6.3', + alternative: 'value prop as string', + link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/', + } ); + + value = childrenSource.toHTML( props.value ); + onChange = ( newValue ) => + props.onChange( + childrenSource.fromDOM( + __unstableCreateElement( document, newValue ).childNodes + ) + ); + } + + const NewComponent = props.multiline ? RichTextMultiline : Component; + const instanceId = useInstanceId( NewComponent ); + + return ( + + ); + } ); +} diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index 1ec0dd3ade3bf..6d905e743581e 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -155,7 +155,6 @@ class PostTitle extends Component { tagsToEliminate={ [ 'strong' ] } unstableOnFocus={ this.props.onSelect } onBlur={ this.props.onBlur } // Always assign onBlur as a props. - multiline={ false } style={ titleStyles } styles={ styles } fontSize={ 24 } From 67bea2afc3f4bc3e3f23525f53bcdf70ee634f85 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Thu, 7 Dec 2023 19:01:51 -0600 Subject: [PATCH 17/38] Remove BlockTools BackCompat (#56874) It was deprecated in 5.8 and slated to be removed in 6.3. --- .../src/components/block-tools/back-compat.js | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 packages/block-editor/src/components/block-tools/back-compat.js diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js deleted file mode 100644 index 14027760be1e6..0000000000000 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { useContext } from '@wordpress/element'; -import { Disabled } from '@wordpress/components'; -import deprecated from '@wordpress/deprecated'; - -/** - * Internal dependencies - */ -import InsertionPoint, { InsertionPointOpenRef } from './insertion-point'; -import BlockToolbarPopover from './block-toolbar-popover'; - -export default function BlockToolsBackCompat( { children } ) { - const openRef = useContext( InsertionPointOpenRef ); - const isDisabled = useContext( Disabled.Context ); - - // If context is set, `BlockTools` is a parent component. - if ( openRef || isDisabled ) { - return children; - } - - deprecated( 'wp.components.Popover.Slot name="block-toolbar"', { - alternative: 'wp.blockEditor.BlockTools', - since: '5.8', - version: '6.3', - } ); - - return ( - - - { children } - - ); -} From 6a35fdfb7ec95dc78ab9bb45ef2fc6584e705e3b Mon Sep 17 00:00:00 2001 From: Derek Blank Date: Fri, 8 Dec 2023 11:11:07 +1000 Subject: [PATCH 18/38] Add OfflineStatus component to block-list behind __DEV__ flag --- .../block-editor/src/components/block-list/index.native.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 810e23e4c1442..4c4d7914b1806 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -30,6 +30,7 @@ import { import { BlockDraggableWrapper } from '../block-draggable'; import { useEditorWrapperStyles } from '../../hooks/use-editor-wrapper-styles'; import { store as blockEditorStore } from '../../store'; +import OfflineStatus from '../offline-status'; const identity = ( x ) => x; @@ -235,6 +236,10 @@ export default function BlockList( { onLayout={ onLayout } testID="block-list-wrapper" > + { + // eslint-disable-next-line no-undef + __DEV__ && + } { isRootList ? ( Date: Thu, 7 Dec 2023 17:29:31 -0800 Subject: [PATCH 19/38] `BorderControl`: Fix button styles (#56730) * `BorderControl`: Fix button styles * Add __next40pxDefaultSize to Button when BorderControl has large size * Update changelog * Remove explicit return leftover from unused changes --- packages/components/CHANGELOG.md | 1 + .../border-control-dropdown/component.tsx | 4 +++- .../border-control/border-control-dropdown/hook.ts | 5 +++-- packages/components/src/border-control/styles.ts | 11 ++--------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8a6d52e758283..ed33677b55a82 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -16,6 +16,7 @@ - `ToggleGroupControl`: react correctly to external controlled updates ([#56678](https://github.com/WordPress/gutenberg/pull/56678)). - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). +- `BorderControl`: adjust `BorderControlDropdown` Button size to fix misaligned border ([#56730](https://github.com/WordPress/gutenberg/pull/56730)). ## 25.13.0 (2023-11-29) diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx index 4f43a6ed0ce55..3ee01bcda8f3b 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -149,6 +149,7 @@ const BorderControlDropdown = ( popoverControlsClassName, resetButtonClassName, showDropdownHeader, + size, __unstablePopoverProps, ...otherProps } = useBorderControlDropdown( props ); @@ -178,6 +179,7 @@ const BorderControlDropdown = ( tooltipPosition={ dropdownPosition } label={ __( 'Border color and style picker' ) } showTooltip={ true } + __next40pxDefaultSize={ size === '__unstable-large' ? true : false } > { __( 'Border color' ) }
); } - const sortValues = { asc: 'ascending', desc: 'descending' }; - return (
- { hasRows && ( + { hasData && ( - { dataView.getHeaderGroups().map( ( headerGroup ) => ( - - { headerGroup.headers.map( ( header ) => ( - - ) ) } - - ) ) } + + { visibleFields.map( ( field ) => ( + + ) ) } + { !! actions?.length && ( + + ) } + - { rows.map( ( row ) => ( - - { row.getVisibleCells().map( ( cell ) => ( + { usedData.map( ( item, index ) => ( + + { visibleFields.map( ( field ) => ( ) ) } + { !! actions?.length && ( + + ) } ) ) }
- -
+ + + { __( 'Actions' ) } +
- { flexRender( - cell.column.columnDef.cell, - cell.getContext() - ) } + { field.render( { + item, + } ) } + +
) } - { ! hasRows && ( + { ! hasData && (

{ __( 'No results' ) }

From d87092cae0dda2284ffcea05ef768bc097497579 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 8 Dec 2023 08:05:30 +0000 Subject: [PATCH 21/38] Simplify page list edit warning (#56829) * Alter wording * Make button name match intention * Update wording * Use tweaked wording * Copy tweak + remove emphasis * Tweak --------- Co-authored-by: Rich Tabor --- .../block-library/src/page-list/convert-to-links-modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/page-list/convert-to-links-modal.js b/packages/block-library/src/page-list/convert-to-links-modal.js index cd4049fecff58..f47b5e3de259d 100644 --- a/packages/block-library/src/page-list/convert-to-links-modal.js +++ b/packages/block-library/src/page-list/convert-to-links-modal.js @@ -5,7 +5,7 @@ import { Button, Modal } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; export const convertDescription = __( - 'This page list is synced with the published pages on your site. Detach the page list to add, delete, or reorder pages yourself.' + "This navigation menu displays your website's pages. Editing it will enable you to add, delete, or reorder pages. However, new pages will no longer be added automatically." ); export function ConvertToLinksModal( { onClick, onClose, disabled } ) { @@ -30,7 +30,7 @@ export function ConvertToLinksModal( { onClick, onClose, disabled } ) { disabled={ disabled } onClick={ onClick } > - { __( 'Detach' ) } + { __( 'Edit' ) }
From 11862cf7fe251ebb64993c76aa828f2557e745ed Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Fri, 8 Dec 2023 09:32:02 -0500 Subject: [PATCH 22/38] Components: replace `TabPanel` with `Tabs` in the editor's `ColorPanel` (#56878) * replace `TabPanel` with `Tabs` * implement initial tab to track current value * focusable false * defer unlock call --- .../components/global-styles/color-panel.js | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index 3b55ec36fc91d..99a5519e9dd00 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -12,12 +12,12 @@ import { __experimentalHStack as HStack, __experimentalZStack as ZStack, __experimentalDropdownContentWrapper as DropdownContentWrapper, - TabPanel, ColorIndicator, Flex, FlexItem, Dropdown, Button, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -29,6 +29,7 @@ import ColorGradientControl from '../colors-gradients/control'; import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks'; import { getValueFromVariable } from './utils'; import { setImmutably } from '../../utils/object'; +import { unlock } from '../../lock-unlock'; export function useHasColorPanel( settings ) { const hasTextPanel = useHasTextPanel( settings ); @@ -203,12 +204,11 @@ function ColorPanelDropdown( { colorGradientControlSettings, panelId, } ) { - const tabConfigs = tabs.map( ( { key, label: tabLabel } ) => { - return { - name: key, - title: tabLabel, - }; - } ); + const currentTab = tabs.find( ( tab ) => tab.userValue !== undefined ); + // Unlocking `Tabs` too early causes the `unlock` method to receive an empty + // object, due to circular dependencies. + // See https://github.com/WordPress/gutenberg/issues/52692 + const { Tabs } = unlock( componentsPrivateApis ); return ( ) } { tabs.length > 1 && ( - - { ( tab ) => { - const selectedTab = tabs.find( - ( t ) => t.key === tab.name - ); - - if ( ! selectedTab ) { - return null; - } - + + + { tabs.map( ( tab ) => ( + + { tab.label } + + ) ) } + + + { tabs.map( ( tab ) => { return ( - + + + ); - } } - + } ) } + ) }
From 3613e9ee566da85f9a1cafe95e9a9dc235bc63bb Mon Sep 17 00:00:00 2001 From: mimi Date: Fri, 8 Dec 2023 23:49:34 +0900 Subject: [PATCH 23/38] copy/fix capitalization of WordPress (#56834) --- .../fundamentals/javascript-in-the-block-editor.md | 2 +- packages/block-editor/src/components/link-control/test/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 73c6a6c56e632..615f7f74ce151 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -44,7 +44,7 @@ Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/ho - [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) - [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) -- [Wordpress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) +- [WordPress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) - [Javascript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript) | MDN Web Docs - [block-development-examples](https://github.com/WordPress/block-development-examples) | GitHub repository - [block-theme-examples](https://github.com/wptrainingteam/block-theme-examples) | GitHub repository diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index e0366a3f27ef5..32db57a55d76e 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -2140,7 +2140,7 @@ describe( 'Post types', () => { describe( 'Rich link previews', () => { const selectedLink = { id: '1', - title: 'Wordpress.org', // Customize this for differentiation in assertions. + title: 'WordPress.org', // Customize this for differentiation in assertions. url: 'https://www.wordpress.org', type: 'URL', }; From cb1bbc79e3abf86d95f9f52b1a57311dbdd3ed0a Mon Sep 17 00:00:00 2001 From: Taylor Gorman Date: Fri, 8 Dec 2023 08:55:23 -0600 Subject: [PATCH 24/38] Link to Dashicons (#56872) --- docs/reference-guides/block-api/block-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index edc61d138128e..d023742092df1 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -197,7 +197,7 @@ The `ancestor` property makes a block available inside the specified block types { "icon": "smile" } ``` -An icon property should be specified to make it easier to identify a block. These can be any of WordPress' Dashicons (slug serving also as a fallback in non-js contexts). +An icon property should be specified to make it easier to identify a block. These can be any of [WordPress' Dashicons](https://developer.wordpress.org/resource/dashicons/) (slug serving also as a fallback in non-js contexts). **Note:** It's also possible to override this property on the client-side with the source of the SVG element. In addition, this property can be defined with JavaScript as an object containing background and foreground colors. This colors will appear with the icon when they are applicable e.g.: in the inserter. Custom SVG icons are automatically wrapped in the [wp.primitives.SVG](/packages/primitives/README.md) component to add accessibility attributes (aria-hidden, role, and focusable). From 46ea0ff0bb96db3b50d06f466cf3af7bc0b21ed5 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:00:09 +0200 Subject: [PATCH 25/38] Fix PHP linter failing (#56905) --- .../plugins/interactive-blocks/router-navigate/render.php | 1 + .../plugins/interactive-blocks/router-regions/render.php | 1 + .../e2e-tests/plugins/interactive-blocks/store-tag/render.php | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php index 6dceffa32da8f..3fbddf623db60 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php @@ -3,6 +3,7 @@ * HTML for testing the router navigate function. * * @package gutenberg-test-interactive-blocks + * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable */ diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php index 0bcc14ccb266f..33c319e130fe2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php @@ -3,6 +3,7 @@ * HTML for testing the hydration of router regions. * * @package gutenberg-test-interactive-blocks + * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable */ diff --git a/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php b/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php index 57200f295c33b..06deea9e1169d 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php @@ -3,6 +3,7 @@ * HTML for testing the hydration of the serialized store. * * @package gutenberg-test-interactive-blocks + * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable */ From 78721950a400c13442aa1e42f1207fc129cb6a99 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Fri, 8 Dec 2023 16:37:30 +0100 Subject: [PATCH 26/38] DropdownMenuV2Ariakit: prevent prefix collapsing if all radios or checkboxes are unselected (#56720) * DropdownMenuV2Ariakit: prevent prefix collapsing if all radios or checkboxes are unselected * CHANGELOG * DRY it up --- packages/components/CHANGELOG.md | 4 ++++ .../src/dropdown-menu-v2-ariakit/styles.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ed33677b55a82..d7a838344f5cd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -18,6 +18,10 @@ - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). - `BorderControl`: adjust `BorderControlDropdown` Button size to fix misaligned border ([#56730](https://github.com/WordPress/gutenberg/pull/56730)). +### Internal + +- `DropdownMenuV2Ariakit`: prevent prefix collapsing if all radios or checkboxes are unselected ([#56720](https://github.com/WordPress/gutenberg/pull/56720)). + ## 25.13.0 (2023-11-29) ### Enhancements diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index 465bdb1aebb30..eaa249ae86b78 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -212,6 +212,18 @@ export const ItemPrefixWrapper = styled.span` /* Always occupy the first column, even when auto-collapsing */ grid-column: 1; + /* + * Even when the item is not checked, occupy the same screen space to avoid + * the space collapside when no items are checked. + */ + ${ DropdownMenuCheckboxItem } > &, + ${ DropdownMenuRadioItem } > & { + /* Same width as the check icons */ + min-width: ${ space( 6 ) }; + } + + ${ DropdownMenuCheckboxItem } > &, + ${ DropdownMenuRadioItem } > &, &:not( :empty ) { margin-inline-end: ${ space( 2 ) }; } From 6211c7a61af40c6f0d24c8e140ea81c07eba96fb Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:29:51 -0500 Subject: [PATCH 27/38] Tabs: replace `id` with new `tabId` prop (#56883) * replace `id` with `tabId` * update stories and tests * update `ColorGradientControl` implementation * changelog * use `tabId` in tests for consistency * update `ColorPanel` implementation --- .../components/colors-gradients/control.js | 8 +- .../components/global-styles/color-panel.js | 4 +- packages/components/CHANGELOG.md | 4 + packages/components/src/tabs/README.md | 8 +- .../src/tabs/stories/index.story.tsx | 96 +++++++++---------- packages/components/src/tabs/tab.tsx | 6 +- packages/components/src/tabs/tabpanel.tsx | 10 +- packages/components/src/tabs/test/index.tsx | 48 +++++----- packages/components/src/tabs/types.ts | 11 ++- 9 files changed, 104 insertions(+), 91 deletions(-) diff --git a/packages/block-editor/src/components/colors-gradients/control.js b/packages/block-editor/src/components/colors-gradients/control.js index 0cb2fcdda4487..cf82510f78c89 100644 --- a/packages/block-editor/src/components/colors-gradients/control.js +++ b/packages/block-editor/src/components/colors-gradients/control.js @@ -141,15 +141,15 @@ function ColorGradientControlInner( { } > - + { __( 'Solid' ) } - + { __( 'Gradient' ) } ( { tab.label } @@ -274,7 +274,7 @@ function ColorPanelDropdown( { return ( = ( props ) => { return ( - Tab 1 - Tab 2 - Tab 3 + Tab 1 + Tab 2 + Tab 3 - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

This tabpanel has its focusable prop set to @@ -71,19 +71,19 @@ const DisabledTabTemplate: StoryFn< typeof Tabs > = ( props ) => { return ( - + Tab 1 - Tab 2 - Tab 3 + Tab 2 + Tab 3 - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

@@ -96,31 +96,31 @@ const WithTabIconsAndTooltipsTemplate: StoryFn< typeof Tabs > = ( props ) => { } /> } /> } /> - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

@@ -140,18 +140,18 @@ const UsingSlotFillTemplate: StoryFn< typeof Tabs > = ( props ) => { - Tab 1 - Tab 2 - Tab 3 + Tab 1 + Tab 2 + Tab 3 - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

@@ -196,9 +196,9 @@ const CloseButtonTemplate: StoryFn< typeof Tabs > = ( props ) => { } } > - Tab 1 - Tab 2 - Tab 3 + Tab 1 + Tab 2 + Tab 3
- +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

@@ -251,19 +251,19 @@ const ControlledModeTemplate: StoryFn< typeof Tabs > = ( props ) => { } } > - Tab 1 + Tab 1 - Tab 2 + Tab 2 - Tab 3 + Tab 3 - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

@@ -314,19 +314,19 @@ const TabBecomesDisabledTemplate: StoryFn< typeof Tabs > = ( props ) => { - Tab 1 - + Tab 1 + Tab 2 - Tab 3 + Tab 3 - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

@@ -348,17 +348,17 @@ const TabGetsRemovedTemplate: StoryFn< typeof Tabs > = ( props ) => { - { ! removeTab1 && Tab 1 } - Tab 2 - Tab 3 + { ! removeTab1 && Tab 1 } + Tab 2 + Tab 3 - +

Selected tab: Tab 1

- +

Selected tab: Tab 2

- +

Selected tab: Tab 3

diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index 4bfc99e8ef43b..e1aa85c636cdd 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -15,15 +15,15 @@ import type { WordPressComponentProps } from '../context'; export const Tab = forwardRef< HTMLButtonElement, - WordPressComponentProps< TabProps, 'button', false > ->( function Tab( { children, id, disabled, render, ...otherProps }, ref ) { + Omit< WordPressComponentProps< TabProps, 'button', false >, 'id' > +>( function Tab( { children, tabId, disabled, render, ...otherProps }, ref ) { const context = useTabsContext(); if ( ! context ) { warning( '`Tabs.Tab` must be wrapped in a `Tabs` component.' ); return null; } const { store, instanceId } = context; - const instancedTabId = `${ instanceId }-${ id }`; + const instancedTabId = `${ instanceId }-${ tabId }`; return ( ->( function TabPanel( { children, id, focusable = true, ...otherProps }, ref ) { + Omit< WordPressComponentProps< TabPanelProps, 'div', false >, 'id' > +>( function TabPanel( + { children, tabId, focusable = true, ...otherProps }, + ref +) { const context = useTabsContext(); if ( ! context ) { warning( '`Tabs.TabPanel` must be wrapped in a `Tabs` component.' ); return null; } const { store, instanceId } = context; + const instancedTabId = `${ instanceId }-${ tabId }`; return ( diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index f923dc455fd7b..7e2d467122485 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -16,7 +16,7 @@ import Tabs from '..'; import type { TabsProps } from '../types'; type Tab = { - id: string; + tabId: string; title: string; content: React.ReactNode; tab: { @@ -30,19 +30,19 @@ type Tab = { const TABS: Tab[] = [ { - id: 'alpha', + tabId: 'alpha', title: 'Alpha', content: 'Selected tab: Alpha', tab: { className: 'alpha-class' }, }, { - id: 'beta', + tabId: 'beta', title: 'Beta', content: 'Selected tab: Beta', tab: { className: 'beta-class' }, }, { - id: 'gamma', + tabId: 'gamma', title: 'Gamma', content: 'Selected tab: Gamma', tab: { className: 'gamma-class' }, @@ -52,7 +52,7 @@ const TABS: Tab[] = [ const TABS_WITH_DELTA: Tab[] = [ ...TABS, { - id: 'delta', + tabId: 'delta', title: 'Delta', content: 'Selected tab: Delta', tab: { className: 'delta-class' }, @@ -70,8 +70,8 @@ const UncontrolledTabs = ( { { tabs.map( ( tabObj ) => ( @@ -81,8 +81,8 @@ const UncontrolledTabs = ( { { tabs.map( ( tabObj ) => ( { tabObj.content } @@ -114,8 +114,8 @@ const ControlledTabs = ( { { tabs.map( ( tabObj ) => ( @@ -124,7 +124,7 @@ const ControlledTabs = ( { ) ) } { tabs.map( ( tabObj ) => ( - + { tabObj.content } ) ) } @@ -201,7 +201,7 @@ describe( 'Tabs', () => { } ); it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, content: ( @@ -442,7 +442,7 @@ describe( 'Tabs', () => { const mockOnSelect = jest.fn(); const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.id === 'delta' + tabObj.tabId === 'delta' ? { ...tabObj, tab: { @@ -604,7 +604,7 @@ describe( 'Tabs', () => { } ); it( 'should not load any tab if the active tab is removed and there are no enabled tabs', async () => { const TABS_WITH_BETA_GAMMA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id !== 'alpha' + tabObj.tabId !== 'alpha' ? { ...tabObj, tab: { @@ -726,7 +726,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, tab: { @@ -801,7 +801,7 @@ describe( 'Tabs', () => { const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.id === 'delta' + tabObj.tabId === 'delta' ? { ...tabObj, tab: { @@ -849,7 +849,7 @@ describe( 'Tabs', () => { it( 'should select first enabled tab when the initial tab is disabled', async () => { const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, tab: { @@ -878,7 +878,7 @@ describe( 'Tabs', () => { it( 'should select first enabled tab when the tab associated to `initialTabId` is disabled', async () => { const TABS_ONLY_GAMMA_ENABLED = TABS.map( ( tabObj ) => - tabObj.id !== 'gamma' + tabObj.tabId !== 'gamma' ? { ...tabObj, tab: { @@ -920,7 +920,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, tab: { @@ -967,7 +967,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); const TABS_WITH_GAMMA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'gamma' + tabObj.tabId === 'gamma' ? { ...tabObj, tab: { @@ -1051,7 +1051,7 @@ describe( 'Tabs', () => { // Remove beta rerender( tab.id !== 'beta' ) } + tabs={ TABS.filter( ( tab ) => tab.tabId !== 'beta' ) } selectedTabId="beta" /> ); @@ -1085,7 +1085,7 @@ describe( 'Tabs', () => { it( 'should not render any tab if `selectedTabId` refers to a disabled tab', async () => { const TABS_WITH_DELTA_WITH_BETA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.id === 'beta' + tabObj.tabId === 'beta' ? { ...tabObj, tab: { @@ -1122,7 +1122,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'beta' + tabObj.tabId === 'beta' ? { ...tabObj, tab: { diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 8b07193741091..389665b13357f 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -78,8 +78,10 @@ export type TabListProps = { export type TabProps = { /** * The id of the tab, which is prepended with the `Tabs` instanceId. + * The value of this prop should match with the value of the `tabId` prop on + * the corresponding `Tabs.TabPanel` component. */ - id: string; + tabId: string; /** * The children elements, generally the text to display on the tab. */ @@ -103,9 +105,12 @@ export type TabPanelProps = { */ children?: React.ReactNode; /** - * A unique identifier for the tabpanel, which is used to generate a unique `id` for the underlying element. + * A unique identifier for the tabpanel, which is used to generate an + * instanced id for the underlying element. + * The value of this prop should match with the value of the `tabId` prop on + * the corresponding `Tabs.Tab` component. */ - id: string; + tabId: string; /** * Determines whether or not the tabpanel element should be focusable. * If `false`, pressing the tab key will skip over the tabpanel, and instead From cfa3fd6178706eb52bf4043f1be7af030bdcc660 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 8 Dec 2023 12:16:49 -0500 Subject: [PATCH 28/38] fix: Guard against a block styles crash due to null block values (#56903) * fix: Guard against a block styles crash due to null block values In certain scenarios, e.g., when the editor hangs due to poor performance, the `getBlock` value unexpectedly returns `null`. To guard against a crash in this scenario, we now conditionally access attributes on the block. A ideal resolution would be improving editor performance to avoid the scenario where a race condition causes a crash, but the fact remains that `getBlock` _can_ return `null` values and that we should not presume that it will not. * docs: Add change log entry --- .../src/components/block-styles/index.native.js | 6 ++++-- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/block-styles/index.native.js b/packages/block-editor/src/components/block-styles/index.native.js index 7967e79c2c1cd..40e29476287ff 100644 --- a/packages/block-editor/src/components/block-styles/index.native.js +++ b/packages/block-editor/src/components/block-styles/index.native.js @@ -19,14 +19,16 @@ import StylePreview from './preview'; import containerStyles from './style.scss'; import { store as blockEditorStore } from '../../store'; +const EMPTY_ARRAY = []; + function BlockStyles( { clientId, url } ) { const selector = ( select ) => { const { getBlock } = select( blockEditorStore ); const { getBlockStyles } = select( blocksStore ); const block = getBlock( clientId ); return { - styles: getBlockStyles( block.name ), - className: block.attributes.className || '', + styles: getBlockStyles( block?.name ) || EMPTY_ARRAY, + className: block?.attributes?.className || '', }; }; diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 992d61ea9ce37..791b39fee2c7f 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] [internal] Move InserterButton from components package to block-editor package [#56494] - [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775] +- [*] Guard against an Image block styles crash due to null block values [#56903] ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] From 91cb8fee759eef27e6e2f2c93c2a36ba29eb4082 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:08:24 +0200 Subject: [PATCH 29/38] Block editor: hooks: manage BlockListBlock filters in one place (#56875) --- packages/block-editor/src/hooks/align.js | 41 +---- packages/block-editor/src/hooks/border.js | 117 +++++------- packages/block-editor/src/hooks/color.js | 120 +++++------- packages/block-editor/src/hooks/duotone.js | 149 ++++++--------- packages/block-editor/src/hooks/font-size.js | 89 +++------ packages/block-editor/src/hooks/index.js | 18 +- .../block-editor/src/hooks/layout-child.js | 53 ++++++ packages/block-editor/src/hooks/layout.js | 62 ------- packages/block-editor/src/hooks/position.js | 86 ++++----- packages/block-editor/src/hooks/style.js | 174 +++++++----------- packages/block-editor/src/hooks/test/align.js | 109 +---------- packages/block-editor/src/hooks/test/color.js | 112 ----------- packages/block-editor/src/hooks/utils.js | 107 ++++++++++- 13 files changed, 466 insertions(+), 771 deletions(-) create mode 100644 packages/block-editor/src/hooks/layout-child.js delete mode 100644 packages/block-editor/src/hooks/test/color.js diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 2019228cf2d3e..189f82ccf429f 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -6,7 +6,6 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, @@ -155,54 +154,27 @@ function BlockEditAlignmentToolbarControlsPure( { export default { shareWithChildBlocks: true, edit: BlockEditAlignmentToolbarControlsPure, + useBlockProps, attributeKeys: [ 'align' ], hasSupport( name ) { return hasBlockSupport( name, 'align', false ); }, }; -function BlockListBlockWithDataAlign( { block: BlockListBlock, props } ) { - const { name, attributes } = props; - const { align } = attributes; +function useBlockProps( { name, align } ) { const blockAllowedAlignments = getValidAlignments( getBlockSupport( name, 'align' ), hasBlockSupport( name, 'alignWide', true ) ); const validAlignments = useAvailableAlignments( blockAllowedAlignments ); - let wrapperProps = props.wrapperProps; if ( validAlignments.some( ( alignment ) => alignment.name === align ) ) { - wrapperProps = { ...wrapperProps, 'data-align': align }; + return { 'data-align': align }; } - return ; + return {}; } -/** - * Override the default block element to add alignment wrapper props. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withDataAlign = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - // If an alignment is not assigned, there's no need to go through the - // effort to validate or assign its value. - if ( props.attributes.align === undefined ) { - return ; - } - - return ( - - ); - }, - 'withDataAlign' -); - /** * Override props assigned to save component to inject alignment class name if * block supports it. @@ -237,11 +209,6 @@ addFilter( 'core/editor/align/addAttribute', addAttribute ); -addFilter( - 'editor.BlockListBlock', - 'core/editor/align/with-data-align', - withDataAlign -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/align/addAssignedAlign', diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index d8905b29a2961..6ac4dd2360fb0 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; */ import { getBlockSupport } from '@wordpress/blocks'; import { __experimentalHasSplitBorders as hasSplitBorders } from '@wordpress/components'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { pure } from '@wordpress/compose'; import { Platform, useCallback, useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { useSelect } from '@wordpress/data'; @@ -330,72 +330,55 @@ function addEditProps( settings ) { return settings; } -/** - * This adds inline styles for color palette colors. - * Ideally, this is not needed and themes should load their palettes on the editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withBorderColorPaletteStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const { borderColor, style } = attributes; - const { colors } = useMultipleOriginColorsAndGradients(); - - if ( - ! hasBorderSupport( name, 'color' ) || - shouldSkipSerialization( name, BORDER_SUPPORT_KEY, 'color' ) - ) { - return ; - } +function useBlockProps( { name, borderColor, style } ) { + const { colors } = useMultipleOriginColorsAndGradients(); - const { color: borderColorValue } = getMultiOriginColor( { - colors, - namedColor: borderColor, - } ); - const { color: borderTopColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( style?.border?.top?.color ), - } ); - const { color: borderRightColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( style?.border?.right?.color ), - } ); - - const { color: borderBottomColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( - style?.border?.bottom?.color - ), - } ); - const { color: borderLeftColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( style?.border?.left?.color ), - } ); - - const extraStyles = { - borderTopColor: borderTopColor || borderColorValue, - borderRightColor: borderRightColor || borderColorValue, - borderBottomColor: borderBottomColor || borderColorValue, - borderLeftColor: borderLeftColor || borderColorValue, - }; - const cleanedExtraStyles = cleanEmptyObject( extraStyles ) || {}; - - let wrapperProps = props.wrapperProps; - wrapperProps = { - ...props.wrapperProps, - style: { - ...props.wrapperProps?.style, - ...cleanedExtraStyles, - }, - }; + if ( + ! hasBorderSupport( name, 'color' ) || + shouldSkipSerialization( name, BORDER_SUPPORT_KEY, 'color' ) + ) { + return {}; + } - return ; + const { color: borderColorValue } = getMultiOriginColor( { + colors, + namedColor: borderColor, + } ); + const { color: borderTopColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.top?.color ), + } ); + const { color: borderRightColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.right?.color ), + } ); + + const { color: borderBottomColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.bottom?.color ), + } ); + const { color: borderLeftColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.left?.color ), + } ); + + const extraStyles = { + borderTopColor: borderTopColor || borderColorValue, + borderRightColor: borderRightColor || borderColorValue, + borderBottomColor: borderBottomColor || borderColorValue, + borderLeftColor: borderLeftColor || borderColorValue, + }; + + return { style: cleanEmptyObject( extraStyles ) || {} }; +} + +export default { + useBlockProps, + attributeKeys: [ 'borderColor', 'style' ], + hasSupport( name ) { + return hasBorderSupport( name, 'color' ); }, - 'withBorderColorPaletteStyles' -); +}; addFilter( 'blocks.registerBlockType', @@ -414,9 +397,3 @@ addFilter( 'core/border/addEditProps', addEditProps ); - -addFilter( - 'editor.BlockListBlock', - 'core/border/with-border-color-palette-styles', - withBorderColorPaletteStyles -); diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index d5cb21e5dcf9a..f259ff9c9c086 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -9,7 +9,7 @@ import classnames from 'classnames'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport } from '@wordpress/blocks'; import { useMemo, Platform, useCallback } from '@wordpress/element'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { pure } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; /** @@ -364,72 +364,56 @@ function ColorEditPure( { clientId, name, setAttributes, settings } ) { // and not the whole attributes object. export const ColorEdit = pure( ColorEditPure ); -/** - * This adds inline styles for color palette colors. - * Ideally, this is not needed and themes should load their palettes on the editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withColorPaletteStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const { backgroundColor, textColor } = attributes; - const [ userPalette, themePalette, defaultPalette ] = useSettings( - 'color.palette.custom', - 'color.palette.theme', - 'color.palette.default' - ); - - const colors = useMemo( - () => [ - ...( userPalette || [] ), - ...( themePalette || [] ), - ...( defaultPalette || [] ), - ], - [ userPalette, themePalette, defaultPalette ] - ); - if ( - ! hasColorSupport( name ) || - shouldSkipSerialization( name, COLOR_SUPPORT_KEY ) - ) { - return ; - } - const extraStyles = {}; - - if ( - textColor && - ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'text' ) - ) { - extraStyles.color = getColorObjectByAttributeValues( - colors, - textColor - )?.color; - } - if ( - backgroundColor && - ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'background' ) - ) { - extraStyles.backgroundColor = getColorObjectByAttributeValues( - colors, - backgroundColor - )?.color; - } +function useBlockProps( { name, backgroundColor, textColor } ) { + const [ userPalette, themePalette, defaultPalette ] = useSettings( + 'color.palette.custom', + 'color.palette.theme', + 'color.palette.default' + ); - let wrapperProps = props.wrapperProps; - wrapperProps = { - ...props.wrapperProps, - style: { - ...extraStyles, - ...props.wrapperProps?.style, - }, - }; + const colors = useMemo( + () => [ + ...( userPalette || [] ), + ...( themePalette || [] ), + ...( defaultPalette || [] ), + ], + [ userPalette, themePalette, defaultPalette ] + ); + if ( + ! hasColorSupport( name ) || + shouldSkipSerialization( name, COLOR_SUPPORT_KEY ) + ) { + return {}; + } + const extraStyles = {}; - return ; - }, - 'withColorPaletteStyles' -); + if ( + textColor && + ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'text' ) + ) { + extraStyles.color = getColorObjectByAttributeValues( + colors, + textColor + )?.color; + } + if ( + backgroundColor && + ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'background' ) + ) { + extraStyles.backgroundColor = getColorObjectByAttributeValues( + colors, + backgroundColor + )?.color; + } + + return { style: extraStyles }; +} + +export default { + useBlockProps, + attributeKeys: [ 'backgroundColor', 'textColor' ], + hasSupport: hasColorSupport, +}; const MIGRATION_PATHS = { linkColor: [ [ 'style', 'elements', 'link', 'color', 'text' ] ], @@ -477,12 +461,6 @@ addFilter( addEditProps ); -addFilter( - 'editor.BlockListBlock', - 'core/color/with-color-palette-styles', - withColorPaletteStyles -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/color/addTransforms', diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index c0b76d12cb370..0df0d50d64457 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import classnames from 'classnames'; import { extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; @@ -13,7 +12,7 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; @@ -178,6 +177,7 @@ function DuotonePanelPure( { style, setAttributes, name } ) { export default { shareWithChildBlocks: true, edit: DuotonePanelPure, + useBlockProps, attributeKeys: [ 'style' ], hasSupport( name ) { return hasBlockSupport( name, 'filter.duotone' ); @@ -212,7 +212,7 @@ function addDuotoneAttributes( settings ) { return settings; } -function DuotoneStyles( { +function useDuotoneStyles( { clientId, id: filterId, selector: duotoneSelector, @@ -310,98 +310,69 @@ function DuotoneStyles( { blockElement.style.display = display; } }, [ isValidFilter, blockElement ] ); - - return null; } -/** - * Override the default block element to include duotone styles. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -const withDuotoneStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const id = useInstanceId( BlockListBlock ); - - const selector = useMemo( () => { - const blockType = getBlockType( props.name ); - - if ( blockType ) { - // Backwards compatibility for `supports.color.__experimentalDuotone` - // is provided via the `block_type_metadata_settings` filter. If - // `supports.filter.duotone` has not been set and the - // experimental property has been, the experimental property - // value is copied into `supports.filter.duotone`. - const duotoneSupport = getBlockSupport( - blockType, - 'filter.duotone', - false - ); - if ( ! duotoneSupport ) { - return null; - } - - // If the experimental duotone support was set, that value is - // to be treated as a selector and requires scoping. - const experimentalDuotone = getBlockSupport( - blockType, - 'color.__experimentalDuotone', - false - ); - if ( experimentalDuotone ) { - const rootSelector = getBlockCSSSelector( blockType ); - return typeof experimentalDuotone === 'string' - ? scopeSelector( rootSelector, experimentalDuotone ) - : rootSelector; - } - - // Regular filter.duotone support uses filter.duotone selectors with fallbacks. - return getBlockCSSSelector( blockType, 'filter.duotone', { - fallback: true, - } ); +function useBlockProps( { name, style } ) { + const id = useInstanceId( useBlockProps ); + const selector = useMemo( () => { + const blockType = getBlockType( name ); + + if ( blockType ) { + // Backwards compatibility for `supports.color.__experimentalDuotone` + // is provided via the `block_type_metadata_settings` filter. If + // `supports.filter.duotone` has not been set and the + // experimental property has been, the experimental property + // value is copied into `supports.filter.duotone`. + const duotoneSupport = getBlockSupport( + blockType, + 'filter.duotone', + false + ); + if ( ! duotoneSupport ) { + return null; } - }, [ props.name ] ); - - const attribute = props?.attributes?.style?.color?.duotone; - - const filterClass = `wp-duotone-${ id }`; - - const shouldRender = selector && attribute; - - const className = shouldRender - ? classnames( props?.className, filterClass ) - : props?.className; - - // CAUTION: code added before this line will be executed - // for all blocks, not just those that support duotone. Code added - // above this line should be carefully evaluated for its impact on - // performance. - return ( - <> - { shouldRender && ( - - ) } - - - ); - }, - 'withDuotoneStyles' -); + + // If the experimental duotone support was set, that value is + // to be treated as a selector and requires scoping. + const experimentalDuotone = getBlockSupport( + blockType, + 'color.__experimentalDuotone', + false + ); + if ( experimentalDuotone ) { + const rootSelector = getBlockCSSSelector( blockType ); + return typeof experimentalDuotone === 'string' + ? scopeSelector( rootSelector, experimentalDuotone ) + : rootSelector; + } + + // Regular filter.duotone support uses filter.duotone selectors with fallbacks. + return getBlockCSSSelector( blockType, 'filter.duotone', { + fallback: true, + } ); + } + }, [ name ] ); + + const attribute = style?.color?.duotone; + + const filterClass = `wp-duotone-${ id }`; + + const shouldRender = selector && attribute; + + useDuotoneStyles( { + clientId: id, + id: filterClass, + selector, + attribute, + } ); + + return { + className: shouldRender ? filterClass : '', + }; +} addFilter( 'blocks.registerBlockType', 'core/editor/duotone/add-attributes', addDuotoneAttributes ); -addFilter( - 'editor.BlockListBlock', - 'core/editor/duotone/with-styles', - withDuotoneStyles -); diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index 146abe1d1f72f..a7ef79f0d4f2b 100644 --- a/packages/block-editor/src/hooks/font-size.js +++ b/packages/block-editor/src/hooks/font-size.js @@ -4,7 +4,6 @@ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; import TokenList from '@wordpress/token-list'; -import { createHigherOrderComponent } from '@wordpress/compose'; import { select } from '@wordpress/data'; /** @@ -175,62 +174,38 @@ export function useIsFontSizeDisabled( { name: blockName } = {} ) { ); } -/** - * Add inline styles for font sizes. - * Ideally, this is not needed and themes load the font-size classes on the - * editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -const withFontSizeInlineStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const [ fontSizes ] = useSettings( 'typography.fontSizes' ); - const { - name: blockName, - attributes: { fontSize, style }, - wrapperProps, - } = props; - - // Only add inline styles if the block supports font sizes, - // doesn't skip serialization of font sizes, - // doesn't already have an inline font size, - // and does have a class to extract the font size from. - if ( - ! hasBlockSupport( blockName, FONT_SIZE_SUPPORT_KEY ) || - shouldSkipSerialization( - blockName, - TYPOGRAPHY_SUPPORT_KEY, - 'fontSize' - ) || - ! fontSize || - style?.typography?.fontSize - ) { - return ; - } +function useBlockProps( { name, fontSize, style } ) { + const [ fontSizes ] = useSettings( 'typography.fontSizes' ); - const fontSizeValue = getFontSize( - fontSizes, - fontSize, - style?.typography?.fontSize - ).size; - - const newProps = { - ...props, - wrapperProps: { - ...wrapperProps, - style: { - fontSize: fontSizeValue, - ...wrapperProps?.style, - }, - }, - }; + // Only add inline styles if the block supports font sizes, + // doesn't skip serialization of font sizes, + // doesn't already have an inline font size, + // and does have a class to extract the font size from. + if ( + ! hasBlockSupport( name, FONT_SIZE_SUPPORT_KEY ) || + shouldSkipSerialization( name, TYPOGRAPHY_SUPPORT_KEY, 'fontSize' ) || + ! fontSize || + style?.typography?.fontSize + ) { + return; + } + + const fontSizeValue = getFontSize( + fontSizes, + fontSize, + style?.typography?.fontSize + ).size; + + return { style: { fontSize: fontSizeValue } }; +} - return ; +export default { + useBlockProps, + attributeKeys: [ 'fontSize', 'style' ], + hasSupport( name ) { + return hasBlockSupport( name, FONT_SIZE_SUPPORT_KEY ); }, - 'withFontSizeInlineStyles' -); +}; const MIGRATION_PATHS = { fontSize: [ [ 'fontSize' ], [ 'style', 'typography', 'fontSize' ] ], @@ -332,12 +307,6 @@ addFilter( addFilter( 'blocks.registerBlockType', 'core/font/addEditProps', addEditProps ); -addFilter( - 'editor.BlockListBlock', - 'core/font-size/with-font-size-inline-styles', - withFontSizeInlineStyles -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/font-size/addTransforms', diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 6ae589dd672bf..506f2a50a83a7 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { createBlockEditFilter } from './utils'; +import { createBlockEditFilter, createBlockListBlockFilter } from './utils'; import './compat'; import align from './align'; import './lock'; @@ -11,13 +11,14 @@ import customClassName from './custom-class-name'; import './generated-class-name'; import style from './style'; import './settings'; -import './color'; +import color from './color'; import duotone from './duotone'; import './font-family'; -import './font-size'; -import './border'; +import fontSize from './font-size'; +import border from './border'; import position from './position'; import layout from './layout'; +import childLayout from './layout-child'; import './content-lock-ui'; import './metadata'; import customFields from './custom-fields'; @@ -38,6 +39,15 @@ createBlockEditFilter( blockRenaming, ].filter( Boolean ) ); +createBlockListBlockFilter( [ + align, + color, + duotone, + fontSize, + border, + position, + childLayout, +] ); export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js new file mode 100644 index 0000000000000..58a75a568c40d --- /dev/null +++ b/packages/block-editor/src/hooks/layout-child.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { useStyleOverride } from './utils'; + +function useBlockPropsChildLayoutStyles( { style } ) { + const shouldRenderChildLayoutStyles = useSelect( ( select ) => { + return ! select( blockEditorStore ).getSettings().disableLayoutStyles; + } ); + const layout = style?.layout ?? {}; + const { selfStretch, flexSize } = layout; + const id = useInstanceId( useBlockPropsChildLayoutStyles ); + const selector = `.wp-container-content-${ id }`; + + let css = ''; + if ( shouldRenderChildLayoutStyles ) { + if ( selfStretch === 'fixed' && flexSize ) { + css = `${ selector } { + flex-basis: ${ flexSize }; + box-sizing: border-box; + }`; + } else if ( selfStretch === 'fill' ) { + css = `${ selector } { + flex-grow: 1; + }`; + } + } + + useStyleOverride( { css } ); + + // Only attach a container class if there is generated CSS to be attached. + if ( ! css ) { + return; + } + + // Attach a `wp-container-content` id-based classname. + return { className: `wp-container-content-${ id }` }; +} + +export default { + useBlockProps: useBlockPropsChildLayoutStyles, + attributeKeys: [ 'style' ], + hasSupport() { + return true; + }, +}; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 46239e1de0703..18bb46a87a1b8 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -417,63 +417,6 @@ export const withLayoutStyles = createHigherOrderComponent( 'withLayoutStyles' ); -function BlockWithChildLayoutStyles( { block: BlockListBlock, props } ) { - const layout = props.attributes.style?.layout ?? {}; - const { selfStretch, flexSize } = layout; - - const id = useInstanceId( BlockListBlock ); - const selector = `.wp-container-content-${ id }`; - - let css = ''; - if ( selfStretch === 'fixed' && flexSize ) { - css = `${ selector } { - flex-basis: ${ flexSize }; - box-sizing: border-box; - }`; - } else if ( selfStretch === 'fill' ) { - css = `${ selector } { - flex-grow: 1; - }`; - } - - // Attach a `wp-container-content` id-based classname. - const className = classnames( props.className, { - [ `wp-container-content-${ id }` ]: !! css, // Only attach a container class if there is generated CSS to be attached. - } ); - - useStyleOverride( { css } ); - - return ; -} - -/** - * Override the default block element to add the child layout styles. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withChildLayoutStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const shouldRenderChildLayoutStyles = useSelect( ( select ) => { - return ! select( blockEditorStore ).getSettings() - .disableLayoutStyles; - } ); - - if ( ! shouldRenderChildLayoutStyles ) { - return ; - } - - return ( - - ); - }, - 'withChildLayoutStyles' -); - addFilter( 'blocks.registerBlockType', 'core/layout/addAttribute', @@ -484,8 +427,3 @@ addFilter( 'core/editor/layout/with-layout-styles', withLayoutStyles ); -addFilter( - 'editor.BlockListBlock', - 'core/editor/layout/with-child-layout-styles', - withChildLayoutStyles -); diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index cdeb90822f0ac..5017cb34fc18b 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -12,10 +12,9 @@ import { BaseControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo, Platform } from '@wordpress/element'; -import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies @@ -322,63 +321,44 @@ export default { } return ; }, + useBlockProps, attributeKeys: [ 'style' ], hasSupport( name ) { return hasBlockSupport( name, POSITION_SUPPORT_KEY ); }, }; -/** - * Override the default block element to add the position styles. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withPositionStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const hasPositionBlockSupport = hasBlockSupport( - name, - POSITION_SUPPORT_KEY - ); - const isPositionDisabled = useIsPositionDisabled( props ); - const allowPositionStyles = - hasPositionBlockSupport && ! isPositionDisabled; - - const id = useInstanceId( BlockListBlock ); - - // Higher specificity to override defaults in editor UI. - const positionSelector = `.wp-container-${ id }.wp-container-${ id }`; - - // Get CSS string for the current position values. - let css; - if ( allowPositionStyles ) { - css = - getPositionCSS( { - selector: positionSelector, - style: attributes?.style, - } ) || ''; - } +function useBlockProps( { name, style } ) { + const hasPositionBlockSupport = hasBlockSupport( + name, + POSITION_SUPPORT_KEY + ); + const isPositionDisabled = useIsPositionDisabled( { name } ); + const allowPositionStyles = hasPositionBlockSupport && ! isPositionDisabled; + + const id = useInstanceId( useBlockProps ); + + // Higher specificity to override defaults in editor UI. + const positionSelector = `.wp-container-${ id }.wp-container-${ id }`; + + // Get CSS string for the current position values. + let css; + if ( allowPositionStyles ) { + css = + getPositionCSS( { + selector: positionSelector, + style, + } ) || ''; + } - // Attach a `wp-container-` id-based class name. - const className = classnames( props?.className, { - [ `wp-container-${ id }` ]: allowPositionStyles && !! css, // Only attach a container class if there is generated CSS to be attached. - [ `is-position-${ attributes?.style?.position?.type }` ]: - allowPositionStyles && - !! css && - !! attributes?.style?.position?.type, - } ); + // Attach a `wp-container-` id-based class name. + const className = classnames( { + [ `wp-container-${ id }` ]: allowPositionStyles && !! css, // Only attach a container class if there is generated CSS to be attached. + [ `is-position-${ style?.position?.type }` ]: + allowPositionStyles && !! css && !! style?.position?.type, + } ); - useStyleOverride( { css } ); + useStyleOverride( { css } ); - return ; - }, - 'withPositionStyles' -); - -addFilter( - 'editor.BlockListBlock', - 'core/editor/position/with-position-styles', - withPositionStyles -); + return { className }; +} diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 4036342316887..935e8260fa89f 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -13,7 +8,7 @@ import { hasBlockSupport, __EXPERIMENTAL_ELEMENTS as ELEMENTS, } from '@wordpress/blocks'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { getCSSRules, compileCSS } from '@wordpress/style-engine'; /** @@ -379,6 +374,8 @@ function BlockStyleControls( { export default { edit: BlockStyleControls, hasSupport: hasStyleSupport, + attributeKeys: [ 'style' ], + useBlockProps, }; // Defines which element types are supported, including their hover styles or @@ -393,115 +390,90 @@ const elementTypes = [ }, ]; -/** - * Override the default block element to include elements styles. - * - * @param {Function} BlockListBlock Original component - * @return {Function} Wrapped component - */ -const withElementsStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const blockElementsContainerIdentifier = `wp-elements-${ useInstanceId( - BlockListBlock - ) }`; - - // The .editor-styles-wrapper selector is required on elements styles. As it is - // added to all other editor styles, not providing it causes reset and global - // styles to override element styles because of higher specificity. - const baseElementSelector = `.editor-styles-wrapper .${ blockElementsContainerIdentifier }`; - const blockElementStyles = props.attributes.style?.elements; - - const styles = useMemo( () => { - if ( ! blockElementStyles ) { +function useBlockProps( { name, style } ) { + const blockElementsContainerIdentifier = `wp-elements-${ useInstanceId( + useBlockProps + ) }`; + + // The .editor-styles-wrapper selector is required on elements styles. As it is + // added to all other editor styles, not providing it causes reset and global + // styles to override element styles because of higher specificity. + const baseElementSelector = `.editor-styles-wrapper .${ blockElementsContainerIdentifier }`; + const blockElementStyles = style?.elements; + + const styles = useMemo( () => { + if ( ! blockElementStyles ) { + return; + } + + const elementCSSRules = []; + + elementTypes.forEach( ( { elementType, pseudo, elements } ) => { + const skipSerialization = shouldSkipSerialization( + name, + COLOR_SUPPORT_KEY, + elementType + ); + + if ( skipSerialization ) { return; } - const elementCSSRules = []; + const elementStyles = blockElementStyles?.[ elementType ]; - elementTypes.forEach( ( { elementType, pseudo, elements } ) => { - const skipSerialization = shouldSkipSerialization( - props.name, - COLOR_SUPPORT_KEY, - elementType + // Process primary element type styles. + if ( elementStyles ) { + const selector = scopeSelector( + baseElementSelector, + ELEMENTS[ elementType ] ); - if ( skipSerialization ) { - return; - } - - const elementStyles = blockElementStyles?.[ elementType ]; - - // Process primary element type styles. - if ( elementStyles ) { - const selector = scopeSelector( - baseElementSelector, - ELEMENTS[ elementType ] - ); - - elementCSSRules.push( - compileCSS( elementStyles, { selector } ) - ); - - // Process any interactive states for the element type. - if ( pseudo ) { - pseudo.forEach( ( pseudoSelector ) => { - if ( elementStyles[ pseudoSelector ] ) { - elementCSSRules.push( - compileCSS( - elementStyles[ pseudoSelector ], - { - selector: scopeSelector( - baseElementSelector, - `${ ELEMENTS[ elementType ] }${ pseudoSelector }` - ), - } - ) - ); - } - } ); - } - } + elementCSSRules.push( + compileCSS( elementStyles, { selector } ) + ); - // Process related elements e.g. h1-h6 for headings - if ( elements ) { - elements.forEach( ( element ) => { - if ( blockElementStyles[ element ] ) { + // Process any interactive states for the element type. + if ( pseudo ) { + pseudo.forEach( ( pseudoSelector ) => { + if ( elementStyles[ pseudoSelector ] ) { elementCSSRules.push( - compileCSS( blockElementStyles[ element ], { + compileCSS( elementStyles[ pseudoSelector ], { selector: scopeSelector( baseElementSelector, - ELEMENTS[ element ] + `${ ELEMENTS[ elementType ] }${ pseudoSelector }` ), } ) ); } } ); } - } ); + } - return elementCSSRules.length > 0 - ? elementCSSRules.join( '' ) - : undefined; - }, [ baseElementSelector, blockElementStyles, props.name ] ); - - useStyleOverride( { css: styles } ); - - return ( - - ); - }, - 'withElementsStyles' -); + // Process related elements e.g. h1-h6 for headings + if ( elements ) { + elements.forEach( ( element ) => { + if ( blockElementStyles[ element ] ) { + elementCSSRules.push( + compileCSS( blockElementStyles[ element ], { + selector: scopeSelector( + baseElementSelector, + ELEMENTS[ element ] + ), + } ) + ); + } + } ); + } + } ); + + return elementCSSRules.length > 0 + ? elementCSSRules.join( '' ) + : undefined; + }, [ baseElementSelector, blockElementStyles, name ] ); + + useStyleOverride( { css: styles } ); + return { className: blockElementsContainerIdentifier }; +} addFilter( 'blocks.registerBlockType', @@ -520,9 +492,3 @@ addFilter( 'core/style/addEditProps', addEditProps ); - -addFilter( - 'editor.BlockListBlock', - 'core/editor/with-elements-styles', - withElementsStyles -); diff --git a/packages/block-editor/src/hooks/test/align.js b/packages/block-editor/src/hooks/test/align.js index c695399e993b0..73c55133a4b29 100644 --- a/packages/block-editor/src/hooks/test/align.js +++ b/packages/block-editor/src/hooks/test/align.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { render, screen } from '@testing-library/react'; - /** * WordPress dependencies */ @@ -16,8 +11,7 @@ import { /** * Internal dependencies */ -import BlockEditorProvider from '../../components/provider'; -import { getValidAlignments, withDataAlign, addAssignedAlign } from '../align'; +import { getValidAlignments, addAssignedAlign } from '../align'; const noop = () => {}; @@ -149,107 +143,6 @@ describe( 'align', () => { } ); } ); - describe( 'withDataAlign', () => { - it( 'should render with wrapper props', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: true, - }, - } ); - - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( -
, when walking an input document. + * + * @since 6.2.0 + * @var bool + */ + private $stop_on_tag_closers; + + /** + * How many bytes from the original HTML document have been read and parsed. + * + * This value points to the latest byte offset in the input document which + * has been already parsed. It is the internal cursor for the Tag Processor + * and updates while scanning through the HTML tokens. + * + * @since 6.2.0 + * @var int + */ + private $bytes_already_parsed = 0; + + /** + * Byte offset in input document where current token starts. + * + * Example: + * + *
... + * 01234 + * - token starts at 0 + * + * @since 6.5.0 + * + * @var int|null + */ + private $token_starts_at; + + /** + * Byte length of current token. + * + * Example: + * + *
... + * 012345678901234 + * - token length is 14 - 0 = 14 + * + * a is a token. + * 0123456789 123456789 123456789 + * - token length is 17 - 2 = 15 + * + * @since 6.5.0 + * + * @var int|null + */ + private $token_length; + + /** + * Byte offset in input document where current tag name starts. + * + * Example: + * + *
... + * 01234 + * - tag name starts at 1 + * + * @since 6.2.0 + * + * @var int|null + */ + private $tag_name_starts_at; + + /** + * Byte length of current tag name. + * + * Example: + * + *
... + * 01234 + * --- tag name length is 3 + * + * @since 6.2.0 + * + * @var int|null + */ + private $tag_name_length; + + /** + * Whether the current tag is an opening tag, e.g.
, or a closing tag, e.g.
. + * + * @var bool + */ + private $is_closing_tag; + + /** + * Lazily-built index of attributes found within an HTML tag, keyed by the attribute name. + * + * Example: + * + * // Supposing the parser is working through this content + * // and stops after recognizing the `id` attribute. + * //
+ * // ^ parsing will continue from this point. + * $this->attributes = array( + * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ) + * ); + * + * // When picking up parsing again, or when asking to find the + * // `class` attribute we will continue and add to this array. + * $this->attributes = array( + * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ), + * 'class' => new WP_HTML_Attribute_Token( 'class', 23, 7, 17, 13, false ) + * ); + * + * // Note that only the `class` attribute value is stored in the index. + * // That's because it is the only value used by this class at the moment. + * + * @since 6.2.0 + * @var WP_HTML_Attribute_Token[] + */ + private $attributes = array(); + + /** + * Tracks spans of duplicate attributes on a given tag, used for removing + * all copies of an attribute when calling `remove_attribute()`. + * + * @since 6.3.2 + * + * @var (WP_HTML_Span[])[]|null + */ + private $duplicate_attributes = null; + + /** + * Which class names to add or remove from a tag. + * + * These are tracked separately from attribute updates because they are + * semantically distinct, whereas this interface exists for the common + * case of adding and removing class names while other attributes are + * generally modified as with DOM `setAttribute` calls. + * + * When modifying an HTML document these will eventually be collapsed + * into a single `set_attribute( 'class', $changes )` call. + * + * Example: + * + * // Add the `wp-block-group` class, remove the `wp-group` class. + * $classname_updates = array( + * // Indexed by a comparable class name. + * 'wp-block-group' => WP_HTML_Tag_Processor::ADD_CLASS, + * 'wp-group' => WP_HTML_Tag_Processor::REMOVE_CLASS + * ); + * + * @since 6.2.0 + * @var bool[] + */ + private $classname_updates = array(); + + /** + * Tracks a semantic location in the original HTML which + * shifts with updates as they are applied to the document. + * + * @since 6.2.0 + * @var WP_HTML_Span[] + */ + protected $bookmarks = array(); + + const ADD_CLASS = true; + const REMOVE_CLASS = false; + const SKIP_CLASS = null; + + /** + * Lexical replacements to apply to input HTML document. + * + * "Lexical" in this class refers to the part of this class which + * operates on pure text _as text_ and not as HTML. There's a line + * between the public interface, with HTML-semantic methods like + * `set_attribute` and `add_class`, and an internal state that tracks + * text offsets in the input document. + * + * When higher-level HTML methods are called, those have to transform their + * operations (such as setting an attribute's value) into text diffing + * operations (such as replacing the sub-string from indices A to B with + * some given new string). These text-diffing operations are the lexical + * updates. + * + * As new higher-level methods are added they need to collapse their + * operations into these lower-level lexical updates since that's the + * Tag Processor's internal language of change. Any code which creates + * these lexical updates must ensure that they do not cross HTML syntax + * boundaries, however, so these should never be exposed outside of this + * class or any classes which intentionally expand its functionality. + * + * These are enqueued while editing the document instead of being immediately + * applied to avoid processing overhead, string allocations, and string + * copies when applying many updates to a single document. + * + * Example: + * + * // Replace an attribute stored with a new value, indices + * // sourced from the lazily-parsed HTML recognizer. + * $start = $attributes['src']->start; + * $length = $attributes['src']->length; + * $modifications[] = new WP_HTML_Text_Replacement( $start, $length, $new_value ); + * + * // Correspondingly, something like this will appear in this array. + * $lexical_updates = array( + * WP_HTML_Text_Replacement( 14, 28, 'https://my-site.my-domain/wp-content/uploads/2014/08/kittens.jpg' ) + * ); + * + * @since 6.2.0 + * @var WP_HTML_Text_Replacement[] + */ + protected $lexical_updates = array(); + + /** + * Tracks and limits `seek()` calls to prevent accidental infinite loops. + * + * @since 6.2.0 + * @var int + * + * @see WP_HTML_Tag_Processor::seek() + */ + protected $seek_count = 0; + + /** + * Constructor. + * + * @since 6.2.0 + * + * @param string $html HTML to process. + */ + public function __construct( $html ) { + $this->html = $html; + } + + /** + * Finds the next tag matching the $query. + * + * @since 6.2.0 + * + * @param array|string|null $query { + * Optional. Which tag name to find, having which class, etc. Default is to find any tag. + * + * @type string|null $tag_name Which tag to find, or `null` for "any tag." + * @type int|null $match_offset Find the Nth tag matching all search criteria. + * 1 for "first" tag, 3 for "third," etc. + * Defaults to first tag. + * @type string|null $class_name Tag must contain this whole class name to match. + * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. + * } + * @return bool Whether a tag was matched. + */ + public function next_tag( $query = null ) { + $this->parse_query( $query ); + $already_found = 0; + + do { + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + // Find the next tag if it exists. + if ( false === $this->parse_next_tag() ) { + $this->bytes_already_parsed = strlen( $this->html ); + + return false; + } + + // Parse all of its attributes. + while ( $this->parse_next_attribute() ) { + continue; + } + + // Ensure that the tag closes before the end of the document. + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); + if ( false === $tag_ends_at ) { + return false; + } + $this->token_length = $tag_ends_at - $this->token_starts_at; + $this->bytes_already_parsed = $tag_ends_at; + + // Finally, check if the parsed tag and its attributes match the search query. + if ( $this->matches() ) { + ++$already_found; + } + + /* + * For non-DATA sections which might contain text that looks like HTML tags but + * isn't, scan with the appropriate alternative mode. Looking at the first letter + * of the tag name as a pre-check avoids a string allocation when it's not needed. + */ + $t = $this->html[ $this->tag_name_starts_at ]; + if ( + ! $this->is_closing_tag && + ( + 'i' === $t || 'I' === $t || + 'n' === $t || 'N' === $t || + 's' === $t || 'S' === $t || + 't' === $t || 'T' === $t + ) ) { + $tag_name = $this->get_tag(); + + if ( 'SCRIPT' === $tag_name && ! $this->skip_script_data() ) { + $this->bytes_already_parsed = strlen( $this->html ); + return false; + } elseif ( + ( 'TEXTAREA' === $tag_name || 'TITLE' === $tag_name ) && + ! $this->skip_rcdata( $tag_name ) + ) { + $this->bytes_already_parsed = strlen( $this->html ); + return false; + } elseif ( + ( + 'IFRAME' === $tag_name || + 'NOEMBED' === $tag_name || + 'NOFRAMES' === $tag_name || + 'NOSCRIPT' === $tag_name || + 'STYLE' === $tag_name + ) && + ! $this->skip_rawtext( $tag_name ) + ) { + /* + * "XMP" should be here too but its rules are more complicated and require the + * complexity of the HTML Processor (it needs to close out any open P element, + * meaning it can't be skipped here or else the HTML Processor will lose its + * place). For now, it can be ignored as it's a rare HTML tag in practice and + * any normative HTML should be using PRE instead. + */ + $this->bytes_already_parsed = strlen( $this->html ); + return false; + } + } + } while ( $already_found < $this->sought_match_offset ); + + return true; + } + + + /** + * Generator for a foreach loop to step through each class name for the matched tag. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( "
" ); + * $p->next_tag(); + * foreach ( $p->class_list() as $class_name ) { + * echo "{$class_name} "; + * } + * // Outputs: "free lang-en " + * + * @since 6.4.0 + */ + public function class_list() { + /** @var string $class contains the string value of the class attribute, with character references decoded. */ + $class = $this->get_attribute( 'class' ); + + if ( ! is_string( $class ) ) { + return; + } + + $seen = array(); + + $at = 0; + while ( $at < strlen( $class ) ) { + // Skip past any initial boundary characters. + $at += strspn( $class, " \t\f\r\n", $at ); + if ( $at >= strlen( $class ) ) { + return; + } + + // Find the byte length until the next boundary. + $length = strcspn( $class, " \t\f\r\n", $at ); + if ( 0 === $length ) { + return; + } + + /* + * CSS class names are case-insensitive in the ASCII range. + * + * @see https://www.w3.org/TR/CSS2/syndata.html#x1 + */ + $name = strtolower( substr( $class, $at, $length ) ); + $at += $length; + + /* + * It's expected that the number of class names for a given tag is relatively small. + * Given this, it is probably faster overall to scan an array for a value rather + * than to use the class name as a key and check if it's a key of $seen. + */ + if ( in_array( $name, $seen, true ) ) { + continue; + } + + $seen[] = $name; + yield $name; + } + } + + + /** + * Returns if a matched tag contains the given ASCII case-insensitive class name. + * + * @since 6.4.0 + * + * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive. + * @return bool|null Whether the matched tag contains the given class name, or null if not matched. + */ + public function has_class( $wanted_class ) { + if ( ! $this->tag_name_starts_at ) { + return null; + } + + $wanted_class = strtolower( $wanted_class ); + + foreach ( $this->class_list() as $class_name ) { + if ( $class_name === $wanted_class ) { + return true; + } + } + + return false; + } + + + /** + * Sets a bookmark in the HTML document. + * + * Bookmarks represent specific places or tokens in the HTML + * document, such as a tag opener or closer. When applying + * edits to a document, such as setting an attribute, the + * text offsets of that token may shift; the bookmark is + * kept updated with those shifts and remains stable unless + * the entire span of text in which the token sits is removed. + * + * Release bookmarks when they are no longer needed. + * + * Example: + * + *

Surprising fact you may not know!

+ * ^ ^ + * \-|-- this `H2` opener bookmark tracks the token + * + *

Surprising fact you may no… + * ^ ^ + * \-|-- it shifts with edits + * + * Bookmarks provide the ability to seek to a previously-scanned + * place in the HTML document. This avoids the need to re-scan + * the entire document. + * + * Example: + * + *
  • One
  • Two
  • Three
+ * ^^^^ + * want to note this last item + * + * $p = new WP_HTML_Tag_Processor( $html ); + * $in_list = false; + * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) { + * if ( 'UL' === $p->get_tag() ) { + * if ( $p->is_tag_closer() ) { + * $in_list = false; + * $p->set_bookmark( 'resume' ); + * if ( $p->seek( 'last-li' ) ) { + * $p->add_class( 'last-li' ); + * } + * $p->seek( 'resume' ); + * $p->release_bookmark( 'last-li' ); + * $p->release_bookmark( 'resume' ); + * } else { + * $in_list = true; + * } + * } + * + * if ( 'LI' === $p->get_tag() ) { + * $p->set_bookmark( 'last-li' ); + * } + * } + * + * Bookmarks intentionally hide the internal string offsets + * to which they refer. They are maintained internally as + * updates are applied to the HTML document and therefore + * retain their "position" - the location to which they + * originally pointed. The inability to use bookmarks with + * functions like `substr` is therefore intentional to guard + * against accidentally breaking the HTML. + * + * Because bookmarks allocate memory and require processing + * for every applied update, they are limited and require + * a name. They should not be created with programmatically-made + * names, such as "li_{$index}" with some loop. As a general + * rule they should only be created with string-literal names + * like "start-of-section" or "last-paragraph". + * + * Bookmarks are a powerful tool to enable complicated behavior. + * Consider double-checking that you need this tool if you are + * reaching for it, as inappropriate use could lead to broken + * HTML structure or unwanted processing overhead. + * + * @since 6.2.0 + * + * @param string $name Identifies this particular bookmark. + * @return bool Whether the bookmark was successfully created. + */ + public function set_bookmark( $name ) { + if ( null === $this->tag_name_starts_at ) { + return false; + } + + if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= static::MAX_BOOKMARKS ) { + _doing_it_wrong( + __METHOD__, + __( 'Too many bookmarks: cannot create any more.' ), + '6.2.0' + ); + return false; + } + + $this->bookmarks[ $name ] = new Gutenberg_HTML_Span_6_5( $this->token_starts_at, $this->token_length ); + + return true; + } + + + /** + * Removes a bookmark that is no longer needed. + * + * Releasing a bookmark frees up the small + * performance overhead it requires. + * + * @param string $name Name of the bookmark to remove. + * @return bool Whether the bookmark already existed before removal. + */ + public function release_bookmark( $name ) { + if ( ! array_key_exists( $name, $this->bookmarks ) ) { + return false; + } + + unset( $this->bookmarks[ $name ] ); + + return true; + } + + /** + * Skips contents of generic rawtext elements. + * + * @since 6.3.2 + * + * @see https://html.spec.whatwg.org/#generic-raw-text-element-parsing-algorithm + * + * @param string $tag_name The uppercase tag name which will close the RAWTEXT region. + * @return bool Whether an end to the RAWTEXT region was found before the end of the document. + */ + private function skip_rawtext( $tag_name ) { + /* + * These two functions distinguish themselves on whether character references are + * decoded, and since functionality to read the inner markup isn't supported, it's + * not necessary to implement these two functions separately. + */ + return $this->skip_rcdata( $tag_name ); + } + + /** + * Skips contents of RCDATA elements, namely title and textarea tags. + * + * @since 6.2.0 + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state + * + * @param string $tag_name The uppercase tag name which will close the RCDATA region. + * @return bool Whether an end to the RCDATA region was found before the end of the document. + */ + private function skip_rcdata( $tag_name ) { + $html = $this->html; + $doc_length = strlen( $html ); + $tag_length = strlen( $tag_name ); + + $at = $this->bytes_already_parsed; + + while ( false !== $at && $at < $doc_length ) { + $at = strpos( $this->html, '= $doc_length ) { + $this->bytes_already_parsed = $doc_length; + return false; + } + + $closer_potentially_starts_at = $at; + $at += 2; + + /* + * Find a case-insensitive match to the tag name. + * + * Because tag names are limited to US-ASCII there is no + * need to perform any kind of Unicode normalization when + * comparing; any character which could be impacted by such + * normalization could not be part of a tag name. + */ + for ( $i = 0; $i < $tag_length; $i++ ) { + $tag_char = $tag_name[ $i ]; + $html_char = $html[ $at + $i ]; + + if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { + $at += $i; + continue 2; + } + } + + $at += $tag_length; + $this->bytes_already_parsed = $at; + + /* + * Ensure that the tag name terminates to avoid matching on + * substrings of a longer tag name. For example, the sequence + * "' !== $c ) { + continue; + } + + while ( $this->parse_next_attribute() ) { + continue; + } + $at = $this->bytes_already_parsed; + if ( $at >= strlen( $this->html ) ) { + return false; + } + + if ( '>' === $html[ $at ] || '/' === $html[ $at ] ) { + $this->bytes_already_parsed = $closer_potentially_starts_at; + return true; + } + } + + return false; + } + + /** + * Skips contents of script tags. + * + * @since 6.2.0 + * + * @return bool Whether the script tag was closed before the end of the document. + */ + private function skip_script_data() { + $state = 'unescaped'; + $html = $this->html; + $doc_length = strlen( $html ); + $at = $this->bytes_already_parsed; + + while ( false !== $at && $at < $doc_length ) { + $at += strcspn( $html, '-<', $at ); + + /* + * For all script states a "-->" transitions + * back into the normal unescaped script mode, + * even if that's the current state. + */ + if ( + $at + 2 < $doc_length && + '-' === $html[ $at ] && + '-' === $html[ $at + 1 ] && + '>' === $html[ $at + 2 ] + ) { + $at += 3; + $state = 'unescaped'; + continue; + } + + // Everything of interest past here starts with "<". + if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) { + continue; + } + + /* + * Unlike with "-->", the " + * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( + strlen( $html ) > $at + 3 && + '-' === $html[ $at + 2 ] && + '-' === $html[ $at + 3 ] + ) { + $closer_at = $at + 4; + // If it's not possible to close the comment then there is nothing more to scan. + if ( strlen( $html ) <= $closer_at ) { + return false; + } + + // Abruptly-closed empty comments are a sequence of dashes followed by `>`. + $span_of_dashes = strspn( $html, '-', $closer_at ); + if ( '>' === $html[ $closer_at + $span_of_dashes ] ) { + $at = $closer_at + $span_of_dashes + 1; + continue; + } + + /* + * Comments may be closed by either a --> or an invalid --!>. + * The first occurrence closes the comment. + * + * See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment + */ + --$closer_at; // Pre-increment inside condition below reduces risk of accidental infinite looping. + while ( ++$closer_at < strlen( $html ) ) { + $closer_at = strpos( $html, '--', $closer_at ); + if ( false === $closer_at ) { + return false; + } + + if ( $closer_at + 2 < strlen( $html ) && '>' === $html[ $closer_at + 2 ] ) { + $at = $closer_at + 3; + continue 2; + } + + if ( $closer_at + 3 < strlen( $html ) && '!' === $html[ $closer_at + 2 ] && '>' === $html[ $closer_at + 3 ] ) { + $at = $closer_at + 4; + continue 2; + } + } + } + + /* + * + * The CDATA is case-sensitive. + * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( + strlen( $html ) > $at + 8 && + '[' === $html[ $at + 2 ] && + 'C' === $html[ $at + 3 ] && + 'D' === $html[ $at + 4 ] && + 'A' === $html[ $at + 5 ] && + 'T' === $html[ $at + 6 ] && + 'A' === $html[ $at + 7 ] && + '[' === $html[ $at + 8 ] + ) { + $closer_at = strpos( $html, ']]>', $at + 9 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 3; + continue; + } + + /* + * + * These are ASCII-case-insensitive. + * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( + strlen( $html ) > $at + 8 && + ( 'D' === $html[ $at + 2 ] || 'd' === $html[ $at + 2 ] ) && + ( 'O' === $html[ $at + 3 ] || 'o' === $html[ $at + 3 ] ) && + ( 'C' === $html[ $at + 4 ] || 'c' === $html[ $at + 4 ] ) && + ( 'T' === $html[ $at + 5 ] || 't' === $html[ $at + 5 ] ) && + ( 'Y' === $html[ $at + 6 ] || 'y' === $html[ $at + 6 ] ) && + ( 'P' === $html[ $at + 7 ] || 'p' === $html[ $at + 7 ] ) && + ( 'E' === $html[ $at + 8 ] || 'e' === $html[ $at + 8 ] ) + ) { + $closer_at = strpos( $html, '>', $at + 9 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 1; + continue; + } + + /* + * Anything else here is an incorrectly-opened comment and transitions + * to the bogus comment state - skip to the nearest >. + */ + $at = strpos( $html, '>', $at + 1 ); + continue; + } + + /* + * is a missing end tag name, which is ignored. + * + * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name + */ + if ( '>' === $html[ $at + 1 ] ) { + ++$at; + continue; + } + + /* + * + * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( '?' === $html[ $at + 1 ] ) { + $closer_at = strpos( $html, '>', $at + 2 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 1; + continue; + } + + /* + * If a non-alpha starts the tag name in a tag closer it's a comment. + * Find the first `>`, which closes the comment. + * + * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name + */ + if ( $this->is_closing_tag ) { + $closer_at = strpos( $html, '>', $at + 3 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 1; + continue; + } + + ++$at; + } + + return false; + } + + /** + * Parses the next attribute. + * + * @since 6.2.0 + * + * @return bool Whether an attribute was found before the end of the document. + */ + private function parse_next_attribute() { + // Skip whitespace and slashes. + $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed ); + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + /* + * Treat the equal sign as a part of the attribute + * name if it is the first encountered byte. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state + */ + $name_length = '=' === $this->html[ $this->bytes_already_parsed ] + ? 1 + strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed + 1 ) + : strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed ); + + // No attribute, just tag closer. + if ( 0 === $name_length || $this->bytes_already_parsed + $name_length >= strlen( $this->html ) ) { + return false; + } + + $attribute_start = $this->bytes_already_parsed; + $attribute_name = substr( $this->html, $attribute_start, $name_length ); + $this->bytes_already_parsed += $name_length; + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + $this->skip_whitespace(); + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + $has_value = '=' === $this->html[ $this->bytes_already_parsed ]; + if ( $has_value ) { + ++$this->bytes_already_parsed; + $this->skip_whitespace(); + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + switch ( $this->html[ $this->bytes_already_parsed ] ) { + case "'": + case '"': + $quote = $this->html[ $this->bytes_already_parsed ]; + $value_start = $this->bytes_already_parsed + 1; + $value_length = strcspn( $this->html, $quote, $value_start ); + $attribute_end = $value_start + $value_length + 1; + $this->bytes_already_parsed = $attribute_end; + break; + + default: + $value_start = $this->bytes_already_parsed; + $value_length = strcspn( $this->html, "> \t\f\r\n", $value_start ); + $attribute_end = $value_start + $value_length; + $this->bytes_already_parsed = $attribute_end; + } + } else { + $value_start = $this->bytes_already_parsed; + $value_length = 0; + $attribute_end = $attribute_start + $name_length; + } + + if ( $attribute_end >= strlen( $this->html ) ) { + return false; + } + + if ( $this->is_closing_tag ) { + return true; + } + + /* + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + */ + $comparable_name = strtolower( $attribute_name ); + + // If an attribute is listed many times, only use the first declaration and ignore the rest. + if ( ! array_key_exists( $comparable_name, $this->attributes ) ) { + $this->attributes[ $comparable_name ] = new Gutenberg_HTML_Attribute_Token_6_5( + $attribute_name, + $value_start, + $value_length, + $attribute_start, + $attribute_end - $attribute_start, + ! $has_value + ); + + return true; + } + + /* + * Track the duplicate attributes so if we remove it, all disappear together. + * + * While `$this->duplicated_attributes` could always be stored as an `array()`, + * which would simplify the logic here, storing a `null` and only allocating + * an array when encountering duplicates avoids needless allocations in the + * normative case of parsing tags with no duplicate attributes. + */ + $duplicate_span = new Gutenberg_HTML_Span_6_5( $attribute_start, $attribute_end - $attribute_start ); + if ( null === $this->duplicate_attributes ) { + $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); + } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { + $this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span ); + } else { + $this->duplicate_attributes[ $comparable_name ][] = $duplicate_span; + } + + return true; + } + + /** + * Move the internal cursor past any immediate successive whitespace. + * + * @since 6.2.0 + */ + private function skip_whitespace() { + $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n", $this->bytes_already_parsed ); + } + + /** + * Applies attribute updates and cleans up once a tag is fully parsed. + * + * @since 6.2.0 + */ + private function after_tag() { + $this->get_updated_html(); + $this->token_starts_at = null; + $this->token_length = null; + $this->tag_name_starts_at = null; + $this->tag_name_length = null; + $this->is_closing_tag = null; + $this->attributes = array(); + $this->duplicate_attributes = null; + } + + /** + * Converts class name updates into tag attributes updates + * (they are accumulated in different data formats for performance). + * + * @since 6.2.0 + * + * @see WP_HTML_Tag_Processor::$lexical_updates + * @see WP_HTML_Tag_Processor::$classname_updates + */ + private function class_name_updates_to_attributes_updates() { + if ( count( $this->classname_updates ) === 0 ) { + return; + } + + $existing_class = $this->get_enqueued_attribute_value( 'class' ); + if ( null === $existing_class || true === $existing_class ) { + $existing_class = ''; + } + + if ( false === $existing_class && isset( $this->attributes['class'] ) ) { + $existing_class = substr( + $this->html, + $this->attributes['class']->value_starts_at, + $this->attributes['class']->value_length + ); + } + + if ( false === $existing_class ) { + $existing_class = ''; + } + + /** + * Updated "class" attribute value. + * + * This is incrementally built while scanning through the existing class + * attribute, skipping removed classes on the way, and then appending + * added classes at the end. Only when finished processing will the + * value contain the final new value. + + * @var string $class + */ + $class = ''; + + /** + * Tracks the cursor position in the existing + * class attribute value while parsing. + * + * @var int $at + */ + $at = 0; + + /** + * Indicates if there's any need to modify the existing class attribute. + * + * If a call to `add_class()` and `remove_class()` wouldn't impact + * the `class` attribute value then there's no need to rebuild it. + * For example, when adding a class that's already present or + * removing one that isn't. + * + * This flag enables a performance optimization when none of the enqueued + * class updates would impact the `class` attribute; namely, that the + * processor can continue without modifying the input document, as if + * none of the `add_class()` or `remove_class()` calls had been made. + * + * This flag is set upon the first change that requires a string update. + * + * @var bool $modified + */ + $modified = false; + + // Remove unwanted classes by only copying the new ones. + $existing_class_length = strlen( $existing_class ); + while ( $at < $existing_class_length ) { + // Skip to the first non-whitespace character. + $ws_at = $at; + $ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at ); + $at += $ws_length; + + // Capture the class name – it's everything until the next whitespace. + $name_length = strcspn( $existing_class, " \t\f\r\n", $at ); + if ( 0 === $name_length ) { + // If no more class names are found then that's the end. + break; + } + + $name = substr( $existing_class, $at, $name_length ); + $at += $name_length; + + // If this class is marked for removal, start processing the next one. + $remove_class = ( + isset( $this->classname_updates[ $name ] ) && + self::REMOVE_CLASS === $this->classname_updates[ $name ] + ); + + // If a class has already been seen then skip it; it should not be added twice. + if ( ! $remove_class ) { + $this->classname_updates[ $name ] = self::SKIP_CLASS; + } + + if ( $remove_class ) { + $modified = true; + continue; + } + + /* + * Otherwise, append it to the new "class" attribute value. + * + * There are options for handling whitespace between tags. + * Preserving the existing whitespace produces fewer changes + * to the HTML content and should clarify the before/after + * content when debugging the modified output. + * + * This approach contrasts normalizing the inter-class + * whitespace to a single space, which might appear cleaner + * in the output HTML but produce a noisier change. + */ + $class .= substr( $existing_class, $ws_at, $ws_length ); + $class .= $name; + } + + // Add new classes by appending those which haven't already been seen. + foreach ( $this->classname_updates as $name => $operation ) { + if ( self::ADD_CLASS === $operation ) { + $modified = true; + + $class .= strlen( $class ) > 0 ? ' ' : ''; + $class .= $name; + } + } + + $this->classname_updates = array(); + if ( ! $modified ) { + return; + } + + if ( strlen( $class ) > 0 ) { + $this->set_attribute( 'class', $class ); + } else { + $this->remove_attribute( 'class' ); + } + } + + /** + * Applies attribute updates to HTML document. + * + * @since 6.2.0 + * @since 6.2.1 Accumulates shift for internal cursor and passed pointer. + * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten. + * + * @param int $shift_this_point Accumulate and return shift for this position. + * @return int How many bytes the given pointer moved in response to the updates. + */ + private function apply_attributes_updates( $shift_this_point = 0 ) { + if ( ! count( $this->lexical_updates ) ) { + return 0; + } + + $accumulated_shift_for_given_point = 0; + + /* + * Attribute updates can be enqueued in any order but updates + * to the document must occur in lexical order; that is, each + * replacement must be made before all others which follow it + * at later string indices in the input document. + * + * Sorting avoid making out-of-order replacements which + * can lead to mangled output, partially-duplicated + * attributes, and overwritten attributes. + */ + usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) ); + + $bytes_already_copied = 0; + $output_buffer = ''; + foreach ( $this->lexical_updates as $diff ) { + $shift = strlen( $diff->text ) - $diff->length; + + // Adjust the cursor position by however much an update affects it. + if ( $diff->start <= $this->bytes_already_parsed ) { + $this->bytes_already_parsed += $shift; + } + + // Accumulate shift of the given pointer within this function call. + if ( $diff->start <= $shift_this_point ) { + $accumulated_shift_for_given_point += $shift; + } + + $output_buffer .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied ); + $output_buffer .= $diff->text; + $bytes_already_copied = $diff->start + $diff->length; + } + + $this->html = $output_buffer . substr( $this->html, $bytes_already_copied ); + + /* + * Adjust bookmark locations to account for how the text + * replacements adjust offsets in the input document. + */ + foreach ( $this->bookmarks as $bookmark_name => $bookmark ) { + $bookmark_end = $bookmark->start + $bookmark->length; + + /* + * Each lexical update which appears before the bookmark's endpoints + * might shift the offsets for those endpoints. Loop through each change + * and accumulate the total shift for each bookmark, then apply that + * shift after tallying the full delta. + */ + $head_delta = 0; + $tail_delta = 0; + + foreach ( $this->lexical_updates as $diff ) { + $diff_end = $diff->start + $diff->length; + + if ( $bookmark->start < $diff->start && $bookmark_end < $diff->start ) { + break; + } + + if ( $bookmark->start >= $diff->start && $bookmark_end < $diff_end ) { + $this->release_bookmark( $bookmark_name ); + continue 2; + } + + $delta = strlen( $diff->text ) - $diff->length; + + if ( $bookmark->start >= $diff->start ) { + $head_delta += $delta; + } + + if ( $bookmark_end >= $diff_end ) { + $tail_delta += $delta; + } + } + + $bookmark->start += $head_delta; + $bookmark->length += $tail_delta - $head_delta; + } + + $this->lexical_updates = array(); + + return $accumulated_shift_for_given_point; + } + + /** + * Checks whether a bookmark with the given name exists. + * + * @since 6.3.0 + * + * @param string $bookmark_name Name to identify a bookmark that potentially exists. + * @return bool Whether that bookmark exists. + */ + public function has_bookmark( $bookmark_name ) { + return array_key_exists( $bookmark_name, $this->bookmarks ); + } + + /** + * Move the internal cursor in the Tag Processor to a given bookmark's location. + * + * In order to prevent accidental infinite loops, there's a + * maximum limit on the number of times seek() can be called. + * + * @since 6.2.0 + * + * @param string $bookmark_name Jump to the place in the document identified by this bookmark name. + * @return bool Whether the internal cursor was successfully moved to the bookmark's location. + */ + public function seek( $bookmark_name ) { + if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Unknown bookmark name.' ), + '6.2.0' + ); + return false; + } + + if ( ++$this->seek_count > static::MAX_SEEK_OPS ) { + _doing_it_wrong( + __METHOD__, + __( 'Too many calls to seek() - this can lead to performance issues.' ), + '6.2.0' + ); + return false; + } + + // Flush out any pending updates to the document. + $this->get_updated_html(); + + // Point this tag processor before the sought tag opener and consume it. + $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start; + return $this->next_tag( array( 'tag_closers' => 'visit' ) ); + } + + /** + * Compare two WP_HTML_Text_Replacement objects. + * + * @since 6.2.0 + * + * @param WP_HTML_Text_Replacement $a First attribute update. + * @param WP_HTML_Text_Replacement $b Second attribute update. + * @return int Comparison value for string order. + */ + private static function sort_start_ascending( $a, $b ) { + $by_start = $a->start - $b->start; + if ( 0 !== $by_start ) { + return $by_start; + } + + $by_text = isset( $a->text, $b->text ) ? strcmp( $a->text, $b->text ) : 0; + if ( 0 !== $by_text ) { + return $by_text; + } + + /* + * This code should be unreachable, because it implies the two replacements + * start at the same location and contain the same text. + */ + return $a->length - $b->length; + } + + /** + * Return the enqueued value for a given attribute, if one exists. + * + * Enqueued updates can take different data types: + * - If an update is enqueued and is boolean, the return will be `true` + * - If an update is otherwise enqueued, the return will be the string value of that update. + * - If an attribute is enqueued to be removed, the return will be `null` to indicate that. + * - If no updates are enqueued, the return will be `false` to differentiate from "removed." + * + * @since 6.2.0 + * + * @param string $comparable_name The attribute name in its comparable form. + * @return string|boolean|null Value of enqueued update if present, otherwise false. + */ + private function get_enqueued_attribute_value( $comparable_name ) { + if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) { + return false; + } + + $enqueued_text = $this->lexical_updates[ $comparable_name ]->text; + + // Removed attributes erase the entire span. + if ( '' === $enqueued_text ) { + return null; + } + + /* + * Boolean attribute updates are just the attribute name without a corresponding value. + * + * This value might differ from the given comparable name in that there could be leading + * or trailing whitespace, and that the casing follows the name given in `set_attribute`. + * + * Example: + * + * $p->set_attribute( 'data-TEST-id', 'update' ); + * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' ); + * + * Detect this difference based on the absence of the `=`, which _must_ exist in any + * attribute containing a value, e.g. ``. + * ¹ ² + * 1. Attribute with a string value. + * 2. Boolean attribute whose value is `true`. + */ + $equals_at = strpos( $enqueued_text, '=' ); + if ( false === $equals_at ) { + return true; + } + + /* + * Finally, a normal update's value will appear after the `=` and + * be double-quoted, as performed incidentally by `set_attribute`. + * + * e.g. `type="text"` + * ¹² ³ + * 1. Equals is here. + * 2. Double-quoting starts one after the equals sign. + * 3. Double-quoting ends at the last character in the update. + */ + $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 ); + return html_entity_decode( $enqueued_value ); + } + + /** + * Returns the value of a requested attribute from a matched tag opener if that attribute exists. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '
Test
' ); + * $p->next_tag( array( 'class_name' => 'test' ) ) === true; + * $p->get_attribute( 'data-test-id' ) === '14'; + * $p->get_attribute( 'enabled' ) === true; + * $p->get_attribute( 'aria-label' ) === null; + * + * $p->next_tag() === false; + * $p->get_attribute( 'class' ) === null; + * + * @since 6.2.0 + * + * @param string $name Name of attribute whose value is requested. + * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. + */ + public function get_attribute( $name ) { + if ( null === $this->tag_name_starts_at ) { + return null; + } + + $comparable = strtolower( $name ); + + /* + * For every attribute other than `class` it's possible to perform a quick check if + * there's an enqueued lexical update whose value takes priority over what's found in + * the input document. + * + * The `class` attribute is special though because of the exposed helpers `add_class` + * and `remove_class`. These form a builder for the `class` attribute, so an additional + * check for enqueued class changes is required in addition to the check for any enqueued + * attribute values. If any exist, those enqueued class changes must first be flushed out + * into an attribute value update. + */ + if ( 'class' === $name ) { + $this->class_name_updates_to_attributes_updates(); + } + + // Return any enqueued attribute value updates if they exist. + $enqueued_value = $this->get_enqueued_attribute_value( $comparable ); + if ( false !== $enqueued_value ) { + return $enqueued_value; + } + + if ( ! isset( $this->attributes[ $comparable ] ) ) { + return null; + } + + $attribute = $this->attributes[ $comparable ]; + + /* + * This flag distinguishes an attribute with no value + * from an attribute with an empty string value. For + * unquoted attributes this could look very similar. + * It refers to whether an `=` follows the name. + * + * e.g.
+ * ¹ ² + * 1. Attribute `boolean-attribute` is `true`. + * 2. Attribute `empty-attribute` is `""`. + */ + if ( true === $attribute->is_true ) { + return true; + } + + $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length ); + + return html_entity_decode( $raw_value ); + } + + /** + * Gets lowercase names of all attributes matching a given prefix in the current tag. + * + * Note that matching is case-insensitive. This is in accordance with the spec: + * + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '
Test
' ); + * $p->next_tag( array( 'class_name' => 'test' ) ) === true; + * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); + * + * $p->next_tag() === false; + * $p->get_attribute_names_with_prefix( 'data-' ) === null; + * + * @since 6.2.0 + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + * + * @param string $prefix Prefix of requested attribute names. + * @return array|null List of attribute names, or `null` when no tag opener is matched. + */ + public function get_attribute_names_with_prefix( $prefix ) { + if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { + return null; + } + + $comparable = strtolower( $prefix ); + + $matches = array(); + foreach ( array_keys( $this->attributes ) as $attr_name ) { + if ( str_starts_with( $attr_name, $comparable ) ) { + $matches[] = $attr_name; + } + } + return $matches; + } + + /** + * Returns the uppercase name of the matched tag. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '
Test
' ); + * $p->next_tag() === true; + * $p->get_tag() === 'DIV'; + * + * $p->next_tag() === false; + * $p->get_tag() === null; + * + * @since 6.2.0 + * + * @return string|null Name of currently matched tag in input HTML, or `null` if none found. + */ + public function get_tag() { + if ( null === $this->tag_name_starts_at ) { + return null; + } + + $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ); + + return strtoupper( $tag_name ); + } + + /** + * Indicates if the currently matched tag contains the self-closing flag. + * + * No HTML elements ought to have the self-closing flag and for those, the self-closing + * flag will be ignored. For void elements this is benign because they "self close" + * automatically. For non-void HTML elements though problems will appear if someone + * intends to use a self-closing element in place of that element with an empty body. + * For HTML foreign elements and custom elements the self-closing flag determines if + * they self-close or not. + * + * This function does not determine if a tag is self-closing, + * but only if the self-closing flag is present in the syntax. + * + * @since 6.3.0 + * + * @return bool Whether the currently matched tag contains the self-closing flag. + */ + public function has_self_closing_flag() { + if ( ! $this->tag_name_starts_at ) { + return false; + } + + /* + * The self-closing flag is the solidus at the _end_ of the tag, not the beginning. + * + * Example: + * + *
+ * ^ this appears one character before the end of the closing ">". + */ + return '/' === $this->html[ $this->token_starts_at + $this->token_length - 1 ]; + } + + /** + * Indicates if the current tag token is a tag closer. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '
' ); + * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); + * $p->is_tag_closer() === false; + * + * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); + * $p->is_tag_closer() === true; + * + * @since 6.2.0 + * + * @return bool Whether the current tag is a tag closer. + */ + public function is_tag_closer() { + return $this->is_closing_tag; + } + + /** + * Updates or creates a new attribute on the currently matched tag with the passed value. + * + * For boolean attributes special handling is provided: + * - When `true` is passed as the value, then only the attribute name is added to the tag. + * - When `false` is passed, the attribute gets removed if it existed before. + * + * For string attributes, the value is escaped using the `esc_attr` function. + * + * @since 6.2.0 + * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names. + * + * @param string $name The attribute name to target. + * @param string|bool $value The new attribute value. + * @return bool Whether an attribute value was set. + */ + public function set_attribute( $name, $value ) { + if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { + return false; + } + + /* + * WordPress rejects more characters than are strictly forbidden + * in HTML5. This is to prevent additional security risks deeper + * in the WordPress and plugin stack. Specifically the + * less-than (<) greater-than (>) and ampersand (&) aren't allowed. + * + * The use of a PCRE match enables looking for specific Unicode + * code points without writing a UTF-8 decoder. Whereas scanning + * for one-byte characters is trivial (with `strcspn`), scanning + * for the longer byte sequences would be more complicated. Given + * that this shouldn't be in the hot path for execution, it's a + * reasonable compromise in efficiency without introducing a + * noticeable impact on the overall system. + * + * @see https://html.spec.whatwg.org/#attributes-2 + * + * @todo As the only regex pattern maybe we should take it out? + * Are Unicode patterns available broadly in Core? + */ + if ( preg_match( + '~[' . + // Syntax-like characters. + '"\'>& The values "true" and "false" are not allowed on boolean attributes. + * > To represent a false value, the attribute has to be omitted altogether. + * - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes + */ + if ( false === $value ) { + return $this->remove_attribute( $name ); + } + + if ( true === $value ) { + $updated_attribute = $name; + } else { + $escaped_new_value = esc_attr( $value ); + $updated_attribute = "{$name}=\"{$escaped_new_value}\""; + } + + /* + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + */ + $comparable_name = strtolower( $name ); + + if ( isset( $this->attributes[ $comparable_name ] ) ) { + /* + * Update an existing attribute. + * + * Example – set attribute id to "new" in
: + * + *
+ * ^-------------^ + * start end + * replacement: `id="new"` + * + * Result:
+ */ + $existing_attribute = $this->attributes[ $comparable_name ]; + $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $existing_attribute->start, + $existing_attribute->length, + $updated_attribute + ); + } else { + /* + * Create a new attribute at the tag's name end. + * + * Example – add attribute id="new" to
: + * + *
+ * ^ + * start and end + * replacement: ` id="new"` + * + * Result:
+ */ + $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->tag_name_starts_at + $this->tag_name_length, + 0, + ' ' . $updated_attribute + ); + } + + /* + * Any calls to update the `class` attribute directly should wipe out any + * enqueued class changes from `add_class` and `remove_class`. + */ + if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) { + $this->classname_updates = array(); + } + + return true; + } + + /** + * Remove an attribute from the currently-matched tag. + * + * @since 6.2.0 + * + * @param string $name The attribute name to remove. + * @return bool Whether an attribute was removed. + */ + public function remove_attribute( $name ) { + if ( $this->is_closing_tag ) { + return false; + } + + /* + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + */ + $name = strtolower( $name ); + + /* + * Any calls to update the `class` attribute directly should wipe out any + * enqueued class changes from `add_class` and `remove_class`. + */ + if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) { + $this->classname_updates = array(); + } + + /* + * If updating an attribute that didn't exist in the input + * document, then remove the enqueued update and move on. + * + * For example, this might occur when calling `remove_attribute()` + * after calling `set_attribute()` for the same attribute + * and when that attribute wasn't originally present. + */ + if ( ! isset( $this->attributes[ $name ] ) ) { + if ( isset( $this->lexical_updates[ $name ] ) ) { + unset( $this->lexical_updates[ $name ] ); + } + return false; + } + + /* + * Removes an existing tag attribute. + * + * Example – remove the attribute id from
: + *
+ * ^-------------^ + * start end + * replacement: `` + * + * Result:
+ */ + $this->lexical_updates[ $name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->attributes[ $name ]->start, + $this->attributes[ $name ]->length, + '' + ); + + // Removes any duplicated attributes if they were also present. + if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { + foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { + $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( + $attribute_token->start, + $attribute_token->length, + '' + ); + } + } + + return true; + } + + /** + * Adds a new class name to the currently matched tag. + * + * @since 6.2.0 + * + * @param string $class_name The class name to add. + * @return bool Whether the class was set to be added. + */ + public function add_class( $class_name ) { + if ( $this->is_closing_tag ) { + return false; + } + + if ( null !== $this->tag_name_starts_at ) { + $this->classname_updates[ $class_name ] = self::ADD_CLASS; + } + + return true; + } + + /** + * Removes a class name from the currently matched tag. + * + * @since 6.2.0 + * + * @param string $class_name The class name to remove. + * @return bool Whether the class was set to be removed. + */ + public function remove_class( $class_name ) { + if ( $this->is_closing_tag ) { + return false; + } + + if ( null !== $this->tag_name_starts_at ) { + $this->classname_updates[ $class_name ] = self::REMOVE_CLASS; + } + + return true; + } + + /** + * Returns the string representation of the HTML Tag Processor. + * + * @since 6.2.0 + * + * @see WP_HTML_Tag_Processor::get_updated_html() + * + * @return string The processed HTML. + */ + public function __toString() { + return $this->get_updated_html(); + } + + /** + * Returns the string representation of the HTML Tag Processor. + * + * @since 6.2.0 + * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. + * @since 6.4.0 No longer calls subclass method `next_tag()` after updating HTML. + * + * @return string The processed HTML. + */ + public function get_updated_html() { + $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates ); + + /* + * When there is nothing more to update and nothing has already been + * updated, return the original document and avoid a string copy. + */ + if ( $requires_no_updating ) { + return $this->html; + } + + /* + * Keep track of the position right before the current tag. This will + * be necessary for reparsing the current tag after updating the HTML. + */ + $before_current_tag = $this->token_starts_at; + + /* + * 1. Apply the enqueued edits and update all the pointers to reflect those changes. + */ + $this->class_name_updates_to_attributes_updates(); + $before_current_tag += $this->apply_attributes_updates( $before_current_tag ); + + /* + * 2. Rewind to before the current tag and reparse to get updated attributes. + * + * At this point the internal cursor points to the end of the tag name. + * Rewind before the tag name starts so that it's as if the cursor didn't + * move; a call to `next_tag()` will reparse the recently-updated attributes + * and additional calls to modify the attributes will apply at this same + * location, but in order to avoid issues with subclasses that might add + * behaviors to `next_tag()`, the internal methods should be called here + * instead. + * + * It's important to note that in this specific place there will be no change + * because the processor was already at a tag when this was called and it's + * rewinding only to the beginning of this very tag before reprocessing it + * and its attributes. + * + *

Previous HTMLMore HTML

+ * ↑ │ back up by the length of the tag name plus the opening < + * └←─┘ back up by strlen("em") + 1 ==> 3 + */ + $this->bytes_already_parsed = $before_current_tag; + $this->parse_next_tag(); + // Reparse the attributes. + while ( $this->parse_next_attribute() ) { + continue; + } + + $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); + $this->token_length = $tag_ends_at - $this->token_starts_at; + $this->bytes_already_parsed = $tag_ends_at; + + return $this->html; + } + + /** + * Parses tag query input into internal search criteria. + * + * @since 6.2.0 + * + * @param array|string|null $query { + * Optional. Which tag name to find, having which class, etc. Default is to find any tag. + * + * @type string|null $tag_name Which tag to find, or `null` for "any tag." + * @type int|null $match_offset Find the Nth tag matching all search criteria. + * 1 for "first" tag, 3 for "third," etc. + * Defaults to first tag. + * @type string|null $class_name Tag must contain this class name to match. + * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. + * } + */ + private function parse_query( $query ) { + if ( null !== $query && $query === $this->last_query ) { + return; + } + + $this->last_query = $query; + $this->sought_tag_name = null; + $this->sought_class_name = null; + $this->sought_match_offset = 1; + $this->stop_on_tag_closers = false; + + // A single string value means "find the tag of this name". + if ( is_string( $query ) ) { + $this->sought_tag_name = $query; + return; + } + + // An empty query parameter applies no restrictions on the search. + if ( null === $query ) { + return; + } + + // If not using the string interface, an associative array is required. + if ( ! is_array( $query ) ) { + _doing_it_wrong( + __METHOD__, + __( 'The query argument must be an array or a tag name.' ), + '6.2.0' + ); + return; + } + + if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) { + $this->sought_tag_name = $query['tag_name']; + } + + if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) { + $this->sought_class_name = $query['class_name']; + } + + if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) { + $this->sought_match_offset = $query['match_offset']; + } + + if ( isset( $query['tag_closers'] ) ) { + $this->stop_on_tag_closers = 'visit' === $query['tag_closers']; + } + } + + + /** + * Checks whether a given tag and its attributes match the search criteria. + * + * @since 6.2.0 + * + * @return bool Whether the given tag and its attribute match the search criteria. + */ + private function matches() { + if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) { + return false; + } + + // Does the tag name match the requested tag name in a case-insensitive manner? + if ( null !== $this->sought_tag_name ) { + /* + * String (byte) length lookup is fast. If they aren't the + * same length then they can't be the same string values. + */ + if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) { + return false; + } + + /* + * Check each character to determine if they are the same. + * Defer calls to `strtoupper()` to avoid them when possible. + * Calling `strcasecmp()` here tested slowed than comparing each + * character, so unless benchmarks show otherwise, it should + * not be used. + * + * It's expected that most of the time that this runs, a + * lower-case tag name will be supplied and the input will + * contain lower-case tag names, thus normally bypassing + * the case comparison code. + */ + for ( $i = 0; $i < $this->tag_name_length; $i++ ) { + $html_char = $this->html[ $this->tag_name_starts_at + $i ]; + $tag_char = $this->sought_tag_name[ $i ]; + + if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { + return false; + } + } + } + + if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) { + return false; + } + + return true; + } +} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php new file mode 100644 index 0000000000000..6409255833c81 --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php @@ -0,0 +1,64 @@ +start = $start; + $this->length = $length; + $this->text = $text; + } +} diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index e717b2e553943..cf55a048bb9fa 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -20,7 +20,7 @@ * available. Please restrain from investing unnecessary time and effort trying * to improve this code. */ -class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 { +class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 { /** * An array of root blocks. @@ -195,7 +195,7 @@ public function get_inner_html() { } list( $start_name, $end_name ) = $bookmarks; - $start = $this->bookmarks[ $start_name ]->end + 1; + $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; $end = $this->bookmarks[ $end_name ]->start; $this->seek( $start_name ); // Return to original position. @@ -225,14 +225,14 @@ public function set_inner_html( $new_html ) { } list( $start_name, $end_name ) = $bookmarks; - $start = $this->bookmarks[ $start_name ]->end + 1; + $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; $end = $this->bookmarks[ $end_name ]->start; $this->seek( $start_name ); // Return to original position. $this->release_bookmark( $start_name ); $this->release_bookmark( $end_name ); - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); + $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, $new_html ); return true; } diff --git a/lib/load.php b/lib/load.php index 9c7618dbfc678..59fb75541ac41 100644 --- a/lib/load.php +++ b/lib/load.php @@ -76,6 +76,10 @@ function gutenberg_is_experiment_enabled( $name ) { * always be loaded so that Gutenberg code can run the newest version of the Tag Processor. */ require __DIR__ . '/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php'; /* * The HTML Processor appeared after WordPress 6.3. If Gutenberg is running on a version of From dc54432290ca4757c8fa2c48904d2b34505e2cbb Mon Sep 17 00:00:00 2001 From: Derek Blank Date: Mon, 11 Dec 2023 09:21:30 +0800 Subject: [PATCH 38/38] Replace offline icon and update OfflineStatus text alignment --- .../offline-status/style.native.scss | 2 +- packages/icons/src/library/offline.js | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/offline-status/style.native.scss b/packages/block-editor/src/components/offline-status/style.native.scss index 2b6e808fdbb49..714d899a7b9ca 100644 --- a/packages/block-editor/src/components/offline-status/style.native.scss +++ b/packages/block-editor/src/components/offline-status/style.native.scss @@ -2,6 +2,7 @@ background-color: $light-ultra-dim; padding: $grid-unit; justify-content: center; + align-items: center; flex-direction: row; } @@ -11,7 +12,6 @@ .text { padding-left: 3; - padding-top: 2; } .text--dark { diff --git a/packages/icons/src/library/offline.js b/packages/icons/src/library/offline.js index 2e397fb23745a..f0daa1aaeb79e 100644 --- a/packages/icons/src/library/offline.js +++ b/packages/icons/src/library/offline.js @@ -4,16 +4,18 @@ import { SVG, Path } from '@wordpress/primitives'; const offline = ( - - { /* - "no internet" by Heztasia is licensed under CCBY3.0 - https://creativecommons.org/licenses/by/3.0/ - */ } - - - - - + + );