From b5c694085671c8e22a070bdff6d13f8c4fc6a66b Mon Sep 17 00:00:00 2001 From: Karol Manijak <20098064+kmanijak@users.noreply.github.com> Date: Mon, 11 Sep 2023 13:22:11 +0200 Subject: [PATCH 01/42] Remove unnecessary padding-right on the dismissible notice (#52240) --- packages/components/src/notice/style.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/notice/style.scss b/packages/components/src/notice/style.scss index 23378cefe1ec63..ebd76d35ce7065 100644 --- a/packages/components/src/notice/style.scss +++ b/packages/components/src/notice/style.scss @@ -9,7 +9,6 @@ align-items: center; &.is-dismissible { - padding-right: 36px; position: relative; } From bb4ed887caa415e3c724094f55914ded2deec02e Mon Sep 17 00:00:00 2001 From: Chintan hingrajiya <38949444+chintu51@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:55:30 +0530 Subject: [PATCH 02/42] Add missing useState import in Border Control documentation. (#49476) * Fix retrieving autosaves when using a custom rest_namespace When specifying `rest_namespace` in a custom post type, a hardcoded `wp/v2` in `getAutosaves` results in a 404 * linter fixes for rest namespace resolution in autosave resolver * try to fix autosave resolver and namespaces not present * update resolvers for autosaves endpoint * Fix lints. * Add missing useState import in Border Control documentation. --------- Co-authored-by: Tom J Nowell Co-authored-by: Jonny Harris Co-authored-by: Jonny Harris --- packages/components/src/border-control/border-control/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/components/src/border-control/border-control/README.md b/packages/components/src/border-control/border-control/README.md index 901859df2395e9..14b3df6afaf6c9 100644 --- a/packages/components/src/border-control/border-control/README.md +++ b/packages/components/src/border-control/border-control/README.md @@ -22,6 +22,7 @@ a "shape" abstraction. ```jsx import { __experimentalBorderControl as BorderControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; const colors = [ { name: 'Blue 20', color: '#72aee6' }, From d9208b8ee4037d8b7b4e2801b4ba7c2ff13bc094 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 11 Sep 2023 12:34:13 +0100 Subject: [PATCH 03/42] Extract undo/redo as a separate package (#54292) Co-authored-by: Ella <4710635+ellatrix@users.noreply.github.com> --- docs/manifest.json | 6 + docs/reference-guides/data/data-core.md | 2 +- package-lock.json | 26 ++ package.json | 1 + packages/core-data/README.md | 2 +- packages/core-data/package.json | 1 + packages/core-data/src/actions.js | 49 ++-- packages/core-data/src/private-selectors.ts | 21 +- packages/core-data/src/reducer.js | 182 ++----------- packages/core-data/src/selectors.ts | 53 +--- packages/core-data/src/test/reducer.js | 233 ---------------- packages/core-data/src/test/selectors.js | 54 ---- packages/core-data/tsconfig.json | 1 + .../lib/util.js | 6 +- packages/undo-manager/.npmrc | 1 + packages/undo-manager/CHANGELOG.md | 4 + packages/undo-manager/README.md | 33 +++ packages/undo-manager/package.json | 37 +++ packages/undo-manager/src/index.js | 171 ++++++++++++ packages/undo-manager/src/test/index.js | 257 ++++++++++++++++++ packages/undo-manager/src/types.ts | 19 ++ packages/undo-manager/tsconfig.json | 10 + tools/webpack/packages.js | 6 +- tsconfig.json | 1 + 24 files changed, 651 insertions(+), 525 deletions(-) create mode 100644 packages/undo-manager/.npmrc create mode 100644 packages/undo-manager/CHANGELOG.md create mode 100644 packages/undo-manager/README.md create mode 100644 packages/undo-manager/package.json create mode 100644 packages/undo-manager/src/index.js create mode 100644 packages/undo-manager/src/test/index.js create mode 100644 packages/undo-manager/src/types.ts create mode 100644 packages/undo-manager/tsconfig.json diff --git a/docs/manifest.json b/docs/manifest.json index 75ae7d0b31f67d..354a81a70abba6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1919,6 +1919,12 @@ "markdown_source": "../packages/token-list/README.md", "parent": "packages" }, + { + "title": "@wordpress/undo-manager", + "slug": "packages-undo-manager", + "markdown_source": "../packages/undo-manager/README.md", + "parent": "packages" + }, { "title": "@wordpress/url", "slug": "packages-url", diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index f2bc3374f9e721..95401834ad4391 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -371,7 +371,7 @@ _Usage_ _Parameters_ -- _state_ `State`: Editor state. +- _state_ Editor state. _Returns_ diff --git a/package-lock.json b/package-lock.json index 29d2296841dee7..340130affbd181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "@wordpress/style-engine": "file:packages/style-engine", "@wordpress/sync": "file:packages/sync", "@wordpress/token-list": "file:packages/token-list", + "@wordpress/undo-manager": "file:packages/undo-manager", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "@wordpress/warning": "file:packages/warning", @@ -15655,6 +15656,10 @@ "resolved": "packages/token-list", "link": true }, + "node_modules/@wordpress/undo-manager": { + "resolved": "packages/undo-manager", + "link": true + }, "node_modules/@wordpress/url": { "resolved": "packages/url", "link": true @@ -55009,6 +55014,7 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", "@wordpress/sync": "file:../sync", + "@wordpress/undo-manager": "file:../undo-manager", "@wordpress/url": "file:../url", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", @@ -56561,6 +56567,18 @@ "node": ">=12" } }, + "packages/undo-manager": { + "name": "@wordpress/undo-manager", + "version": "0.1.0", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/is-shallow-equal": "file:../is-shallow-equal" + }, + "engines": { + "node": ">=12" + } + }, "packages/url": { "name": "@wordpress/url", "version": "3.42.0", @@ -67892,6 +67910,7 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", "@wordpress/sync": "file:../sync", + "@wordpress/undo-manager": "file:../undo-manager", "@wordpress/url": "file:../url", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", @@ -68899,6 +68918,13 @@ "@babel/runtime": "^7.16.0" } }, + "@wordpress/undo-manager": { + "version": "file:packages/undo-manager", + "requires": { + "@babel/runtime": "^7.16.0", + "@wordpress/is-shallow-equal": "file:../is-shallow-equal" + } + }, "@wordpress/url": { "version": "file:packages/url", "requires": { diff --git a/package.json b/package.json index 7d46a0c7273a20..1cd8535bfc803c 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@wordpress/style-engine": "file:packages/style-engine", "@wordpress/sync": "file:packages/sync", "@wordpress/token-list": "file:packages/token-list", + "@wordpress/undo-manager": "file:packages/undo-manager", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "@wordpress/warning": "file:packages/warning", diff --git a/packages/core-data/README.md b/packages/core-data/README.md index c778b724149ef3..18e131cd7ab6f1 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -548,7 +548,7 @@ _Usage_ _Parameters_ -- _state_ `State`: Editor state. +- _state_ Editor state. _Returns_ diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 80bc41ff0a5afe..1f83dc9814400c 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -43,6 +43,7 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", "@wordpress/sync": "file:../sync", + "@wordpress/undo-manager": "file:../undo-manager", "@wordpress/url": "file:../url", "change-case": "^4.1.2", "equivalent-key-map": "^0.2.2", diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 1969d2cd717a2a..a79d1236682582 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -391,20 +391,29 @@ export const editEntityRecord = edit.edits ); } else { + if ( ! options.undoIgnore ) { + select.getUndoManager().addRecord( + [ + { + id: { kind, name, recordId }, + changes: Object.keys( edits ).reduce( + ( acc, key ) => { + acc[ key ] = { + from: editedRecord[ key ], + to: edits[ key ], + }; + return acc; + }, + {} + ), + }, + ], + options.isCached + ); + } dispatch( { type: 'EDIT_ENTITY_RECORD', ...edit, - meta: { - undo: ! options.undoIgnore && { - ...edit, - // Send the current values for things like the first undo stack entry. - edits: Object.keys( edits ).reduce( ( acc, key ) => { - acc[ key ] = editedRecord[ key ]; - return acc; - }, {} ), - isCached: options.isCached, - }, - }, } ); } }; @@ -416,13 +425,14 @@ export const editEntityRecord = export const undo = () => ( { select, dispatch } ) => { - const undoEdit = select.getUndoEdits(); + const undoEdit = select.getUndoManager().getUndoRecord(); if ( ! undoEdit ) { return; } + select.getUndoManager().undo(); dispatch( { type: 'UNDO', - stackedEdits: undoEdit, + record: undoEdit, } ); }; @@ -433,13 +443,14 @@ export const undo = export const redo = () => ( { select, dispatch } ) => { - const redoEdit = select.getRedoEdits(); + const redoEdit = select.getUndoManager().getRedoRecord(); if ( ! redoEdit ) { return; } + select.getUndoManager().redo(); dispatch( { type: 'REDO', - stackedEdits: redoEdit, + record: redoEdit, } ); }; @@ -448,9 +459,11 @@ export const redo = * * @return {Object} Action object. */ -export function __unstableCreateUndoLevel() { - return { type: 'CREATE_UNDO_LEVEL' }; -} +export const __unstableCreateUndoLevel = + () => + ( { select } ) => { + select.getUndoManager().addRecord(); + }; /** * Action triggered to save an entity record. diff --git a/packages/core-data/src/private-selectors.ts b/packages/core-data/src/private-selectors.ts index 1e253b900e1cbb..94aa00e1c8de45 100644 --- a/packages/core-data/src/private-selectors.ts +++ b/packages/core-data/src/private-selectors.ts @@ -1,9 +1,8 @@ /** * Internal dependencies */ -import type { State, UndoEdit } from './selectors'; +import type { State } from './selectors'; -type Optional< T > = T | undefined; type EntityRecordKey = string | number; /** @@ -12,22 +11,10 @@ type EntityRecordKey = string | number; * * @param state State tree. * - * @return The edit. + * @return The undo manager. */ -export function getUndoEdits( state: State ): Optional< UndoEdit[] > { - return state.undo.list[ state.undo.list.length - 1 + state.undo.offset ]; -} - -/** - * Returns the next edit from the current undo offset - * for the entity records edits history, if any. - * - * @param state State tree. - * - * @return The edit. - */ -export function getRedoEdits( state: State ): Optional< UndoEdit[] > { - return state.undo.list[ state.undo.list.length + state.undo.offset ]; +export function getUndoManager( state: State ) { + return state.undoManager; } /** diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 20755dad4be8d2..f097d07d047746 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -8,7 +8,7 @@ import fastDeepEqual from 'fast-deep-equal/es6'; */ import { compose } from '@wordpress/compose'; import { combineReducers } from '@wordpress/data'; -import isShallowEqual from '@wordpress/is-shallow-equal'; +import { createUndoManager } from '@wordpress/undo-manager'; /** * Internal dependencies @@ -185,22 +185,25 @@ export function themeGlobalStyleVariations( state = {}, action ) { const withMultiEntityRecordEdits = ( reducer ) => ( state, action ) => { if ( action.type === 'UNDO' || action.type === 'REDO' ) { - const { stackedEdits } = action; + const { record } = action; let newState = state; - stackedEdits.forEach( - ( { kind, name, recordId, property, from, to } ) => { - newState = reducer( newState, { - type: 'EDIT_ENTITY_RECORD', - kind, - name, - recordId, - edits: { - [ property ]: action.type === 'UNDO' ? from : to, + record.forEach( ( { id: { kind, name, recordId }, changes } ) => { + newState = reducer( newState, { + type: 'EDIT_ENTITY_RECORD', + kind, + name, + recordId, + edits: Object.entries( changes ).reduce( + ( acc, [ key, value ] ) => { + acc[ key ] = + action.type === 'UNDO' ? value.from : value.to; + return acc; }, - } ); - } - ); + {} + ), + } ); + } ); return newState; } @@ -435,151 +438,19 @@ export const entities = ( state = {}, action ) => { }; /** - * @typedef {Object} UndoStateMeta - * - * @property {number} list The undo stack. - * @property {number} offset Where in the undo stack we are. - * @property {Object} cache Cache of unpersisted edits. - */ - -/** @typedef {Array & UndoStateMeta} UndoState */ - -/** - * @type {UndoState} - * - * @todo Given how we use this we might want to make a custom class for it. - */ -const UNDO_INITIAL_STATE = { list: [], offset: 0 }; - -/** - * Reducer keeping track of entity edit undo history. - * - * @param {UndoState} state Current state. - * @param {Object} action Dispatched action. - * - * @return {UndoState} Updated state. + * @type {UndoManager} */ -export function undo( state = UNDO_INITIAL_STATE, action ) { - const omitPendingRedos = ( currentState ) => { - return { - ...currentState, - list: currentState.list.slice( - 0, - currentState.offset || undefined - ), - offset: 0, - }; - }; - - const appendCachedEditsToLastUndo = ( currentState ) => { - if ( ! currentState.cache ) { - return currentState; - } - - let nextState = { - ...currentState, - list: [ ...currentState.list ], - }; - nextState = omitPendingRedos( nextState ); - const previousUndoState = nextState.list.pop(); - const updatedUndoState = currentState.cache.reduce( - appendEditToStack, - previousUndoState - ); - nextState.list.push( updatedUndoState ); - - return { - ...nextState, - cache: undefined, - }; - }; - - const appendEditToStack = ( - stack = [], - { kind, name, recordId, property, from, to } - ) => { - const existingEditIndex = stack?.findIndex( - ( { kind: k, name: n, recordId: r, property: p } ) => { - return ( - k === kind && n === name && r === recordId && p === property - ); - } - ); - const nextStack = [ ...stack ]; - if ( existingEditIndex !== -1 ) { - // If the edit is already in the stack leave the initial "from" value. - nextStack[ existingEditIndex ] = { - ...nextStack[ existingEditIndex ], - to, - }; - } else { - nextStack.push( { - kind, - name, - recordId, - property, - from, - to, - } ); - } - return nextStack; - }; +export function undoManager( state = createUndoManager() ) { + return state; +} +export function editsReference( state = {}, action ) { switch ( action.type ) { - case 'CREATE_UNDO_LEVEL': - return appendCachedEditsToLastUndo( state ); - + case 'EDIT_ENTITY_RECORD': case 'UNDO': - case 'REDO': { - const nextState = appendCachedEditsToLastUndo( state ); - return { - ...nextState, - offset: state.offset + ( action.type === 'UNDO' ? -1 : 1 ), - }; - } - - case 'EDIT_ENTITY_RECORD': { - if ( ! action.meta.undo ) { - return state; - } - - const edits = Object.keys( action.edits ).map( ( key ) => { - return { - kind: action.kind, - name: action.name, - recordId: action.recordId, - property: key, - from: action.meta.undo.edits[ key ], - to: action.edits[ key ], - }; - } ); - - if ( action.meta.undo.isCached ) { - return { - ...state, - cache: edits.reduce( appendEditToStack, state.cache ), - }; - } - - let nextState = omitPendingRedos( state ); - nextState = appendCachedEditsToLastUndo( nextState ); - nextState = { ...nextState, list: [ ...nextState.list ] }; - // When an edit is a function it's an optimization to avoid running some expensive operation. - // We can't rely on the function references being the same so we opt out of comparing them here. - const comparisonUndoEdits = Object.values( - action.meta.undo.edits - ).filter( ( edit ) => typeof edit !== 'function' ); - const comparisonEdits = Object.values( action.edits ).filter( - ( edit ) => typeof edit !== 'function' - ); - if ( ! isShallowEqual( comparisonUndoEdits, comparisonEdits ) ) { - nextState.list.push( edits ); - } - - return nextState; - } + case 'REDO': + return {}; } - return state; } @@ -704,7 +575,8 @@ export default combineReducers( { themeGlobalStyleRevisions, taxonomies, entities, - undo, + editsReference, + undoManager, embedPreviews, userPermissions, autosaves, diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 377134ab7c9a3d..e4fb2eada0cf87 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -22,7 +22,7 @@ import { setNestedValue, } from './utils'; import type * as ET from './entity-types'; -import { getUndoEdits, getRedoEdits } from './private-selectors'; +import type { UndoManager } from '@wordpress/undo-manager'; // This is an incomplete, high-level approximation of the State type. // It makes the selectors slightly more safe, but is intended to evolve @@ -40,7 +40,7 @@ export interface State { themeBaseGlobalStyles: Record< string, Object >; themeGlobalStyleVariations: Record< string, string >; themeGlobalStyleRevisions: Record< number, Object >; - undo: UndoState; + undoManager: UndoManager; userPermissions: Record< string, boolean >; users: UserState; navigationFallbackId: EntityRecordKey; @@ -74,20 +74,6 @@ interface EntityConfig { kind: string; } -export interface UndoEdit { - name: string; - kind: string; - recordId: string; - from: any; - to: any; -} - -interface UndoState { - list: Array< UndoEdit[] >; - offset: number; - cache: UndoEdit[]; -} - interface UserState { queries: Record< string, EntityRecordKey[] >; byId: Record< EntityRecordKey, ET.User< 'edit' > >; @@ -875,21 +861,6 @@ export function getLastEntityDeleteError( ?.error; } -/** - * Returns the current undo offset for the - * entity records edits history. The offset - * represents how many items from the end - * of the history stack we are at. 0 is the - * last edit, -1 is the second last, and so on. - * - * @param state State tree. - * - * @return The current undo offset. - */ -function getCurrentUndoOffset( state: State ): number { - return state.undo.offset; -} - /** * Returns the previous edit from the current undo offset * for the entity records edits history, if any. @@ -904,9 +875,7 @@ export function getUndoEdit( state: State ): Optional< any > { deprecated( "select( 'core' ).getUndoEdit()", { since: '6.3', } ); - return state.undo.list[ - state.undo.list.length - 2 + getCurrentUndoOffset( state ) - ]?.[ 0 ]; + return undefined; } /** @@ -923,9 +892,7 @@ export function getRedoEdit( state: State ): Optional< any > { deprecated( "select( 'core' ).getRedoEdit()", { since: '6.3', } ); - return state.undo.list[ - state.undo.list.length + getCurrentUndoOffset( state ) - ]?.[ 0 ]; + return undefined; } /** @@ -937,7 +904,7 @@ export function getRedoEdit( state: State ): Optional< any > { * @return Whether there is a previous edit or not. */ export function hasUndo( state: State ): boolean { - return Boolean( getUndoEdits( state ) ); + return Boolean( state.undoManager.getUndoRecord() ); } /** @@ -949,7 +916,7 @@ export function hasUndo( state: State ): boolean { * @return Whether there is a next edit or not. */ export function hasRedo( state: State ): boolean { - return Boolean( getRedoEdits( state ) ); + return Boolean( state.undoManager.getRedoRecord() ); } /** @@ -1163,11 +1130,9 @@ export const hasFetchedAutosaves = createRegistrySelector( * * @return A value whose reference will change only when an edit occurs. */ -export const getReferenceByDistinctEdits = createSelector( - // This unused state argument is listed here for the documentation generating tool (docgen). - ( state: State ) => [], - ( state: State ) => [ state.undo.list.length, state.undo.offset ] -); +export function getReferenceByDistinctEdits( state ) { + return state.editsReference; +} /** * Retrieve the frontend template used for a given link. diff --git a/packages/core-data/src/test/reducer.js b/packages/core-data/src/test/reducer.js index 7fac52c33c4b36..4142f65af4c7c4 100644 --- a/packages/core-data/src/test/reducer.js +++ b/packages/core-data/src/test/reducer.js @@ -9,7 +9,6 @@ import deepFreeze from 'deep-freeze'; import { terms, entities, - undo, embedPreviews, userPermissions, autosaves, @@ -142,238 +141,6 @@ describe( 'entities', () => { } ); } ); -describe( 'undo', () => { - let lastValues; - let undoState; - let expectedUndoState; - - const createExpectedDiff = ( property, { from, to } ) => ( { - kind: 'someKind', - name: 'someName', - recordId: 'someRecordId', - property, - from, - to, - } ); - const createNextEditAction = ( edits, isCached ) => { - let action = { - kind: 'someKind', - name: 'someName', - recordId: 'someRecordId', - edits, - }; - action = { - type: 'EDIT_ENTITY_RECORD', - ...action, - meta: { - undo: { - isCached, - edits: lastValues, - }, - }, - }; - lastValues = { ...lastValues, ...edits }; - return action; - }; - const createNextUndoState = ( ...args ) => { - let action = {}; - if ( args[ 0 ] === 'isUndo' || args[ 0 ] === 'isRedo' ) { - // We need to "apply" the undo level here and build - // the action to move the offset. - const lastEdits = - undoState.list[ - undoState.list.length - - ( args[ 0 ] === 'isUndo' ? 1 : 0 ) + - undoState.offset - ]; - lastEdits.forEach( ( { property, from, to } ) => { - lastValues[ property ] = args[ 0 ] === 'isUndo' ? from : to; - } ); - action = { - type: args[ 0 ] === 'isUndo' ? 'UNDO' : 'REDO', - }; - } else if ( args[ 0 ] === 'isCreate' ) { - action = { type: 'CREATE_UNDO_LEVEL' }; - } else if ( args.length ) { - action = createNextEditAction( ...args ); - } - return deepFreeze( undo( undoState, action ) ); - }; - beforeEach( () => { - lastValues = {}; - undoState = undefined; - expectedUndoState = { list: [], offset: 0 }; - } ); - - it( 'initializes', () => { - expect( createNextUndoState() ).toEqual( expectedUndoState ); - } ); - - it( 'stacks undo levels', () => { - undoState = createNextUndoState(); - - // Check that the first edit creates an undo level for the current state and - // one for the new one. - undoState = createNextUndoState( { value: 1 } ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: undefined, to: 1 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - - // Check that the second and third edits just create an undo level for - // themselves. - undoState = createNextUndoState( { value: 2 } ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: 1, to: 2 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - undoState = createNextUndoState( { value: 3 } ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: 2, to: 3 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - } ); - - it( 'stacks multi-property undo levels', () => { - undoState = createNextUndoState(); - - undoState = createNextUndoState( { value: 1 } ); - undoState = createNextUndoState( { value2: 2 } ); - expectedUndoState.list.push( - [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ], - [ createExpectedDiff( 'value2', { from: undefined, to: 2 } ) ] - ); - expect( undoState ).toEqual( expectedUndoState ); - - // Check that that creating another undo level merges the "edits" - undoState = createNextUndoState( { value: 2 } ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: 1, to: 2 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - } ); - - it( 'handles undos/redos', () => { - undoState = createNextUndoState(); - undoState = createNextUndoState( { value: 1 } ); - undoState = createNextUndoState( { value: 2 } ); - undoState = createNextUndoState( { value: 3 } ); - expectedUndoState.list.push( - [ createExpectedDiff( 'value', { from: undefined, to: 1 } ) ], - [ createExpectedDiff( 'value', { from: 1, to: 2 } ) ], - [ createExpectedDiff( 'value', { from: 2, to: 3 } ) ] - ); - expect( undoState ).toEqual( expectedUndoState ); - - // Check that undoing and redoing an equal - // number of steps does not lose edits. - undoState = createNextUndoState( 'isUndo' ); - expectedUndoState.offset--; - expect( undoState ).toEqual( expectedUndoState ); - undoState = createNextUndoState( 'isUndo' ); - expectedUndoState.offset--; - expect( undoState ).toEqual( expectedUndoState ); - undoState = createNextUndoState( 'isRedo' ); - expectedUndoState.offset++; - expect( undoState ).toEqual( expectedUndoState ); - undoState = createNextUndoState( 'isRedo' ); - expectedUndoState.offset++; - expect( undoState ).toEqual( expectedUndoState ); - - // Check that another edit will go on top when there - // is no undo level offset. - undoState = createNextUndoState( { value: 4 } ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: 3, to: 4 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - - // Check that undoing and editing will slice of - // all the levels after the current one. - undoState = createNextUndoState( 'isUndo' ); - undoState = createNextUndoState( 'isUndo' ); - - undoState = createNextUndoState( { value: 5 } ); - expectedUndoState.list.pop(); - expectedUndoState.list.pop(); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: 2, to: 5 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - } ); - - it( 'handles flattened undos/redos', () => { - undoState = createNextUndoState(); - undoState = createNextUndoState( { value: 1 } ); - undoState = createNextUndoState( { transientValue: 2 }, true ); - undoState = createNextUndoState( { value: 3 } ); - expectedUndoState.list.push( - [ - createExpectedDiff( 'value', { from: undefined, to: 1 } ), - createExpectedDiff( 'transientValue', { - from: undefined, - to: 2, - } ), - ], - [ createExpectedDiff( 'value', { from: 1, to: 3 } ) ] - ); - expect( undoState ).toEqual( expectedUndoState ); - } ); - - it( 'handles explicit undo level creation', () => { - undoState = createNextUndoState(); - - // Check that nothing happens if there are no pending - // transient edits. - undoState = createNextUndoState( { value: 1 } ); - undoState = createNextUndoState( 'isCreate' ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: undefined, to: 1 } ), - ] ); - expect( undoState ).toEqual( expectedUndoState ); - - // Check that transient edits are merged into the last - // edits. - undoState = createNextUndoState( { transientValue: 2 }, true ); - undoState = createNextUndoState( 'isCreate' ); - expectedUndoState.list[ expectedUndoState.list.length - 1 ].push( - createExpectedDiff( 'transientValue', { from: undefined, to: 2 } ) - ); - expect( undoState ).toEqual( expectedUndoState ); - - // Check that create after undo does nothing. - undoState = createNextUndoState( { value: 3 } ); - undoState = createNextUndoState( 'isUndo' ); - undoState = createNextUndoState( 'isCreate' ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: 1, to: 3 } ), - ] ); - expectedUndoState.offset = -1; - expect( undoState ).toEqual( expectedUndoState ); - } ); - - it( 'explicitly creates an undo level when undoing while there are pending transient edits', () => { - undoState = createNextUndoState(); - undoState = createNextUndoState( { value: 1 } ); - undoState = createNextUndoState( { transientValue: 2 }, true ); - undoState = createNextUndoState( 'isUndo' ); - expectedUndoState.list.push( [ - createExpectedDiff( 'value', { from: undefined, to: 1 } ), - createExpectedDiff( 'transientValue', { from: undefined, to: 2 } ), - ] ); - expectedUndoState.offset--; - expect( undoState ).toEqual( expectedUndoState ); - } ); - - it( 'does not create new levels for the same function edits', () => { - const value = () => {}; - undoState = createNextUndoState(); - undoState = createNextUndoState( { value } ); - undoState = createNextUndoState( { value: () => {} } ); - expect( undoState ).toEqual( expectedUndoState ); - } ); -} ); - describe( 'embedPreviews()', () => { it( 'returns an empty object by default', () => { const state = embedPreviews( undefined, {} ); diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index 84fecc7d07cda9..161d0af4ea5bca 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -22,7 +22,6 @@ import { getAutosave, getAutosaves, getCurrentUser, - getReferenceByDistinctEdits, } from '../selectors'; // getEntityRecord and __experimentalGetEntityRecordNoResolver selectors share the same tests. describe.each( [ @@ -835,56 +834,3 @@ describe( 'getCurrentUser', () => { expect( getCurrentUser( state ) ).toEqual( currentUser ); } ); } ); - -describe( 'getReferenceByDistinctEdits', () => { - it( 'should return referentially equal values across empty states', () => { - const state = { undo: { list: [] } }; - expect( getReferenceByDistinctEdits( state ) ).toBe( - getReferenceByDistinctEdits( state ) - ); - - const beforeState = { undo: { list: [] } }; - const afterState = { undo: { list: [] } }; - expect( getReferenceByDistinctEdits( beforeState ) ).toBe( - getReferenceByDistinctEdits( afterState ) - ); - } ); - - it( 'should return referentially equal values across unchanging non-empty state', () => { - const undoStates = { list: [ {} ] }; - const state = { undo: undoStates }; - expect( getReferenceByDistinctEdits( state ) ).toBe( - getReferenceByDistinctEdits( state ) - ); - - const beforeState = { undo: undoStates }; - const afterState = { undo: undoStates }; - expect( getReferenceByDistinctEdits( beforeState ) ).toBe( - getReferenceByDistinctEdits( afterState ) - ); - } ); - - describe( 'when adding edits', () => { - it( 'should return referentially different values across changing states', () => { - const beforeState = { undo: { list: [ {} ] } }; - beforeState.undo.offset = 0; - const afterState = { undo: { list: [ {}, {} ] } }; - afterState.undo.offset = 1; - expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe( - getReferenceByDistinctEdits( afterState ) - ); - } ); - } ); - - describe( 'when using undo', () => { - it( 'should return referentially different values across changing states', () => { - const beforeState = { undo: { list: [ {}, {} ] } }; - beforeState.undo.offset = 1; - const afterState = { undo: { list: [ {}, {} ] } }; - afterState.undo.offset = 0; - expect( getReferenceByDistinctEdits( beforeState ) ).not.toBe( - getReferenceByDistinctEdits( afterState ) - ); - } ); - } ); -} ); diff --git a/packages/core-data/tsconfig.json b/packages/core-data/tsconfig.json index 031d697f8dbe6b..3fe698f758b54d 100644 --- a/packages/core-data/tsconfig.json +++ b/packages/core-data/tsconfig.json @@ -19,6 +19,7 @@ { "path": "../is-shallow-equal" }, { "path": "../private-apis" }, { "path": "../sync" }, + { "path": "../undo-manager" }, { "path": "../url" } ], "include": [ "src/**/*" ] diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index a22837af6a72e9..ac637eb33fbb2f 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -1,5 +1,9 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; +const BUNDLED_PACKAGES = [ + '@wordpress/icons', + '@wordpress/interface', + '@wordpress/undo-manager', +]; /** * Default request to global transformation diff --git a/packages/undo-manager/.npmrc b/packages/undo-manager/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/undo-manager/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/undo-manager/CHANGELOG.md b/packages/undo-manager/CHANGELOG.md new file mode 100644 index 00000000000000..32b01e3da3a957 --- /dev/null +++ b/packages/undo-manager/CHANGELOG.md @@ -0,0 +1,4 @@ + + +## Unreleased + diff --git a/packages/undo-manager/README.md b/packages/undo-manager/README.md new file mode 100644 index 00000000000000..cc9727469bcadf --- /dev/null +++ b/packages/undo-manager/README.md @@ -0,0 +1,33 @@ +# Undo Manager + +A simple undo manager. + +## Installation + +Install the module + +```bash +npm install @wordpress/undo-manager --save +``` + +## API + + + +### createUndoManager + +Creates an undo manager. + +_Returns_ + +- `UndoManager`: Undo manager. + + + +## Contributing to this package + +This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. + +To find out more about contributing to this package or Gutenberg as a whole, please read the project's main [contributor guide](https://github.com/WordPress/gutenberg/tree/HEAD/CONTRIBUTING.md). + +

Code is Poetry.

diff --git a/packages/undo-manager/package.json b/packages/undo-manager/package.json new file mode 100644 index 00000000000000..7ca465023b5e83 --- /dev/null +++ b/packages/undo-manager/package.json @@ -0,0 +1,37 @@ +{ + "name": "@wordpress/undo-manager", + "version": "0.1.0", + "description": "A small package to manage undo/redo.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "undo", + "history" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/undo-manager/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/undo-manager" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=12" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "types": "build-types", + "sideEffects": false, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/is-shallow-equal": "file:../is-shallow-equal" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/undo-manager/src/index.js b/packages/undo-manager/src/index.js new file mode 100644 index 00000000000000..379172943dfb02 --- /dev/null +++ b/packages/undo-manager/src/index.js @@ -0,0 +1,171 @@ +/** + * WordPress dependencies + */ +import isShallowEqual from '@wordpress/is-shallow-equal'; + +/** @typedef {import('./types').HistoryRecord} HistoryRecord */ +/** @typedef {import('./types').HistoryChange} HistoryChange */ +/** @typedef {import('./types').HistoryChanges} HistoryChanges */ +/** @typedef {import('./types').UndoManager} UndoManager */ + +/** + * Merge changes for a single item into a record of changes. + * + * @param {Record< string, HistoryChange >} changes1 Previous changes + * @param {Record< string, HistoryChange >} changes2 NextChanges + * + * @return {Record< string, HistoryChange >} Merged changes + */ +function mergeHistoryChanges( changes1, changes2 ) { + /** + * @type {Record< string, HistoryChange >} + */ + const newChanges = { ...changes1 }; + Object.entries( changes2 ).forEach( ( [ key, value ] ) => { + if ( newChanges[ key ] ) { + newChanges[ key ] = { ...newChanges[ key ], to: value.to }; + } else { + newChanges[ key ] = value; + } + } ); + + return newChanges; +} + +/** + * Adds history changes for a single item into a record of changes. + * + * @param {HistoryRecord} record The record to merge into. + * @param {HistoryChanges} changes The changes to merge. + */ +const addHistoryChangesIntoRecord = ( record, changes ) => { + const existingChangesIndex = record?.findIndex( + ( { id: recordIdentifier } ) => { + return typeof recordIdentifier === 'string' + ? recordIdentifier === changes.id + : isShallowEqual( recordIdentifier, changes.id ); + } + ); + const nextRecord = [ ...record ]; + + if ( existingChangesIndex !== -1 ) { + // If the edit is already in the stack leave the initial "from" value. + nextRecord[ existingChangesIndex ] = { + id: changes.id, + changes: mergeHistoryChanges( + nextRecord[ existingChangesIndex ].changes, + changes.changes + ), + }; + } else { + nextRecord.push( changes ); + } + return nextRecord; +}; + +/** + * Creates an undo manager. + * + * @return {UndoManager} Undo manager. + */ +export function createUndoManager() { + /** + * @type {HistoryRecord[]} + */ + let history = []; + /** + * @type {HistoryRecord} + */ + let stagedRecord = []; + /** + * @type {number} + */ + let offset = 0; + + const dropPendingRedos = () => { + history = history.slice( 0, offset || undefined ); + offset = 0; + }; + + const appendStagedRecordToLatestHistoryRecord = () => { + const index = history.length === 0 ? 0 : history.length - 1; + let latestRecord = history[ index ] ?? []; + stagedRecord.forEach( ( changes ) => { + latestRecord = addHistoryChangesIntoRecord( latestRecord, changes ); + } ); + stagedRecord = []; + history[ index ] = latestRecord; + }; + + /** + * Checks whether a record is empty. + * A record is considered empty if it the changes keep the same values. + * Also updates to function values are ignored. + * + * @param {HistoryRecord} record + * @return {boolean} Whether the record is empty. + */ + const isRecordEmpty = ( record ) => { + const filteredRecord = record.filter( ( { changes } ) => { + return Object.values( changes ).some( + ( { from, to } ) => + typeof from !== 'function' && + typeof to !== 'function' && + ! isShallowEqual( from, to ) + ); + } ); + return ! filteredRecord.length; + }; + + return { + /** + * Record changes into the history. + * + * @param {HistoryRecord=} record A record of changes to record. + * @param {boolean} isStaged Whether to immediately create an undo point or not. + */ + addRecord( record, isStaged = false ) { + const isEmpty = ! record || isRecordEmpty( record ); + if ( isStaged ) { + if ( isEmpty ) { + return; + } + record.forEach( ( changes ) => { + stagedRecord = addHistoryChangesIntoRecord( + stagedRecord, + changes + ); + } ); + } else { + dropPendingRedos(); + if ( stagedRecord.length ) { + appendStagedRecordToLatestHistoryRecord(); + } + if ( isEmpty ) { + return; + } + history.push( record ); + } + }, + + undo() { + if ( stagedRecord.length ) { + dropPendingRedos(); + appendStagedRecordToLatestHistoryRecord(); + } + offset -= 1; + }, + + redo() { + offset += 1; + }, + + getUndoRecord() { + return history[ history.length - 1 + offset ]; + }, + + getRedoRecord() { + return history[ history.length + offset ]; + }, + }; +} diff --git a/packages/undo-manager/src/test/index.js b/packages/undo-manager/src/test/index.js new file mode 100644 index 00000000000000..32ec2713f7bc28 --- /dev/null +++ b/packages/undo-manager/src/test/index.js @@ -0,0 +1,257 @@ +/** + * Internal dependencies + */ +import { createUndoManager } from '../'; + +describe( 'Undo Manager', () => { + it( 'stacks undo levels', () => { + const undo = createUndoManager(); + + undo.addRecord( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + + undo.addRecord( [ + { id: '1', changes: { value: { from: 1, to: 2 } } }, + ] ); + undo.addRecord( [ + { id: '1', changes: { value: { from: 2, to: 3 } } }, + ] ); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 2, to: 3 } } }, + ] ); + } ); + + it( 'handles undos/redos', () => { + const undo = createUndoManager(); + undo.addRecord( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + undo.addRecord( [ + { id: '1', changes: { value: { from: 1, to: 2 } } }, + ] ); + undo.addRecord( [ + { id: '1', changes: { value: { from: 2, to: 3 } } }, + ] ); + + undo.undo(); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 1, to: 2 } } }, + ] ); + expect( undo.getRedoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 2, to: 3 } } }, + ] ); + + undo.undo(); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + expect( undo.getRedoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 1, to: 2 } } }, + ] ); + + undo.redo(); + undo.redo(); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 2, to: 3 } } }, + ] ); + + undo.addRecord( [ + { id: '1', changes: { value: { from: 3, to: 4 } } }, + ] ); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 3, to: 4 } } }, + ] ); + + // Check that undoing and editing will slice of + // all the levels after the current one. + undo.undo(); + undo.undo(); + undo.addRecord( [ + { id: '1', changes: { value: { from: 2, to: 5 } } }, + ] ); + undo.undo(); + expect( undo.getUndoRecord() ).toEqual( [ + { id: '1', changes: { value: { from: 1, to: 2 } } }, + ] ); + } ); + + it( 'handles staged edits', () => { + const undo = createUndoManager(); + undo.addRecord( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + undo.addRecord( + [ { id: '1', changes: { value2: { from: undefined, to: 2 } } } ], + true + ); + undo.addRecord( + [ { id: '1', changes: { value: { from: 1, to: 3 } } } ], + true + ); + undo.addRecord( [ + { id: '1', changes: { value: { from: 3, to: 4 } } }, + ] ); + undo.undo(); + expect( undo.getUndoRecord() ).toEqual( [ + { + id: '1', + changes: { + value: { from: undefined, to: 3 }, + value2: { from: undefined, to: 2 }, + }, + }, + ] ); + } ); + + it( 'handles explicit undo level creation', () => { + const undo = createUndoManager(); + undo.addRecord( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + // These three calls do nothing because they're empty. + undo.addRecord( [] ); + undo.addRecord(); + undo.addRecord( [ + { id: '1', changes: { value: { from: 1, to: 1 } } }, + ] ); + // Check that nothing happens if there are no pending + // transient edits. + undo.undo(); + expect( undo.getUndoRecord() ).toBe( undefined ); + undo.redo(); + + // Check that transient edits are merged into the last + // edits. + undo.addRecord( + [ { id: '1', changes: { value2: { from: undefined, to: 2 } } } ], + true + ); + undo.addRecord( [] ); // Records the staged edits. + undo.undo(); + expect( undo.getRedoRecord() ).toEqual( [ + { + id: '1', + changes: { + value: { from: undefined, to: 1 }, + value2: { from: undefined, to: 2 }, + }, + }, + ] ); + } ); + + it( 'explicitly creates an undo level when undoing while there are pending transient edits', () => { + const undo = createUndoManager(); + undo.addRecord( [ + { id: '1', changes: { value: { from: undefined, to: 1 } } }, + ] ); + undo.addRecord( + [ { id: '1', changes: { value2: { from: undefined, to: 2 } } } ], + true + ); + undo.undo(); + expect( undo.getRedoRecord() ).toEqual( [ + { + id: '1', + changes: { + value: { from: undefined, to: 1 }, + value2: { from: undefined, to: 2 }, + }, + }, + ] ); + } ); + + it( 'supports records as ids', () => { + const undo = createUndoManager(); + + undo.addRecord( + [ + { + id: { kind: 'postType', name: 'post', recordId: 1 }, + changes: { value: { from: undefined, to: 1 } }, + }, + ], + true + ); + undo.addRecord( + [ + { + id: { kind: 'postType', name: 'post', recordId: 1 }, + changes: { value2: { from: undefined, to: 2 } }, + }, + ], + true + ); + undo.addRecord( + [ + { + id: { kind: 'postType', name: 'post', recordId: 2 }, + changes: { value: { from: undefined, to: 3 } }, + }, + ], + true + ); + undo.addRecord(); + expect( undo.getUndoRecord() ).toEqual( [ + { + id: { kind: 'postType', name: 'post', recordId: 1 }, + changes: { + value: { from: undefined, to: 1 }, + value2: { from: undefined, to: 2 }, + }, + }, + { + id: { kind: 'postType', name: 'post', recordId: 2 }, + changes: { + value: { from: undefined, to: 3 }, + }, + }, + ] ); + } ); + + it( 'should ignore empty records', () => { + const undo = createUndoManager(); + + // All the following changes are considered empty for different reasons. + undo.addRecord(); + undo.addRecord( [] ); + undo.addRecord( [ + { id: '1', changes: { a: { from: 'value', to: 'value' } } }, + ] ); + undo.addRecord( [ + { + id: '1', + changes: { + a: { from: 'value', to: 'value' }, + b: { from: () => {}, to: () => {} }, + }, + }, + ] ); + + expect( undo.getUndoRecord() ).toBeUndefined(); + + // The following changes is not empty + // and should also record the function changes in the history. + + undo.addRecord( [ + { + id: '1', + changes: { + a: { from: 'value1', to: 'value2' }, + b: { from: () => {}, to: () => {} }, + }, + }, + ] ); + + const undoRecord = undo.getUndoRecord(); + expect( undoRecord ).not.toBeUndefined(); + // b is included in the changes. + expect( Object.keys( undoRecord[ 0 ].changes ) ).toEqual( [ + 'a', + 'b', + ] ); + } ); +} ); diff --git a/packages/undo-manager/src/types.ts b/packages/undo-manager/src/types.ts new file mode 100644 index 00000000000000..e2e1d995f8e5db --- /dev/null +++ b/packages/undo-manager/src/types.ts @@ -0,0 +1,19 @@ +export type HistoryChange = { + from: any; + to: any; +}; + +export type HistoryChanges = { + id: string | Record< string, any >; + changes: Record< string, HistoryChange >; +}; + +export type HistoryRecord = Array< HistoryChanges >; + +export type UndoManager = { + addRecord: ( record: HistoryRecord, isStaged: boolean ) => void; + undo: () => void; + redo: () => void; + getUndoRecord: () => HistoryRecord; + getRedoRecord: () => HistoryRecord; +}; diff --git a/packages/undo-manager/tsconfig.json b/packages/undo-manager/tsconfig.json new file mode 100644 index 00000000000000..e53ddb4792e576 --- /dev/null +++ b/packages/undo-manager/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "types": [ "node" ] + }, + "references": [ { "path": "../is-shallow-equal" } ], + "include": [ "src/**/*" ] +} diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index e5bb74abdb0a1f..3dc8407d7974b9 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -24,7 +24,11 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; // Experimental or other packages that should be private are bundled when used. // That way, we can iterate on these package without making them part of the public API. // See: https://github.com/WordPress/gutenberg/pull/19809 -const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; +const BUNDLED_PACKAGES = [ + '@wordpress/icons', + '@wordpress/interface', + '@wordpress/undo-manager', +]; // PHP files in packages that have to be copied during build. const bundledPackagesPhpConfig = [ diff --git a/tsconfig.json b/tsconfig.json index 2c395450fb6a0e..4ee1787a247cf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,6 +44,7 @@ { "path": "packages/style-engine" }, { "path": "packages/sync" }, { "path": "packages/token-list" }, + { "path": "packages/undo-manager" }, { "path": "packages/url" }, { "path": "packages/warning" }, { "path": "packages/wordcount" } From 5c74b3dc7f77f24985196a381c8a0c2e0409a76a Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 11 Sep 2023 14:49:38 +0300 Subject: [PATCH 04/42] adds distraction free mode off animation state to post editor header (#54244) --- packages/edit-post/src/components/header/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index ab4bbd4bbc5d15..4d567fe37493d5 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -22,11 +22,13 @@ import DocumentActions from './document-actions'; const slideY = { hidden: { y: '-50px' }, + distractionFreeInactive: { y: 0 }, hover: { y: 0, transition: { type: 'tween', delay: 0.2 } }, }; const slideX = { hidden: { x: '-100%' }, + distractionFreeInactive: { x: 0 }, hover: { x: 0, transition: { type: 'tween', delay: 0.2 } }, }; From 3f65c725c7ec2b8b64607f27b21f19dce9e644bb Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 11 Sep 2023 14:50:27 +0300 Subject: [PATCH 05/42] Make order of pinned items consistent (#53908) * move the editor pinned items last in post editor * try identifying pinned items by aria controls attribute * update snapshots with the new attrs * remove snapshot as on trunk * update snapshots * undo unrelated snapshot update --- .../editor/plugins/__snapshots__/plugins-api.test.js.snap | 4 ++-- packages/edit-post/src/components/layout/index.js | 3 ++- .../src/components/complementary-area-toggle/index.js | 1 + .../interface/src/components/complementary-area/index.js | 7 +++++-- packages/interface/src/components/pinned-items/style.scss | 7 +++++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap index 4cac5cd6893cda..9e4a5ac3ad6f61 100644 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap +++ b/packages/e2e-tests/specs/editor/plugins/__snapshots__/plugins-api.test.js.snap @@ -2,6 +2,6 @@ exports[`Using Plugins API Document Setting Custom Panel Should render a custom panel inside Document Setting sidebar 1`] = `"My Custom Panel"`; -exports[`Using Plugins API Sidebar Medium screen Should open plugins sidebar using More Menu item and render content 1`] = `"
(no title)
Plugin title
"`; +exports[`Using Plugins API Sidebar Medium screen Should open plugins sidebar using More Menu item and render content 1`] = `"
(no title)
Plugin title
"`; -exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"
(no title)
Plugin title
"`; +exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"
(no title)
Plugin title
"`; diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index c0018d40d6ef82..870a3e9044dfcb 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -266,7 +266,7 @@ function Layout() { - + + ); } diff --git a/packages/interface/src/components/complementary-area-toggle/index.js b/packages/interface/src/components/complementary-area-toggle/index.js index 1abdeb2c84f260..b6690b7df5fc5d 100644 --- a/packages/interface/src/components/complementary-area-toggle/index.js +++ b/packages/interface/src/components/complementary-area-toggle/index.js @@ -31,6 +31,7 @@ function ComplementaryAreaToggle( { return ( { if ( isSelected ) { disableComplementaryArea( scope ); diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index de69762b6a15c4..887c447d9291e4 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -27,10 +27,12 @@ function ComplementaryAreaSlot( { scope, ...props } ) { return ; } -function ComplementaryAreaFill( { scope, children, className } ) { +function ComplementaryAreaFill( { scope, children, className, id } ) { return ( -
{ children }
+
+ { children } +
); } @@ -200,6 +202,7 @@ function ComplementaryArea( { className ) } scope={ scope } + id={ identifier.replace( '/', ':' ) } > Date: Mon, 11 Sep 2023 21:59:47 +1000 Subject: [PATCH 06/42] [RNMobile] Fix issue with missing characters in Add Media placeholder button (#54281) * Fix media placeholder text issue * Update CHANGELOG * Update Gallery block media placeholder text * Update letter case for File block button --- .../src/components/media-placeholder/index.native.js | 11 +++++------ packages/block-library/src/file/edit.native.js | 2 +- packages/block-library/src/gallery/edit.js | 2 +- packages/react-native-editor/CHANGELOG.md | 1 + 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index 938c01e92b4ed5..e597b65d63b865 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -2,7 +2,6 @@ * External dependencies */ import { View, Text, TouchableOpacity } from 'react-native'; -import { sentenceCase } from 'change-case'; /** * WordPress dependencies @@ -104,13 +103,13 @@ function MediaPlaceholder( props ) { let instructions = labels.instructions; if ( instructions === undefined ) { if ( isImage ) { - instructions = __( 'ADD IMAGE' ); + instructions = __( 'Add image' ); } else if ( isVideo ) { - instructions = __( 'ADD VIDEO' ); + instructions = __( 'Add video' ); } else if ( isAudio ) { - instructions = __( 'ADD AUDIO' ); + instructions = __( 'Add audio' ); } else { - instructions = __( 'ADD IMAGE OR VIDEO' ); + instructions = __( 'Add image or video' ); } } @@ -171,7 +170,7 @@ function MediaPlaceholder( props ) { onPress={ onButtonPress( open ) } > - { sentenceCase( instructions ) } + { instructions } diff --git a/packages/block-library/src/file/edit.native.js b/packages/block-library/src/file/edit.native.js index 3266adf26098de..c4217c60263069 100644 --- a/packages/block-library/src/file/edit.native.js +++ b/packages/block-library/src/file/edit.native.js @@ -552,7 +552,7 @@ export class FileEdit extends Component { icon={ } labels={ { title: __( 'File' ), - instructions: __( 'CHOOSE A FILE' ), + instructions: __( 'Choose a file' ), } } onSelect={ this.onSelectFile } onFocus={ this.props.onFocus } diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 70e006560642e6..68ada058abefa8 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -75,7 +75,7 @@ const ALLOWED_MEDIA_TYPES = [ 'image' ]; const allowedBlocks = [ 'core/image' ]; const PLACEHOLDER_TEXT = Platform.isNative - ? __( 'ADD MEDIA' ) + ? __( 'Add media' ) : __( 'Drag images, upload new ones or select files from your library.' ); const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.isNative diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index f95aba8b9d4d6e..031c77b8ee8ed7 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Fix the obscurred "Insert from URL" input for media blocks when using a device in landscape orientation. [#54096] +- [*] Fix issue with missing characters in Add Media placeholder button [#54281] ## 1.103.1 - [**] Fix long-press gestures not working in RichText component [Android] [#54213] From fee14675244a818e1a9e6e5bd0c5824d7d225ec2 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:10:14 +0300 Subject: [PATCH 07/42] RichText: replace deprecated multiline prop with simple multiple instances (#54310) --- .../block-api/block-attributes.md | 33 --- packages/block-editor/CHANGELOG.md | 4 + .../src/components/rich-text/index.js | 87 ++++---- .../src/components/rich-text/index.native.js | 56 ++--- .../src/components/rich-text/multiline.js | 121 +++++++++++ .../src/components/rich-text/split-value.js | 24 +-- .../src/components/rich-text/use-enter.js | 53 ++--- .../components/rich-text/use-paste-handler.js | 35 +--- packages/block-editor/src/store/utils.js | 13 +- .../block-library/src/pullquote/deprecated.js | 21 +- packages/editor/README.md | 5 - packages/rich-text/README.md | 7 +- packages/rich-text/src/component/index.js | 15 +- .../rich-text/src/component/index.native.js | 23 +- .../src/component/use-copy-handler.js | 4 +- .../rich-text/src/component/use-delete.js | 28 +-- packages/rich-text/src/create.js | 147 +------------ packages/rich-text/src/get-text-content.js | 14 +- packages/rich-text/src/index.ts | 4 +- .../rich-text/src/insert-line-separator.js | 43 ---- packages/rich-text/src/is-empty.js | 36 ---- .../rich-text/src/remove-line-separator.js | 55 ----- packages/rich-text/src/special-characters.js | 5 - packages/rich-text/src/split.js | 8 +- .../src/test/__snapshots__/to-dom.js.snap | 124 ----------- packages/rich-text/src/test/create.js | 51 ++--- packages/rich-text/src/test/helpers/index.js | 196 ------------------ .../src/test/insert-line-separator.js | 104 ---------- packages/rich-text/src/test/is-empty.js | 61 +----- packages/rich-text/src/test/split.js | 33 --- packages/rich-text/src/test/to-dom.js | 21 +- packages/rich-text/src/test/to-html-string.js | 15 -- packages/rich-text/src/to-dom.js | 8 +- packages/rich-text/src/to-html-string.js | 7 +- packages/rich-text/src/to-tree.js | 85 +------- schemas/json/block.json | 4 - .../rich-text-deprecated-multiline.spec.js | 126 +++++++++++ 37 files changed, 402 insertions(+), 1274 deletions(-) create mode 100644 packages/block-editor/src/components/rich-text/multiline.js delete mode 100644 packages/rich-text/src/insert-line-separator.js delete mode 100644 packages/rich-text/src/remove-line-separator.js delete mode 100644 packages/rich-text/src/test/insert-line-separator.js create mode 100644 test/e2e/specs/editor/various/rich-text-deprecated-multiline.spec.js diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index eafc73c79938f3..0fbbeeb13680e6 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -305,39 +305,6 @@ Attribute available in the block: { "content": "The inner text of the figcaption element" } ``` -Use the `multiline` property to extract the inner HTML of matching tag names for the use in `RichText` with the `multiline` prop. - -_Example_: Extract the `content` attribute from a blockquote element found in the block's markup. - -Saved content: -```html -
- Block Content - -
-

First line

-

Second line

-
-
-``` - -Attribute definition: -```js -{ - content: { - type: 'string', - source: 'html', - multiline: 'p', - selector: 'blockquote', - } -} -``` - -Attribute available in the block: -```js -{ "content": "

First line

Second line

" } -``` - ### `query` source Use `query` to extract an array of values from markup. Entries of the array are determined by the `selector` argument, where each matched element within the block will have an entry structured corresponding to the second argument, an object of attribute sources. diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 63a6281f78b426..5441536e1ab112 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- The Deprecated multiline prop on RichText will now fall back to using multiple + rich text instances instead of a single multiline instance. The prop remains + deprecated. + ## 12.9.0 (2023-08-31) ### Enhancements diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index a22b251dd607c2..aa6a44461ee73c 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -44,8 +44,9 @@ import { useInputEvents } from './use-input-events'; import { useInsertReplacementText } from './use-insert-replacement-text'; import { useFirefoxCompat } from './use-firefox-compat'; import FormatEdit from './format-edit'; -import { getMultilineTag, getAllowedFormats } from './utils'; +import { getAllowedFormats } from './utils'; import { Content } from './content'; +import RichTextMultiline from './multiline'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -81,12 +82,12 @@ function removeNativeProps( props ) { return restProps; } -function RichTextWrapper( +export function RichTextWrapper( { children, tagName = 'div', - value: originalValue = '', - onChange: originalOnChange, + value: adjustedValue = '', + onChange: adjustedOnChange, isSelected: originalIsSelected, multiline, inlineToolbar, @@ -111,18 +112,6 @@ function RichTextWrapper( }, forwardedRef ) { - if ( multiline ) { - deprecated( 'wp.blockEditor.RichText multiline prop', { - since: '6.1', - version: '6.3', - alternative: 'nested blocks (InnerBlocks)', - link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/', - } ); - } - - const instanceId = useInstanceId( RichTextWrapper ); - - identifier = identifier || instanceId; props = removeNativeProps( props ); const anchorRef = useRef(); @@ -157,33 +146,12 @@ function RichTextWrapper( const { getSelectionStart, getSelectionEnd, getBlockRootClientId } = useSelect( blockEditorStore ); const { selectionChange } = useDispatch( blockEditorStore ); - const multilineTag = getMultilineTag( multiline ); const adjustedAllowedFormats = getAllowedFormats( { allowedFormats, disableFormats, } ); const hasFormats = ! adjustedAllowedFormats || adjustedAllowedFormats.length > 0; - let adjustedValue = originalValue; - let adjustedOnChange = originalOnChange; - - // Handle deprecated format. - if ( Array.isArray( originalValue ) ) { - 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/', - } ); - - adjustedValue = childrenSource.toHTML( originalValue ); - adjustedOnChange = ( newValue ) => - originalOnChange( - childrenSource.fromDOM( - __unstableCreateElement( document, newValue ).childNodes - ) - ); - } const onSelectionChange = useCallback( ( start, end ) => { @@ -292,7 +260,6 @@ function RichTextWrapper( onSelectionChange, placeholder, __unstableIsSelected: isSelected, - __unstableMultilineTag: multilineTag, __unstableDisableFormats: disableFormats, preserveWhiteSpace, __unstableDependencies: [ ...dependencies, tagName ], @@ -380,7 +347,6 @@ function RichTextWrapper( onReplace, onSplit, __unstableEmbedURLOnPaste, - multilineTag, preserveWhiteSpace, pastePlainText, } ), @@ -394,7 +360,6 @@ function RichTextWrapper( value, onReplace, onSplit, - multilineTag, onChange, disableLineBreaks, onSplitAtEnd, @@ -421,7 +386,47 @@ function RichTextWrapper( ); } -const ForwardedRichTextContainer = forwardRef( 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 ); 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 b0c82848db6876..67f41f9ae8c108 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -20,13 +20,9 @@ import { __experimentalRichText as RichText, __unstableCreateElement, isEmpty, - __unstableIsEmptyLine as isEmptyLine, insert, - __unstableInsertLineSeparator as insertLineSeparator, create, - replace, split, - __UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR, toHTMLString, slice, } from '@wordpress/rich-text'; @@ -338,32 +334,20 @@ function RichTextWrapper( onCustomEnter(); } - if ( multiline ) { - if ( shiftKey ) { - if ( ! disableLineBreaks ) { - onChange( insert( value, '\n' ) ); - } - } else if ( canSplit && isEmptyLine( value ) ) { - splitValue( value ); - } else { - onChange( insertLineSeparator( value ) ); - } - } else { - const { text, start: splitStart, end: splitEnd } = value; - const canSplitAtEnd = - onSplitAtEnd && - splitStart === splitEnd && - splitEnd === text.length; - - if ( shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { - if ( ! disableLineBreaks ) { - onChange( insert( value, '\n' ) ); - } - } else if ( ! canSplit && canSplitAtEnd ) { - onSplitAtEnd(); - } else if ( canSplit ) { - splitValue( value ); + const { text, start: splitStart, end: splitEnd } = value; + const canSplitAtEnd = + onSplitAtEnd && + splitStart === splitEnd && + splitEnd === text.length; + + if ( shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { + if ( ! disableLineBreaks ) { + onChange( insert( value, '\n' ) ); } + } else if ( ! canSplit && canSplitAtEnd ) { + onSplitAtEnd(); + } else if ( canSplit ) { + splitValue( value ); } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -471,20 +455,8 @@ function RichTextWrapper( } ); if ( typeof content === 'string' ) { - let valueToInsert = create( { html: content } ); - + const valueToInsert = create( { html: content } ); addActiveFormats( valueToInsert, activeFormats ); - - // If the content should be multiline, we should process text - // separated by a line break as separate lines. - if ( multilineTag ) { - valueToInsert = replace( - valueToInsert, - /\n+/g, - LINE_SEPARATOR - ); - } - onChange( insert( value, valueToInsert ) ); } else if ( content.length > 0 ) { // When an URL is pasted in an empty paragraph then the EmbedHandlerPicker should showcase options allowing the transformation of that URL diff --git a/packages/block-editor/src/components/rich-text/multiline.js b/packages/block-editor/src/components/rich-text/multiline.js new file mode 100644 index 00000000000000..760a7718fd864a --- /dev/null +++ b/packages/block-editor/src/components/rich-text/multiline.js @@ -0,0 +1,121 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { RichTextWrapper } from './'; +import { store as blockEditorStore } from '../../store'; +import { useBlockEditContext } from '../block-edit'; +import { getMultilineTag } from './utils'; + +function RichTextMultiline( + { + children, + identifier, + tagName: TagName = 'div', + value = '', + onChange, + multiline, + ...props + }, + forwardedRef +) { + deprecated( 'wp.blockEditor.RichText multiline prop', { + since: '6.1', + version: '6.3', + alternative: 'nested blocks (InnerBlocks)', + link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/', + } ); + + const { clientId } = useBlockEditContext(); + const { selectionChange } = useDispatch( blockEditorStore ); + + const multilineTagName = getMultilineTag( multiline ); + value = value || `<${ multilineTagName }>`; + const padded = `${ value }<${ multilineTagName }>`; + const values = padded.split( + `<${ multilineTagName }>` + ); + + values.shift(); + values.pop(); + + function _onChange( newValues ) { + onChange( + `<${ multilineTagName }>${ newValues.join( + `<${ multilineTagName }>` + ) }` + ); + } + + return ( + + { values.map( ( _value, index ) => { + return ( + { + const newValues = values.slice(); + newValues[ index ] = newValue; + _onChange( newValues ); + } } + isSelected={ undefined } + onSplit={ ( v ) => v } + onReplace={ ( array ) => { + const newValues = values.slice(); + newValues.splice( index, 1, ...array ); + _onChange( newValues ); + selectionChange( + clientId, + `${ identifier }-${ index + 1 }`, + 0, + 0 + ); + } } + onMerge={ ( forward ) => { + const newValues = values.slice(); + let offset = 0; + if ( forward ) { + if ( ! newValues[ index + 1 ] ) return; + newValues.splice( + index, + 2, + newValues[ index ] + newValues[ index + 1 ] + ); + offset = newValues[ index ].length - 1; + } else { + if ( ! newValues[ index - 1 ] ) return; + newValues.splice( + index - 1, + 2, + newValues[ index - 1 ] + newValues[ index ] + ); + offset = newValues[ index - 1 ].length - 1; + } + _onChange( newValues ); + selectionChange( + clientId, + `${ identifier }-${ + index - ( forward ? 0 : 1 ) + }`, + offset, + offset + ); + } } + { ...props } + /> + ); + } ) } + + ); +} + +export default forwardRef( RichTextMultiline ); diff --git a/packages/block-editor/src/components/rich-text/split-value.js b/packages/block-editor/src/components/rich-text/split-value.js index 0ec083c9fe1e50..17f54d9c9edd01 100644 --- a/packages/block-editor/src/components/rich-text/split-value.js +++ b/packages/block-editor/src/components/rich-text/split-value.js @@ -8,13 +8,7 @@ import { isEmpty, split, toHTMLString } from '@wordpress/rich-text'; * as a result of splitting the block by pressing enter, or with blocks as a * result of splitting the block by pasting block content in the instance. */ -export function splitValue( { - value, - pastedBlocks = [], - onReplace, - onSplit, - multilineTag, -} ) { +export function splitValue( { value, pastedBlocks = [], onReplace, onSplit } ) { if ( ! onReplace || ! onSplit ) { return; } @@ -38,13 +32,7 @@ export function splitValue( { // the enter key. if ( ! hasPastedBlocks || ! isEmpty( before ) ) { blocks.push( - onSplit( - toHTMLString( { - value: before, - multilineTag, - } ), - ! isAfterOriginal - ) + onSplit( toHTMLString( { value: before } ), ! isAfterOriginal ) ); lastPastedBlockIndex += 1; } @@ -60,13 +48,7 @@ export function splitValue( { // the enter key. if ( ! hasPastedBlocks || ! isEmpty( after ) ) { blocks.push( - onSplit( - toHTMLString( { - value: after, - multilineTag, - } ), - isAfterOriginal - ) + onSplit( toHTMLString( { value: after } ), isAfterOriginal ) ); } diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js index 623203fb687df0..08c5abf9d7b6bc 100644 --- a/packages/block-editor/src/components/rich-text/use-enter.js +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -4,11 +4,7 @@ import { useRef } from '@wordpress/element'; import { useRefEffect } from '@wordpress/compose'; import { ENTER } from '@wordpress/keycodes'; -import { - insert, - __unstableIsEmptyLine as isEmptyLine, - __unstableInsertLineSeparator as insertLineSeparator, -} from '@wordpress/rich-text'; +import { insert } from '@wordpress/rich-text'; import { getBlockTransforms, findTransform } from '@wordpress/blocks'; import { useDispatch } from '@wordpress/data'; @@ -37,7 +33,6 @@ export function useEnter( props ) { value, onReplace, onSplit, - multilineTag, onChange, disableLineBreaks, onSplitAtEnd, @@ -67,40 +62,22 @@ export function useEnter( props ) { } } - if ( multilineTag ) { - if ( event.shiftKey ) { - if ( ! disableLineBreaks ) { - onChange( insert( _value, '\n' ) ); - } - } else if ( canSplit && isEmptyLine( _value ) ) { - splitValue( { - value: _value, - onReplace, - onSplit, - multilineTag, - } ); - } else { - onChange( insertLineSeparator( _value ) ); - } - } else { - const { text, start, end } = _value; - const canSplitAtEnd = - onSplitAtEnd && start === end && end === text.length; + const { text, start, end } = _value; + const canSplitAtEnd = + onSplitAtEnd && start === end && end === text.length; - if ( event.shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { - if ( ! disableLineBreaks ) { - onChange( insert( _value, '\n' ) ); - } - } else if ( ! canSplit && canSplitAtEnd ) { - onSplitAtEnd(); - } else if ( canSplit ) { - splitValue( { - value: _value, - onReplace, - onSplit, - multilineTag, - } ); + if ( event.shiftKey || ( ! canSplit && ! canSplitAtEnd ) ) { + if ( ! disableLineBreaks ) { + onChange( insert( _value, '\n' ) ); } + } else if ( ! canSplit && canSplitAtEnd ) { + onSplitAtEnd(); + } else if ( canSplit ) { + splitValue( { + value: _value, + onReplace, + onSplit, + } ); } } diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index d64d7ca6b15bb5..6ebd33e507d6c3 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -9,13 +9,7 @@ import { findTransform, getBlockTransforms, } from '@wordpress/blocks'; -import { - isEmpty, - insert, - create, - replace, - __UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR, -} from '@wordpress/rich-text'; +import { isEmpty, insert, create } from '@wordpress/rich-text'; import { isURL } from '@wordpress/url'; /** @@ -27,23 +21,6 @@ import { shouldDismissPastedFiles } from '../../utils/pasting'; /** @typedef {import('@wordpress/rich-text').RichTextValue} RichTextValue */ -/** - * Replaces line separators with line breaks if not multiline. - * Replaces line breaks with line separators if multiline. - * - * @param {RichTextValue} value Value to adjust. - * @param {boolean} isMultiline Whether to adjust to multiline or not. - * - * @return {RichTextValue} Adjusted value. - */ -function adjustLines( value, isMultiline ) { - if ( isMultiline ) { - return replace( value, /\n+/g, LINE_SEPARATOR ); - } - - return replace( value, new RegExp( LINE_SEPARATOR, 'g' ), '\n' ); -} - export function usePasteHandler( props ) { const propsRef = useRef( props ); propsRef.current = props; @@ -59,7 +36,6 @@ export function usePasteHandler( props ) { onReplace, onSplit, __unstableEmbedURLOnPaste, - multilineTag, preserveWhiteSpace, pastePlainText, } = propsRef.current; @@ -178,7 +154,6 @@ export function usePasteHandler( props ) { pastedBlocks: blocks, onReplace, onSplit, - multilineTag, } ); } @@ -220,12 +195,7 @@ export function usePasteHandler( props ) { } ); if ( typeof content === 'string' ) { - let valueToInsert = create( { html: content } ); - - // If the content should be multiline, we should process text - // separated by a line break as separate lines. - valueToInsert = adjustLines( valueToInsert, !! multilineTag ); - + const valueToInsert = create( { html: content } ); addActiveFormats( valueToInsert, value.activeFormats ); onChange( insert( value, valueToInsert ) ); } else if ( content.length > 0 ) { @@ -237,7 +207,6 @@ export function usePasteHandler( props ) { pastedBlocks: content, onReplace, onSplit, - multilineTag, } ); } } diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 66749753a9b802..4a19d76d1a4723 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -6,14 +6,7 @@ * @return {Object} The mapped object. */ export function mapRichTextSettings( attributeDefinition ) { - const { - multiline: multilineTag, - __unstableMultilineWrapperTags: multilineWrapperTags, - __unstablePreserveWhiteSpace: preserveWhiteSpace, - } = attributeDefinition; - return { - multilineTag, - multilineWrapperTags, - preserveWhiteSpace, - }; + const { __unstablePreserveWhiteSpace: preserveWhiteSpace } = + attributeDefinition; + return { preserveWhiteSpace }; } diff --git a/packages/block-library/src/pullquote/deprecated.js b/packages/block-library/src/pullquote/deprecated.js index 46ac8d2f13708b..2839a6d2b88042 100644 --- a/packages/block-library/src/pullquote/deprecated.js +++ b/packages/block-library/src/pullquote/deprecated.js @@ -14,12 +14,6 @@ import { useBlockProps, } from '@wordpress/block-editor'; import { select } from '@wordpress/data'; -import { - create, - replace, - toHTMLString, - __UNSTABLE_LINE_SEPARATOR, -} from '@wordpress/rich-text'; /** * Internal dependencies @@ -64,13 +58,14 @@ function parseBorderColor( styleString ) { } function multilineToInline( value ) { - return toHTMLString( { - value: replace( - create( { html: value, multilineTag: 'p' } ), - new RegExp( __UNSTABLE_LINE_SEPARATOR, 'g' ), - '\n' - ), - } ); + value = value || `

`; + const padded = `

${ value }

`; + const values = padded.split( `

` ); + + values.shift(); + values.pop(); + + return values.join( '
' ); } const v5 = { diff --git a/packages/editor/README.md b/packages/editor/README.md index dc8ec694de07a6..157136b28d0d66 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -91,11 +91,6 @@ The following properties (non-exhaustive list) are made available: - `placeholder: string` - A text hint to be shown to the user when the field value is empty, similar to the [`input` and `textarea` attribute of the same name](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/HTML5_updates#The_placeholder_attribute). -- `multiline: String` - A tag name to use for the tag that should be inserted - when Enter is pressed. For example: `li` in a list block, and `p` for a - block that can contain multiple paragraphs. The default is that only inline - elements are allowed to be used in inserted into the text, effectively - disabling the behavior of the "Enter" key. Example: diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index b88d0ffea6b521..84f33bc3afaf19 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -151,7 +151,7 @@ _Returns_ ### create -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 called without any input, an empty value will be created. If `multilineTag` is provided, any content of direct children whose type matches `multilineTag` will be separated by two newlines. The optional functions can be used to filter out content. +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 called without any input, an empty value will be created. The optional functions can be used to filter out content. A value will have the following shape, which you are strongly encouraged not to modify without the use of helper functions: @@ -174,8 +174,6 @@ _Parameters_ - _$1.text_ `[string]`: Text to create value from. - _$1.html_ `[string]`: HTML to create value from. - _$1.range_ `[Range]`: Range to create value from. -- _$1.multilineTag_ `[string]`: Multiline tag if the structure is multiline. -- _$1.multilineWrapperTags_ `[Array]`: Tags where lines can be found if nesting is possible. - _$1.preserveWhiteSpace_ `[boolean]`: Whether or not to collapse white space characters. - _$1.\_\_unstableIsEditableTree_ `[boolean]`: @@ -416,13 +414,12 @@ _Returns_ ### toHTMLString -Create an HTML string from a Rich Text value. If a `multilineTag` is provided, text separated by a line separator will be wrapped in it. +Create an HTML string from a Rich Text value. _Parameters_ - _$1_ `Object`: Named argements. - _$1.value_ `RichTextValue`: Rich text value. -- _$1.multilineTag_ `[string]`: Multiline tag. - _$1.preserveWhiteSpace_ `[boolean]`: Whether or not to use newline characters for line breaks. _Returns_ diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 2b437e4436777b..4aaa0b88ebc148 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -28,7 +28,6 @@ export function useRichText( { preserveWhiteSpace, onSelectionChange, onChange, - __unstableMultilineTag: multilineTag, __unstableDisableFormats: disableFormats, __unstableIsSelected: isSelected, __unstableDependencies = [], @@ -51,9 +50,6 @@ export function useRichText( { return create( { element: ref.current, range, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, __unstableIsEditableTree: true, preserveWhiteSpace, } ); @@ -63,9 +59,6 @@ export function useRichText( { apply( { value: newRecord, current: ref.current, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, prepareEditableTree: __unstableAddInvisibleFormats, __unstableDomOnly: domOnly, placeholder, @@ -80,9 +73,6 @@ export function useRichText( { _value.current = value; record.current = create( { html: value, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, preserveWhiteSpace, } ); if ( disableFormats ) { @@ -149,7 +139,6 @@ export function useRichText( { formats: __unstableBeforeSerialize( newRecord ), } : newRecord, - multilineTag, preserveWhiteSpace, } ); } @@ -179,7 +168,6 @@ export function useRichText( { formats: __unstableBeforeSerialize( newRecord ), } : newRecord, - multilineTag, preserveWhiteSpace, } ); @@ -227,13 +215,12 @@ export function useRichText( { ref, useDefaultStyle(), useBoundaryStyle( { record } ), - useCopyHandler( { record, multilineTag, preserveWhiteSpace } ), + useCopyHandler( { record, preserveWhiteSpace } ), useSelectObject(), useFormatBoundaries( { record, applyRecord } ), useDelete( { createRecord, handleChange, - multilineTag, } ), useInputAndSelection( { record, diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 14fd806d95b3a6..e4beb09acaa443 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -40,10 +40,9 @@ import { getActiveFormat } from '../get-active-format'; import { getActiveFormats } from '../get-active-formats'; import { insert } from '../insert'; import { getTextContent } from '../get-text-content'; -import { isEmpty, isEmptyLine } from '../is-empty'; +import { isEmpty } from '../is-empty'; import { create } from '../create'; import { toHTMLString } from '../to-html-string'; -import { removeLineSeparator } from '../remove-line-separator'; import { isCollapsed } from '../is-collapsed'; import { remove } from '../remove'; import { getFormatColors } from '../get-format-colors'; @@ -436,7 +435,7 @@ export class RichText extends Component { } const isReverse = keyCode === BACKSPACE; - const { onDelete, __unstableMultilineTag: multilineTag } = this.props; + const { onDelete } = this.props; this.lastEventCount = event.nativeEvent.eventCount; this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; @@ -452,24 +451,6 @@ export class RichText extends Component { return; } - if ( multilineTag ) { - if ( - isReverse && - value.start === 0 && - value.end === 0 && - isEmptyLine( value ) - ) { - newValue = removeLineSeparator( value, ! isReverse ); - } else { - newValue = removeLineSeparator( value, isReverse ); - } - if ( newValue ) { - this.onFormatChange( newValue ); - event.preventDefault(); - return; - } - } - // Only process delete if the key press occurs at an uncollapsed edge. if ( ! onDelete || diff --git a/packages/rich-text/src/component/use-copy-handler.js b/packages/rich-text/src/component/use-copy-handler.js index b25e64428a1b4d..c62d83351971c3 100644 --- a/packages/rich-text/src/component/use-copy-handler.js +++ b/packages/rich-text/src/component/use-copy-handler.js @@ -17,8 +17,7 @@ export function useCopyHandler( props ) { propsRef.current = props; return useRefEffect( ( element ) => { function onCopy( event ) { - const { record, multilineTag, preserveWhiteSpace } = - propsRef.current; + const { record, preserveWhiteSpace } = propsRef.current; const { ownerDocument } = element; if ( isCollapsed( record.current ) || @@ -33,7 +32,6 @@ export function useCopyHandler( props ) { let html = toHTMLString( { value: selectedRecord, - multilineTag, preserveWhiteSpace, } ); diff --git a/packages/rich-text/src/component/use-delete.js b/packages/rich-text/src/component/use-delete.js index 3694db42c41b39..69ccd0ee0dc0b6 100644 --- a/packages/rich-text/src/component/use-delete.js +++ b/packages/rich-text/src/component/use-delete.js @@ -9,8 +9,6 @@ import { BACKSPACE, DELETE } from '@wordpress/keycodes'; * Internal dependencies */ import { remove } from '../remove'; -import { removeLineSeparator } from '../remove-line-separator'; -import { isEmptyLine } from '../is-empty'; export function useDelete( props ) { const propsRef = useRef( props ); @@ -18,8 +16,7 @@ export function useDelete( props ) { return useRefEffect( ( element ) => { function onKeyDown( event ) { const { keyCode } = event; - const { createRecord, handleChange, multilineTag } = - propsRef.current; + const { createRecord, handleChange } = propsRef.current; if ( event.defaultPrevented ) { return; @@ -31,34 +28,11 @@ export function useDelete( props ) { const currentValue = createRecord(); const { start, end, text } = currentValue; - const isReverse = keyCode === BACKSPACE; // Always handle full content deletion ourselves. if ( start === 0 && end !== 0 && end === text.length ) { handleChange( remove( currentValue ) ); event.preventDefault(); - return; - } - - if ( multilineTag ) { - let newValue; - - // Check to see if we should remove the first item if empty. - if ( - isReverse && - currentValue.start === 0 && - currentValue.end === 0 && - isEmptyLine( currentValue ) - ) { - newValue = removeLineSeparator( currentValue, ! isReverse ); - } else { - newValue = removeLineSeparator( currentValue, isReverse ); - } - - if ( newValue ) { - handleChange( newValue ); - event.preventDefault(); - } } } diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index fa2befc603b7e4..793bdca77f71d5 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -9,11 +9,7 @@ import { select } from '@wordpress/data'; import { store as richTextStore } from './store'; import { createElement } from './create-element'; import { mergePair } from './concat'; -import { - LINE_SEPARATOR, - OBJECT_REPLACEMENT_CHARACTER, - ZWNBSP, -} from './special-characters'; +import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters'; /** @typedef {import('./types').RichTextValue} RichTextValue */ @@ -111,10 +107,8 @@ function toFormat( { tagName, attributes } ) { /** * 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 - * called without any input, an empty value will be created. If - * `multilineTag` is provided, any content of direct children whose type matches - * `multilineTag` will be separated by two newlines. The optional functions can - * be used to filter out content. + * called without any input, an empty value will be created. The optional + * functions can be used to filter out content. * * A value will have the following shape, which you are strongly encouraged not * to modify without the use of helper functions: @@ -141,12 +135,8 @@ function toFormat( { tagName, attributes } ) { * @param {string} [$1.text] Text to create value from. * @param {string} [$1.html] HTML to create value from. * @param {Range} [$1.range] Range to create value from. - * @param {string} [$1.multilineTag] Multiline tag if the structure is - * multiline. - * @param {Array} [$1.multilineWrapperTags] Tags where lines can be found if - * nesting is possible. - * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse white - * space characters. + * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse + * white space characters. * @param {boolean} [$1.__unstableIsEditableTree] * * @return {RichTextValue} A rich text value. @@ -156,8 +146,6 @@ export function create( { text, html, range, - multilineTag, - multilineWrapperTags, __unstableIsEditableTree: isEditableTree, preserveWhiteSpace, } = {} ) { @@ -179,20 +167,9 @@ export function create( { return createEmptyValue(); } - if ( ! multilineTag ) { - return createFromElement( { - element, - range, - isEditableTree, - preserveWhiteSpace, - } ); - } - - return createFromMultilineElement( { + return createFromElement( { element, range, - multilineTag, - multilineWrapperTags, isEditableTree, preserveWhiteSpace, } ); @@ -317,16 +294,11 @@ export function removeReservedCharacters( string ) { /** * Creates a Rich Text value from a DOM element and range. * - * @param {Object} $1 Named argements. - * @param {Element} [$1.element] Element to create value from. - * @param {Range} [$1.range] Range to create value from. - * @param {string} [$1.multilineTag] Multiline tag if the structure is - * multiline. - * @param {Array} [$1.multilineWrapperTags] Tags where lines can be found if - * nesting is possible. - * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse white - * space characters. - * @param {Array} [$1.currentWrapperTags] + * @param {Object} $1 Named argements. + * @param {Element} [$1.element] Element to create value from. + * @param {Range} [$1.range] Range to create value from. + * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse white + * space characters. * @param {boolean} [$1.isEditableTree] * * @return {RichTextValue} A rich text value. @@ -334,9 +306,6 @@ export function removeReservedCharacters( string ) { function createFromElement( { element, range, - multilineTag, - multilineWrapperTags, - currentWrapperTags = [], isEditableTree, preserveWhiteSpace, } ) { @@ -444,30 +413,9 @@ function createFromElement( { if ( format ) delete format.formatType; - if ( - multilineWrapperTags && - multilineWrapperTags.indexOf( tagName ) !== -1 - ) { - const value = createFromMultilineElement( { - element: node, - range, - multilineTag, - multilineWrapperTags, - currentWrapperTags: [ ...currentWrapperTags, format ], - isEditableTree, - preserveWhiteSpace, - } ); - - accumulateSelection( accumulator, node, range, value ); - mergePair( accumulator, value ); - continue; - } - const value = createFromElement( { element: node, range, - multilineTag, - multilineWrapperTags, isEditableTree, preserveWhiteSpace, } ); @@ -516,79 +464,6 @@ function createFromElement( { return accumulator; } -/** - * Creates a rich text value from a DOM element and range that should be - * multiline. - * - * @param {Object} $1 Named argements. - * @param {Element} [$1.element] Element to create value from. - * @param {Range} [$1.range] Range to create value from. - * @param {string} [$1.multilineTag] Multiline tag if the structure is - * multiline. - * @param {Array} [$1.multilineWrapperTags] Tags where lines can be found if - * nesting is possible. - * @param {Array} [$1.currentWrapperTags] Whether to prepend a line - * separator. - * @param {boolean} [$1.preserveWhiteSpace] Whether or not to collapse white - * space characters. - * @param {boolean} [$1.isEditableTree] - * - * @return {RichTextValue} A rich text value. - */ -function createFromMultilineElement( { - element, - range, - multilineTag, - multilineWrapperTags, - currentWrapperTags = [], - isEditableTree, - preserveWhiteSpace, -} ) { - const accumulator = createEmptyValue(); - - if ( ! element || ! element.hasChildNodes() ) { - return accumulator; - } - - const length = element.children.length; - - // Optimise for speed. - for ( let index = 0; index < length; index++ ) { - const node = element.children[ index ]; - - if ( node.nodeName.toLowerCase() !== multilineTag ) { - continue; - } - - const value = createFromElement( { - element: node, - range, - multilineTag, - multilineWrapperTags, - currentWrapperTags, - isEditableTree, - preserveWhiteSpace, - } ); - - // Multiline value text should be separated by a line separator. - if ( index !== 0 || currentWrapperTags.length > 0 ) { - mergePair( accumulator, { - formats: [ , ], - replacements: - currentWrapperTags.length > 0 - ? [ currentWrapperTags ] - : [ , ], - text: LINE_SEPARATOR, - } ); - } - - accumulateSelection( accumulator, node, range, value ); - mergePair( accumulator, value ); - } - - return accumulator; -} - /** * Gets the attributes of an element in object shape. * diff --git a/packages/rich-text/src/get-text-content.js b/packages/rich-text/src/get-text-content.js index f400e4b56497f6..71fc9e7a749f0b 100644 --- a/packages/rich-text/src/get-text-content.js +++ b/packages/rich-text/src/get-text-content.js @@ -1,18 +1,10 @@ /** * Internal dependencies */ -import { - OBJECT_REPLACEMENT_CHARACTER, - LINE_SEPARATOR, -} from './special-characters'; +import { OBJECT_REPLACEMENT_CHARACTER } from './special-characters'; /** @typedef {import('./types').RichTextValue} RichTextValue */ -const pattern = new RegExp( - `[${ OBJECT_REPLACEMENT_CHARACTER }${ LINE_SEPARATOR }]`, - 'g' -); - /** * Get the textual content of a Rich Text value. This is similar to * `Element.textContent`. @@ -22,7 +14,5 @@ const pattern = new RegExp( * @return {string} The text content. */ export function getTextContent( { text } ) { - return text.replace( pattern, ( c ) => - c === OBJECT_REPLACEMENT_CHARACTER ? '' : '\n' - ); + return text.replace( OBJECT_REPLACEMENT_CHARACTER, '' ); } diff --git a/packages/rich-text/src/index.ts b/packages/rich-text/src/index.ts index 1f59320df3a63c..f487845a4cd76f 100644 --- a/packages/rich-text/src/index.ts +++ b/packages/rich-text/src/index.ts @@ -7,21 +7,19 @@ export { getActiveFormats } from './get-active-formats'; export { getActiveObject } from './get-active-object'; export { getTextContent } from './get-text-content'; export { isCollapsed } from './is-collapsed'; -export { isEmpty, isEmptyLine as __unstableIsEmptyLine } from './is-empty'; +export { isEmpty } from './is-empty'; export { join } from './join'; export { registerFormatType } from './register-format-type'; export { removeFormat } from './remove-format'; export { remove } from './remove'; export { replace } from './replace'; export { insert } from './insert'; -export { insertLineSeparator as __unstableInsertLineSeparator } from './insert-line-separator'; export { insertObject } from './insert-object'; export { slice } from './slice'; export { split } from './split'; export { toDom as __unstableToDom } from './to-dom'; export { toHTMLString } from './to-html-string'; export { toggleFormat } from './toggle-format'; -export { LINE_SEPARATOR as __UNSTABLE_LINE_SEPARATOR } from './special-characters'; export { unregisterFormatType } from './unregister-format-type'; export { createElement as __unstableCreateElement } from './create-element'; diff --git a/packages/rich-text/src/insert-line-separator.js b/packages/rich-text/src/insert-line-separator.js deleted file mode 100644 index d7a5aa0f97593f..00000000000000 --- a/packages/rich-text/src/insert-line-separator.js +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Internal dependencies - */ - -import { insert } from './insert'; -import { LINE_SEPARATOR } from './special-characters'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Insert a line break character into a Rich Text value at the given - * `startIndex`. Any content between `startIndex` and `endIndex` will be - * removed. Indices are retrieved from the selection if none are provided. - * - * @param {RichTextValue} value Value to modify. - * @param {number} [startIndex] Start index. - * @param {number} [endIndex] End index. - * - * @return {RichTextValue} A new value with the value inserted. - */ -export function insertLineSeparator( - value, - startIndex = value.start, - endIndex = value.end -) { - const beforeText = value.text.slice( 0, startIndex ); - const previousLineSeparatorIndex = beforeText.lastIndexOf( LINE_SEPARATOR ); - const previousLineSeparatorFormats = - value.replacements[ previousLineSeparatorIndex ]; - let replacements = [ , ]; - - if ( previousLineSeparatorFormats ) { - replacements = [ previousLineSeparatorFormats ]; - } - - const valueToInsert = { - formats: [ , ], - replacements, - text: LINE_SEPARATOR, - }; - - return insert( value, valueToInsert, startIndex, endIndex ); -} diff --git a/packages/rich-text/src/is-empty.js b/packages/rich-text/src/is-empty.js index 7baf296bd2a3d6..86bc64bb36b565 100644 --- a/packages/rich-text/src/is-empty.js +++ b/packages/rich-text/src/is-empty.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import { LINE_SEPARATOR } from './special-characters'; - /** @typedef {import('./types').RichTextValue} RichTextValue */ /** @@ -16,34 +11,3 @@ import { LINE_SEPARATOR } from './special-characters'; export function isEmpty( { text } ) { return text.length === 0; } - -/** - * Check if the current collapsed selection is on an empty line in case of a - * multiline value. - * - * @param {RichTextValue} value Value te check. - * - * @return {boolean} True if the line is empty, false if not. - */ -export function isEmptyLine( { text, start, end } ) { - if ( start !== end ) { - return false; - } - - if ( text.length === 0 ) { - return true; - } - - if ( start === 0 && text.slice( 0, 1 ) === LINE_SEPARATOR ) { - return true; - } - - if ( start === text.length && text.slice( -1 ) === LINE_SEPARATOR ) { - return true; - } - - return ( - text.slice( start - 1, end + 1 ) === - `${ LINE_SEPARATOR }${ LINE_SEPARATOR }` - ); -} diff --git a/packages/rich-text/src/remove-line-separator.js b/packages/rich-text/src/remove-line-separator.js deleted file mode 100644 index fa45616a45a72a..00000000000000 --- a/packages/rich-text/src/remove-line-separator.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Internal dependencies - */ - -import { LINE_SEPARATOR } from './special-characters'; -import { isCollapsed } from './is-collapsed'; -import { remove } from './remove'; - -/** @typedef {import('./types').RichTextValue} RichTextValue */ - -/** - * Removes a line separator character, if existing, from a Rich Text value at - * the current indices. If no line separator exists on the indices it will - * return undefined. - * - * @param {RichTextValue} value Value to modify. - * @param {boolean} backward Indicates if are removing from the start - * index or the end index. - * - * @return {RichTextValue|undefined} A new value with the line separator - * removed. Or undefined if no line separator - * is found on the position. - */ -export function removeLineSeparator( value, backward = true ) { - const { replacements, text, start, end } = value; - const collapsed = isCollapsed( value ); - let index = start - 1; - let removeStart = collapsed ? start - 1 : start; - let removeEnd = end; - if ( ! backward ) { - index = end; - removeStart = start; - removeEnd = collapsed ? end + 1 : end; - } - - if ( text[ index ] !== LINE_SEPARATOR ) { - return; - } - - let newValue; - // If the line separator that is about te be removed - // contains wrappers, remove the wrappers first. - if ( collapsed && replacements[ index ] && replacements[ index ].length ) { - const newReplacements = replacements.slice(); - - newReplacements[ index ] = replacements[ index ].slice( 0, -1 ); - newValue = { - ...value, - replacements: newReplacements, - }; - } else { - newValue = remove( value, removeStart, removeEnd ); - } - return newValue; -} diff --git a/packages/rich-text/src/special-characters.js b/packages/rich-text/src/special-characters.js index 078525ec1777e6..a05f614daf94b7 100644 --- a/packages/rich-text/src/special-characters.js +++ b/packages/rich-text/src/special-characters.js @@ -1,8 +1,3 @@ -/** - * Line separator character, used for multiline text. - */ -export const LINE_SEPARATOR = '\u2028'; - /** * Object replacement character, used as a placeholder for objects. */ diff --git a/packages/rich-text/src/split.js b/packages/rich-text/src/split.js index cf329d7ef0985e..1853fb7bce585d 100644 --- a/packages/rich-text/src/split.js +++ b/packages/rich-text/src/split.js @@ -2,8 +2,6 @@ * Internal dependencies */ -import { replace } from './replace'; - /** @typedef {import('./types').RichTextValue} RichTextValue */ /** @@ -76,9 +74,5 @@ function splitAtSelection( end: 0, }; - return [ - // Ensure newlines are trimmed. - replace( before, /\u2028+$/, '' ), - replace( after, /^\u2028+/, '' ), - ]; + return [ before, after ]; } diff --git a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap index a0eb7b3cb0a3ce..5e2fbcac00de67 100644 --- a/packages/rich-text/src/test/__snapshots__/to-dom.js.snap +++ b/packages/rich-text/src/test/__snapshots__/to-dom.js.snap @@ -201,114 +201,6 @@ exports[`recordToDom should handle double br 1`] = ` `; -exports[`recordToDom should handle empty list value 1`] = ` - -

  • - -  -
  • - -`; - -exports[`recordToDom should handle empty multiline value 1`] = ` - -

    - -  -

    - -`; - -exports[`recordToDom should handle middle empty list value 1`] = ` - -
  • - -  -
  • -
  • - -  -
  • -
  • - -  -
  • - -`; - -exports[`recordToDom should handle multiline list value 1`] = ` - -
  • - one -
      -
    • - a -
    • -
    • - b -
        -
      1. - 1 -
      2. -
      3. - 2 -
      4. -
      -
    • -
    -
  • -
  • - three -
  • - -`; - -exports[`recordToDom should handle multiline value 1`] = ` - -

    - one -

    -

    - two -

    - -`; - -exports[`recordToDom should handle multiline value with element selection 1`] = ` - -
  • - one -
  • - -`; - -exports[`recordToDom should handle multiline value with empty 1`] = ` - -

    - one -

    -

    - -  -

    - -`; - -exports[`recordToDom should handle nested empty list value 1`] = ` - -
  • - -  -
      -
    • - -  -
    • -
    -
  • - -`; - exports[`recordToDom should handle selection before br 1`] = ` a @@ -323,22 +215,6 @@ exports[`recordToDom should handle selection before br 1`] = ` `; -exports[`recordToDom should ignore formats at line separator 1`] = ` - -

    - - one - -

    -

    - - two - - -

    - -`; - exports[`recordToDom should ignore manually added object replacement character 1`] = ` test diff --git a/packages/rich-text/src/test/create.js b/packages/rich-text/src/test/create.js index 3f79f4f0fb78ef..212496c8cf6b8c 100644 --- a/packages/rich-text/src/test/create.js +++ b/packages/rich-text/src/test/create.js @@ -17,39 +17,28 @@ describe( 'create', () => { require( '../store' ); } ); - spec.forEach( - ( { - description, - multilineTag, - multilineWrapperTags, - html, - createRange, - record, - } ) => { - if ( html === undefined ) { - return; - } + spec.forEach( ( { description, html, createRange, record } ) => { + if ( html === undefined ) { + return; + } - // eslint-disable-next-line jest/valid-title - it( description, () => { - const element = createElement( document, html ); - const range = createRange( element ); - const createdRecord = create( { - element, - range, - multilineTag, - multilineWrapperTags, - } ); - const formatsLength = getSparseArrayLength( record.formats ); - const createdFormatsLength = getSparseArrayLength( - createdRecord.formats - ); - - expect( createdRecord ).toEqual( record ); - expect( createdFormatsLength ).toEqual( formatsLength ); + // eslint-disable-next-line jest/valid-title + it( description, () => { + const element = createElement( document, html ); + const range = createRange( element ); + const createdRecord = create( { + element, + range, } ); - } - ); + const formatsLength = getSparseArrayLength( record.formats ); + const createdFormatsLength = getSparseArrayLength( + createdRecord.formats + ); + + expect( createdRecord ).toEqual( record ); + expect( createdFormatsLength ).toEqual( formatsLength ); + } ); + } ); specWithRegistration.forEach( ( { diff --git a/packages/rich-text/src/test/helpers/index.js b/packages/rich-text/src/test/helpers/index.js index ae7521e55e25bf..cff9daa3e24ece 100644 --- a/packages/rich-text/src/test/helpers/index.js +++ b/packages/rich-text/src/test/helpers/index.js @@ -11,8 +11,6 @@ const em = { type: 'em' }; const strong = { type: 'strong' }; const img = { type: 'img', attributes: { src: '' } }; const a = { type: 'a', attributes: { href: '#' } }; -const ul = { type: 'ul' }; -const ol = { type: 'ol' }; export const spec = [ { @@ -440,200 +438,6 @@ export const spec = [ end: 2, }, }, - { - description: 'should handle empty multiline value', - multilineTag: 'p', - html: '

    ', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.firstChild, - endOffset: 0, - endContainer: element.firstChild, - } ), - startPath: [ 0, 0, 0 ], - endPath: [ 0, 0, 0 ], - record: { - start: 0, - end: 0, - formats: [], - replacements: [], - text: '', - }, - }, - { - description: 'should handle multiline value', - multilineTag: 'p', - html: '

    one

    two

    ', - createRange: ( element ) => ( { - startOffset: 1, - startContainer: element.querySelector( 'p' ).firstChild, - endOffset: 0, - endContainer: element.lastChild, - } ), - startPath: [ 0, 0, 1 ], - endPath: [ 1, 0, 0 ], - record: { - start: 1, - end: 4, - formats: [ , , , , , , , ], - replacements: [ , , , , , , , ], - text: 'one\u2028two', - }, - }, - { - description: 'should handle multiline list value', - multilineTag: 'li', - multilineWrapperTags: [ 'ul', 'ol' ], - html: '
  • one
    • a
    • b
      1. 1
      2. 2
  • three
  • ', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element, - endOffset: 1, - endContainer: element.querySelector( 'ol > li' ).firstChild, - } ), - startPath: [ 0, 0, 0 ], - endPath: [ 0, 1, 1, 1, 0, 0, 1 ], - record: { - start: 0, - end: 9, - formats: [ , , , , , , , , , , , , , , , , , ], - replacements: [ - , - , - , - [ ul ], - , - [ ul ], - , - [ ul, ol ], - , - [ ul, ol ], - , - , - , - , - , - , - , - ], - text: 'one\u2028a\u2028b\u20281\u20282\u2028three', - }, - }, - { - description: 'should handle empty list value', - multilineTag: 'li', - multilineWrapperTags: [ 'ul', 'ol' ], - html: '
  • ', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.firstChild, - endOffset: 0, - endContainer: element.firstChild, - } ), - startPath: [ 0, 0, 0 ], - endPath: [ 0, 0, 0 ], - record: { - start: 0, - end: 0, - formats: [], - replacements: [], - text: '', - }, - }, - { - description: 'should handle nested empty list value', - multilineTag: 'li', - multilineWrapperTags: [ 'ul', 'ol' ], - html: '
  • ', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.querySelector( 'ul > li' ), - endOffset: 0, - endContainer: element.querySelector( 'ul > li' ), - } ), - startPath: [ 0, 2, 0, 0, 0 ], - endPath: [ 0, 2, 0, 0, 0 ], - record: { - start: 1, - end: 1, - formats: [ , ], - replacements: [ [ ul ] ], - text: '\u2028', - }, - }, - { - description: 'should handle middle empty list value', - multilineTag: 'li', - multilineWrapperTags: [ 'ul', 'ol' ], - html: '
  • ', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.firstChild.nextSibling, - endOffset: 0, - endContainer: element.firstChild.nextSibling, - } ), - startPath: [ 1, 1, 1 ], - endPath: [ 1, 1, 1 ], - record: { - start: 1, - end: 1, - formats: [ , , ], - replacements: [ , , ], - text: '\u2028\u2028', - }, - }, - { - description: 'should handle multiline value with empty', - multilineTag: 'p', - html: '

    one

    ', - createRange: ( element ) => ( { - startOffset: 0, - startContainer: element.lastChild, - endOffset: 0, - endContainer: element.lastChild, - } ), - startPath: [ 1, 0, 0 ], - endPath: [ 1, 0, 0 ], - record: { - start: 4, - end: 4, - formats: [ , , , , ], - replacements: [ , , , , ], - text: 'one\u2028', - }, - }, - { - description: 'should handle multiline value with element selection', - multilineTag: 'li', - multilineWrapperTags: [ 'ul', 'ol' ], - html: '
  • one
  • ', - createRange: ( element ) => ( { - startOffset: 1, - startContainer: element.firstChild, - endOffset: 1, - endContainer: element.firstChild, - } ), - startPath: [ 0, 0, 3 ], - endPath: [ 0, 0, 3 ], - record: { - start: 3, - end: 3, - formats: [ , , , ], - replacements: [ , , , ], - text: 'one', - }, - }, - { - description: 'should ignore formats at line separator', - multilineTag: 'p', - startPath: [], - endPath: [], - record: { - formats: [ [ em ], [ em ], [ em ], [ em ], [ em ], [ em ], [ em ] ], - replacements: [ , , , , , , , ], - text: 'one\u2028two', - }, - }, { description: 'should remove padding', html: ZWNBSP, diff --git a/packages/rich-text/src/test/insert-line-separator.js b/packages/rich-text/src/test/insert-line-separator.js deleted file mode 100644 index 497555cc4a01a0..00000000000000 --- a/packages/rich-text/src/test/insert-line-separator.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * External dependencies - */ -import deepFreeze from 'deep-freeze'; - -/** - * Internal dependencies - */ - -import { insertLineSeparator } from '../insert-line-separator'; -import { LINE_SEPARATOR } from '../special-characters'; -import { getSparseArrayLength } from './helpers'; - -describe( 'insertLineSeparator', () => { - const ol = { type: 'ol' }; - - it( 'should insert line separator at end', () => { - const value = { - formats: [ , ], - replacements: [ , ], - text: '1', - start: 1, - end: 1, - }; - const expected = { - formats: [ , , ], - replacements: [ , , ], - text: `1${ LINE_SEPARATOR }`, - start: 2, - end: 2, - }; - const result = insertLineSeparator( deepFreeze( value ) ); - - expect( result ).not.toBe( value ); - expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); - } ); - - it( 'should insert line separator at start', () => { - const value = { - formats: [ , ], - replacements: [ , ], - text: '1', - start: 0, - end: 0, - }; - const expected = { - formats: [ , , ], - replacements: [ , , ], - text: `${ LINE_SEPARATOR }1`, - start: 1, - end: 1, - }; - const result = insertLineSeparator( deepFreeze( value ) ); - - expect( result ).not.toBe( value ); - expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); - } ); - - it( 'should insert line separator with previous line separator formats', () => { - const value = { - formats: [ , , , , , ], - replacements: [ , , , [ ol ], , ], - text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, - start: 5, - end: 5, - }; - const expected = { - formats: [ , , , , , , ], - replacements: [ , , , [ ol ], , [ ol ] ], - text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, - start: 6, - end: 6, - }; - const result = insertLineSeparator( deepFreeze( value ) ); - - expect( result ).not.toBe( value ); - expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.replacements ) ).toBe( 2 ); - } ); - - it( 'should insert line separator without formats if previous line separator did not have any', () => { - const value = { - formats: [ , , , , , ], - replacements: [ , , , , , ], - text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a`, - start: 5, - end: 5, - }; - const expected = { - formats: [ , , , , , , ], - replacements: [ , , , , , , ], - text: `1${ LINE_SEPARATOR }2${ LINE_SEPARATOR }a${ LINE_SEPARATOR }`, - start: 6, - end: 6, - }; - const result = insertLineSeparator( deepFreeze( value ) ); - - expect( result ).not.toBe( value ); - expect( result ).toEqual( expected ); - expect( getSparseArrayLength( result.replacements ) ).toBe( 0 ); - } ); -} ); diff --git a/packages/rich-text/src/test/is-empty.js b/packages/rich-text/src/test/is-empty.js index 0080ba3a0914fb..6cd1d0b6d08275 100644 --- a/packages/rich-text/src/test/is-empty.js +++ b/packages/rich-text/src/test/is-empty.js @@ -2,7 +2,7 @@ * Internal dependencies */ -import { isEmpty, isEmptyLine } from '../is-empty'; +import { isEmpty } from '../is-empty'; describe( 'isEmpty', () => { it( 'should return true', () => { @@ -23,62 +23,3 @@ describe( 'isEmpty', () => { expect( isEmpty( one ) ).toBe( false ); } ); } ); - -describe( 'isEmptyLine', () => { - it( 'should return true', () => { - const one = { - formats: [], - text: '', - start: 0, - end: 0, - }; - const two = { - formats: [ , , ], - text: '\u2028', - start: 0, - end: 0, - }; - const three = { - formats: [ , , ], - text: '\u2028', - start: 1, - end: 1, - }; - const four = { - formats: [ , , , , ], - text: '\u2028\u2028', - start: 1, - end: 1, - }; - const five = { - formats: [ , , , , ], - text: 'a\u2028\u2028b', - start: 2, - end: 2, - }; - - expect( isEmptyLine( one ) ).toBe( true ); - expect( isEmptyLine( two ) ).toBe( true ); - expect( isEmptyLine( three ) ).toBe( true ); - expect( isEmptyLine( four ) ).toBe( true ); - expect( isEmptyLine( five ) ).toBe( true ); - } ); - - it( 'should return false', () => { - const one = { - formats: [ , , , , ], - text: '\u2028a\u2028', - start: 1, - end: 1, - }; - const two = { - formats: [ , , , , ], - text: '\u2028\n', - start: 1, - end: 1, - }; - - expect( isEmptyLine( one ) ).toBe( false ); - expect( isEmptyLine( two ) ).toBe( false ); - } ); -} ); diff --git a/packages/rich-text/src/test/split.js b/packages/rich-text/src/test/split.js index 7de61c1b3efeb8..05a76889ee6864 100644 --- a/packages/rich-text/src/test/split.js +++ b/packages/rich-text/src/test/split.js @@ -112,39 +112,6 @@ describe( 'split', () => { } ); } ); - it( 'should split multiline', () => { - const record = { - formats: [ , , , , , , , , , , ], - replacements: [ , , , , , , , , , , ], - text: 'test\u2028\u2028test', - start: 5, - end: 5, - }; - const expected = [ - { - formats: [ , , , , ], - replacements: [ , , , , ], - text: 'test', - }, - { - formats: [ , , , , ], - replacements: [ , , , , ], - text: 'test', - start: 0, - end: 0, - }, - ]; - const result = split( deepFreeze( record ) ); - - expect( result ).toEqual( expected ); - result.forEach( ( item, index ) => { - expect( item ).not.toBe( record ); - expect( getSparseArrayLength( item.formats ) ).toBe( - getSparseArrayLength( expected[ index ].formats ) - ); - } ); - } ); - it( 'should split search', () => { const record = { start: 6, diff --git a/packages/rich-text/src/test/to-dom.js b/packages/rich-text/src/test/to-dom.js index 1c6aa50623e951..4f9b2df86cad35 100644 --- a/packages/rich-text/src/test/to-dom.js +++ b/packages/rich-text/src/test/to-dom.js @@ -11,19 +11,16 @@ describe( 'recordToDom', () => { require( '../store' ); } ); - spec.forEach( - ( { description, multilineTag, record, startPath, endPath } ) => { - // eslint-disable-next-line jest/valid-title - it( description, () => { - const { body, selection } = toDom( { - value: record, - multilineTag, - } ); - expect( body ).toMatchSnapshot(); - expect( selection ).toEqual( { startPath, endPath } ); + spec.forEach( ( { description, record, startPath, endPath } ) => { + // eslint-disable-next-line jest/valid-title + it( description, () => { + const { body, selection } = toDom( { + value: record, } ); - } - ); + expect( body ).toMatchSnapshot(); + expect( selection ).toEqual( { startPath, endPath } ); + } ); + } ); } ); describe( 'applyValue', () => { diff --git a/packages/rich-text/src/test/to-html-string.js b/packages/rich-text/src/test/to-html-string.js index 507359e99ddcf0..32360e6805d69f 100644 --- a/packages/rich-text/src/test/to-html-string.js +++ b/packages/rich-text/src/test/to-html-string.js @@ -97,21 +97,6 @@ describe( 'toHTMLString', () => { ); } ); - it( 'should extract recreate HTML 6', () => { - const HTML = '
  • one
    • two
  • three
  • '; - const element = createNode( `
      ${ HTML }
    ` ); - const multilineTag = 'li'; - const multilineWrapperTags = [ 'ul', 'ol' ]; - const value = create( { element, multilineTag, multilineWrapperTags } ); - const result = toHTMLString( { - value, - multilineTag, - multilineWrapperTags, - } ); - - expect( result ).toEqual( HTML ); - } ); - it( 'should serialize neighbouring formats of same type', () => { const HTML = 'aa'; const element = createNode( `

    ${ HTML }

    ` ); diff --git a/packages/rich-text/src/to-dom.js b/packages/rich-text/src/to-dom.js index 305eebaf3e4a6e..e7288e4ba16332 100644 --- a/packages/rich-text/src/to-dom.js +++ b/packages/rich-text/src/to-dom.js @@ -104,7 +104,6 @@ function remove( node ) { export function toDom( { value, - multilineTag, prepareEditableTree, isEditableTree = true, placeholder, @@ -134,7 +133,6 @@ export function toDom( { const tree = toTree( { value, - multilineTag, createEmpty, append, getLastChild, @@ -165,13 +163,11 @@ export function toDom( { /** * Create an `Element` tree from a Rich Text value and applies the difference to - * the `Element` tree contained by `current`. If a `multilineTag` is provided, - * text separated by two new lines will be wrapped in an `Element` of that type. + * the `Element` tree contained by `current`. * * @param {Object} $1 Named arguments. * @param {RichTextValue} $1.value Value to apply. * @param {HTMLElement} $1.current The live root node to apply the element tree to. - * @param {string} [$1.multilineTag] Multiline tag. * @param {Function} [$1.prepareEditableTree] Function to filter editorable formats. * @param {boolean} [$1.__unstableDomOnly] Only apply elements, no selection. * @param {string} [$1.placeholder] Placeholder text. @@ -179,7 +175,6 @@ export function toDom( { export function apply( { value, current, - multilineTag, prepareEditableTree, __unstableDomOnly, placeholder, @@ -187,7 +182,6 @@ export function apply( { // Construct a new element tree in memory. const { body, selection } = toDom( { value, - multilineTag, prepareEditableTree, placeholder, doc: current.ownerDocument, diff --git a/packages/rich-text/src/to-html-string.js b/packages/rich-text/src/to-html-string.js index 0b2689248afb72..66ae1d82b38450 100644 --- a/packages/rich-text/src/to-html-string.js +++ b/packages/rich-text/src/to-html-string.js @@ -17,21 +17,18 @@ import { toTree } from './to-tree'; /** @typedef {import('./types').RichTextValue} RichTextValue */ /** - * Create an HTML string from a Rich Text value. If a `multilineTag` is - * provided, text separated by a line separator will be wrapped in it. + * Create an HTML string from a Rich Text value. * * @param {Object} $1 Named argements. * @param {RichTextValue} $1.value Rich text value. - * @param {string} [$1.multilineTag] Multiline tag. * @param {boolean} [$1.preserveWhiteSpace] Whether or not to use newline * characters for line breaks. * * @return {string} HTML string. */ -export function toHTMLString( { value, multilineTag, preserveWhiteSpace } ) { +export function toHTMLString( { value, preserveWhiteSpace } ) { const tree = toTree( { value, - multilineTag, preserveWhiteSpace, createEmpty, append, diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index 4db974aaad7142..c380570db561de 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -4,11 +4,7 @@ import { getActiveFormats } from './get-active-formats'; import { getFormatType } from './get-format-type'; -import { - LINE_SEPARATOR, - OBJECT_REPLACEMENT_CHARACTER, - ZWNBSP, -} from './special-characters'; +import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters'; function restoreOnAttributes( attributes, isEditableTree ) { if ( isEditableTree ) { @@ -133,7 +129,6 @@ function isEqualUntil( a, b, index ) { export function toTree( { value, - multilineTag, preserveWhiteSpace, createEmpty, append, @@ -151,21 +146,13 @@ export function toTree( { const { formats, replacements, text, start, end } = value; const formatsLength = formats.length + 1; const tree = createEmpty(); - const multilineFormat = { type: multilineTag }; const activeFormats = getActiveFormats( value ); const deepestActiveFormat = activeFormats[ activeFormats.length - 1 ]; - let lastSeparatorFormats; let lastCharacterFormats; let lastCharacter; - // If we're building a multiline tree, start off with a multiline element. - if ( multilineTag ) { - append( append( tree, { type: multilineTag } ), '' ); - lastCharacterFormats = lastSeparatorFormats = [ multilineFormat ]; - } else { - append( tree, '' ); - } + append( tree, '' ); for ( let i = 0; i < formatsLength; i++ ) { const character = text.charAt( i ); @@ -173,62 +160,13 @@ export function toTree( { isEditableTree && // Pad the line if the line is empty. ( ! lastCharacter || - lastCharacter === LINE_SEPARATOR || // Pad the line if the previous character is a line break, otherwise // the line break won't be visible. lastCharacter === '\n' ); - let characterFormats = formats[ i ]; - - // Set multiline tags in queue for building the tree. - if ( multilineTag ) { - if ( character === LINE_SEPARATOR ) { - characterFormats = lastSeparatorFormats = ( - replacements[ i ] || [] - ).reduce( - ( accumulator, format ) => { - accumulator.push( format, multilineFormat ); - return accumulator; - }, - [ multilineFormat ] - ); - } else { - characterFormats = [ - ...lastSeparatorFormats, - ...( characterFormats || [] ), - ]; - } - } - + const characterFormats = formats[ i ]; let pointer = getLastChild( tree ); - if ( shouldInsertPadding && character === LINE_SEPARATOR ) { - let node = pointer; - - while ( ! isText( node ) ) { - node = getLastChild( node ); - } - - append( getParent( node ), ZWNBSP ); - } - - // Set selection for the start of line. - if ( lastCharacter === LINE_SEPARATOR ) { - let node = pointer; - - while ( ! isText( node ) ) { - node = getLastChild( node ); - } - - if ( onStartIndex && start === i ) { - onStartIndex( tree, node ); - } - - if ( onEndIndex && end === i ) { - onEndIndex( tree, node ); - } - } - if ( characterFormats ) { characterFormats.forEach( ( format, formatIndex ) => { if ( @@ -239,11 +177,7 @@ export function toTree( { characterFormats, lastCharacterFormats, formatIndex - ) && - // Do not reuse the last element if the character is a - // line separator. - ( character !== LINE_SEPARATOR || - characterFormats.length - 1 !== formatIndex ) + ) ) { pointer = getLastChild( pointer ); return; @@ -253,9 +187,7 @@ export function toTree( { format; const boundaryClass = - isEditableTree && - character !== LINE_SEPARATOR && - format === deepestActiveFormat; + isEditableTree && format === deepestActiveFormat; const parent = getParent( pointer ); const newNode = append( @@ -278,13 +210,6 @@ export function toTree( { } ); } - // No need for further processing if the character is a line separator. - if ( character === LINE_SEPARATOR ) { - lastCharacterFormats = characterFormats; - lastCharacter = character; - continue; - } - // If there is selection at 0, handle it before characters are inserted. if ( i === 0 ) { if ( onStartIndex && start === 0 ) { diff --git a/schemas/json/block.json b/schemas/json/block.json index f20fb5b0dea972..b181bf2d9ab403 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -173,10 +173,6 @@ "type": "string", "description": "Use an attribute source to extract the value from an attribute in the markup. The attribute is specified by the attribute field, which must be supplied.\n\nExample: Extract the src attribute from an image found in the block’s markup." }, - "multiline": { - "type": "string", - "description": "Use the multiline property to extract the inner HTML of matching tag names for the use in RichText with the multiline prop." - }, "query": { "type": "object", "description": "Use query to extract an array of values from markup. Entries of the array are determined by the selector argument, where each matched element within the block will have an entry structured corresponding to the second argument, an object of attribute sources." diff --git a/test/e2e/specs/editor/various/rich-text-deprecated-multiline.spec.js b/test/e2e/specs/editor/various/rich-text-deprecated-multiline.spec.js new file mode 100644 index 00000000000000..9fa3fef903c7fc --- /dev/null +++ b/test/e2e/specs/editor/various/rich-text-deprecated-multiline.spec.js @@ -0,0 +1,126 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'RichText deprecated multiline', () => { + test.beforeEach( async ( { admin, page, editor } ) => { + await admin.createNewPost(); + await page.evaluate( () => { + const registerBlockType = window.wp.blocks.registerBlockType; + const { useBlockProps, RichText } = window.wp.blockEditor; + const el = window.wp.element.createElement; + registerBlockType( 'core/rich-text-deprecated-multiline', { + apiVersion: 3, + title: 'Deprecated RichText multiline', + attributes: { + value: { + type: 'string', + source: 'html', + selector: 'blockquote', + }, + }, + edit: function Edit( { attributes, setAttributes } ) { + return el( RichText, { + ...useBlockProps(), + tagName: 'blockquote', + multiline: 'p', + value: attributes.value, + onChange( value ) { + setAttributes( { value } ); + }, + } ); + }, + save( { attributes } ) { + return el( RichText.Content, { + tagName: 'blockquote', + multiline: 'p', + value: attributes.value, + } ); + }, + } ); + } ); + await editor.insertBlock( { + name: 'core/rich-text-deprecated-multiline', + } ); + await page.keyboard.press( 'ArrowDown' ); + } ); + + test( 'should save', async ( { page, editor } ) => { + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/rich-text-deprecated-multiline', + attributes: { + value: '

    1

    2

    ', + }, + }, + ] ); + + // Test serialised output. + expect( await editor.getEditedPostContent() ).toBe( + ` +

    1

    2

    +` + ); + } ); + + test( 'should split in middle', async ( { page, editor } ) => { + await page.keyboard.type( '12' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Enter' ); + // Test selection after split. + await page.keyboard.type( '‸' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/rich-text-deprecated-multiline', + attributes: { + value: '

    1

    ‸2

    ', + }, + }, + ] ); + } ); + + test( 'should merge two lines', async ( { page, editor } ) => { + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Backspace' ); + // Test selection after merge. + await page.keyboard.type( '‸' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/rich-text-deprecated-multiline', + attributes: { + value: '

    1‸2

    ', + }, + }, + ] ); + } ); + + test( 'should merge two lines (forward)', async ( { page, editor } ) => { + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'Delete' ); + // Test selection after merge. + await page.keyboard.type( '‸' ); + + expect( await editor.getBlocks() ).toMatchObject( [ + { + name: 'core/rich-text-deprecated-multiline', + attributes: { + value: '

    1‸2

    ', + }, + }, + ] ); + } ); +} ); From c10b0f6f7d6a83ade593d5ba4bcfe23a58ff5a1b Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:42:55 +0900 Subject: [PATCH 08/42] Edit Widgets: Fix invisible action area when the top toolbar is enabled (#54329) --- .../src/components/block-tools/block-contextual-toolbar.js | 4 ++-- packages/edit-widgets/src/components/header/style.scss | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index b2087ac2ff5f6d..9ac0bcb8e1beb5 100644 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js @@ -112,9 +112,9 @@ function BlockContextualToolbar( { focusOnMount, isFixed, ...props } ) { return; } - // get the width of the pinned items in the post editor + // get the width of the pinned items in the post editor or widget editor const pinnedItems = document.querySelector( - '.edit-post-header__settings' + '.edit-post-header__settings, .edit-widgets-header__actions' ); // get the width of the left header in the site editor diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index 06ea6bf19d7774..64a9f124bb7502 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -3,7 +3,6 @@ align-items: center; justify-content: space-between; height: $header-height; - padding: 0 $grid-unit-20; overflow: auto; background: #fff; @@ -16,6 +15,7 @@ display: flex; align-items: center; justify-content: center; + padding-left: $grid-unit-20; } .edit-widgets-header__title { @@ -27,7 +27,7 @@ .edit-widgets-header__actions { display: flex; align-items: center; - + padding-right: $grid-unit-20; gap: $grid-unit-05; @include break-small() { From 375c58274a7adb97d2fbd5057a252da67031b22e Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Mon, 11 Sep 2023 14:51:33 +0100 Subject: [PATCH 09/42] Fix: Remove unrequired code from BorderControl documentation. (#54348) --- .../components/src/border-control/border-control/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/src/border-control/border-control/README.md b/packages/components/src/border-control/border-control/README.md index 14b3df6afaf6c9..55c860720f2394 100644 --- a/packages/components/src/border-control/border-control/README.md +++ b/packages/components/src/border-control/border-control/README.md @@ -31,13 +31,12 @@ const colors = [ const MyBorderControl = () => { const [ border, setBorder ] = useState(); - const onChange = ( newBorder ) => setBorder( newBorder ); return ( ); From a9a2557145f440c1abb0f06e0d1d03c5fc0418d6 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:56:56 +0200 Subject: [PATCH 10/42] Add manual SSR to the Interactivity API blocks (#54343) * Add manual SSR to the navigation block * Use a CSS media query to show/hide the PDF embed --------- Co-authored-by: Luis Herranz --- packages/block-library/src/file/index.php | 3 +-- packages/block-library/src/file/style.scss | 7 ++++++- packages/block-library/src/file/view.js | 4 ++-- packages/block-library/src/navigation/index.php | 3 +++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 212cb571e99f7f..4d8e8e13b19717 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -59,8 +59,7 @@ static function ( $matches ) { $processor->next_tag(); $processor->set_attribute( 'data-wp-interactive', '' ); $processor->next_tag( 'object' ); - $processor->set_attribute( 'data-wp-bind--hidden', '!selectors.core.file.hasPdfPreview' ); - $processor->set_attribute( 'hidden', true ); + $processor->set_attribute( 'data-wp-style--display', 'selectors.core.file.hasPdfPreview' ); return $processor->get_updated_html(); } diff --git a/packages/block-library/src/file/style.scss b/packages/block-library/src/file/style.scss index 5c9a2f7be2adc6..11d0a8a46f321d 100644 --- a/packages/block-library/src/file/style.scss +++ b/packages/block-library/src/file/style.scss @@ -29,6 +29,12 @@ margin-bottom: 1em; } +@media (max-width: 768px) { + .wp-block-file__embed { + display: none; + } +} + //This needs a low specificity so it won't override the rules from the button element if defined in theme.json. :where(.wp-block-file__button) { border-radius: 2em; @@ -36,7 +42,6 @@ display: inline-block; &:is(a) { - &:hover, &:visited, &:focus, diff --git a/packages/block-library/src/file/view.js b/packages/block-library/src/file/view.js index 9d09ca2b7f4340..51c726d0cbe5ec 100644 --- a/packages/block-library/src/file/view.js +++ b/packages/block-library/src/file/view.js @@ -5,13 +5,13 @@ import { store } from '@wordpress/interactivity'; /** * Internal dependencies */ -import { browserSupportsPdfs as hasPdfPreview } from './utils'; +import { browserSupportsPdfs } from './utils'; store( { selectors: { core: { file: { - hasPdfPreview, + hasPdfPreview: browserSupportsPdfs() ? 'inherit' : 'none', }, }, }, diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index f90c5f9563166d..6d5bb07335f2af 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -104,6 +104,7 @@ function block_core_navigation_add_directives_to_submenu( $w, $block_attributes ) ) { $w->set_attribute( 'data-wp-on--click', 'actions.core.navigation.toggleMenuOnClick' ); $w->set_attribute( 'data-wp-bind--aria-expanded', 'selectors.core.navigation.isMenuOpen' ); + // The `aria-expanded` attribute for SSR is already added in the submenu block. }; // Add directives to the submenu. if ( $w->next_tag( @@ -713,7 +714,9 @@ function render_block_core_navigation( $attributes, $content, $block ) { '; $responsive_dialog_directives = ' data-wp-bind--aria-modal="selectors.core.navigation.isMenuOpen" + aria-modal="false" data-wp-bind--role="selectors.core.navigation.roleAttribute" + role="" data-wp-effect="effects.core.navigation.focusFirstElement" '; $close_button_directives = ' From 1eb2f1e32231da44f6cc2dcb42e0c3ab03f0db3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:15:45 -0400 Subject: [PATCH 11/42] Bump actions/cache from 3.3.1 to 3.3.2 (#54306) Bumps [actions/cache](https://github.com/actions/cache) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8...704facf57e6136b1bc63b828d79edcd491f0ee84) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/pull-request-automation.yml | 2 +- .github/workflows/rnmobile-android-runner.yml | 2 +- .github/workflows/rnmobile-ios-runner.yml | 4 ++-- .github/workflows/unit-test.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index 6691fec3341c9d..a34df5282d6d8c 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -26,7 +26,7 @@ jobs: node-version: ${{ matrix.node }} - name: Cache NPM packages - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 with: # npm cache files are stored in `~/.npm` on Linux/macOS path: ~/.npm diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 4b5a4393b70c5b..1d4e010558a6e1 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -40,7 +40,7 @@ jobs: uses: gradle/gradle-build-action@ef76a971e2fa3f867b617efd72f2fbd72cf6f8bc # v2.8.0 - name: AVD cache - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 id: avd-cache with: path: | diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 3b82180be5567b..6a03547966fe10 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -34,7 +34,7 @@ jobs: run: find package-lock.json packages/react-native-editor/ios packages/react-native-aztec/ios packages/react-native-bridge/ios -type f -print0 | sort -z | xargs -0 shasum | tee ios-checksums.txt - name: Restore build cache - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 with: path: | packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app @@ -42,7 +42,7 @@ jobs: key: ${{ runner.os }}-ios-build-${{ matrix.xcode }}-${{ matrix.device }}-${{ hashFiles('ios-checksums.txt') }} - name: Restore pods cache - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 with: path: | packages/react-native-editor/ios/Pods diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index f48386cde43729..fd021b1b338154 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -239,7 +239,7 @@ jobs: run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> $GITHUB_OUTPUT - name: Cache PHPCS scan cache - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 with: path: .cache/phpcs.json key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-phpcs-cache-${{ hashFiles('**/composer.json', 'phpcs.xml.dist') }} From 7c73a89241d837198a27278f2a4e62c12220c83b Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 11 Sep 2023 18:17:45 +0300 Subject: [PATCH 12/42] Add aspect ratio to image placeholder (#54216) * adds aspect ratio control and behavior to image block placeholder * Allow the image placeholder to shrink to small sizes and then allow it to scroll * Align the placeholder to the top * remove resolution tool * refactor duplicate code --------- Co-authored-by: scruffian --- packages/block-library/src/image/edit.js | 45 +++++---- packages/block-library/src/image/image.js | 96 ++++++++++--------- packages/components/src/placeholder/index.tsx | 1 + .../components/src/placeholder/style.scss | 6 +- 4 files changed, 83 insertions(+), 65 deletions(-) diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 6c9505d8b2d986..2c1e0ee3a41767 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -111,6 +111,8 @@ export function ImageEdit( { width, height, sizeSlug, + aspectRatio, + scale, } = attributes; const [ temporaryURL, setTemporaryURL ] = useState(); @@ -335,7 +337,16 @@ export function ImageEdit( { instructions={ __( 'Upload an image file, pick one from your media library, or add one with a URL.' ) } - style={ isSelected ? undefined : borderProps.style } + style={ { + aspectRatio: + ! ( width && height ) && aspectRatio + ? aspectRatio + : undefined, + width: height && aspectRatio ? '100%' : width, + height: width && aspectRatio ? '100%' : height, + objectFit: scale, + ...borderProps.style, + } } > { content } @@ -344,23 +355,21 @@ export function ImageEdit( { return (
    - { ( temporaryURL || url ) && ( - - ) } + { ! url && blockEditingMode === 'default' && ( { + // Rebuilding the object forces setting `undefined` + // for values that are removed since setAttributes + // doesn't do anything with keys that aren't set. + setAttributes( { + // CSS includes `height: auto`, but we need + // `width: auto` to fix the aspect ratio when + // only height is set due to the width and + // height attributes set via the server. + width: ! newWidth && newHeight ? 'auto' : newWidth, + height: newHeight, + scale: newScale, + aspectRatio: newAspectRatio, + } ); + } } + defaultScale="cover" + defaultAspectRatio="auto" + scaleOptions={ scaleOptions } + unitsOptions={ dimensionsUnitsOptions } + /> + ); + + const resetAll = () => { + setAttributes( { + width: undefined, + height: undefined, + scale: undefined, + aspectRatio: undefined, + } ); + }; + + const sizeControls = ( + + + { isResizable && dimensionsControl } + + + ); + const controls = ( <> @@ -443,17 +490,7 @@ export default function Image( { ) } - - setAttributes( { - width: undefined, - height: undefined, - scale: undefined, - aspectRatio: undefined, - } ) - } - > + { ! multiImageSelection && ( ) } - { isResizable && ( - { - // Rebuilding the object forces setting `undefined` - // for values that are removed since setAttributes - // doesn't do anything with keys that aren't set. - setAttributes( { - // CSS includes `height: auto`, but we need - // `width: auto` to fix the aspect ratio when - // only height is set due to the width and - // height attributes set via the server. - width: - ! newWidth && newHeight - ? 'auto' - : newWidth, - height: newHeight, - scale: newScale, - aspectRatio: newAspectRatio, - } ); - } } - defaultScale="cover" - defaultAspectRatio="auto" - scaleOptions={ scaleOptions } - unitsOptions={ dimensionsUnitsOptions } - /> - ) } + { isResizable && dimensionsControl } { /* Hide controls during upload to avoid component remount, diff --git a/packages/components/src/placeholder/index.tsx b/packages/components/src/placeholder/index.tsx index bbd17f9972188a..13634f6710d945 100644 --- a/packages/components/src/placeholder/index.tsx +++ b/packages/components/src/placeholder/index.tsx @@ -75,6 +75,7 @@ export function Placeholder( const fieldsetClasses = classnames( 'components-placeholder__fieldset', { 'is-column-layout': isColumnLayout, } ); + return (
    { withIllustration ? PlaceholderIllustration : null } diff --git a/packages/components/src/placeholder/style.scss b/packages/components/src/placeholder/style.scss index df06969852fdae..1cb3edfcfdbba7 100644 --- a/packages/components/src/placeholder/style.scss +++ b/packages/components/src/placeholder/style.scss @@ -3,7 +3,6 @@ box-sizing: border-box; position: relative; padding: 1em; - min-height: 200px; width: 100%; text-align: left; margin: 0; @@ -17,7 +16,7 @@ @supports (position: sticky) { display: flex; flex-direction: column; - justify-content: center; + justify-content: top; align-items: flex-start; } @@ -180,7 +179,6 @@ color: inherit; display: flex; box-shadow: none; - min-width: 100px; // Blur the background so layered dashed placeholders are still visually separate. // Make the background transparent to not interfere with the background overlay in placeholder-style() pseudo element @@ -225,7 +223,7 @@ // By painting the borders here, we enable them to be replaced by the Border control. @include placeholder-style(); - overflow: hidden; + overflow: auto; } // Position the spinner. From f5d2c1553a8cff4e24eadc78bba6e4327f240a32 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:52:39 +0200 Subject: [PATCH 13/42] Search block: switch interactivity to the Interactivity API (#53343) * Fist commit * Come back to default style, as we updated wp-class directive * Remove not needed test * Add interactions * Use `wp_store` to handle ARIA label We now use WP Store to handle ARIA labels for button functionality, doing away with hard-coding labels. The labels now adjust based on the state of the search field, providing more explicit instructions for users. Co-authored-by: David Arenas * Move `FORM` directives directly to the HTML * Move `aria-label` directive inside conditional * Change context variable name * Add search button directives * Add input directives and needed actions * Remove old functions * Load the search directives only if using Gutenberg * Change PHP formatting * Fix PHP coding standard issues * Remove trailing comma * Don't submit form when input is closed * Focus button when closed with ESC key * Add selector in SSR * Format PHP * Fix typo Co-authored-by: Michal * Add comment for `$open_by_default` variable * Remove extra space * Rename search block function * Add `supports.interactivity` * Remove old files and * Add comment to manual SSR for core * Remove wp_store * Use `null` instead of `undefined Co-authored-by: Luis Herranz * Remove unnecessary conditional Co-authored-by: Luis Herranz --------- Co-authored-by: Michal Czaplinski Co-authored-by: David Arenas Co-authored-by: Mario Santos Co-authored-by: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Co-authored-by: Luis Herranz --- docs/reference-guides/core-blocks.md | 2 +- packages/block-library/src/search/block.json | 1 + packages/block-library/src/search/index.php | 30 ++- packages/block-library/src/search/view.js | 239 ++++++------------- 4 files changed, 99 insertions(+), 173 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 0f1e9da1f21799..6e66df88bbac82 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -759,7 +759,7 @@ Help visitors find your content. ([Source](https://github.com/WordPress/gutenber - **Name:** core/search - **Category:** widgets -- **Supports:** align (center, left, right), color (background, gradients, text), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align (center, left, right), color (background, gradients, text), interactivity, typography (fontSize, lineHeight), ~~html~~ - **Attributes:** buttonBehavior, buttonPosition, buttonText, buttonUseIcon, isSearchFieldHidden, label, placeholder, query, showLabel, width, widthUnit ## Separator diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index b2873bfa8e5729..5669a9089d0e03 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -62,6 +62,7 @@ "text": true } }, + "interactivity": true, "typography": { "__experimentalSkipSerialization": true, "__experimentalSelector": ".wp-block-search__label, .wp-block-search__input, .wp-block-search__button", diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 670ceb0eb66c54..da04a0478e3069 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -46,6 +46,9 @@ function render_block_core_search( $attributes, $content, $block ) { 'button-inside' === $attributes['buttonPosition']; // Border color classes need to be applied to the elements that have a border color. $border_color_classes = get_border_color_classes_for_block_core_search( $attributes ); + // This variable is a constant and its value is always false at this moment. + // It is defined this way because some values depend on it, in case it changes in the future. + $open_by_default = 'false'; $label_inner_html = empty( $attributes['label'] ) ? __( 'Search' ) : wp_kses_post( $attributes['label'] ); $label = new WP_HTML_Tag_Processor( sprintf( '', $inline_styles['label'], $label_inner_html ) ); @@ -77,6 +80,9 @@ function render_block_core_search( $attributes, $content, $block ) { $is_expandable_searchfield = 'button-only' === $button_position && 'expand-searchfield' === $button_behavior; if ( $is_expandable_searchfield ) { + $input->set_attribute( 'data-wp-bind--aria-hidden', '!context.core.search.isSearchInputVisible' ); + $input->set_attribute( 'data-wp-bind--tabindex', 'selectors.core.search.tabindex' ); + // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); } @@ -139,11 +145,16 @@ function render_block_core_search( $attributes, $content, $block ) { if ( $button->next_tag() ) { $button->add_class( implode( ' ', $button_classes ) ); if ( 'expand-searchfield' === $attributes['buttonBehavior'] && 'button-only' === $attributes['buttonPosition'] ) { + $button->set_attribute( 'data-wp-bind--aria-label', 'selectors.core.search.ariaLabel' ); + $button->set_attribute( 'data-wp-bind--aria-controls', 'selectors.core.search.ariaControls' ); + $button->set_attribute( 'data-wp-bind--aria-expanded', 'context.core.search.isSearchInputVisible' ); + $button->set_attribute( 'data-wp-bind--type', 'selectors.core.search.type' ); + $button->set_attribute( 'data-wp-on--click', 'actions.core.search.openSearchInput' ); + // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. $button->set_attribute( 'aria-label', __( 'Expand search field' ) ); - $button->set_attribute( 'data-toggled-aria-label', __( 'Submit Search' ) ); $button->set_attribute( 'aria-controls', 'wp-block-search__input-' . $input_id ); $button->set_attribute( 'aria-expanded', 'false' ); - $button->set_attribute( 'type', 'button' ); // Will be set to submit after clicking. + $button->set_attribute( 'type', 'button' ); } else { $button->set_attribute( 'aria-label', wp_strip_all_tags( $attributes['buttonText'] ) ); } @@ -160,11 +171,24 @@ function render_block_core_search( $attributes, $content, $block ) { $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classnames ) ); + $form_directives = ''; + if ( $is_expandable_searchfield ) { + $aria_label_expanded = __( 'Submit Search' ); + $aria_label_collapsed = __( 'Expand search field' ); + $form_directives = ' + data-wp-interactive + data-wp-context=\'{ "core": { "search": { "isSearchInputVisible": ' . $open_by_default . ', "inputId": "' . $input_id . '", "ariaLabelExpanded": "' . $aria_label_expanded . '", "ariaLabelCollapsed": "' . $aria_label_collapsed . '" } } }\' + data-wp-class--wp-block-search__searchfield-hidden="!context.core.search.isSearchInputVisible" + data-wp-on--keydown="actions.core.search.handleSearchKeydown" + data-wp-on--focusout="actions.core.search.handleSearchFocusout" + '; + }; return sprintf( - '
    %s
    ', + '
    %4s
    ', esc_url( home_url( '/' ) ), $wrapper_attributes, + $form_directives, $label . $field_markup ); } diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 5aaf1dd1ef3add..d99dfc5696ccbb 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -1,172 +1,73 @@ -/*eslint-env browser*/ - -/** @type {?HTMLFormElement} */ -let expandedSearchBlock = null; - -const hiddenClass = 'wp-block-search__searchfield-hidden'; - -/** - * Toggles aria-label with data-toggled-aria-label. - * - * @param {HTMLElement} element - */ -function toggleAriaLabel( element ) { - if ( ! ( 'toggledAriaLabel' in element.dataset ) ) { - throw new Error( 'Element lacks toggledAriaLabel in dataset.' ); - } - - const ariaLabel = element.dataset.toggledAriaLabel; - element.dataset.toggledAriaLabel = element.ariaLabel; - element.ariaLabel = ariaLabel; -} - -/** - * Gets search input. - * - * @param {HTMLFormElement} block Search block. - * @return {HTMLInputElement} Search input. - */ -function getSearchInput( block ) { - return block.querySelector( '.wp-block-search__input' ); -} - -/** - * Gets search button. - * - * @param {HTMLFormElement} block Search block. - * @return {HTMLButtonElement} Search button. - */ -function getSearchButton( block ) { - return block.querySelector( '.wp-block-search__button' ); -} - -/** - * Handles keydown event to collapse an expanded Search block (when pressing Escape key). - * - * @param {KeyboardEvent} event - */ -function handleKeydownEvent( event ) { - if ( ! expandedSearchBlock ) { - // In case the event listener wasn't removed in time. - return; - } - - if ( event.key === 'Escape' ) { - const block = expandedSearchBlock; // This is nullified by collapseExpandedSearchBlock(). - collapseExpandedSearchBlock(); - getSearchButton( block ).focus(); - } -} - -/** - * Handles keyup event to collapse an expanded Search block (e.g. when tabbing out of expanded Search block). - * - * @param {KeyboardEvent} event - */ -function handleKeyupEvent( event ) { - if ( ! expandedSearchBlock ) { - // In case the event listener wasn't removed in time. - return; - } - - if ( event.target.closest( '.wp-block-search' ) !== expandedSearchBlock ) { - collapseExpandedSearchBlock(); - } -} - -/** - * Expands search block. - * - * Inverse of what is done in collapseExpandedSearchBlock(). - * - * @param {HTMLFormElement} block Search block. - */ -function expandSearchBlock( block ) { - // Make sure only one is open at a time. - if ( expandedSearchBlock ) { - collapseExpandedSearchBlock(); - } - - const searchField = getSearchInput( block ); - const searchButton = getSearchButton( block ); - - searchButton.type = 'submit'; - searchField.ariaHidden = 'false'; - searchField.tabIndex = 0; - searchButton.ariaExpanded = 'true'; - searchButton.removeAttribute( 'aria-controls' ); // Note: Seemingly not reflected with searchButton.ariaControls. - toggleAriaLabel( searchButton ); - block.classList.remove( hiddenClass ); - - searchField.focus(); // Note that Chrome seems to do this automatically. - - // The following two must be inverse of what is done in collapseExpandedSearchBlock(). - document.addEventListener( 'keydown', handleKeydownEvent, { - passive: true, - } ); - document.addEventListener( 'keyup', handleKeyupEvent, { - passive: true, - } ); - - expandedSearchBlock = block; -} - /** - * Collapses the expanded search block. - * - * Inverse of what is done in expandSearchBlock(). + * WordPress dependencies */ -function collapseExpandedSearchBlock() { - if ( ! expandedSearchBlock ) { - throw new Error( 'Expected expandedSearchBlock to be defined.' ); - } - const block = expandedSearchBlock; - const searchField = getSearchInput( block ); - const searchButton = getSearchButton( block ); - - searchButton.type = 'button'; - searchField.ariaHidden = 'true'; - searchField.tabIndex = -1; - searchButton.ariaExpanded = 'false'; - searchButton.setAttribute( 'aria-controls', searchField.id ); // Note: Seemingly not reflected with searchButton.ariaControls. - toggleAriaLabel( searchButton ); - block.classList.add( hiddenClass ); - - // The following two must be inverse of what is done in expandSearchBlock(). - document.removeEventListener( 'keydown', handleKeydownEvent, { - passive: true, - } ); - document.removeEventListener( 'keyup', handleKeyupEvent, { - passive: true, - } ); - - expandedSearchBlock = null; -} - -// Listen for click events anywhere on the document so this script can be loaded asynchronously in the head. -document.addEventListener( - 'click', - ( event ) => { - // Get the ancestor expandable Search block of the clicked element. - const block = event.target.closest( - '.wp-block-search__button-behavior-expand' - ); - - /* - * If there is already an expanded search block and either the current click was not for a Search block or it was - * for another block, then collapse the currently-expanded block. - */ - if ( expandedSearchBlock && block !== expandedSearchBlock ) { - collapseExpandedSearchBlock(); - } - - // If the click was on or inside a collapsed Search block, expand it. - if ( - block instanceof HTMLFormElement && - block.classList.contains( hiddenClass ) - ) { - expandSearchBlock( block ); - } +import { store as wpStore } from '@wordpress/interactivity'; + +wpStore( { + selectors: { + core: { + search: { + ariaLabel: ( { context } ) => { + const { ariaLabelCollapsed, ariaLabelExpanded } = + context.core.search; + return context.core.search.isSearchInputVisible + ? ariaLabelExpanded + : ariaLabelCollapsed; + }, + ariaControls: ( { context } ) => { + return context.core.search.isSearchInputVisible + ? null + : context.core.search.inputId; + }, + type: ( { context } ) => { + return context.core.search.isSearchInputVisible + ? 'submit' + : 'button'; + }, + tabindex: ( { context } ) => { + return context.core.search.isSearchInputVisible + ? '0' + : '-1'; + }, + }, + }, + }, + actions: { + core: { + search: { + openSearchInput: ( { context, event, ref } ) => { + if ( ! context.core.search.isSearchInputVisible ) { + event.preventDefault(); + context.core.search.isSearchInputVisible = true; + ref.parentElement.querySelector( 'input' ).focus(); + } + }, + closeSearchInput: ( { context } ) => { + context.core.search.isSearchInputVisible = false; + }, + handleSearchKeydown: ( store ) => { + const { actions, event, ref } = store; + // If Escape close the menu. + if ( event?.key === 'Escape' ) { + actions.core.search.closeSearchInput( store ); + ref.querySelector( 'button' ).focus(); + } + }, + handleSearchFocusout: ( store ) => { + const { actions, event, ref } = store; + // If focus is outside search form, and in the document, close menu + // event.target === The element losing focus + // event.relatedTarget === The element receiving focus (if any) + // When focusout is outside the document, + // `window.document.activeElement` doesn't change. + if ( + ! ref.contains( event.relatedTarget ) && + event.target !== window.document.activeElement + ) { + actions.core.search.closeSearchInput( store ); + } + }, + }, + }, }, - { passive: true } -); +} ); From f602d228550113033837451864b1111e0d5fbed8 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:55:06 +0300 Subject: [PATCH 14/42] Remove accidental addition of perf test results (#54355) --- .../performance/site-editor.test.results.json | 60 ------------------- 1 file changed, 60 deletions(-) delete mode 100644 packages/e2e-tests/specs/performance/site-editor.test.results.json diff --git a/packages/e2e-tests/specs/performance/site-editor.test.results.json b/packages/e2e-tests/specs/performance/site-editor.test.results.json deleted file mode 100644 index a043c552f12d3f..00000000000000 --- a/packages/e2e-tests/specs/performance/site-editor.test.results.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "serverResponse": [ - 409.40000009536743, 405.59999990463257, 410.09999990463257 - ], - "firstPaint": [ 438.59999990463257, 447.3999996185303, 449.59999990463257 ], - "domContentLoaded": [ 676, 690.0999999046326, 693 ], - "loaded": [ 1405.6999998092651, 1400.3999996185303, 1425.9000000953674 ], - "firstContentfulPaint": [ - 903.2999997138977, 921.0999999046326, 925.2999997138977 - ], - "firstBlock": [ - 3166.7999997138977, 3206.5999999046326, 3238.4000000953674 - ], - "type": [ - 81.45900000000002, 37.088, 36.051, 38.596000000000004, 49.931, 40.322, - 38.99999999999999, 34.235, 33.608999999999995, 32.88399999999999, 30.44, - 37.113, 31.534999999999997, 33.792, 36.942, 35.251000000000005, 33.722, - 33.471999999999994, 35.26499999999999, 29.682, 30.173, - 30.674999999999997, 35.668000000000006, 38.278, 37.62, 37.562, 38.091, - 32.237, 28.119999999999997, 31.342, 39.89, 37.443, 37.761, 40.262, - 37.922, 30.727, 30.955000000000002, 36.53000000000001, 32.293, 37.299, - 38.55800000000001, 39.85699999999999, 33.721999999999994, 30.139, - 29.294, 31.016, 35.7, 36.839, 31.061000000000003, 29.540000000000003, - 48.998999999999995, 35.423, 33.650000000000006, 29.404999999999998, - 32.744, 30.584999999999997, 30.705, 31.873, 28.907, 30.516, 30.882, - 29.257, 29.794, 31.150000000000002, 32.095, 31.066000000000003, 32.872, - 31.894, 31.331, 31.796, 31.675, 30.427999999999997, 30.872, 30.974, - 32.707, 31.849999999999998, 28.935, 28.441000000000003, - 30.566000000000003, 29.014, 33.158, 32.272, 28.990000000000002, 28.76, - 28.967000000000002, 29.418, 28.503, 31.255000000000003, 28.703, - 30.369000000000003, 34.910000000000004, 31.03, 28.523, - 32.361999999999995, 33.870000000000005, 30.11, 30.944000000000003, - 28.601, 30.572999999999997, 33.216, 30.822, 28.892000000000003, - 32.95099999999999, 31.228, 28.251, 34.89, 30.131000000000004, 29.395, - 31.557000000000002, 28.137, 32.051, 38.242, 36.382999999999996, 35.037, - 36.2, 31.717999999999996, 28.927999999999997, 32.540000000000006, - 35.448, 28.292, 35.059999999999995, 31.345000000000002, 36.122, 31.69, - 28.492, 29.308, 30.793000000000003, 28.784000000000002, - 28.275999999999996, 36.577999999999996, 30.220000000000002, 35.832, - 31.192, 36.102999999999994, 30.733999999999998, 30.574, - 35.455999999999996, 29.963, 37.967, 29.323999999999998, 36.643, - 31.200000000000003, 36.864999999999995, 32.344, 30.321, 29.214, 28.627, - 29.71, 29.006, 36.067, 29.583, 29.562, 37.795, 30.166999999999998, - 30.811999999999998, 33.319, 32.939, 39.233999999999995, 28.856, - 34.81700000000001, 30.324, 33.611000000000004, 33.707, - 30.191000000000003, 29.191, 29.23, 30.715, 29.281, 28.168, - 33.449000000000005, 36.36600000000001, 29.086, 30.589, 29.13, 28.789, - 29.156000000000002, 43.327, 34.439, 28.777, 30.586, 28.973000000000003, - 30.026, 40.023, 30.203, 28.328000000000003, 30.825000000000003, 29.739, - 31.504, 43.708000000000006, 29.296999999999997, 32.294, 31.733, 30.44, - 28.879, 30.349999999999998, 29.466, 29.302999999999997, 30, - 29.468999999999998, 28.740000000000002 - ], - "typeContainer": [], - "focus": [], - "inserterOpen": [], - "inserterHover": [], - "inserterSearch": [], - "listViewOpen": [] -} From d31f96af531b89e00e4952c2f98a105d21b5dde1 Mon Sep 17 00:00:00 2001 From: Rich Tabor Date: Mon, 11 Sep 2023 12:50:30 -0400 Subject: [PATCH 15/42] Solid accent color selection in command palette (#54318) --- packages/commands/src/components/style.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/commands/src/components/style.scss b/packages/commands/src/components/style.scss index 8466fabe9dd799..aeb818507f0730 100644 --- a/packages/commands/src/components/style.scss +++ b/packages/commands/src/components/style.scss @@ -78,11 +78,11 @@ &[aria-selected="true"], &:active { - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); - color: var(--wp-admin-theme-color); + background: var(--wp-admin-theme-color); + color: $white; svg { - fill: var(--wp-admin-theme-color); + fill: $white; } } From 0246dab961147d51cee3498b3d5d913355eecdac Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Mon, 11 Sep 2023 20:44:29 +0200 Subject: [PATCH 16/42] Mobile Release v1.103.2 (#54353) * Release script: Update react-native-editor version to 1.103.0 * Release script: Update with changes from 'npm run core preios' * Update Changelog * Update package-lock.json * Release script: Update react-native-editor version to 1.103.1 * Release script: Update with changes from 'npm run core preios' * Fix long-press gestures not working in `RichText` component [Android] (#54213) * Use different touchable components in RichText based on platform * Update mobile test snapshots * Update `package-lock.json` file * Update `react-native-editor` changelog * Release script: Update react-native-editor version to 1.103.2 * Release script: Update with changes from 'npm run core preios' * [RNMobile] Fix issue with missing characters in Add Media placeholder button (#54281) * Fix media placeholder text issue * Update CHANGELOG * Update Gallery block media placeholder text * Update letter case for File block button * Update package-lock.json --------- Co-authored-by: Carlos Garcia Co-authored-by: Derek Blank --- package-lock.json | 6 +++--- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 2 ++ packages/react-native-editor/ios/Podfile.lock | 8 ++++---- packages/react-native-editor/package.json | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 340130affbd181..530c3d48d08708 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56198,7 +56198,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.103.1", + "version": "1.103.2", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56211,7 +56211,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.103.1", + "version": "1.103.2", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56222,7 +56222,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.103.1", + "version": "1.103.2", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index 2c63816b661d91..d0879be9a1d770 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.103.1", + "version": "1.103.2", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 203bcf11e60c5d..a5b817cc115e36 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.103.1", + "version": "1.103.2", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 031c77b8ee8ed7..1f1d4cba4dd991 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,8 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Fix the obscurred "Insert from URL" input for media blocks when using a device in landscape orientation. [#54096] + +## 1.103.2 - [*] Fix issue with missing characters in Add Media placeholder button [#54281] ## 1.103.1 diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index c8ad1e5b6958dc..2b67491e5b14fd 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.11) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.103.1): + - Gutenberg (1.103.2): - React-Core (= 0.71.11) - React-CoreModules (= 0.71.11) - React-RCTImage (= 0.71.11) @@ -394,7 +394,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.103.1): + - RNTAztecView (1.103.2): - React-Core - WordPress-Aztec-iOS (~> 1.19.8) - SDWebImage (5.11.1): @@ -577,7 +577,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: 55a519e153b2e1f30974ad3bbe04bbfacbf7f88b + Gutenberg: fe1145de4daa0d40dd21f838f449305f54d408b7 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: f6187ec763637e6a57f5728dd9a3bdabc6d6b4e0 @@ -620,7 +620,7 @@ SPEC CHECKSUMS: RNReanimated: df2567658c01135f9ff4709d372675bcb9fd1d83 RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: 3711b6ce7e5142f5d3db825cc06f8b9ed5c16198 + RNTAztecView: 44c6114d1f76fb4bb6f325ee5289ed7a08f077f3 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: 7d11d598f14c82c727c08b56bd35fbeb7dafb504 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 69da3937d0da45..fb614dfaa11f28 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.103.1", + "version": "1.103.2", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From ef5bd83b643be2da33e7ba6cc453a81c233bbbd4 Mon Sep 17 00:00:00 2001 From: Clement Boirie Date: Mon, 11 Sep 2023 21:03:56 +0200 Subject: [PATCH 17/42] Buttons Block: Show inserter if button have variations (#53498) (#53783) * Buttons Block: Show inserter if button have variations (#53498) * Combine `useSelect` variations with the existing one * Add comment about the custom check for the `directInsert` option value that should be handled in the `Inserter` --- packages/block-library/src/buttons/edit.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/buttons/edit.js b/packages/block-library/src/buttons/edit.js index c901655dd42631..dce160dfb2fea8 100644 --- a/packages/block-library/src/buttons/edit.js +++ b/packages/block-library/src/buttons/edit.js @@ -12,6 +12,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies @@ -42,17 +43,26 @@ function ButtonsEdit( { attributes, className } ) { 'has-custom-font-size': fontSize || style?.typography?.fontSize, } ), } ); - const preferredStyle = useSelect( ( select ) => { + const { preferredStyle, hasButtonVariations } = useSelect( ( select ) => { const preferredStyleVariations = select( blockEditorStore ).getSettings() .__experimentalPreferredStyleVariations; - return preferredStyleVariations?.value?.[ buttonBlockName ]; + const buttonVariations = select( blocksStore ).getBlockVariations( + buttonBlockName, + 'inserter' + ); + return { + preferredStyle: + preferredStyleVariations?.value?.[ buttonBlockName ], + hasButtonVariations: buttonVariations.length > 0, + }; }, [] ); const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: ALLOWED_BLOCKS, defaultBlock: DEFAULT_BLOCK, - directInsert: true, + // This check should be handled by the `Inserter` internally to be consistent across all blocks that use it. + directInsert: ! hasButtonVariations, template: [ [ buttonBlockName, From b18a270ae4b5d71cc0d880d40d21b5163686cd92 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:36:38 +0300 Subject: [PATCH 18/42] Iframe: skip scoping styles (#46752) --- .../src/components/block-canvas/index.js | 5 ++++- .../src/components/block-list/content.scss | 1 - .../src/components/editor-styles/index.js | 17 +++++++---------- packages/block-library/src/reset.scss | 2 +- .../src/components/block-editor/style.scss | 1 + 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/components/block-canvas/index.js b/packages/block-editor/src/components/block-canvas/index.js index 14910e3007768f..70d6bbad99e522 100644 --- a/packages/block-editor/src/components/block-canvas/index.js +++ b/packages/block-editor/src/components/block-canvas/index.js @@ -34,7 +34,10 @@ export function ExperimentalBlockCanvas( { if ( ! shouldIframe ) { return ( <> - + { if ( ! node ) { @@ -28,9 +27,7 @@ function useDarkThemeBodyClassName( styles ) { const { ownerDocument } = node; const { defaultView, body } = ownerDocument; - const canvas = ownerDocument.querySelector( - EDITOR_STYLES_SELECTOR - ); + const canvas = scope ? ownerDocument.querySelector( scope ) : body; let backgroundColor; @@ -63,11 +60,11 @@ function useDarkThemeBodyClassName( styles ) { body.classList.add( 'is-dark-theme' ); } }, - [ styles ] + [ styles, scope ] ); } -export default function EditorStyles( { styles } ) { +export default function EditorStyles( { styles, scope } ) { const stylesArray = useMemo( () => Object.values( styles ?? [] ), [ styles ] @@ -76,9 +73,9 @@ export default function EditorStyles( { styles } ) { () => transformStyles( stylesArray.filter( ( style ) => style?.css ), - EDITOR_STYLES_SELECTOR + scope ), - [ stylesArray ] + [ stylesArray, scope ] ); const transformedSvgs = useMemo( @@ -94,7 +91,7 @@ export default function EditorStyles( { styles } ) { <> { /* Use an empty style element to have a document reference, but this could be any element. */ } - ) ) } diff --git a/packages/block-library/src/reset.scss b/packages/block-library/src/reset.scss index 6c3cc7b1d45be8..cf5e5a1596df59 100644 --- a/packages/block-library/src/reset.scss +++ b/packages/block-library/src/reset.scss @@ -7,7 +7,7 @@ // We use :where to keep specificity minimal. // https://css-tricks.com/almanac/selectors/w/where/ -html :where(.editor-styles-wrapper) { +:where(.editor-styles-wrapper) { /** * The following styles revert to the browser defaults overriding the WPAdmin styles. * This is only needed while the block editor is not being loaded in an iframe. diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index dce224998c0c07..15fc8e180f818f 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -31,6 +31,7 @@ display: block; width: 100%; height: 100%; + background: $white; } .edit-site-visual-editor__editor-canvas { From 8f6386552544082203f058ab5cba52bd428c09d7 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 12 Sep 2023 09:03:41 +1200 Subject: [PATCH 19/42] Remove experimental prefixes from pattern package methods as they are not exported (#54338) --- packages/patterns/src/components/create-pattern-modal.js | 2 +- .../patterns/src/components/patterns-manage-button.js | 7 ++++--- packages/patterns/src/store/actions.js | 8 ++++---- packages/patterns/src/store/selectors.js | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index a6aa4007764fcc..5623a04fcba611 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -35,7 +35,7 @@ export default function CreatePatternModal( { } ) { const [ syncType, setSyncType ] = useState( SYNC_TYPES.full ); const [ title, setTitle ] = useState( '' ); - const { __experimentalCreatePattern: createPattern } = useDispatch( store ); + const { createPattern } = useDispatch( store ); const { createErrorNotice } = useDispatch( noticesStore ); const onCreate = useCallback( diff --git a/packages/patterns/src/components/patterns-manage-button.js b/packages/patterns/src/components/patterns-manage-button.js index bfa36521a28c1f..eae307f1838dec 100644 --- a/packages/patterns/src/components/patterns-manage-button.js +++ b/packages/patterns/src/components/patterns-manage-button.js @@ -51,8 +51,7 @@ function PatternsManageButton( { clientId } ) { [ clientId ] ); - const { __experimentalConvertSyncedPatternToStatic: convertBlockToStatic } = - useDispatch( editorStore ); + const { convertSyncedPatternToStatic } = useDispatch( editorStore ); if ( ! isVisible ) { return null; @@ -64,7 +63,9 @@ function PatternsManageButton( { clientId } ) { { __( 'Manage patterns' ) } { canRemove && ( - convertBlockToStatic( clientId ) }> + convertSyncedPatternToStatic( clientId ) } + > { innerBlockCount > 1 ? __( 'Detach patterns' ) : __( 'Detach pattern' ) } diff --git a/packages/patterns/src/store/actions.js b/packages/patterns/src/store/actions.js index 2128526b2b3194..0a0306c736621a 100644 --- a/packages/patterns/src/store/actions.js +++ b/packages/patterns/src/store/actions.js @@ -13,7 +13,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; * @param {'full'|'unsynced'} syncType They way block is synced, 'full' or 'unsynced'. * @param {string[]|undefined} clientIds Optional client IDs of blocks to convert to pattern. */ -export const __experimentalCreatePattern = +export const createPattern = ( title, syncType, clientIds ) => async ( { registry, dispatch } ) => { const meta = @@ -50,7 +50,7 @@ export const __experimentalCreatePattern = registry .dispatch( blockEditorStore ) .replaceBlocks( clientIds, newBlock ); - dispatch.__experimentalSetEditingPattern( newBlock.clientId, true ); + dispatch.setEditingPattern( newBlock.clientId, true ); return updatedRecord; }; @@ -59,7 +59,7 @@ export const __experimentalCreatePattern = * * @param {string} clientId The client ID of the block to attach. */ -export const __experimentalConvertSyncedPatternToStatic = +export const convertSyncedPatternToStatic = ( clientId ) => ( { registry } ) => { const oldBlock = registry @@ -90,7 +90,7 @@ export const __experimentalConvertSyncedPatternToStatic = * @param {boolean} isEditing Whether the block should be in editing state. * @return {Object} Action descriptor. */ -export function __experimentalSetEditingPattern( clientId, isEditing ) { +export function setEditingPattern( clientId, isEditing ) { return { type: 'SET_EDITING_PATTERN', clientId, diff --git a/packages/patterns/src/store/selectors.js b/packages/patterns/src/store/selectors.js index 3089737c856c86..63a4d5ff9dfbdf 100644 --- a/packages/patterns/src/store/selectors.js +++ b/packages/patterns/src/store/selectors.js @@ -5,6 +5,6 @@ * @param {number} clientId the clientID of the block. * @return {boolean} Whether the pattern is in the editing state. */ -export function __experimentalIsEditingPattern( state, clientId ) { +export function isEditingPattern( state, clientId ) { return state.isEditingPattern[ clientId ]; } From 3704da47147a5f426f930a09e1351f03f0271cd0 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:56:23 -0700 Subject: [PATCH 20/42] Navigation Link: restore tooltip (#54263) --- .../block-library/src/navigation-link/edit.js | 57 +++++++------------ .../src/navigation-link/editor.scss | 8 --- 2 files changed, 21 insertions(+), 44 deletions(-) diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index df7ca36d7272af..4a902871abdee0 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -486,13 +486,8 @@ export default function NavigationLinkEdit( { { /* eslint-enable */ } { ! url ? (
    - - <> - { missingText } - - { tooltipText } - - + + { missingText }
    ) : ( @@ -548,35 +543,25 @@ export default function NavigationLinkEdit( { isDraft || isLabelFieldFocused ) && (
    - - <> - - { - // Some attributes are stored in an escaped form. It's a legacy issue. - // Ideally they would be stored in a raw, unescaped form. - // Unescape is used here to "recover" the escaped characters - // so they display without encoding. - // See `updateAttributes` for more details. - `${ decodeEntities( - label - ) } ${ - isInvalid || isDraft - ? placeholderText - : '' - }`.trim() - } - - - { tooltipText } - - + + + { + // Some attributes are stored in an escaped form. It's a legacy issue. + // Ideally they would be stored in a raw, unescaped form. + // Unescape is used here to "recover" the escaped characters + // so they display without encoding. + // See `updateAttributes` for more details. + `${ decodeEntities( label ) } ${ + isInvalid || isDraft + ? placeholderText + : '' + }`.trim() + } +
    ) } diff --git a/packages/block-library/src/navigation-link/editor.scss b/packages/block-library/src/navigation-link/editor.scss index 327f89c58ae3ab..6d1dc32e5310b5 100644 --- a/packages/block-library/src/navigation-link/editor.scss +++ b/packages/block-library/src/navigation-link/editor.scss @@ -67,14 +67,6 @@ color: #000; } -.wp-block-navigation-link__missing_text-tooltip { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; -} /** * Menu item setup state. Is shown when a menu item has no URL configured. */ From bd52276857a70d4ea33c7947ac71247e5ab933ac Mon Sep 17 00:00:00 2001 From: Alex Stine Date: Mon, 11 Sep 2023 22:49:31 -0500 Subject: [PATCH 21/42] Table block: Fix semantic structure for screen readers on back-end (#54324) * Fix table block edit structure for screen readers. * Fix td props. Fix first cell focus. * Move className to td. --- packages/block-library/src/table/edit.js | 40 +++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index 4f186f77a72918..17a9e1ecfdd5bf 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -349,7 +349,7 @@ function TableEdit( { useEffect( () => { if ( hasTableCreated ) { tableRef?.current - ?.querySelector( 'td[contentEditable="true"]' ) + ?.querySelector( 'td div[contentEditable="true"]' ) ?.focus(); setHasTableCreated( false ); } @@ -414,31 +414,33 @@ function TableEdit( { }, columnIndex ) => ( - { - setSelectedCell( { - sectionName: name, - rowIndex, - columnIndex, - type: 'cell', - } ); - } } - aria-label={ cellAriaLabel[ name ] } - placeholder={ placeholder[ name ] } - /> + > + { + setSelectedCell( { + sectionName: name, + rowIndex, + columnIndex, + type: 'cell', + } ); + } } + aria-label={ cellAriaLabel[ name ] } + placeholder={ placeholder[ name ] } + /> + ) ) } From 94aad94984d2845ecc06d765fd0edb846df9870d Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 12 Sep 2023 06:24:37 +0200 Subject: [PATCH 22/42] Change dialogdescription for renaming group block (#54358) --- packages/block-editor/src/hooks/block-rename-ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/hooks/block-rename-ui.js b/packages/block-editor/src/hooks/block-rename-ui.js index 189090668cbb45..835a09556aed5e 100644 --- a/packages/block-editor/src/hooks/block-rename-ui.js +++ b/packages/block-editor/src/hooks/block-rename-ui.js @@ -71,7 +71,7 @@ function RenameModal( { blockName, originalBlockName, onClose, onSave } ) { } } >

    - { __( 'Choose a custom name for this block.' ) } + { __( 'Enter a custom name for this block.' ) }

    { From c04e42042579f3bb4bc26c8c3b8685883369b9c7 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:32:38 +0900 Subject: [PATCH 23/42] PreviewOptions: Fix critical error when children not passed (#54284) * Preview Options: Fix critical error when children not passed * Use optional chaining --- .../block-editor/src/components/preview-options/README.md | 7 +++++++ .../block-editor/src/components/preview-options/index.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/preview-options/README.md b/packages/block-editor/src/components/preview-options/README.md index 6e9a029fa83a57..baf886c71bd65b 100644 --- a/packages/block-editor/src/components/preview-options/README.md +++ b/packages/block-editor/src/components/preview-options/README.md @@ -82,6 +82,13 @@ Used to set the device type that will be used to display the preview inside the - Type: `func` - Required: yes +#### children + +A function that returns nodes to be rendered within the dropdown. + +- Type: `Function` +- Required: No + ## Related components Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/preview-options/index.js b/packages/block-editor/src/components/preview-options/index.js index 9f5f820c4edcb2..8dc5a70a91397d 100644 --- a/packages/block-editor/src/components/preview-options/index.js +++ b/packages/block-editor/src/components/preview-options/index.js @@ -81,7 +81,7 @@ export default function PreviewOptions( { { __( 'Mobile' ) } - { children( renderProps ) } + { children?.( renderProps ) } ) } From f76d48a59bdf1f1d744fae2a2e4763b1579bcb34 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 12 Sep 2023 09:34:44 +0100 Subject: [PATCH 24/42] Extract WordPress specific styles from the BlockTools component (#54356) --- .../src/components/block-tools/style.scss | 98 ------------------- .../src/components/visual-editor/style.scss | 89 +++++++++++++++++ .../src/components/block-editor/style.scss | 86 ++++++++++++++++ .../style.scss | 98 +++++++++++++++++++ storybook/stories/playground/index.story.js | 39 +++++++- 5 files changed, 311 insertions(+), 99 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index a6b7d636f491ba..796c513d6d7c2c 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -89,9 +89,6 @@ * Block Toolbar when contextual. */ -// Base left position for the toolbar when fixed. -@include editor-left(".block-editor-block-contextual-toolbar.is-fixed"); - .block-editor-block-contextual-toolbar { // Block UI appearance. display: inline-flex; @@ -105,11 +102,6 @@ } &.is-fixed { - position: sticky; - top: 0; - z-index: z-index(".block-editor-block-popover"); - display: block; - width: 100%; overflow: hidden; .block-editor-block-toolbar { @@ -137,51 +129,8 @@ background: linear-gradient(to right, $white, transparent); } - // on desktop and tablet viewports the toolbar is fixed - // on top of interface header - $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; @include break-medium() { &.is-fixed { - // leave room for block inserter, undo and redo, list view - margin-left: $toolbar-margin; - // position on top of interface header - position: fixed; - top: $admin-bar-height; - // Don't fill up when empty - min-height: initial; - // remove the border - border-bottom: none; - // has to be flex for collapse button to fit - display: flex; - - // Mimic the height of the parent, vertically align center, and provide a max-height. - height: $header-height; - align-items: center; - - &.is-collapsed { - width: initial; - } - - &:empty { - width: initial; - } - - .is-fullscreen-mode & { - // leave room for block inserter, undo and redo, list view - // and some margin left - margin-left: $grid-unit-80 * 4 - 2 * $grid-unit; - - top: 0; - - &.is-collapsed { - width: initial; - } - - &:empty { - width: initial; - } - } - & > .block-editor-block-toolbar { flex-grow: initial; width: initial; @@ -264,13 +213,6 @@ } .show-icon-labels & { - - margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin - - .is-fullscreen-mode & { - margin-left: $grid-unit * 18; // site hub, inserter and margin - } - .block-editor-block-parent-selector .block-editor-block-parent-selector__button::after { left: 0; } @@ -287,14 +229,6 @@ } } } - - .blocks-widgets-container & { - margin-left: $grid-unit-80 * 2.4; - - &.is-collapsed { - margin-left: $grid-unit-80 * 4.2; - } - } } &.is-fixed .block-editor-block-parent-selector { @@ -332,38 +266,6 @@ } } } - - // on tablet viewports the toolbar is fixed - // on top of interface header and covers the whole header - // except for the inserter on the left - @include break-medium() { - &.is-fixed { - width: calc(100% - #{$toolbar-margin}); - - .show-icon-labels & { - width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons - } - - } - } - - // on desktop viewports the toolbar is fixed - // on top of interface header and leaves room - // for the block inserter the publish button - @include break-large() { - &.is-fixed { - width: auto; - .show-icon-labels & { - width: auto; //there are no undo, redo and list view buttons - } - } - .is-fullscreen-mode &.is-fixed { - // in full screen mode we need to account for - // the combined with of the tools at the right of the header and the margin left - // of the toolbar which includes four buttons - width: calc(100% - 280px - #{4 * $grid-unit-80}); - } - } } /** diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index fa61cc9889cf9c..40043958fcaad5 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -67,3 +67,92 @@ // See also https://www.w3.org/TR/CSS22/visudet.html#the-height-property flex-grow: 1; } + +// Fixed contextual toolbar +@include editor-left(".edit-post-visual-editor .block-editor-block-contextual-toolbar.is-fixed"); + +.edit-post-visual-editor .block-editor-block-contextual-toolbar.is-fixed { + position: sticky; + top: 0; + z-index: z-index(".block-editor-block-popover"); + display: block; + width: 100%; + + // on desktop and tablet viewports the toolbar is fixed + // on top of interface header + $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; + + @include break-medium() { + // leave room for block inserter, undo and redo, list view + margin-left: $toolbar-margin; + // position on top of interface header + position: fixed; + top: $admin-bar-height; + // Don't fill up when empty + min-height: initial; + // remove the border + border-bottom: none; + // has to be flex for collapse button to fit + display: flex; + + // Mimic the height of the parent, vertically align center, and provide a max-height. + height: $header-height; + align-items: center; + + + // on tablet viewports the toolbar is fixed + // on top of interface header and covers the whole header + // except for the inserter on the left + width: calc(100% - #{$toolbar-margin}); + + &.is-collapsed { + width: initial; + } + + &:empty { + width: initial; + } + + .is-fullscreen-mode & { + // leave room for block inserter, undo and redo, list view + // and some margin left + margin-left: $grid-unit-80 * 4 - 2 * $grid-unit; + + top: 0; + + &.is-collapsed { + width: initial; + } + + &:empty { + width: initial; + } + } + + .show-icon-labels & { + width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons + margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin + + .is-fullscreen-mode & { + margin-left: $grid-unit * 18; // site hub, inserter and margin + } + } + } + + // on desktop viewports the toolbar is fixed + // on top of interface header and leaves room + // for the block inserter the publish button + @include break-large() { + width: auto; + .show-icon-labels & { + width: auto; //there are no undo, redo and list view buttons + } + + .is-fullscreen-mode & { + // in full screen mode we need to account for + // the combined with of the tools at the right of the header and the margin left + // of the toolbar which includes four buttons + width: calc(100% - 280px - #{4 * $grid-unit-80}); + } + } +} diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index 15fc8e180f818f..0ed00c91097a30 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -173,3 +173,89 @@ } } +// Fixed contextual toolbar +@include editor-left(".edit-site-visual-editor .block-editor-block-contextual-toolbar.is-fixed"); + +.edit-site-visual-editor .block-editor-block-contextual-toolbar.is-fixed { + position: sticky; + top: 0; + z-index: z-index(".block-editor-block-popover"); + display: block; + width: 100%; + + // on desktop and tablet viewports the toolbar is fixed + // on top of interface header + $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; + + @include break-medium() { + // leave room for block inserter, undo and redo, list view + margin-left: $toolbar-margin; + // position on top of interface header + position: fixed; + top: $admin-bar-height; + // Don't fill up when empty + min-height: initial; + // has to be flex for collapse button to fit + display: flex; + + // Mimic the height of the parent, vertically align center, and provide a max-height. + height: $header-height; + align-items: center; + + + // on tablet viewports the toolbar is fixed + // on top of interface header and covers the whole header + // except for the inserter on the left + width: calc(100% - #{$toolbar-margin}); + + &.is-collapsed { + width: initial; + } + + &:empty { + width: initial; + } + + .is-fullscreen-mode & { + // leave room for block inserter, undo and redo, list view + // and some margin left + margin-left: $grid-unit-80 * 4 - 2 * $grid-unit; + + top: 0; + + &.is-collapsed { + width: initial; + } + + &:empty { + width: initial; + } + } + + .show-icon-labels & { + margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin + width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons + + .is-fullscreen-mode & { + margin-left: $grid-unit * 18; // site hub, inserter and margin + } + } + } + + // on desktop viewports the toolbar is fixed + // on top of interface header and leaves room + // for the block inserter the publish button + @include break-large() { + width: auto; + .show-icon-labels & { + width: auto; //there are no undo, redo and list view buttons + } + + .is-fullscreen-mode & { + // in full screen mode we need to account for + // the combined with of the tools at the right of the header and the margin left + // of the toolbar which includes four buttons + width: calc(100% - 280px - #{4 * $grid-unit-80}); + } + } +} diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss index 97a084b220393d..35e83c7339e1db 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss @@ -35,3 +35,101 @@ } } } + +// Fixed contextual toolbar +@include editor-left(".edit-widgets-block-editor .block-editor-block-contextual-toolbar.is-fixed"); + + +.edit-widgets-block-editor .block-editor-block-contextual-toolbar.is-fixed { + position: sticky; + top: 0; + z-index: z-index(".block-editor-block-popover"); + display: block; + width: 100%; + + // on desktop and tablet viewports the toolbar is fixed + // on top of interface header + $toolbar-margin: $grid-unit-80 * 3 - 2 * $grid-unit + $grid-unit-05; + + @include break-medium() { + // leave room for block inserter, undo and redo, list view + margin-left: $toolbar-margin; + // position on top of interface header + position: fixed; + top: $admin-bar-height; + // Don't fill up when empty + min-height: initial; + // remove the border + border-bottom: none; + // has to be flex for collapse button to fit + display: flex; + + // Mimic the height of the parent, vertically align center, and provide a max-height. + height: $header-height; + align-items: center; + + + // on tablet viewports the toolbar is fixed + // on top of interface header and covers the whole header + // except for the inserter on the left + width: calc(100% - #{$toolbar-margin}); + + &.is-collapsed { + width: initial; + } + + &:empty { + width: initial; + } + + .is-fullscreen-mode & { + // leave room for block inserter, undo and redo, list view + // and some margin left + margin-left: $grid-unit-80 * 4 - 2 * $grid-unit; + + top: 0; + + &.is-collapsed { + width: initial; + } + + &:empty { + width: initial; + } + } + + .show-icon-labels & { + margin-left: $grid-unit-80 + 2 * $grid-unit; // inserter and margin + width: calc(100% + 40px - #{$toolbar-margin}); //there are no undo, redo and list view buttons + + .is-fullscreen-mode & { + margin-left: $grid-unit * 18; // site hub, inserter and margin + } + } + + .blocks-widgets-container & { + margin-left: $grid-unit-80 * 2.4; + + &.is-collapsed { + margin-left: $grid-unit-80 * 4.2; + } + } + } + + // on desktop viewports the toolbar is fixed + // on top of interface header and leaves room + // for the block inserter the publish button + @include break-large() { + width: auto; + .show-icon-labels & { + width: auto; //there are no undo, redo and list view buttons + } + + .is-fullscreen-mode & { + // in full screen mode we need to account for + // the combined with of the tools at the right of the header and the margin left + // of the toolbar which includes four buttons + width: calc(100% - 280px - #{4 * $grid-unit-80}); + } + } +} diff --git a/storybook/stories/playground/index.story.js b/storybook/stories/playground/index.story.js index bde670adc009d4..d3d44e0dedf707 100644 --- a/storybook/stories/playground/index.story.js +++ b/storybook/stories/playground/index.story.js @@ -33,7 +33,11 @@ function App() { } ); return ( -
    + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
    event.stopPropagation() } + > { return ; }; + +function EditorBox() { + const [ blocks, updateBlocks ] = useState( [] ); + + useEffect( () => { + registerCoreBlocks(); + }, [] ); + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
    event.stopPropagation() } + > + + + + +
    + ); +} + +export const Box = () => { + return ; +}; From 397b2bf4fdc57866383deb31d69dd05778c4b505 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 12 Sep 2023 12:30:13 +0200 Subject: [PATCH 25/42] Popover: remove custom frame scroll/resize listeners (#54286) * Popover: remove custom frame scroll/resize listeners * Upgrade floating-ui/utils to v0.1.2 * Add changelog entry --- package-lock.json | 12 ++-- packages/components/CHANGELOG.md | 1 + packages/components/src/popover/index.tsx | 43 ------------ packages/components/src/popover/utils.ts | 85 +++++------------------ 4 files changed, 24 insertions(+), 117 deletions(-) diff --git a/package-lock.json b/package-lock.json index 530c3d48d08708..c2843123b40dcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3814,9 +3814,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", - "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.2.tgz", + "integrity": "sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ==" }, "node_modules/@hapi/hoek": { "version": "9.2.1", @@ -59101,9 +59101,9 @@ } }, "@floating-ui/utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", - "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.2.tgz", + "integrity": "sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ==" }, "@hapi/hoek": { "version": "9.2.1", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d0e758045f4817..adabc2b4412dee 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -25,6 +25,7 @@ - `Popover`: Remove unused `overlay` type from `positionToPlacement` utility function ([#54101](https://github.com/WordPress/gutenberg/pull/54101)). - `Higher Order` -- `with-focus-outside`: Convert to TypeScript ([#53980](https://github.com/WordPress/gutenberg/pull/53980)). - `IsolatedEventContainer`: Convert unit test to TypeScript ([#54316](https://github.com/WordPress/gutenberg/pull/54316)). +- `Popover`: Remove `scroll` and `resize` listeners for iframe overflow parents and rely on recently added native Floating UI support ([#54286](https://github.com/WordPress/gutenberg/pull/54286)). ### Experimental diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 403f10de57a425..bb51252b549c52 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -40,7 +40,6 @@ import { import { close } from '@wordpress/icons'; import deprecated from '@wordpress/deprecated'; import { Path, SVG } from '@wordpress/primitives'; -import { getScrollContainer } from '@wordpress/dom'; /** * Internal dependencies @@ -52,7 +51,6 @@ import { computePopoverPosition, positionToPlacement, placementToMotionAnimationProps, - getReferenceOwnerDocument, getReferenceElement, } from './utils'; import type { WordPressComponentProps } from '../ui/context'; @@ -199,9 +197,6 @@ const UnforwardedPopover = ( const [ fallbackReferenceElement, setFallbackReferenceElement ] = useState< HTMLSpanElement | null >( null ); - const [ referenceOwnerDocument, setReferenceOwnerDocument ] = useState< - Document | undefined - >(); const anchorRefFallback: RefCallback< HTMLSpanElement > = useCallback( ( node ) => { @@ -315,15 +310,6 @@ const UnforwardedPopover = ( ?.current; useLayoutEffect( () => { - const resultingReferenceOwnerDoc = getReferenceOwnerDocument( { - anchor, - anchorRef, - anchorRect, - getAnchorRect, - fallbackReferenceElement, - fallbackDocument: document, - } ); - const resultingReferenceElement = getReferenceElement( { anchor, anchorRef, @@ -333,8 +319,6 @@ const UnforwardedPopover = ( } ); refs.setReference( resultingReferenceElement ); - - setReferenceOwnerDocument( resultingReferenceOwnerDoc ); }, [ anchor, anchorRef, @@ -348,33 +332,6 @@ const UnforwardedPopover = ( refs, ] ); - // If the reference element is in a different ownerDocument (e.g. iFrame), - // we need to manually update the floating's position as the reference's owner - // document scrolls. - useLayoutEffect( () => { - if ( - ! referenceOwnerDocument || - ! referenceOwnerDocument.defaultView - ) { - return; - } - - const { defaultView } = referenceOwnerDocument; - const { frameElement } = defaultView; - - const scrollContainer = frameElement - ? getScrollContainer( frameElement ) - : null; - - defaultView.addEventListener( 'resize', update ); - scrollContainer?.addEventListener( 'scroll', update ); - - return () => { - defaultView.removeEventListener( 'resize', update ); - scrollContainer?.removeEventListener( 'scroll', update ); - }; - }, [ referenceOwnerDocument, update ] ); - const mergedFloatingRef = useMergeRefs( [ refs.setFloating, dialogRef, diff --git a/packages/components/src/popover/utils.ts b/packages/components/src/popover/utils.ts index e03ef036803309..50e452222677e9 100644 --- a/packages/components/src/popover/utils.ts +++ b/packages/components/src/popover/utils.ts @@ -3,11 +3,7 @@ */ // eslint-disable-next-line no-restricted-imports import type { MotionProps } from 'framer-motion'; -import type { - Placement, - ReferenceType, - VirtualElement, -} from '@floating-ui/react-dom'; +import type { Placement, ReferenceType } from '@floating-ui/react-dom'; /** * Internal dependencies @@ -142,58 +138,17 @@ export const placementToMotionAnimationProps = ( }; }; -export const getReferenceOwnerDocument = ( { - anchor, - anchorRef, - anchorRect, - getAnchorRect, - fallbackReferenceElement, - fallbackDocument, -}: Pick< - PopoverProps, - 'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor' -> & { - fallbackReferenceElement: Element | null; - fallbackDocument: Document; -} ): Document => { - // In floating-ui's terms: - // - "reference" refers to the popover's anchor element. - // - "floating" refers the floating popover's element. - // A floating element can also be positioned relative to a virtual element, - // instead of a real one. A virtual element is represented by an object - // with the `getBoundingClientRect()` function (like real elements). - // See https://floating-ui.com/docs/virtual-elements for more info. - let resultingReferenceOwnerDoc; - if ( ( anchor as VirtualElement )?.contextElement ) { - resultingReferenceOwnerDoc = ( anchor as VirtualElement ).contextElement - ?.ownerDocument; - } else if ( anchor ) { - resultingReferenceOwnerDoc = anchor.ownerDocument; - } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { - resultingReferenceOwnerDoc = ( anchorRef as PopoverAnchorRefTopBottom ) - ?.top.ownerDocument; - } else if ( ( anchorRef as Range | undefined )?.startContainer ) { - resultingReferenceOwnerDoc = ( anchorRef as Range ).startContainer - .ownerDocument; - } else if ( - ( anchorRef as PopoverAnchorRefReference | undefined )?.current - ) { - resultingReferenceOwnerDoc = ( - ( anchorRef as PopoverAnchorRefReference ).current as Element - ).ownerDocument; - } else if ( anchorRef as Element | undefined ) { - // This one should be deprecated. - resultingReferenceOwnerDoc = ( anchorRef as Element ).ownerDocument; - } else if ( anchorRect && anchorRect?.ownerDocument ) { - resultingReferenceOwnerDoc = anchorRect.ownerDocument; - } else if ( getAnchorRect ) { - resultingReferenceOwnerDoc = getAnchorRect( - fallbackReferenceElement - )?.ownerDocument; - } +function isTopBottom( + anchorRef: PopoverProps[ 'anchorRef' ] +): anchorRef is PopoverAnchorRefTopBottom { + return !! ( anchorRef as PopoverAnchorRefTopBottom )?.top; +} - return resultingReferenceOwnerDoc ?? fallbackDocument; -}; +function isRef( + anchorRef: PopoverProps[ 'anchorRef' ] +): anchorRef is PopoverAnchorRefReference { + return !! ( anchorRef as PopoverAnchorRefReference )?.current; +} export const getReferenceElement = ( { anchor, @@ -211,19 +166,15 @@ export const getReferenceElement = ( { if ( anchor ) { referenceElement = anchor; - } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { + } else if ( isTopBottom( anchorRef ) ) { // Create a virtual element for the ref. The expectation is that // if anchorRef.top is defined, then anchorRef.bottom is defined too. // Seems to be used by the block toolbar, when multiple blocks are selected // (top and bottom blocks are used to calculate the resulting rect). referenceElement = { getBoundingClientRect() { - const topRect = ( - anchorRef as PopoverAnchorRefTopBottom - ).top.getBoundingClientRect(); - const bottomRect = ( - anchorRef as PopoverAnchorRefTopBottom - ).bottom.getBoundingClientRect(); + const topRect = anchorRef.top.getBoundingClientRect(); + const bottomRect = anchorRef.bottom.getBoundingClientRect(); return new window.DOMRect( topRect.x, topRect.y, @@ -232,12 +183,10 @@ export const getReferenceElement = ( { ); }, }; - } else if ( - ( anchorRef as PopoverAnchorRefReference | undefined )?.current - ) { + } else if ( isRef( anchorRef ) ) { // Standard React ref. - referenceElement = ( anchorRef as PopoverAnchorRefReference ).current; - } else if ( anchorRef as Element | undefined ) { + referenceElement = anchorRef.current; + } else if ( anchorRef ) { // If `anchorRef` holds directly the element's value (no `current` key) // This is a weird scenario and should be deprecated. referenceElement = anchorRef as Element; From fb0a6aff91059e1e4e0d7dca46b54a419b51c1fa Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Tue, 12 Sep 2023 11:47:39 +0100 Subject: [PATCH 26/42] Theme Previews: Make the back button customizable (#54242) * Theme Previews: Make the back button customizable * Add a comment about a filter * Theme Previews: Make the backlink text customizable * Fix PHPCS errors --------- Co-authored-by: okmttdhr --- lib/compat/wordpress-6.4/theme-previews.php | 27 +++++++++++++++++++ lib/load.php | 1 + .../sidebar-navigation-screen/index.js | 13 +++------ 3 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 lib/compat/wordpress-6.4/theme-previews.php diff --git a/lib/compat/wordpress-6.4/theme-previews.php b/lib/compat/wordpress-6.4/theme-previews.php new file mode 100644 index 00000000000000..5755fc992921d4 --- /dev/null +++ b/lib/compat/wordpress-6.4/theme-previews.php @@ -0,0 +1,27 @@ + { + const { dashboardLink, dashboardLinkText } = useSelect( ( select ) => { const { getSettings } = unlock( select( editSiteStore ) ); return { dashboardLink: getSettings().__experimentalDashboardLink, + dashboardLinkText: getSettings().__experimentalDashboardLinkText, }; }, [] ); const { getTheme } = useSelect( coreStore ); @@ -92,15 +93,9 @@ export default function SidebarNavigationScreen( { ) } Date: Tue, 12 Sep 2023 03:59:12 -0700 Subject: [PATCH 27/42] Add children back to toolbar item render for rendered components (#53314) * Add children back to toolbar item render for rendered components * Add changelog entry * Address feedback * More CHANGELOG entry to unreleased section * format --------- Co-authored-by: Marco Ciampini --- packages/components/CHANGELOG.md | 1 + packages/components/src/toolbar/toolbar-item/index.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index adabc2b4412dee..2e88ccc3e23076 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ - `Tooltip`: dynamically render in the DOM only when visible ([#54312](https://github.com/WordPress/gutenberg/pull/54312)). - `PaletteEdit`: Fix padding in RTL languages ([#54034](https://github.com/WordPress/gutenberg/pull/54034)). +- `ToolbarItem`: Fix children not showing in rendered components ([#53314](https://github.com/WordPress/gutenberg/pull/53314)). - `CircularOptionPicker`: make focus styles resilient to button size changes ([#54196](https://github.com/WordPress/gutenberg/pull/54196)). ### Internal diff --git a/packages/components/src/toolbar/toolbar-item/index.tsx b/packages/components/src/toolbar/toolbar-item/index.tsx index ccfe5084545577..3bf55aedd8a48a 100644 --- a/packages/components/src/toolbar/toolbar-item/index.tsx +++ b/packages/components/src/toolbar/toolbar-item/index.tsx @@ -43,7 +43,9 @@ function ToolbarItem( return children( allProps ); } - const render = isRenderProp ? children : Component && ; + const render = isRenderProp + ? children + : Component && { children }; return ( Date: Tue, 12 Sep 2023 22:13:01 +0900 Subject: [PATCH 28/42] Edit Widgets: Fix broken layout (#54372) * Edit Widgets: Fix broken layout * Add copy handler --- .../index.js | 23 +++++++++---------- .../style.scss | 3 +-- .../index.js | 7 ++++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js index 22a5b352ea580e..12a70e2e4da279 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js @@ -4,7 +4,9 @@ import { BlockList, BlockTools, - privateApis as blockEditorPrivateApis, + BlockSelectionClearer, + WritingFlow, + __unstableEditorStyles as EditorStyles, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; @@ -15,11 +17,6 @@ import { store as preferencesStore } from '@wordpress/preferences'; */ import Notices from '../notices'; import KeyboardShortcuts from '../keyboard-shortcuts'; -import { unlock } from '../../lock-unlock'; - -const { ExperimentalBlockCanvas: BlockCanvas } = unlock( - blockEditorPrivateApis -); export default function WidgetAreasBlockEditorContent( { blockEditorSettings, @@ -42,13 +39,15 @@ export default function WidgetAreasBlockEditorContent( { - - - + scope=".editor-styles-wrapper" + /> + + + + +
    ); diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss index 35e83c7339e1db..062214ef147bf1 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss @@ -11,8 +11,7 @@ flex-grow: 1; > div:last-of-type, - .block-editor-writing-flow, - .block-editor-writing-flow > div { + .block-editor-writing-flow { display: flex; flex-direction: column; flex-grow: 1; diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index d538e37ad1ec8a..f3a7b84d5ded68 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -10,7 +10,10 @@ import { useResourcePermissions, } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { + CopyHandler, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -104,7 +107,7 @@ export default function WidgetAreasBlockEditorProvider( { useSubRegistry={ false } { ...props } > - { children } + { children } From 699819ccede2a14321ccb17137662de60accb687 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 12 Sep 2023 15:16:55 +0100 Subject: [PATCH 29/42] Deprecated `get_file_path_from_theme` method. (#45831) * Deprecated function * Update lib/class-wp-theme-json-resolver-gutenberg.php Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --------- Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --- lib/class-wp-theme-json-resolver-gutenberg.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 39721742946cd1..c5a4c662221aeb 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -241,9 +241,9 @@ public static function get_theme_data( $deprecated = array(), $options = array() $options = wp_parse_args( $options, array( 'with_supports' => true ) ); if ( null === static::$theme || ! static::has_same_registered_blocks( 'theme' ) ) { - $theme_json_file = static::get_file_path_from_theme( 'theme.json' ); $wp_theme = wp_get_theme(); - if ( '' !== $theme_json_file ) { + $theme_json_file = $wp_theme->get_file_path( 'theme.json' ); + if ( is_readable( $theme_json_file ) ) { $theme_json_data = static::read_json_file( $theme_json_file ); $theme_json_data = static::translate( $theme_json_data, $wp_theme->get( 'TextDomain' ) ); } else { @@ -263,8 +263,8 @@ public static function get_theme_data( $deprecated = array(), $options = array() if ( $wp_theme->parent() ) { // Get parent theme.json. - $parent_theme_json_file = static::get_file_path_from_theme( 'theme.json', true ); - if ( '' !== $parent_theme_json_file ) { + $parent_theme_json_file = $wp_theme->parent()->get_file_path( 'theme.json' ); + if ( $theme_json_file !== $parent_theme_json_file && is_readable( $parent_theme_json_file ) ) { $parent_theme_json_data = static::read_json_file( $parent_theme_json_file ); $parent_theme_json_data = static::translate( $parent_theme_json_data, $wp_theme->parent()->get( 'TextDomain' ) ); $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); @@ -670,6 +670,8 @@ public static function theme_has_support() { * @return string The whole file path or empty if the file doesn't exist. */ protected static function get_file_path_from_theme( $file_name, $template = false ) { + // TODO: Remove this method from core on 6.3 release. + _deprecated_function( __METHOD__, '6.3.0' ); $path = $template ? get_template_directory() : get_stylesheet_directory(); $candidate = $path . '/' . $file_name; From 7d054a2d08d86291041fb2be35c3bbf694c18af1 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 13 Sep 2023 00:20:31 +1000 Subject: [PATCH 30/42] Background Image block support: Add reset menu item (#54341) --- packages/block-editor/src/hooks/background.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 2b8f355c344989..25815ad5d2cae1 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -8,6 +8,7 @@ import { Button, DropZone, FlexItem, + MenuItem, __experimentalItemGroup as ItemGroup, __experimentalHStack as HStack, __experimentalTruncate as Truncate, @@ -236,7 +237,13 @@ function BackgroundImagePanelItem( props ) { /> } variant="secondary" - /> + > + resetBackgroundImage( props ) } + > + { __( 'Reset ' ) } + + ) } { ! url && ( From bba4bbf8c998432159c6b1ea7db40bfe4fc23d5d Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 12 Sep 2023 17:36:39 +0300 Subject: [PATCH 31/42] [Site Editor - Page Inspector]: Add ability to switch templates (#51477) * [Site Editor - Page Inspector]: Add ability to switch templates * try setting the page/template from edited entity record * Update label * Truncate * update z-index of templates swap modal * fix typo * add + update tests * fix suggestions per template's `postTypes` and test * change order of calling in `onTemplateSelect` --------- Co-authored-by: James Koster --- .../data/data-core-edit-site.md | 2 +- packages/base-styles/_z-index.scss | 1 + .../core-data/src/hooks/use-entity-record.ts | 4 +- .../page-panels/edit-template.js | 80 ++++++----- .../sidebar-edit-mode/page-panels/hooks.js | 83 ++++++++++++ .../sidebar-edit-mode/page-panels/index.js | 4 - .../page-panels/page-summary.js | 2 + .../page-panels/reset-default-template.js | 44 ++++++ .../sidebar-edit-mode/page-panels/style.scss | 51 +++++-- .../page-panels/swap-template-button.js | 82 +++++++++++ packages/edit-site/src/store/actions.js | 52 +++++-- test/e2e/specs/site-editor/pages.spec.js | 128 +++++++++++++++--- .../emptytheme/templates/custom-template.html | 3 + test/emptytheme/theme.json | 7 + 14 files changed, 460 insertions(+), 83 deletions(-) create mode 100644 packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js create mode 100644 packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js create mode 100644 packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js create mode 100644 test/emptytheme/templates/custom-template.html diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md index 0cac2268b2ab29..6dea8e9b77d1b2 100644 --- a/docs/reference-guides/data/data-core-edit-site.md +++ b/docs/reference-guides/data/data-core-edit-site.md @@ -291,7 +291,7 @@ _Parameters_ _Returns_ -- `number`: The resolved template ID for the page route. +- `Object`: Action object. ### setHasPageContentFocus diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 4f8bbab5a16095..12443a30a96656 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -127,6 +127,7 @@ $z-layers: ( ".block-editor-template-part__selection-modal": 1000001, ".block-editor-block-rename-modal": 1000001, ".edit-site-list__rename-modal": 1000001, + ".edit-site-swap-template-modal": 1000001, // Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts // because it uses emotion and not sass. We need it to render on top its parent popover. diff --git a/packages/core-data/src/hooks/use-entity-record.ts b/packages/core-data/src/hooks/use-entity-record.ts index 01eee562644cf1..7f3630b1dd4ceb 100644 --- a/packages/core-data/src/hooks/use-entity-record.ts +++ b/packages/core-data/src/hooks/use-entity-record.ts @@ -155,8 +155,8 @@ export default function useEntityRecord< RecordType >( const mutations = useMemo( () => ( { - edit: ( record ) => - editEntityRecord( kind, name, recordId, record ), + edit: ( record, editOptions: any = {} ) => + editEntityRecord( kind, name, recordId, record, editOptions ), save: ( saveOptions: any = {} ) => saveEditedEntityRecord( kind, name, recordId, { throwOnError: true, diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js index b49e8ac459e3fe..2295ee12f45049 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js @@ -2,21 +2,31 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useMemo } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; -import { BlockContextProvider, BlockPreview } from '@wordpress/block-editor'; -import { Button, __experimentalVStack as VStack } from '@wordpress/components'; +import { + DropdownMenu, + MenuGroup, + MenuItem, + __experimentalHStack as HStack, + __experimentalText as Text, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; -import { parse } from '@wordpress/blocks'; /** * Internal dependencies */ import { store as editSiteStore } from '../../../store'; +import SwapTemplateButton from './swap-template-button'; +import ResetDefaultTemplate from './reset-default-template'; + +const POPOVER_PROPS = { + className: 'edit-site-page-panels-edit-template__dropdown', + placement: 'bottom-start', +}; export default function EditTemplate() { - const { context, hasResolved, template } = useSelect( ( select ) => { + const { hasResolved, template } = useSelect( ( select ) => { const { getEditedPostContext, getEditedPostType, getEditedPostId } = select( editSiteStore ); const { getEditedEntityRecord, hasFinishedResolution } = @@ -39,39 +49,43 @@ export default function EditTemplate() { const { setHasPageContentFocus } = useDispatch( editSiteStore ); - const blockContext = useMemo( - () => ( { ...context, postType: null, postId: null } ), - [ context ] - ); - - const blocks = useMemo( - () => - template.blocks ?? - ( template.content && typeof template.content !== 'function' - ? parse( template.content ) - : [] ), - [ template.blocks, template.content ] - ); - if ( ! hasResolved ) { return null; } return ( - -
    { decodeEntities( template.title ) }
    -
    - - - -
    - -
    + { ( { onClose } ) => ( + <> + + { + setHasPageContentFocus( false ); + onClose(); + } } + > + { __( 'Edit template' ) } + + + + + + ) } + + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js new file mode 100644 index 00000000000000..3000d21ab13662 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js @@ -0,0 +1,83 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; + +export function useEditedPostContext() { + return useSelect( + ( select ) => select( editSiteStore ).getEditedPostContext(), + [] + ); +} + +export function useIsPostsPage() { + const { postId } = useEditedPostContext(); + return useSelect( + ( select ) => + +postId === + select( coreStore ).getEntityRecord( 'root', 'site' ) + ?.page_for_posts, + [ postId ] + ); +} + +function useTemplates() { + return useSelect( + ( select ) => + select( coreStore ).getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + post_type: 'page', + } ), + [] + ); +} + +export function useAvailableTemplates() { + const currentTemplateSlug = useCurrentTemplateSlug(); + const isPostsPage = useIsPostsPage(); + const templates = useTemplates(); + return useMemo( + () => + // The posts page template cannot be changed. + ! isPostsPage && + templates?.filter( + ( template ) => + template.is_custom && + template.slug !== currentTemplateSlug && + !! template.content.raw // Skip empty templates. + ), + [ templates, currentTemplateSlug, isPostsPage ] + ); +} + +export function useCurrentTemplateSlug() { + const { postType, postId } = useEditedPostContext(); + const templates = useTemplates(); + const entityTemplate = useSelect( + ( select ) => { + const post = select( coreStore ).getEditedEntityRecord( + 'postType', + postType, + postId + ); + return post?.template; + }, + [ postType, postId ] + ); + + if ( ! entityTemplate ) { + return; + } + // If a page has a `template` set and is not included in the list + // of the theme's templates, do not return it, in order to resolve + // to the current theme's default template. + return templates?.find( ( template ) => template.slug === entityTemplate ) + ?.slug; +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index 69971d1ad413ae..df59dffe66be69 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -20,7 +20,6 @@ import { store as editSiteStore } from '../../../store'; import SidebarCard from '../sidebar-card'; import PageContent from './page-content'; import PageSummary from './page-summary'; -import EditTemplate from './edit-template'; export default function PagePanels() { const { id, type, hasResolved, status, date, password, title, modified } = @@ -81,9 +80,6 @@ export default function PagePanels() { - - - ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js index 3dce743b298d45..c4dafeab6cb372 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js @@ -7,6 +7,7 @@ import { __experimentalVStack as VStack } from '@wordpress/components'; */ import PageStatus from './page-status'; import PublishDate from './publish-date'; +import EditTemplate from './edit-template'; export default function PageSummary( { status, @@ -30,6 +31,7 @@ export default function PageSummary( { postId={ postId } postType={ postType } /> + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js new file mode 100644 index 00000000000000..bc61b82a8d0057 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { MenuGroup, MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useEntityRecord } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { + useCurrentTemplateSlug, + useEditedPostContext, + useIsPostsPage, +} from './hooks'; +import { store as editSiteStore } from '../../../store'; + +export default function ResetDefaultTemplate( { onClick } ) { + const currentTemplateSlug = useCurrentTemplateSlug(); + const isPostsPage = useIsPostsPage(); + const { postType, postId } = useEditedPostContext(); + const entity = useEntityRecord( 'postType', postType, postId ); + const { setPage } = useDispatch( editSiteStore ); + // The default template in a post is indicated by an empty string. + if ( ! currentTemplateSlug || isPostsPage ) { + return null; + } + return ( + + { + entity.edit( { template: '' }, { undoIgnore: true } ); + onClick(); + await setPage( { + context: { postType, postId }, + } ); + } } + > + { __( 'Reset' ) } + + + ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index 8c10b32085612b..aedcf5e46ca9ea 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -1,12 +1,37 @@ -.edit-site-page-panels__edit-template-preview { - border: 1px solid $gray-200; - height: 200px; - max-height: 200px; - overflow: hidden; +.edit-site-swap-template-modal { + z-index: z-index(".edit-site-swap-template-modal"); } -.edit-site-page-panels__edit-template-button { - justify-content: center; +.edit-site-page-panels__swap-template__confirm-modal__actions { + margin-top: $grid-unit-30; +} + +.edit-site-page-panels__swap-template__modal-content .block-editor-block-patterns-list { + column-count: 2; + column-gap: $grid-unit-30; + + // Small top padding required to avoid cutting off the visible outline when hovering items + padding-top: $border-width-focus-fallback; + + @include break-medium() { + column-count: 3; + } + + @include break-wide() { + column-count: 4; + } + + .block-editor-block-patterns-list__list-item { + break-inside: avoid-column; + } + + .block-editor-block-patterns-list__item { + // Avoid to override the BlockPatternList component + // default hover and focus styles. + &:not(:focus):not(:hover) .block-editor-block-preview__container { + box-shadow: 0 0 0 1px $gray-300; + } + } } .edit-site-change-status__content { @@ -36,15 +61,21 @@ .edit-site-summary-field { .components-dropdown { - flex-grow: 1; + width: 70%; } .edit-site-summary-field__trigger { - width: 100%; + max-width: 100%; + + // Truncate + display: block; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .edit-site-summary-field__label { width: 30%; } } - diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js new file mode 100644 index 00000000000000..fee4f22a3ae2bc --- /dev/null +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useMemo, useState, useCallback } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __experimentalBlockPatternsList as BlockPatternsList } from '@wordpress/block-editor'; +import { MenuItem, Modal } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useEntityRecord } from '@wordpress/core-data'; +import { parse } from '@wordpress/blocks'; +import { useAsyncList } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { store as editSiteStore } from '../../../store'; +import { useAvailableTemplates, useEditedPostContext } from './hooks'; + +export default function SwapTemplateButton( { onClick } ) { + const [ showModal, setShowModal ] = useState( false ); + const availableTemplates = useAvailableTemplates(); + const onClose = useCallback( () => { + setShowModal( false ); + }, [] ); + const { postType, postId } = useEditedPostContext(); + const entitiy = useEntityRecord( 'postType', postType, postId ); + const { setPage } = useDispatch( editSiteStore ); + if ( ! availableTemplates?.length ) { + return null; + } + const onTemplateSelect = async ( template ) => { + entitiy.edit( { template: template.name }, { undoIgnore: true } ); + await setPage( { + context: { postType, postId }, + } ); + onClose(); // Close the template suggestions modal first. + onClick(); + }; + return ( + <> + setShowModal( true ) }> + { __( 'Swap template' ) } + + { showModal && ( + +
    + +
    +
    + ) } + + ); +} + +function TemplatesList( { onSelect } ) { + const availableTemplates = useAvailableTemplates(); + const templatesAsPatterns = useMemo( + () => + availableTemplates.map( ( template ) => ( { + name: template.slug, + blocks: parse( template.content.raw ), + title: decodeEntities( template.title.rendered ), + id: template.id, + } ) ), + [ availableTemplates ] + ); + const shownTemplates = useAsyncList( templatesAsPatterns ); + return ( + + ); +} diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 0ad521a9c9a54e..f690075c311489 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -4,7 +4,7 @@ import apiFetch from '@wordpress/api-fetch'; import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; -import { addQueryArgs, getPathAndQueryString } from '@wordpress/url'; +import { addQueryArgs } from '@wordpress/url'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; @@ -233,7 +233,7 @@ export function setHomeTemplateId() { * * @param {Object} context The context object. * - * @return {number} The resolved template ID for the page route. + * @return {Object} Action object. */ export function setEditedPostContext( context ) { return { @@ -257,22 +257,48 @@ export function setEditedPostContext( context ) { export const setPage = ( page ) => async ( { dispatch, registry } ) => { - if ( ! page.path && page.context?.postId ) { - const entity = await registry + let template; + const getDefaultTemplate = async ( slug ) => + apiFetch( { + path: addQueryArgs( '/wp/v2/templates/lookup', { + slug: `page-${ slug }`, + } ), + } ); + + if ( page.path ) { + template = await registry + .resolveSelect( coreStore ) + .__experimentalGetTemplateForLink( page.path ); + } else { + const editedEntity = await registry .resolveSelect( coreStore ) - .getEntityRecord( + .getEditedEntityRecord( 'postType', - page.context.postType || 'post', - page.context.postId + page.context?.postType || 'post', + page.context?.postId ); - // If the entity is undefined for some reason, path will resolve to "/" - page.path = getPathAndQueryString( entity?.link ); + const currentTemplateSlug = editedEntity?.template; + if ( currentTemplateSlug ) { + const currentTemplate = ( + await registry + .resolveSelect( coreStore ) + .getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + } ) + )?.find( ( { slug } ) => slug === currentTemplateSlug ); + if ( currentTemplate ) { + template = currentTemplate; + } else { + // If a page has a `template` set and is not included in the list + // of the current theme's templates, query for current theme's default template. + template = await getDefaultTemplate( editedEntity?.link ); + } + } else { + // Page's `template` is empty, that indicates we need to use the default template for the page. + template = await getDefaultTemplate( editedEntity?.link ); + } } - const template = await registry - .resolveSelect( coreStore ) - .__experimentalGetTemplateForLink( page.path ); - if ( ! template ) { return; } diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 1c0cb8c4b69686..e6a0b7f266cbf4 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -3,33 +3,48 @@ */ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); +async function draftNewPage( page ) { + await page.getByRole( 'button', { name: 'Pages' } ).click(); + await page.getByRole( 'button', { name: 'Draft a new page' } ).click(); + await page + .locator( 'role=dialog[name="Draft a new page"i]' ) + .locator( 'role=textbox[name="Page title"i]' ) + .fill( 'Test Page' ); + await page.keyboard.press( 'Enter' ); + await expect( + page.locator( + `role=button[name="Dismiss this notice"i] >> text='"Test Page" successfully created.'` + ) + ).toBeVisible(); +} + test.describe( 'Pages', () => { test.beforeAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'emptytheme' ); + await Promise.all( [ + requestUtils.deleteAllTemplates( 'wp_template' ), + requestUtils.deleteAllPages(), + ] ); } ); test.afterAll( async ( { requestUtils } ) => { await requestUtils.activateTheme( 'twentytwentyone' ); + await Promise.all( [ + requestUtils.deleteAllTemplates( 'wp_template' ), + requestUtils.deleteAllPages(), + ] ); } ); - test.beforeEach( async ( { admin } ) => { + test.beforeEach( async ( { requestUtils, admin } ) => { + await Promise.all( [ + requestUtils.deleteAllTemplates( 'wp_template' ), + requestUtils.deleteAllPages(), + ] ); await admin.visitSiteEditor(); } ); test( 'create a new page', async ( { page, editor } ) => { - // Draft a new page. - await page.getByRole( 'button', { name: 'Pages' } ).click(); - await page.getByRole( 'button', { name: 'Draft a new page' } ).click(); - await page - .locator( 'role=dialog[name="Draft a new page"i]' ) - .locator( 'role=textbox[name="Page title"i]' ) - .fill( 'Test Page' ); - await page.keyboard.press( 'Enter' ); - await expect( - page.locator( - `role=button[name="Dismiss this notice"i] >> text='"Test Page" successfully created.'` - ) - ).toBeVisible(); + await draftNewPage( page ); // Insert into Page Content using default block. await editor.canvas @@ -79,15 +94,11 @@ test.describe( 'Pages', () => { // Switch to template editing focus. await editor.openDocumentSettingsSidebar(); - await expect( - page.locator( - '.edit-site-page-panels__edit-template-preview iframe' - ) - ).toBeVisible(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Edit template' } ) + .getByRole( 'button', { name: 'Template options' } ) .click(); + await page.getByRole( 'button', { name: 'Edit template' } ).click(); await expect( editor.canvas.getByRole( 'document', { name: 'Block: Content', @@ -125,4 +136,81 @@ test.describe( 'Pages', () => { ) ).toBeVisible(); } ); + test( 'swap template and reset to default', async ( { + admin, + page, + editor, + } ) => { + // Create a custom template first. + const templateName = 'demo'; + await page.getByRole( 'button', { name: 'Templates' } ).click(); + await page.getByRole( 'button', { name: 'Add New Template' } ).click(); + await page + .getByRole( 'button', { + name: 'A custom template can be manually applied to any post or page.', + } ) + .click(); + // Fill the template title and submit. + const newTemplateDialog = page.locator( + 'role=dialog[name="Create custom template"i]' + ); + const templateNameInput = newTemplateDialog.locator( + 'role=textbox[name="Name"i]' + ); + await templateNameInput.fill( templateName ); + await page.keyboard.press( 'Enter' ); + await page + .locator( '.block-editor-block-patterns-list__list-item' ) + .click(); + await editor.saveSiteEditorEntities(); + await admin.visitSiteEditor(); + + // Create new page that has the default template so as to swap it. + await draftNewPage( page ); + await editor.openDocumentSettingsSidebar(); + const templateOptionsButton = page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'button', { name: 'Template options' } ); + await expect( templateOptionsButton ).toHaveText( 'Single Entries' ); + await templateOptionsButton.click(); + await page + .getByRole( 'menu', { name: 'Template options' } ) + .getByText( 'Swap template' ) + .click(); + const templateItem = page.locator( + '.block-editor-block-patterns-list__item-title' + ); + // Empty theme's custom template with `postTypes: ['post']`, should not be suggested. + await expect( templateItem ).toHaveCount( 1 ); + await templateItem.click(); + await expect( templateOptionsButton ).toHaveText( 'demo' ); + await editor.saveSiteEditorEntities(); + + // Now reset, and apply the default template back. + await templateOptionsButton.click(); + const resetButton = page + .getByRole( 'menu', { name: 'Template options' } ) + .getByText( 'Reset' ); + await expect( resetButton ).toBeVisible(); + await resetButton.click(); + await expect( templateOptionsButton ).toHaveText( 'Single Entries' ); + } ); + test( 'swap template options should respect the declared `postTypes`', async ( { + page, + editor, + } ) => { + await draftNewPage( page ); + await editor.openDocumentSettingsSidebar(); + const templateOptionsButton = page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'button', { name: 'Template options' } ); + await templateOptionsButton.click(); + // Empty theme has only one custom template with `postTypes: ['post']`, + // so it should not be suggested. + await expect( + page + .getByRole( 'menu', { name: 'Template options' } ) + .getByText( 'Swap template' ) + ).toHaveCount( 0 ); + } ); } ); diff --git a/test/emptytheme/templates/custom-template.html b/test/emptytheme/templates/custom-template.html new file mode 100644 index 00000000000000..e4e8c11a39ef6f --- /dev/null +++ b/test/emptytheme/templates/custom-template.html @@ -0,0 +1,3 @@ + +

    Custom template for Posts

    + diff --git a/test/emptytheme/theme.json b/test/emptytheme/theme.json index b28e6c9f274b2f..d95ed844e6b1cb 100644 --- a/test/emptytheme/theme.json +++ b/test/emptytheme/theme.json @@ -8,5 +8,12 @@ "wideSize": "1100px" } }, + "customTemplates": [ + { + "name": "custom-template", + "title": "Custom", + "postTypes": [ "post" ] + } + ], "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] } From 71f75e734ea6bcdb250220482890c54ae94d2753 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:07:31 +0200 Subject: [PATCH 32/42] Remove `wp_store` from query block (#54359) * Remove wp_store from query block * Add comment * Remove unnecessary object cast --------- Co-authored-by: Luis Herranz --- packages/block-library/src/query/index.php | 26 +++++++++------------- packages/block-library/src/query/view.js | 8 +++---- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index f06073cc952000..4b8e8dee02a31a 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -23,9 +23,19 @@ function render_block_core_query( $attributes, $content, $block ) { // Add the necessary directives. $p->set_attribute( 'data-wp-interactive', true ); $p->set_attribute( 'data-wp-navigation-id', 'query-' . $attributes['queryId'] ); + // Use context to send translated strings. $p->set_attribute( 'data-wp-context', - wp_json_encode( array( 'core' => array( 'query' => (object) array() ) ) ) + wp_json_encode( + array( + 'core' => array( + 'query' => array( + 'loadingText' => __( 'Loading page, please wait.' ), + 'loadedText' => __( 'Page Loaded.' ), + ), + ), + ) + ) ); $content = $p->get_updated_html(); @@ -49,20 +59,6 @@ class="wp-block-query__enhanced-pagination-animation" $last_div_position, 0 ); - - // Use state to send translated strings. - wp_store( - array( - 'state' => array( - 'core' => array( - 'query' => array( - 'loadingText' => __( 'Loading page, please wait.' ), - 'loadedText' => __( 'Page Loaded.' ), - ), - ), - ), - ) - ); } } diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index cbd5573e05c6f9..78cc423c80661a 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -32,7 +32,7 @@ store( { actions: { core: { query: { - navigate: async ( { event, ref, context, state } ) => { + navigate: async ( { event, ref, context } ) => { if ( isValidLink( ref ) && isValidEvent( event ) ) { event.preventDefault(); @@ -42,7 +42,7 @@ store( { // Don't announce the navigation immediately, wait 300 ms. const timeout = setTimeout( () => { context.core.query.message = - state.core.query.loadingText; + context.core.query.loadingText; context.core.query.animation = 'start'; }, 300 ); @@ -55,9 +55,9 @@ store( { // same, we use a no-break space similar to the @wordpress/a11y // package: https://github.com/WordPress/gutenberg/blob/c395242b8e6ee20f8b06c199e4fc2920d7018af1/packages/a11y/src/filter-message.js#L20-L26 context.core.query.message = - state.core.query.loadedText + + context.core.query.loadedText + ( context.core.query.message === - state.core.query.loadedText + context.core.query.loadedText ? '\u00A0' : '' ); From d27d9d5f2b80f6c0b935927a7890b17823ec6ca9 Mon Sep 17 00:00:00 2001 From: Rich Tabor Date: Tue, 12 Sep 2023 12:35:16 -0400 Subject: [PATCH 33/42] Tweak border control button to proper metrics and simpler action (#53998) * Fix button styles to updated metrics * Simplify button text * Update CHANGELOG.md * Use proper border top color * Fix tests --- packages/components/CHANGELOG.md | 1 + .../border-control-dropdown/component.tsx | 2 +- packages/components/src/border-control/styles.ts | 12 ++++++------ packages/components/src/border-control/test/index.js | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2e88ccc3e23076..5426a4d6d6dda0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased +- `BorderControl`: Apply proper metrics and simpler text ([#53998](https://github.com/WordPress/gutenberg/pull/53998)). ### Enhancements 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 64e92ca79e32f4..4a34d6ed7ffe34 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -235,7 +235,7 @@ const BorderControlDropdown = ( onClose(); } } > - { __( 'Reset to default' ) } + { __( 'Reset' ) } ) } diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts index 7dec1d7dde434d..3aedef1445cc6c 100644 --- a/packages/components/src/border-control/styles.ts +++ b/packages/components/src/border-control/styles.ts @@ -165,10 +165,10 @@ export const resetButton = css` /* Override button component styling */ && { - border-top: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] }; + border-top: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 400 ] }; border-top-left-radius: 0; border-top-right-radius: 0; - height: 46px; + height: 40px; } `; @@ -180,10 +180,10 @@ export const borderControlStylePicker = css` export const borderStyleButton = css` &&&&& { - min-width: 30px; - width: 30px; - height: 30px; - padding: 3px; + min-width: 32px; + width: 32px; + height: 32px; + padding: 4px; } `; diff --git a/packages/components/src/border-control/test/index.js b/packages/components/src/border-control/test/index.js index 4ca8c1fb8bb67a..c41dce687cc522 100644 --- a/packages/components/src/border-control/test/index.js +++ b/packages/components/src/border-control/test/index.js @@ -137,7 +137,7 @@ describe( 'BorderControl', () => { const solidButton = getButton( 'Solid' ); const dashedButton = getButton( 'Dashed' ); const dottedButton = getButton( 'Dotted' ); - const resetButton = getButton( 'Reset to default' ); + const resetButton = getButton( 'Reset' ); expect( customColorPicker ).toBeInTheDocument(); expect( colorSwatchButtons.length ).toEqual( colors.length ); @@ -359,7 +359,7 @@ describe( 'BorderControl', () => { const props = createProps(); render( ); await openPopover( user ); - await user.click( getButton( 'Reset to default' ) ); + await user.click( getButton( 'Reset' ) ); expect( props.onChange ).toHaveBeenNthCalledWith( 1, { color: undefined, From 5331fb6d9f49852fa7e96e5fe1923d49e0a4d611 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Tue, 12 Sep 2023 10:29:15 -0700 Subject: [PATCH 34/42] Button: Update test assertion to match test name (#54260) * Button: Update test assertion to match test name * Restore replaced assertion for button * Update assertions after changes to tooltip in #54312 --- packages/components/src/button/test/index.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/components/src/button/test/index.tsx b/packages/components/src/button/test/index.tsx index 7f65337508bd89..d70b8b075aa1eb 100644 --- a/packages/components/src/button/test/index.tsx +++ b/packages/components/src/button/test/index.tsx @@ -242,6 +242,8 @@ describe( 'Button', () => { } ); it( 'should populate tooltip with description content for buttons with visible labels (buttons with children)', async () => { + const user = userEvent.setup(); + render(