From da16125f6f6d7099207fc3d5d2da2ed8c2927022 Mon Sep 17 00:00:00 2001 From: Dani Guardiola Date: Sat, 21 Sep 2024 23:11:17 +0200 Subject: [PATCH 01/53] BoxControl: Promote to stable (#65469) * Minor docs tweaks. * Export without experimental prefix * Replace references across gutenberg * Replace import example in a README * Auto-format README with prettier * Move Storybook stories and add redirect * Add deprecated hint to experimental export * Add changelog entries Co-authored-by: DaniGuardiola Co-authored-by: ciampo --- .../global-styles/dimensions-panel.js | 2 +- packages/components/CHANGELOG.md | 2 ++ packages/components/src/box-control/README.md | 21 ++++++++----------- packages/components/src/box-control/index.tsx | 8 +++---- .../src/box-control/stories/index.story.tsx | 2 +- packages/components/src/box-control/types.ts | 6 +++--- packages/components/src/index.ts | 2 ++ .../src/tools-panel/tools-panel/README.md | 20 +++++++++--------- storybook/manager-head.html | 1 + 9 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index ce508a5ebc89e5..4c52de6a3d7d11 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -10,7 +10,7 @@ import { __ } from '@wordpress/i18n'; import { __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, - __experimentalBoxControl as BoxControl, + BoxControl, __experimentalUnitControl as UnitControl, __experimentalUseCustomUnits as useCustomUnits, __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6607700b2d2c25..90481e58edd7bb 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -15,6 +15,7 @@ ### Deprecations - Deprecate `__unstableComposite`, `__unstableCompositeGroup`, `__unstableCompositeItem` and `__unstableUseCompositeState`. Consumers of the package should use the stable `Composite` component instead ([#63572](https://github.com/WordPress/gutenberg/pull/63572)). +- `__experimentalBoxControl` can now be imported as a stable `BoxControl` ([#65469](https://github.com/WordPress/gutenberg/pull/65469)). ### New Features @@ -35,6 +36,7 @@ - `Tooltip`: Adopt elevation scale ([#65159](https://github.com/WordPress/gutenberg/pull/65159)). - `Modal`: add exit animation for internally triggered events ([#65203](https://github.com/WordPress/gutenberg/pull/65203)). - `Card`: Adopt radius scale ([#65053](https://github.com/WordPress/gutenberg/pull/65053)). +- `BoxControl`: promote to stable ([#65469](https://github.com/WordPress/gutenberg/pull/65469)). ### Bug Fixes diff --git a/packages/components/src/box-control/README.md b/packages/components/src/box-control/README.md index b03b03a85466ae..77176b49eeb6d8 100644 --- a/packages/components/src/box-control/README.md +++ b/packages/components/src/box-control/README.md @@ -1,18 +1,14 @@ # BoxControl -
-This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. -
- -BoxControl components let users set values for Top, Right, Bottom, and Left. This can be used as an input control for values like `padding` or `margin`. +A control that lets users set values for top, right, bottom, and left. Can be used as an input control for values like `padding` or `margin`. ## Usage ```jsx import { useState } from 'react'; -import { __experimentalBoxControl as BoxControl } from '@wordpress/components'; +import { BoxControl } from '@wordpress/components'; -const Example = () => { +function Example() { const [ values, setValues ] = useState( { top: '50px', left: '10%', @@ -26,23 +22,24 @@ const Example = () => { onChange={ ( nextValues ) => setValues( nextValues ) } /> ); -}; +} ``` ## Props + ### `allowReset`: `boolean` If this property is true, a button to reset the box control is rendered. -- Required: No -- Default: `true` +- Required: No +- Default: `true` ### `splitOnAxis`: `boolean` If this property is true, when the box control is unlinked, vertical and horizontal controls can be used instead of updating individual sides. -- Required: No -- Default: `false` +- Required: No +- Default: `false` ### `inputProps`: `object` diff --git a/packages/components/src/box-control/index.tsx b/packages/components/src/box-control/index.tsx index 9c3452d4ccb806..41e95aa88bea37 100644 --- a/packages/components/src/box-control/index.tsx +++ b/packages/components/src/box-control/index.tsx @@ -47,14 +47,14 @@ function useUniqueId( idProp?: string ) { } /** - * BoxControl components let users set values for Top, Right, Bottom, and Left. - * This can be used as an input control for values like `padding` or `margin`. + * A control that lets users set values for top, right, bottom, and left. Can be + * used as an input control for values like `padding` or `margin`. * * ```jsx - * import { __experimentalBoxControl as BoxControl } from '@wordpress/components'; + * import { BoxControl } from '@wordpress/components'; * import { useState } from '@wordpress/element'; * - * const Example = () => { + * function Example() { * const [ values, setValues ] = useState( { * top: '50px', * left: '10%', diff --git a/packages/components/src/box-control/stories/index.story.tsx b/packages/components/src/box-control/stories/index.story.tsx index 1b6604048f6d52..783f9d047b1bb0 100644 --- a/packages/components/src/box-control/stories/index.story.tsx +++ b/packages/components/src/box-control/stories/index.story.tsx @@ -14,7 +14,7 @@ import { useState } from '@wordpress/element'; import BoxControl from '../'; const meta: Meta< typeof BoxControl > = { - title: 'Components (Experimental)/BoxControl', + title: 'Components/BoxControl', component: BoxControl, argTypes: { values: { control: { type: null } }, diff --git a/packages/components/src/box-control/types.ts b/packages/components/src/box-control/types.ts index eeb72df14bb9c1..5f4071aeed88a7 100644 --- a/packages/components/src/box-control/types.ts +++ b/packages/components/src/box-control/types.ts @@ -37,13 +37,13 @@ export type BoxControlProps = Pick< /** * Props for the internal `UnitControl` components. * - * @default `{ min: 0 }` + * @default { min: 0 } */ inputProps?: UnitControlPassthroughProps; /** * Heading label for the control. * - * @default `__( 'Box Control' )` + * @default __( 'Box Control' ) */ label?: string; /** @@ -53,7 +53,7 @@ export type BoxControlProps = Pick< /** * The `top`, `right`, `bottom`, and `left` box dimension values to use when the control is reset. * - * @default `{ top: undefined, right: undefined, bottom: undefined, left: undefined }` + * @default { top: undefined, right: undefined, bottom: undefined, left: undefined } */ resetValues?: BoxControlValue; /** diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 32195ebc444ce6..a59d258012807d 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -40,7 +40,9 @@ export { } from './border-box-control'; export { BorderControl as __experimentalBorderControl } from './border-control'; export { + /** @deprecated Import `BoxControl` instead. */ default as __experimentalBoxControl, + default as BoxControl, applyValueToSides as __experimentalApplyValueToSides, } from './box-control'; export { default as Button } from './button'; diff --git a/packages/components/src/tools-panel/tools-panel/README.md b/packages/components/src/tools-panel/tools-panel/README.md index df41b623eefb6c..1daa7537335e1c 100644 --- a/packages/components/src/tools-panel/tools-panel/README.md +++ b/packages/components/src/tools-panel/tools-panel/README.md @@ -60,7 +60,7 @@ import styled from '@emotion/styled'; * WordPress dependencies */ import { - __experimentalBoxControl as BoxControl, + BoxControl, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalUnitControl as UnitControl, @@ -91,8 +91,8 @@ export function DimensionPanel() { return ( - Select dimensions or spacing related settings from the - menu for additional controls. + Select dimensions or spacing related settings from the menu for + additional controls. !! height } @@ -154,8 +154,8 @@ export function DimensionPanel() { Flags that the items in this ToolsPanel will be contained within an inner wrapper element allowing the panel to lay them out accordingly. -- Required: No -- Default: `false` +- Required: No +- Default: `false` ### `dropdownMenuProps`: `{}` @@ -176,7 +176,7 @@ The heading level of the panel's header. Text to be displayed within the panel's header and as the `aria-label` for the panel's dropdown menu. -- Required: Yes +- Required: Yes ### `panelId`: `string | null` @@ -185,13 +185,13 @@ to restrict panel items. When a `panelId` is set, items can only register themselves if the `panelId` is explicitly `null` or the item's `panelId` matches exactly. -- Required: No +- Required: No ### `resetAll`: `( filters?: ResetAllFilter[] ) => void` A function to call when the `Reset all` menu option is selected. As an argument, it receives an array containing the `resetAllFilter` callbacks of all the valid registered `ToolsPanelItems`. -- Required: Yes +- Required: Yes ### `shouldRenderPlaceholderItems`: `boolean` @@ -201,5 +201,5 @@ placeholder content (instead of `null`) when they are toggled off and hidden. Note that placeholder items won't apply the `className` that would be normally applied to a visible `ToolsPanelItem` via the `className` prop. -- Required: No -- Default: `false` +- Required: No +- Default: `false` diff --git a/storybook/manager-head.html b/storybook/manager-head.html index ebf2d6891ba0bb..7293248ae3e472 100644 --- a/storybook/manager-head.html +++ b/storybook/manager-head.html @@ -2,6 +2,7 @@ ( function redirectIfStoryMoved() { const PREVIOUSLY_EXPERIMENTAL_COMPONENTS = [ 'alignmentmatrixcontrol', + 'boxcontrol', 'customselectcontrol-v2', 'dimensioncontrol', 'navigation', From 93edca04f68ae899f1271dc98547904abeb77ad0 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:10:30 +1000 Subject: [PATCH 02/53] Time To Read: Add block example (#65512) --- packages/block-library/src/post-time-to-read/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/post-time-to-read/index.js b/packages/block-library/src/post-time-to-read/index.js index 95b379f55f0b3f..039923161ca81d 100644 --- a/packages/block-library/src/post-time-to-read/index.js +++ b/packages/block-library/src/post-time-to-read/index.js @@ -12,6 +12,7 @@ export { metadata, name }; export const settings = { icon, edit, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From e76923fb26968534b5a8a4cb0c738fc633ddeb5d Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:19:32 +1000 Subject: [PATCH 03/53] Avatar: Add block example (#65509) --- packages/block-library/src/avatar/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/avatar/index.js b/packages/block-library/src/avatar/index.js index d318450aec3903..0b3ad9c62c4e30 100644 --- a/packages/block-library/src/avatar/index.js +++ b/packages/block-library/src/avatar/index.js @@ -16,6 +16,7 @@ export { metadata, name }; export const settings = { icon, edit, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From 42090cab97df447241b8f28da7db04825877023f Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 23 Sep 2024 02:31:50 +0000 Subject: [PATCH 04/53] Bump plugin version to 19.3.0-rc.2 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 8dddcfeccd5282..e15cb113ea65a1 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 19.3.0-rc.1 + * Version: 19.3.0-rc.2 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 035018e97f13f1..f5293bdf00ca04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.3.0-rc.1", + "version": "19.3.0-rc.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.3.0-rc.1", + "version": "19.3.0-rc.2", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index a4cc002adbf8e7..4b81f249be76c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.3.0-rc.1", + "version": "19.3.0-rc.2", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 5b6e6cbd2375f45249876b49accf72b86361a351 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:34:45 +1000 Subject: [PATCH 05/53] Table of Contents: Try maintaining block example attributes (#65549) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- .../src/table-of-contents/block.json | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/table-of-contents/block.json b/packages/block-library/src/table-of-contents/block.json index 451d245d867b07..5eb6e729d3f03e 100644 --- a/packages/block-library/src/table-of-contents/block.json +++ b/packages/block-library/src/table-of-contents/block.json @@ -62,6 +62,57 @@ } } }, - "example": {}, + "example": { + "innerBlocks": [ + { + "name": "core/heading", + "attributes": { + "level": 2, + "content": "Heading" + } + }, + { + "name": "core/heading", + "attributes": { + "level": 3, + "content": "Subheading" + } + }, + { + "name": "core/heading", + "attributes": { + "level": 2, + "content": "Heading" + } + }, + { + "name": "core/heading", + "attributes": { + "level": 3, + "content": "Subheading" + } + } + ], + "attributes": { + "headings": [ + { + "content": "Heading", + "level": 2 + }, + { + "content": "Subheading", + "level": 3 + }, + { + "content": "Heading", + "level": 2 + }, + { + "content": "Subheading", + "level": 3 + } + ] + } + }, "style": "wp-block-table-of-contents" } From 4b2b2a55ffdda7851f07509572d8a1ef540af488 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 23 Sep 2024 02:50:12 +0000 Subject: [PATCH 06/53] Update Changelog for 19.3.0-rc.2 --- changelog.txt | 275 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) diff --git a/changelog.txt b/changelog.txt index dca31f9afc622e..b04fa0e9bbf8e2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,280 @@ == Changelog == += 19.3.0-rc.2 = + +## Changelog + +### Features + +#### Zoom Out +- Remove experimental flag. ([65404](https://github.com/WordPress/gutenberg/pull/65404)) + +### Enhancements + +- Create Block: Update the minimum required PHP version to 7.2. ([65166](https://github.com/WordPress/gutenberg/pull/65166)) +- DataViews: remove unused `.dataviews-view-table__cell-content-wrapper:Empty` style rule. ([65084](https://github.com/WordPress/gutenberg/pull/65084)) +- Media Utils: Add TypeScript support and export more utils. ([64784](https://github.com/WordPress/gutenberg/pull/64784)) +- Media placeholders: Add "drag" to the text. ([65149](https://github.com/WordPress/gutenberg/pull/65149)) +- Restore: Move to trash button in Document settings. ([65087](https://github.com/WordPress/gutenberg/pull/65087)) +- Inspector Controls: Use custom block name in inspector controls when available. ([65398](https://github.com/WordPress/gutenberg/pull/65398)) +- Icons: Adds bell and bell-unread icons. ([65324](https://github.com/WordPress/gutenberg/pull/65324)) +- Editor topbar: Reorder the actions on the right. ([65163](https://github.com/WordPress/gutenberg/pull/65163)) +- Patterns: Add opt out preference to the 'Choose a Pattern' modal when adding a page. ([65026](https://github.com/WordPress/gutenberg/pull/65026)) +- Locked Templates: Blocks with contentOnly locking should not be transformable. ([64917](https://github.com/WordPress/gutenberg/pull/64917)) +- Block Locking: Add border to Replace item in content only image toolbar. ([64849](https://github.com/WordPress/gutenberg/pull/64849)) + +#### Components +- Styling: Apply elevation scale in components package. ([65159](https://github.com/WordPress/gutenberg/pull/65159)) +- Tabs: Improve Tabs indicator animation and related utils. ([64926](https://github.com/WordPress/gutenberg/pull/64926)) +- Modal + - Add exit animation. ([65203](https://github.com/WordPress/gutenberg/pull/65203)) + - Decrease close button size. ([65131](https://github.com/WordPress/gutenberg/pull/65131)) +- Navigator Screen: Warn if path doesn't follow a URL-like scheme. ([65231](https://github.com/WordPress/gutenberg/pull/65231)) +- Card: Update Card radius. ([65053](https://github.com/WordPress/gutenberg/pull/65053)) +- Combobox Control: Add placeholder attribute. ([65254](https://github.com/WordPress/gutenberg/pull/65254)) + +#### Block Library +- Allow dropping multiple images to the image block. ([65030](https://github.com/WordPress/gutenberg/pull/65030)) +- Categories List block: Add dropdown for taxonomies. ([65272](https://github.com/WordPress/gutenberg/pull/65272)) +- Image: Adds the block controls for uploading image. ([64320](https://github.com/WordPress/gutenberg/pull/64320)) +- Remove colons from control labels. ([65205](https://github.com/WordPress/gutenberg/pull/65205)) +- Terms List block: Add Categories-specific variation. ([65434](https://github.com/WordPress/gutenberg/pull/65434)) + +#### Zoom Out +- Add Zoom Out toggle to editor header when experiment enabled. ([65183](https://github.com/WordPress/gutenberg/pull/65183)) +- Add prompt for drag and drop in Patterns tab in Zoom Out mode. ([65115](https://github.com/WordPress/gutenberg/pull/65115)) +- Close inserter on exiting Zoom Out to edit. ([65194](https://github.com/WordPress/gutenberg/pull/65194)) +- Show top level sections in List View. ([65202](https://github.com/WordPress/gutenberg/pull/65202)) +- Try vertical displacement when dragging a pattern between existing patterns/sections. ([63896](https://github.com/WordPress/gutenberg/pull/63896)) + +#### Block Editor +- Link Editing: Automatically add tel to phone number when linking URL. ([64865](https://github.com/WordPress/gutenberg/pull/64865)) +thub.com/WordPress/gutenberg/pull/65300)) +- Drag and Drop: When dragging a mix of video, audio, and image blocks, create individual blocks as appropriate. ([65144](https://github.com/WordPress/gutenberg/pull/65144)) +- URLInput: Replace input with InputControl. ([65158](https://github.com/WordPress/gutenberg/pull/65158)) +- Normalize block inspector controls spacing. ([64526](https://github.com/WordPress/gutenberg/pull/64526)) + +#### Post Editor +- Add new Media section to preferences modal. ([64846](https://github.com/WordPress/gutenberg/pull/64846)) +- DocumentBar: Replace icon with post type label. ([65170](https://github.com/WordPress/gutenberg/pull/65170)) +- Page editor: Double-click to edit template part. ([65024](https://github.com/WordPress/gutenberg/pull/65024)) +- Post publish upload media dialog: Handle more block types. ([65122](https://github.com/WordPress/gutenberg/pull/65122)) + +#### Block bindings +- Populate block context with inherited post type from template slug. ([65062](https://github.com/WordPress/gutenberg/pull/65062)) +- Try gap 0 on attribute items. ([65277](https://github.com/WordPress/gutenberg/pull/65277)) +- Use post meta label from `register_meta` in block bindings workflows. ([65099](https://github.com/WordPress/gutenberg/pull/65099)) + +#### Global Styles +- Refactor site background controls and move site global styles into Background group. ([65304](https://github.com/WordPress/gutenberg/pull/65304)) +- Spacing control: Replace sides dropdwon with link button. ([65193](https://github.com/WordPress/gutenberg/pull/65193)) + +#### Data Views +- DataViews Sidebar: Display item count on DataViews sidebar. ([65223](https://github.com/WordPress/gutenberg/pull/65223)) +- DataViews: Improve UX of bundled views for Pages. ([65295](https://github.com/WordPress/gutenberg/pull/65295)) + +#### Interactivity API +- Refactor context proxies. ([64713](https://github.com/WordPress/gutenberg/pull/64713)) +- Update: Rephrase "Force page reload" and move to Advanced. ([65081](https://github.com/WordPress/gutenberg/pull/65081)) + +#### REST API +- Global Styles: Allow read access to users with `edit_posts` capabilities. ([65071](https://github.com/WordPress/gutenberg/pull/65071)) +- Query loop / Post template: Enable post format filter. ([64167](https://github.com/WordPress/gutenberg/pull/64167)) + +### New APIs +- Add @wordpress/fields package. + - Introduce the package. ([65230](https://github.com/WordPress/gutenberg/pull/65230)) + - Make the package private. ([65269](https://github.com/WordPress/gutenberg/pull/65269)) +- Interactivity API: Add `getServerState()` and `getServerContext()`. ([65151](https://github.com/WordPress/gutenberg/pull/65151)) + +### Bug Fixes + +- Align popover alt variant styling with block toolbar. ([65263](https://github.com/WordPress/gutenberg/pull/65263)) +- Compose: Correctly call timer cleanup in 'useFocusOnMount'. ([65184](https://github.com/WordPress/gutenberg/pull/65184)) +- Fix some docblock types related to the Template Registration API. ([65187](https://github.com/WordPress/gutenberg/pull/65187)) +- Fix the issue where block spacing control not shown. ([65371](https://github.com/WordPress/gutenberg/pull/65371)) +- Fix unintentional block toolbar shadow. ([65182](https://github.com/WordPress/gutenberg/pull/65182)) +- Fix: Moving a page to the trash on the site editor does not goes back to the pages list. ([65119](https://github.com/WordPress/gutenberg/pull/65119)) +- Fix: Moving the last page item to the the trash causes a crash. ([65236](https://github.com/WordPress/gutenberg/pull/65236)) +- Preferences: Fix back button on mobile. ([65141](https://github.com/WordPress/gutenberg/pull/65141)) +- Post Summary Panel: Restore `height:Auto` for toggle buttons. ([65362](https://github.com/WordPress/gutenberg/pull/65362)) +- Fix Tabs styling in Font Library modal. ([65330](https://github.com/WordPress/gutenberg/pull/65330)) +- E2E: Change deprecated social icons for standard in end-to-end. ([65312](https://github.com/WordPress/gutenberg/pull/65312)) +- Typography: Make title blocks apply typographic styles consistently. ([65307](https://github.com/WordPress/gutenberg/pull/65307)) +- Target Hints REST API: Add missing param sanitization. ([65280](https://github.com/WordPress/gutenberg/pull/65280)) +- Interactivity API: Update iterable signals when `deepMerge()` adds new properties. ([65135](https://github.com/WordPress/gutenberg/pull/65135)) +- Navigation Menus: Typography styling support to the navigation submenu block. ([65060](https://github.com/WordPress/gutenberg/pull/65060)) +- Grid: In RTL languages, the resize handles point in the opposite direction. ([64995](https://github.com/WordPress/gutenberg/pull/64995)) +- Block Locking: Fix Content Only Toolbar icon focus style. ([64940](https://github.com/WordPress/gutenberg/pull/64940)) +- Image: Fix resizing to max width in classic themes. ([64819](https://github.com/WordPress/gutenberg/pull/64819)) +- Meta Boxes: Try split content view. ([64351](https://github.com/WordPress/gutenberg/pull/64351)) +- Distraction Free: Fix blurry edge along editor header. ([64277](https://github.com/WordPress/gutenberg/pull/64277)) + +#### Block Library +- Comments Pagination: Fix warning returned by comments pagination blocks. ([65435](https://github.com/WordPress/gutenberg/pull/65435)) +- Cover: Explicitly set isUserOverlayColor to false when media is updated. ([65105](https://github.com/WordPress/gutenberg/pull/65105)) +- Disallow setting grid block rows/columns to zero. ([65217](https://github.com/WordPress/gutenberg/pull/65217)) +- Fix image block crash. ([65222](https://github.com/WordPress/gutenberg/pull/65222)) +- Fix: Buttons block: Block spacing value does not apply to both vertical and horizontal alignment. ([64971](https://github.com/WordPress/gutenberg/pull/64971)) +- Fix: Embed blocks: Figcaption inserted via toolbar not nested within figure element - #64960. ([64970](https://github.com/WordPress/gutenberg/pull/64970)) +- Image cropping: Skip making an API request if there are no changes to apply. ([65384](https://github.com/WordPress/gutenberg/pull/65384)) +- Comments Pagination: Pass the comments query `paged` arg to functions `get_next_comments_link` and `get_previous_comments_link`. ([63698](https://github.com/WordPress/gutenberg/pull/63698)) +- Query Loop + - Default to querying posts when on singular content. ([65067](https://github.com/WordPress/gutenberg/pull/65067)) + - Remove is_singular() check and fix test. ([65483](https://github.com/WordPress/gutenberg/pull/65483)) + +#### Block Editor +- Inserter: Fix loading indicator for reusable blocks. ([64839](https://github.com/WordPress/gutenberg/pull/64839)) +- Normalize spacing in Layout hook controls. ([65132](https://github.com/WordPress/gutenberg/pull/65132)) +- Pattern Inserter: Fix pattern list overflow. ([65192](https://github.com/WordPress/gutenberg/pull/65192)) +- Remove reset styles RTL from the iframe. ([65150](https://github.com/WordPress/gutenberg/pull/65150)) +- Revert "Block Insertion: Clear the insertion point when selecting a d…. ([65208](https://github.com/WordPress/gutenberg/pull/65208)) + +#### Components +- BoxControl: Unify input filed width whether linked or not. ([65348](https://github.com/WordPress/gutenberg/pull/65348)) +- ComboboxControl: Add more unit tests. ([65255](https://github.com/WordPress/gutenberg/pull/65255)) +- Fix: Button Replace remaining 40px default size violations [Edit widgets]. ([65367](https://github.com/WordPress/gutenberg/pull/65367)) +- Tabs: Fix vertical indicator. ([65385](https://github.com/WordPress/gutenberg/pull/65385)) + +#### Block bindings +- Fix empty strings placeholders in post meta bindings. ([65089](https://github.com/WordPress/gutenberg/pull/65089)) +- Remove key fallback in bindings get values and rely on source label. ([65517](https://github.com/WordPress/gutenberg/pull/65517)) + +#### Zoom Out +- Force device type to Desktop whenever zoom out is invoked. ([64476](https://github.com/WordPress/gutenberg/pull/64476)) +- Hide toolbar icon on smaller viewports. ([65437](https://github.com/WordPress/gutenberg/pull/65437)) +- Remove zoom out toggle when editor is not iframed. ([65452](https://github.com/WordPress/gutenberg/pull/65452)) + +### Accessibility + +- A11y: Add script-module. ([65101](https://github.com/WordPress/gutenberg/pull/65101)) +- Interactivity API: Use a11y Script Module in Gutenberg. ([65123](https://github.com/WordPress/gutenberg/pull/65123)) +- Script Modules API: Print script module live regions HTML in page HTML. ([65380](https://github.com/WordPress/gutenberg/pull/65380)) +- DatePicker: Better hover/focus styles. ([65117](https://github.com/WordPress/gutenberg/pull/65117)) +- Form Input: Don't use `flex-direction: Row-reverse` for checkbox field. ([64232](https://github.com/WordPress/gutenberg/pull/64232)) +- Navigation Menus: Remove Warning and add notice for Navigation. ([63921](https://github.com/WordPress/gutenberg/pull/63921)) +- Global Styles: Fix the shadows Range control accessibility and usability. ([63908](https://github.com/WordPress/gutenberg/pull/63908)) +- Block Editor: Fix accessibility of the hooked blocks toggles. ([63133](https://github.com/WordPress/gutenberg/pull/63133)) + + +#### Post Editor +- Support keyboard resizing of meta boxes pane. ([65325](https://github.com/WordPress/gutenberg/pull/65325)) +- Swap position of the Pre-publish checks buttons. ([65317](https://github.com/WordPress/gutenberg/pull/65317)) + + +### Performance + +- Core Data: Batch remaining actions in resolvers. ([65176](https://github.com/WordPress/gutenberg/pull/65176)) +- Block Editor: Use static access for selector in 'useZoomOutModeExit'. ([65337](https://github.com/WordPress/gutenberg/pull/65337)) +- Editor: Optimize global styles permission check. ([65177](https://github.com/WordPress/gutenberg/pull/65177)) + + +### Experiments + +- Block bindings REST API: Bring bindings UI in Site Editor. ([64072](https://github.com/WordPress/gutenberg/pull/64072)) + + +### Documentation + +- Add JSDoc block for getSectionRootClientId in block editor package. ([65219](https://github.com/WordPress/gutenberg/pull/65219)) +- ButtonGroup: Fix story to show what the component does. ([65336](https://github.com/WordPress/gutenberg/pull/65336)) +- DataViews storybook + - Better styles for combined fields story. ([65078](https://github.com/WordPress/gutenberg/pull/65078)) + - Enable all layouts for combined fields storybook. ([65082](https://github.com/WordPress/gutenberg/pull/65082)) +- Docs: Fix minor typos in Build your first block tutorial. ([64961](https://github.com/WordPress/gutenberg/pull/64961)) +- Docs: Update the content of the API version 3 section in the Block API Reference. ([65375](https://github.com/WordPress/gutenberg/pull/65375)) +- Fix typo in Slot Fills documentation. ([65275](https://github.com/WordPress/gutenberg/pull/65275)) + + +### Code Quality + +- Components: Transition to the new 40px default size. + - Button: + - Add __next40pxDefaultSize for files in editor 3. ([65139](https://github.com/WordPress/gutenberg/pull/65139)) + - Add __next40pxDefaultSize for files in editor 4. ([65140](https://github.com/WordPress/gutenberg/pull/65140)) + - Add props for buttons in editor 1. ([65068](https://github.com/WordPress/gutenberg/pull/65068)) + - Add props for buttons in editor 2. ([65083](https://github.com/WordPress/gutenberg/pull/65083)) + - Fix: Replace remaining 40px default size violations [Block Editor 4]. ([65257](https://github.com/WordPress/gutenberg/pull/65257)) + - Fix: Replace remaining 40px default size violation [Block library 3]. ([65110](https://github.com/WordPress/gutenberg/pull/65110)) + - Fix: Replace remaining 40px default size violation [Block library 4]. ([65143](https://github.com/WordPress/gutenberg/pull/65143)) + - Fix: Replace remaining 40px default size violation [Block library]. ([65075](https://github.com/WordPress/gutenberg/pull/65075)) + - Fix: Replace remaining 40px default size violation [Edit Site 2]. ([65258](https://github.com/WordPress/gutenberg/pull/65258)) + - Fix: Replace remaining 40px default size violations [Block library 1]. ([65033](https://github.com/WordPress/gutenberg/pull/65033)) + - Fix: Replace remaining 40px default size violations [Block Editor 1]. ([65034](https://github.com/WordPress/gutenberg/pull/65034)) + - BoxControl + - Add lint rule for 40px size prop usage. ([65341](https://github.com/WordPress/gutenberg/pull/65341)) + - DimensionsPanel: Apply 40px default size to UI when no spacing preset is available. ([65300](https://github.com/WordPress/gutenberg/pull/65300)) +- Add `useEvent` and revamped `useResizeObserver` to `@wordpress/compose`. ([64943](https://github.com/WordPress/gutenberg/pull/64943)) +- DataViews: Use Dropdown for views configuration dialog. ([65314](https://github.com/WordPress/gutenberg/pull/65314)) +- Platform docs: Upgrade dependencies. ([65445](https://github.com/WordPress/gutenberg/pull/65445)) +- Rename edit-post__fade-in-animation and unify keyframe definitions. ([65377](https://github.com/WordPress/gutenberg/pull/65377)) +- Update minimum required version in PHP. ([65301](https://github.com/WordPress/gutenberg/pull/65301)) +- Editor: Use hooks instead of HoC in `BlockManager`. ([65349](https://github.com/WordPress/gutenberg/pull/65349)) +- Data Views Fields: Migrate store and actions from editor package to fields package. ([65261](https://github.com/WordPress/gutenberg/pull/65261)) +- Plugin: Remove 'function_exists' checks for methods with 'gutenberg' prefix. ([65260](https://github.com/WordPress/gutenberg/pull/65260)) +- Global Styles: Update REST controller override method and backport changes from Core. ([65259](https://github.com/WordPress/gutenberg/pull/65259)) +- Patterns: Remove unused method returned from 'mapSelect'. ([65073](https://github.com/WordPress/gutenberg/pull/65073)) +- Embed: Convert EmbedPreview component to functional component. ([51325](https://github.com/WordPress/gutenberg/pull/51325)) + +#### Components +- BoxControl: Fix critical error when null value is passed. ([65287](https://github.com/WordPress/gutenberg/pull/65287)) +- Composite: + - Deprecate legacy, unstable version. ([63572](https://github.com/WordPress/gutenberg/pull/63572)) + - Remove store prop and useCompositeStore hook. ([64723](https://github.com/WordPress/gutenberg/pull/64723)) + - Stabilize APIs. ([63569](https://github.com/WordPress/gutenberg/pull/63569)) +- `@wordpress/components`: Add local copy of `use-lilius`. ([65097](https://github.com/WordPress/gutenberg/pull/65097)) + +#### Block bindings +- Always prioritize using context in post meta source logic. ([65449](https://github.com/WordPress/gutenberg/pull/65449)) +- Improve getRegisteredPostMeta resolver. ([65450](https://github.com/WordPress/gutenberg/pull/65450)) +- Remove extra filtering of empty sources. ([65447](https://github.com/WordPress/gutenberg/pull/65447)) + +#### Block Editor +- Remove the 'PrivateInserter' component. ([65111](https://github.com/WordPress/gutenberg/pull/65111)) +- Use the tooltip from a button in 'ButtonBlockAppender'. ([65113](https://github.com/WordPress/gutenberg/pull/65113)) +- Remove unused css selectors. ([65276](https://github.com/WordPress/gutenberg/pull/65276)) + +### Tools + +- Scripts: Update stylelint dependency and the default configuration. ([64828](https://github.com/WordPress/gutenberg/pull/64828)) +- Styleling config: Fix stylelint configuration missing files for npm. ([65313](https://github.com/WordPress/gutenberg/pull/65313)) + +#### Build Tooling +- Build Plugin: Simplify and improve zip contents. ([65232](https://github.com/WordPress/gutenberg/pull/65232)) +- Build zip artifact on release and wp production branches. ([65471](https://github.com/WordPress/gutenberg/pull/65471)) +- Build: Include Core blocks' `render` and `variations` files. ([63311](https://github.com/WordPress/gutenberg/pull/63311)) +- Script Modules + - Prepare build for more script modules. ([65064](https://github.com/WordPress/gutenberg/pull/65064)) + - Remove babel from script-modules build. ([65279](https://github.com/WordPress/gutenberg/pull/65279)) + - Remove es-module shims and importmap-polyfill. ([65210](https://github.com/WordPress/gutenberg/pull/65210)) +- Correctly generate PHP files for server-side rendering of blocks on Windows OS. ([65248](https://github.com/WordPress/gutenberg/pull/65248)) +- Packages: Only add polyfills where needed. ([65292](https://github.com/WordPress/gutenberg/pull/65292)) +- Switch from UglifyJS to Terser to build the polyfill script. ([65278](https://github.com/WordPress/gutenberg/pull/65278)) + +#### Testing +- Unit tests: Mock matchMedia to enforce prefers-reduce-motion. ([65438](https://github.com/WordPress/gutenberg/pull/65438)) +- Upgrade Playwright to v1.47. ([65156](https://github.com/WordPress/gutenberg/pull/65156)) + +## First-time contributors + +The following PRs were merged by first-time contributors: + +- @AKSHAT2802: Add __next40pxDefaultSize for files in editor 4. ([65140](https://github.com/WordPress/gutenberg/pull/65140)) +- @devansh016: Automatically add tel to phone number when linking URL. ([64865](https://github.com/WordPress/gutenberg/pull/64865)) +- @dhruvang21: Fix: Button Replace remaining 40px default size violations [Edit widgets]. ([65367](https://github.com/WordPress/gutenberg/pull/65367)) +- @farid-hadi: Docs: Fix minor typos in Build your first block tutorial. ([64961](https://github.com/WordPress/gutenberg/pull/64961)) +- @greenworld: Fix typo in Slot Fills documentation. ([65275](https://github.com/WordPress/gutenberg/pull/65275)) +- @louwie17: Convert EmbedPreview component to functional component. ([51325](https://github.com/WordPress/gutenberg/pull/51325)) +- @rahulharpal1603: URLInput: Replace input with InputControl. ([65158](https://github.com/WordPress/gutenberg/pull/65158)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @AKSHAT2802 @Aljullu @andrewserong @carolinan @cbravobernal @ciampo @colorful-tones @creativecoder @DaniGuardiola @DAreRodz @devansh016 @dhruvang21 @ellatrix @farid-hadi @getdave @gigitux @greenworld @gziolo @hbhalodia @jameskoster @jasmussen @javierarce @jeryj @jorgefilipecosta @jsnajdr @kevin940726 @louwie17 @madhusudhand @MaggieCabrera @Mamaduka @mikeybinns @mirka @ntsekouras @oandregal @ockham @peterwilsoncc @rahulharpal1603 @ramonjd @richtabor @rohitmathur-7 @SantosGuillamot @scruffian @sgomes @sirreal @stokesman @swissspidy @t-hamano @talldan @vipul0425 @zaguiini + + = 19.3.0-rc.1 = ## Changelog From 7cdf002a49c1c4ed3aec46111ace013d0f5a2e70 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:15:02 +1000 Subject: [PATCH 07/53] Post Navigation Link: Add block examples (#65552) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- .../src/post-navigation-link/block.json | 6 ++++++ .../src/post-navigation-link/variations.js | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/block-library/src/post-navigation-link/block.json b/packages/block-library/src/post-navigation-link/block.json index ce733759846fee..5f1b295119822a 100644 --- a/packages/block-library/src/post-navigation-link/block.json +++ b/packages/block-library/src/post-navigation-link/block.json @@ -34,6 +34,12 @@ "default": "" } }, + "example": { + "attributes": { + "label": "Next post", + "arrow": "arrow" + } + }, "usesContext": [ "postType" ], "supports": { "reusable": false, diff --git a/packages/block-library/src/post-navigation-link/variations.js b/packages/block-library/src/post-navigation-link/variations.js index 945d6eb550f276..4f52b21338af1e 100644 --- a/packages/block-library/src/post-navigation-link/variations.js +++ b/packages/block-library/src/post-navigation-link/variations.js @@ -15,6 +15,12 @@ const variations = [ icon: next, attributes: { type: 'next' }, scope: [ 'inserter', 'transform' ], + example: { + attributes: { + label: 'Next post', + arrow: 'arrow', + }, + }, }, { name: 'post-previous', @@ -25,6 +31,12 @@ const variations = [ icon: previous, attributes: { type: 'previous' }, scope: [ 'inserter', 'transform' ], + example: { + attributes: { + label: 'Previous post', + arrow: 'arrow', + }, + }, }, ]; From 2dc9bc5d2349de9c5eeceb30e74a886bb6130226 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:16:33 +1000 Subject: [PATCH 08/53] Term Description: Add block example (#65553) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/term-description/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/term-description/index.js b/packages/block-library/src/term-description/index.js index 0ff710a91f5d50..330ca05bd174e1 100644 --- a/packages/block-library/src/term-description/index.js +++ b/packages/block-library/src/term-description/index.js @@ -16,6 +16,7 @@ export { metadata, name }; export const settings = { icon, edit, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From dad62c622eadfdf71640fb9677aad8e14359ca3e Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:21:44 +1000 Subject: [PATCH 09/53] Query Title: Add block example (#65554) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/query-title/block.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/block-library/src/query-title/block.json b/packages/block-library/src/query-title/block.json index de3e60214685c2..5d5c9113bda084 100644 --- a/packages/block-library/src/query-title/block.json +++ b/packages/block-library/src/query-title/block.json @@ -29,6 +29,11 @@ "default": true } }, + "example": { + "attributes": { + "type": "search" + } + }, "supports": { "align": [ "wide", "full" ], "html": false, From 4209ffdb8e7f61d822c23860cd6dc3812ba97c31 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:21:16 +1000 Subject: [PATCH 10/53] Query Pagination: Add block example (#65556) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/query-pagination/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/query-pagination/index.js b/packages/block-library/src/query-pagination/index.js index b113a8384b043b..158106c4ac185a 100644 --- a/packages/block-library/src/query-pagination/index.js +++ b/packages/block-library/src/query-pagination/index.js @@ -20,6 +20,7 @@ export const settings = { edit, save, deprecated, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From 5284920adb7c34e6c7e1fd9637932dc22472ddc6 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:22:00 +1000 Subject: [PATCH 11/53] Query No Results: Add block example (#65555) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/query-no-results/block.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json index 8f3ba56adcc36a..2f656594afa306 100644 --- a/packages/block-library/src/query-no-results/block.json +++ b/packages/block-library/src/query-no-results/block.json @@ -8,6 +8,16 @@ "parent": [ "core/query" ], "textdomain": "default", "usesContext": [ "queryId", "query" ], + "example": { + "innerBlocks": [ + { + "name": "core/paragraph", + "attributes": { + "content": "No posts were found." + } + } + ] + }, "supports": { "align": true, "reusable": false, From 5afd08f7e8cbf4ed96956e893995f5a0f969ce92 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:14:19 +1000 Subject: [PATCH 12/53] Comments Title: Add block example (#65557) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/comments-title/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/comments-title/index.js b/packages/block-library/src/comments-title/index.js index 86bdab0dbccbff..69b8228eab892b 100644 --- a/packages/block-library/src/comments-title/index.js +++ b/packages/block-library/src/comments-title/index.js @@ -18,6 +18,7 @@ export const settings = { icon, edit, deprecated, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From 842b86a1e3c9f26ec7e0cd27dace1ee899787fcc Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:00:07 +1000 Subject: [PATCH 13/53] Comment Author Name: Add block example (#65558) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/comment-author-name/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/comment-author-name/index.js b/packages/block-library/src/comment-author-name/index.js index 4d85bbebe047be..5bcb6896564807 100644 --- a/packages/block-library/src/comment-author-name/index.js +++ b/packages/block-library/src/comment-author-name/index.js @@ -18,6 +18,7 @@ export const settings = { icon, edit, deprecated, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From a1f8bb5139a746a695700c1dff927addfee8ea3f Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 23 Sep 2024 12:21:29 +0200 Subject: [PATCH 14/53] iAPI: Refactor types and add a "Core Concepts - Using TypeScript" guide (#64577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial version * Add a section with instructions to install the package * Add basic create-block template for typescript * Fix all the types * Reorganize the sections on how to type the store * Add a test for when the store is divided into multiple parts * Update the example of when the store is divided into multiple parts * Try a `typed` function to type promises * Refactor derived state and async actions * Add an example casting the entire state * Add entry to the changelog * Add last section and conclusion * Remove the template * Add basic JSDoc for the typed function * Export `typed` * Delete template from manifest * Add the skeleton for TypeScript variant of the template * Add types and global state to the template * Add the scaffolding section again * Add a note and `@since` annotation to the `typed` function to ensure people don’t use it before WP 6.7 * Added typescript guide to the README of Core Concepts, toc.json and manifest.json * Improve iapi type "tests" * Add tsconfig for type tests * REVERTME: Introduce intentional type error * Revert "REVERTME: Introduce intentional type error" This reverts commit ac5590f0244148a75db0871c16e34548642b10f6. * fixup! Merge branch 'trunk' into iapi-docs-typescript-guide * Remove typed function * Remove references to `typed` function from docs * Update and fix changelog --------- Co-authored-by: Grzegorz Ziółkowski Co-authored-by: JuanMa Garrido Co-authored-by: Jon Surrell Co-authored-by: JuanMa Co-authored-by: luisherranz Co-authored-by: gziolo Co-authored-by: juanmaguitar Co-authored-by: sirreal Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: adamziel Co-authored-by: michalczaplinski Co-authored-by: spencerfinnell Co-authored-by: ryanwelcher --- docs/manifest.json | 6 + .../interactivity-api/core-concepts/README.md | 2 + .../core-concepts/using-typescript.md | 746 ++++++++++++++++++ docs/toc.json | 3 + .../CHANGELOG.md | 6 +- .../README.md | 2 +- .../block-templates/README.md.mustache | 2 - .../block-templates/render.php.mustache | 23 +- .../block-templates/style.scss.mustache | 15 + .../block-templates/view.js.mustache | 19 +- .../block-templates/view.ts.mustache | 46 ++ .../index.js | 11 +- packages/interactivity/CHANGELOG.md | 1 + packages/interactivity/src/hooks.tsx | 10 +- packages/interactivity/src/store.ts | 63 +- packages/interactivity/src/test/store.ts | 286 +++++++ packages/interactivity/tsconfig.test.json | 13 + tsconfig.json | 1 + 18 files changed, 1230 insertions(+), 25 deletions(-) create mode 100644 docs/reference-guides/interactivity-api/core-concepts/using-typescript.md create mode 100644 packages/create-block-interactive-template/block-templates/view.ts.mustache create mode 100644 packages/interactivity/src/test/store.ts create mode 100644 packages/interactivity/tsconfig.test.json diff --git a/docs/manifest.json b/docs/manifest.json index d7f74d47995b63..d76717fbdedfc1 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -521,6 +521,12 @@ "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md", "parent": "core-concepts" }, + { + "title": "Using TypeScript", + "slug": "using-typescript", + "markdown_source": "../docs/reference-guides/interactivity-api/core-concepts/using-typescript.md", + "parent": "core-concepts" + }, { "title": "Quick start guide", "slug": "iapi-quick-start-guide", diff --git a/docs/reference-guides/interactivity-api/core-concepts/README.md b/docs/reference-guides/interactivity-api/core-concepts/README.md index f4e6891c4ff165..695a4d622f6c52 100644 --- a/docs/reference-guides/interactivity-api/core-concepts/README.md +++ b/docs/reference-guides/interactivity-api/core-concepts/README.md @@ -7,3 +7,5 @@ This section provides some guides on important concepts and mental models relate 2. **[Understanding global state, local context and derived state](/docs/reference-guides/interactivity-api/core-concepts/undestanding-global-state-local-context-and-derived-state.md):** The guide explains how to effectively use global state, local context, and derived state within the Interactivity API emphasizing the importance of choosing the appropriate state management technique based on the scope and requirements of your data. 3. **[Server-side rendering: Processing directives on the server](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md):** The Interactivity API allows WordPress to use server-side rendering to create interactive and state-aware HTML, smoothly connected with client-side features while maintaining performance and SEO benefits. + +4. **[Using TypeScript](/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md):** This guide will walk you through the process of using TypeScript with Interactivity API stores, covering everything from basic type definitions to advanced techniques for handling complex store structures. diff --git a/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md b/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md new file mode 100644 index 00000000000000..ed0bdd88211d11 --- /dev/null +++ b/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md @@ -0,0 +1,746 @@ +# Using TypeScript + +The Interactivity API provides robust support for TypeScript, enabling developers to build type-safe stores to enhance the development experience with static type checking, improved code completion, and simplified refactoring. This guide will walk you through the process of using TypeScript with Interactivity API stores, covering everything from basic type definitions to advanced techniques for handling complex store structures. + +These are the core principles of TypeScript's interaction with the Interactivity API: + +- **Inferred client types**: When you create a store using the `store` function, TypeScript automatically infers the types of the store's properties (`state`, `actions`, etc.). This means that you can often get away with just writing plain JavaScript objects, and TypeScript will figure out the types for you. +- **Explicit server types**: When dealing with data defined on the server, like local context or the initial values of the global state, you can explicitly define its types to ensure that everything is correctly typed. +- **Mutiple store parts**: Even if your store is split into multiple parts, you can define or infer the types of each part of the store and then merge them into a single type that represents the entire store. +- **Typed external stores**: You can import typed stores from external namespaces, allowing you to use other plugins' functionality with type safety. + +## Installing `@wordpress/interactivity` locally + +If you haven't done so already, you need to install the package `@wordpress/interactivity` locally so TypeScript can use its types in your IDE. You can do this using the following command: + +`npm install @wordpress/interactivity` + +It is also a good practice to keep that package updated. + +## Scaffolding a new typed interactive block + +If you want to explore an example of an interactive block using TypeScript in your local environment, you can use the `@wordpress/create-block-interactive-template`. + +Start by ensuring you have Node.js and `npm` installed on your computer. Review the [Node.js development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/nodejs-development-environment/) guide if not. + +Next, use the [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package and the [`@wordpress/create-block-interactive-template`](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) template to scaffold the block. + +Choose the folder where you want to create the plugin, execute the following command in the terminal from within that folder, and choose the `typescript` variant when asked. + +``` +npx @wordpress/create-block@latest --template @wordpress/create-block-interactive-template +``` + +**Important**: Do not provide a slug in the terminal. Otherwise, `create-block` will not ask you which variant you want to choose and it will select the default non-TypeScript variant by default. + +Finally, you can keep following the instructions in the [Getting Started Guide](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/iapi-quick-start-guide/) as the rest of the instructions remain the same. + +## Typing the store + +Depending on the structure of your store and your preference, there are three options you can choose from to generate your store's types: + +1. Infer the types from your client store definition. +2. Manually type the server state, but infer the rest from your client store definition. +3. Manually write all the types. + +### 1. Infer the types from your client store definition + +When you create a store using the `store` function, TypeScript automatically infers the types of the store's properties (`state`, `actions`, `callbacks`, etc.). This means that you can often get away with just writing plain JavaScript objects, and TypeScript will figure out the correct types for you. + +Let's start with a basic example of a counter block. We will define the store in the `view.ts` file of the block, which contains the initial global state, an action and a callback. + +```ts +// view.ts +const myStore = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + increment() { + myStore.state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ myStore.state.counter }` ); + }, + }, +} ); +``` + +If you inspect the types of `myStore` using TypeScript, you will see that TypeScript has been able to infer the types correctly. + +```ts +const myStore: { + state: { + counter: number; + }; + actions: { + increment(): void; + }; + callbacks: { + log(): void; + }; +}; +``` + +You can also destructure the `state`, `actions` and `callbacks` properties, and the types will still work correctly. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + increment() { + state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ state.counter }` ); + }, + }, +} ); +``` + +In conclusion, inferring the types is useful when you have a simple store defined in a single call to the `store` function and you do not need to type any state that has been initialized on the server. + +### 2. Manually type the server state, but infer the rest from your client store definition + +The global state that is initialized on the server with the `wp_interactivity_state` function doesn't exist on your client store definition and, therefore, needs to be manually typed. But if you don't want to define all the types of your store, you can infer the types of your client store definition and merge them with the types of your server initialized state. + +_Please, visit [the Server-side Rendering guide](/docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md) to learn more about `wp_interactivity_state` and how directives are processed on the server._ + +Following our previous example, let's move our `counter` state initialization to the server. + +```php +wp_interactivity_state( 'myCounterPlugin', array( + 'counter' => 1, +)); +``` + +Now, let's define the server state types and merge it with the types inferred from the client store definition. + +```ts +// Types the server state. +type ServerState = { + state: { + counter: number; + }; +}; + +// Defines the store in a variable to be able to extract its type later. +const storeDef = { + actions: { + increment() { + state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ state.counter }` ); + }, + }, +}; + +// Merges the types of the server state and the client store definition. +type Store = ServerState & typeof storeDef; + +// Injects the final types when calling the `store` function. +const { state } = store< Store >( 'myCounterPlugin', storeDef ); +``` + +Alternatively, if you don't mind typing the entire state including both the values defined on the server and the values defined on the client, you can cast the `state` property and let TypeScript infer the rest of the store. + +Let's imagine you have an additional property in the client global state called `product`. + +```ts +type State = { + counter: number; // The server state. + product: number; // The client state. +}; + +const { state } = store( 'myCounterPlugin', { + state: { + product: 2, + } as State, // Casts the entire state manually. + actions: { + increment() { + state.counter * state.product; + }, + }, +} ); +``` + +That's it. Now, TypeScript will infer the types of the `actions` and `callbacks` properties from the store definition, but it will use the type `State` for the `state` property so it contains the correct types from both the client and server definitions. + +In conclusion, this approach is useful when you have a server state that needs to be manually typed, but you still want to infer the types of the rest of the store. + +### 3. Manually write all the types + +If you prefer to define all the types of the store manually instead of letting TypeScript infer them from your client store definition, you can do that too. You simply need to pass them to the `store` function. + +```ts +// Defines the store types. +interface Store { + state: { + counter: number; // Initial server state + }; + actions: { + increment(): void; + }; + callbacks: { + log(): void; + }; +} + +// Pass the types when calling the `store` function. +const { state } = store< Store >( 'myCounterPlugin', { + actions: { + increment() { + state.counter += 1; + }, + }, + callbacks: { + log() { + console.log( `counter: ${ state.counter }` ); + }, + }, +} ); +``` + +That's it! In conclusion, this approach is useful when you want to control all the types of your store and you don't mind writing them by hand. + +## Typing the local context + +The initial local context is defined on the server using the `data-wp-context` directive. + +```html +
...
+``` + +For that reason, you need to define its type manually and pass it to the `getContext` function to ensure the returned properties are correctly typed. + +```ts +// Defines the types of your context. +type MyContext = { + counter: number; +}; + +store( 'myCounterPlugin', { + actions: { + increment() { + // Passes it to the getContext function. + const context = getContext< MyContext >(); + // Now `context` is properly typed. + context.counter += 1; + }, + }, +} ); +``` + +To avoid having to pass the context types over and over, you can also define a typed function and use that function instead of `getContext`. + +```ts +// Defines the types of your context. +type MyContext = { + counter: number; +}; + +// Defines a typed function. You only have to do this once. +const getMyContext = getContext< MyContext >; + +store( 'myCounterPlugin', { + actions: { + increment() { + // Use your typed function. + const context = getMyContext(); + // Now `context` is properly typed. + context.counter += 1; + }, + }, +} ); +``` + +That's it! Now you can access the context properties with the correct types. + +## Typing the derived state + +The derived state is data that is calculated based on the global state or local context. In the client store definition, it is defined using a getter in the `state` object. + +_Please, visit the [Understanding global state, local context and derived state](./undestanding-global-state-local-context-and-derived-state.md) guide to learn more about how derived state works in the Interactivity API._ + +Following our previous example, let's create a derived state that is the double of our counter. + +```ts +type MyContext = { + counter: number; +}; + +const myStore = store( 'myCounterPlugin', { + state: { + get double() { + const { counter } = getContext< MyContext >(); + return counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; // This type is number. + }, + }, +} ); +``` + +Normally, when the derived state depends on the local context, TypeScript will be able to infer the correct types: + +```ts +const myStore: { + state: { + readonly double: number; + }; + actions: { + increment(): void; + }; +}; +``` + +But when the return value of the derived state depends directly on some part of the global state, TypeScript will not be able to infer the types because it will claim that it has a circular reference. + +For example, in this case, TypeScript cannot infer the type of `state.double` because it depends on `state.counter`, and the type of `state` is not completed until the type of `state.double` is defined, creating a circular reference. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 0, + get double() { + // TypeScript can't infer this return type because it depends on `state`. + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; // This type is now unknown. + }, + }, +} ); +``` + +In this case, depending on your TypeScript configuration, TypeScript will either warn you about a circular reference or simply add the `any` type to the `state` property. + +However, solving this problem is easy; we simply need to manually provide TypeScript with the return type of that getter. Once we do that, the circular reference disappears, and TypeScript can once again infer all the `state` types. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 1, + get double(): number { + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; // Correctly inferred! + }, + }, +} ); +``` + +These are now the correct inferred types for the previous store. + +```ts +const myStore: { + state: { + counter: number; + readonly double: number; + }; + actions: { + increment(): void; + }; +}; +``` + +When using `wp_interactivity_state` in the server, remember that you also need to define the initial value of your derived state, like this: + +```php +wp_interactivity_state( 'myCounterPlugin', array( + 'counter' => 1, + 'double' => 2, +)); +``` + +But if you are inferring the types, you don't need to manually define the type of the derived state because it already exists in your client's store definition. + +```ts +// You don't need to type `state.double` here. +type ServerState = { + state: { + counter: number; + }; +}; + +// The `state.double` type is inferred from here. +const storeDef = { + state: { + get double(): number { + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; + }, + }, +}; + +// Merges the types of the server state and the client store definition. +type Store = ServerState & typeof storeDef; + +// Injects the final types when calling the `store` function. +const { state } = store< Store >( 'myCounterPlugin', storeDef ); +``` + +That's it! Now you can access the derived state properties with the correct types. + +## Typing asynchronous actions + +Another thing to keep in mind when using TypeScript with the Interactivity API is that asynchronous actions must be defined with generators instead of async functions. + +The reason for using generators in the Interactivity API's asynchronous actions is to be able to restore the scope from the initially triggered action once the asynchronous action continues its execution after yielding. But this is a syntax change only, otherwise, **these functions operate just like regular async functions**, and the inferred types from the `store` function reflect this. + +Following our previous example, let's add an asynchronous action to the store. + +```ts +const { state } = store( 'myCounterPlugin', { + state: { + counter: 0, + get double(): number { + return state.counter * 2; + }, + }, + actions: { + increment() { + state.counter += 1; + }, + *delayedIncrement() { + yield new Promise( ( r ) => setTimeout( r, 1000 ) ); + state.counter += 1; + }, + }, +} ); +``` + +The inferred types for this store are: + +```ts +const myStore: { + state: { + counter: number; + readonly double: number; + }; + actions: { + increment(): void; + // This behaves like a regular async function. + delayedIncrement(): Promise< void >; + }; +}; +``` + +This also means that you can use your async actions in external functions, and TypeScript will correctly use the async function types. + +```ts +const someAsyncFunction = async () => { + // This works fine and it's correctly typed. + await actions.delayedIncrement( 2000 ); +}; +``` + +When you are not inferring types but manually writing the types for your entire store, you can use async function types for your async actions. + +```ts +type Store = { + state: { + counter: number; + readonly double: number; + }; + actions: { + increment(): void; + delayedIncrement(): Promise< void >; // You can use async functions here. + }; +}; +``` + +There's something to keep in mind when when using asynchronous actions. Just like with the derived state, if the asynchronous action needs to return a value and this value directly depends on some part of the global state, TypeScript will not be able to infer the type due to a circular reference. + + ```ts + const { state, actions } = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + *delayedReturn() { + yield new Promise( ( r ) => setTimeout( r, 1000 ) ); + return state.counter; // TypeScript can't infer this return type. + }, + }, + } ); + ``` + + In this case, just as we did with the derived state, we must manually type the return value of the generator. + + ```ts + const { state, actions } = store( 'myCounterPlugin', { + state: { + counter: 0, + }, + actions: { + *delayedReturn(): Generator< uknown, number, uknown > { + yield new Promise( ( r ) => setTimeout( r, 1000 ) ); + return state.counter; // Now this is correctly inferred. + }, + }, + } ); + ``` + + That's it! Remember that the return type of a Generator is the second generic argument: `Generator< unknown, ReturnType, unknown >`. + +## Typing stores that are divided into multiple parts + +Sometimes, stores can be divided into different files. This can happen when different blocks share the same namespace, with each block loading the part of the store it needs. + +Let's look at an example of two blocks: + +- `todo-list`: A block that displays a list of todos. +- `add-post-to-todo`: A block that shows a button to add a new todo item to the list with the text "Read {$post_title}". + +First, let's initialize the global and derived state of the `todo-list` block on the server. + +```php + $todos, + 'filter' => 'all', + 'filteredTodos' => $todos, +)); +?> + + +``` + +Now, let's type the server state and add the client store definition. Remember, `filteredTodos` is derived state, so you don't need to type it manually. + +```ts +// todo-list-block/view.ts +type ServerState = { + state: { + todos: string[]; + filter: 'all' | 'completed'; + }; +}; + +const todoList = { + state: { + get filteredTodos(): string[] { + return state.filter === 'completed' + ? state.todos.filter( ( todo ) => todo.includes( '✅' ) ) + : state.todos; + }, + }, + actions: { + addTodo( todo: string ) { + state.todos.push( todo ); + }, + }, +}; + +// Merges the inferred types with the server state types. +export type TodoList = ServerState & typeof todoList; + +// Injects the final types when calling the `store` function. +const { state } = store< TodoList >( 'myTodoPlugin', todoList ); +``` + +So far, so good. Now let's create our `add-post-to-todo` block. + +First, let's add the current post title to the server state. + +```php + get_the_title(), +)); +?> + + +``` + +Now, let's type that server state and add the client store definition. + +```ts +// add-post-to-todo-block/view.ts +type ServerState = { + state: { + postTitle: string; + }; +}; + +const addPostToTodo = { + actions: { + addPostToTodo() { + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! state.todos.includes( todo ) ) { + actions.addTodo( todo ); + } + }, + }, +}; + +// Merges the inferred types with the server state types. +type Store = ServerState & typeof addPostToTodo; + +// Injects the final types when calling the `store` function. +const { state, actions } = store< Store >( 'myTodoPlugin', addPostToTodo ); +``` + +This works fine in the browser, but TypeScript will complain that, in this block, `state` and `actions` do not include `state.todos` and `actions.addtodo`. + +To fix this, we need to import the `TodoList` type from the `todo-list` block and merge it with the other types. + +```ts +import type { TodoList } from '../todo-list-block/view'; + +// ... + +// Merges the inferred types inferred the server state types. +type Store = TodoList & ServerState & typeof addPostToTodo; +``` + +That's it! Now TypeScript will know that `state.todos` and `actions.addTodo` are available in the `add-post-to-todo` block. + +This approach allows the `add-post-to-todo` block to interact with the existing todo list while maintaining type safety and adding its own functionality to the shared store. + +If you need to use the `add-post-to-todo` types in the `todo-list` block, you simply have to export its types and import them in the other `view.ts` file. + +Finally, if you prefer to define all types manually instead of inferring them, you can define them in a separate file and import that definition into each of your store parts. Here's how you could do that for our todo list example: + +```ts +// types.ts +interface Store { + state: { + todos: string[]; + filter: 'all' | 'completed'; + filtered: string[]; + postTitle: string; + }; + actions: { + addTodo( todo: string ): void; + addPostToTodo(): void; + }; +} + +export default Store; +``` + +```ts +// todo-list-block/view.ts +import type Store from '../types'; + +const { state } = store< Store >( 'myTodoPlugin', { + // Everything is correctly typed here +} ); +``` + +```ts +// add-post-to-todo-block/view.ts +import type Store from '../types'; + +const { state, actions } = store< Store >( 'myTodoPlugin', { + // Everything is correctly typed here +} ); +``` + +This approach allows you to have full control over your types and ensures consistency across all parts of your store. It's particularly useful when you have a complex store structure or when you want to enforce a specific interface across multiple blocks or components. + +## Importing and exporting typed stores + +In the Interactivity API, stores from other namespaces can be accessed using the `store` function. + +Let's go back to our `todo-list` block example, but this time, let's imagine that the `add-post-to-todo` block belongs to a different plugin and therefore will use a different namespace. + +```ts +// Import the store of the `todo-list` block. +const myTodoPlugin = store( 'myTodoPlugin' ); + +store( 'myAddPostToTodoPlugin', { + actions: { + addPostToTodo() { + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! myTodoPlugin.state.todos.includes( todo ) ) { + myTodoPlugin.actions.addTodo( todo ); + } + }, + }, +} ); +``` + +This works fine in the browser, but TypeScript will complain that `myTodoPlugin.state` and `myTodoPlugin.actions` are not typed. + +To fix that, the `myTodoPlugin` plugin can export the result of calling the `store` function with the correct types, and make that available using a script module. + +```ts +// Export the already typed state and actions. +export const { state, actions } = store< TodoList >( 'myTodoPlugin', { + // ... +} ); +``` + +Now, the `add-post-to-todo` block can import the typed store from the `myTodoPlugin` script module, and it not only ensures that the store will be loaded, but that it also contains the correct types. + +```ts +import { store } from '@wordpress/interactivity'; +import { + state as todoState, + actions as todoActions, +} from 'my-todo-plugin-module'; + +store( 'myAddPostToTodoPlugin', { + actions: { + addPostToTodo() { + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! todoState.todos.includes( todo ) ) { + todoActions.addTodo( todo ); + } + }, + }, +} ); +``` + +Remember that you will need to declare the `my-todo-plugin-module` script module as a dependency. + +If the other store is optional and you don't want to load it eagerly, a dynamic import can be used instead of a static import. + +```ts +import { store } from '@wordpress/interactivity'; + +store( 'myAddPostToTodoPlugin', { + actions: { + *addPostToTodo() { + const todoPlugin = yield import( 'my-todo-plugin-module' ); + const todo = `Read: ${ state.postTitle }`.trim(); + if ( ! todoPlugin.state.todos.includes( todo ) ) { + todoPlugin.actions.addTodo( todo ); + } + }, + }, +} ); +``` + +## Conclusion + +In this guide, we explored different approaches to typing the Interactivity API stores, from inferring types automatically to manually defining them. We also covered how to handle server-initialized state, local context, and derived state, as well as how to type asynchronous actions. + +Remember that the choice between inferring types and manually defining them depends on your specific needs and the complexity of your store. Whichever approach you choose, TypeScript will help you build better and more reliable interactive blocks. diff --git a/docs/toc.json b/docs/toc.json index 719ffa344e3744..0d4689811b26ec 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -214,6 +214,9 @@ }, { "docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md": [] + }, + { + "docs/reference-guides/interactivity-api/core-concepts/using-typescript.md": [] } ] }, diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 348c8466836c69..24dcfd52b7b586 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -1,9 +1,11 @@ -## Unreleased - ## 2.8.0 (2024-09-19) +### Enhancements + +- Added TypeScript variant of the template ([#64577](https://github.com/WordPress/gutenberg/pull/64577)). + ## 2.7.0 (2024-09-05) ### Enhancements diff --git a/packages/create-block-interactive-template/README.md b/packages/create-block-interactive-template/README.md index 4417c647495c4c..b50adb49265245 100644 --- a/packages/create-block-interactive-template/README.md +++ b/packages/create-block-interactive-template/README.md @@ -1,6 +1,6 @@ # Create Block Interactive Template -This is a template for [`@wordpress/create-block`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/create-block/README.md) to create interactive blocks +This is a template for [`@wordpress/create-block`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/create-block/README.md) to create interactive blocks. ## Usage diff --git a/packages/create-block-interactive-template/block-templates/README.md.mustache b/packages/create-block-interactive-template/block-templates/README.md.mustache index 3e64ce8f629a3c..4a13743750f748 100644 --- a/packages/create-block-interactive-template/block-templates/README.md.mustache +++ b/packages/create-block-interactive-template/block-templates/README.md.mustache @@ -3,6 +3,4 @@ > **Note** > Check the [Interactivity API Reference docs in the Block Editor handbook](https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/) to learn more about the Interactivity API. -{{#isBasicVariant}} This block has been created with the `create-block-interactive-template` and shows a basic structure of an interactive block that uses the Interactivity API. -{{/isBasicVariant}} \ No newline at end of file diff --git a/packages/create-block-interactive-template/block-templates/render.php.mustache b/packages/create-block-interactive-template/block-templates/render.php.mustache index 3a41a2981cd8cf..4f84b30dbcdbdd 100644 --- a/packages/create-block-interactive-template/block-templates/render.php.mustache +++ b/packages/create-block-interactive-template/block-templates/render.php.mustache @@ -1,4 +1,3 @@ -{{#isBasicVariant}} false, + 'darkText' => esc_html__( 'Switch to Light', '{{textdomain}}' ), + 'lightText' => esc_html__( 'Switch to Dark', '{{textdomain}}' ), + 'themeText' => esc_html__( 'Switch to Dark', '{{textdomain}}' ), + ) +); ?>
false ) ); ?> data-wp-watch="callbacks.logIsOpen" + data-wp-class--dark-theme="state.isDark" > + +
-{{/isBasicVariant}} diff --git a/packages/create-block-interactive-template/block-templates/style.scss.mustache b/packages/create-block-interactive-template/block-templates/style.scss.mustache index 1c73fa1c38ff94..c8aa9f232136e2 100644 --- a/packages/create-block-interactive-template/block-templates/style.scss.mustache +++ b/packages/create-block-interactive-template/block-templates/style.scss.mustache @@ -9,4 +9,19 @@ font-size: 1em; background: #ffff001a; padding: 1em; + + &.dark-theme { + background: #333; + color: #fff; + + button { + background: #555; + color: #fff; + border: 1px solid #777; + } + + p { + color: #ddd; + } + } } diff --git a/packages/create-block-interactive-template/block-templates/view.js.mustache b/packages/create-block-interactive-template/block-templates/view.js.mustache index b4bae3939461dd..3fcf1ba365d265 100644 --- a/packages/create-block-interactive-template/block-templates/view.js.mustache +++ b/packages/create-block-interactive-template/block-templates/view.js.mustache @@ -1,15 +1,23 @@ -{{#isBasicVariant}} +{{#isDefaultVariant}} /** * WordPress dependencies */ -import { store, getContext } from "@wordpress/interactivity"; +import { store, getContext } from '@wordpress/interactivity'; -store( '{{namespace}}', { +const { state } = store( '{{namespace}}', { + state: { + get themeText() { + return state.isDark ? state.darkText : state.lightText; + } + }, actions: { - toggle: () => { + toggleOpen() { const context = getContext(); context.isOpen = ! context.isOpen; }, + toggleTheme() { + state.isDark = ! state.isDark; + } }, callbacks: { logIsOpen: () => { @@ -19,5 +27,4 @@ store( '{{namespace}}', { }, }, } ); - -{{/isBasicVariant}} +{{/isDefaultVariant}} diff --git a/packages/create-block-interactive-template/block-templates/view.ts.mustache b/packages/create-block-interactive-template/block-templates/view.ts.mustache new file mode 100644 index 00000000000000..11670442d73704 --- /dev/null +++ b/packages/create-block-interactive-template/block-templates/view.ts.mustache @@ -0,0 +1,46 @@ +{{#isTypescriptVariant}} +/** + * WordPress dependencies + */ +import { store, getContext } from '@wordpress/interactivity'; + +type ServerState = { + state: { + isDark: boolean; + darkText: string; + lightText: string; + }; +}; + +type Context = { + isOpen: boolean; +}; + +const storeDef = { + state: { + get themeText(): string { + return state.isDark ? state.darkText : state.lightText; + } + }, + actions: { + toggleOpen() { + const context = getContext< Context >(); + context.isOpen = ! context.isOpen; + }, + toggleTheme() { + state.isDark = ! state.isDark; + } + }, + callbacks: { + logIsOpen: () => { + const { isOpen } = getContext< Context >(); + // Log the value of `isOpen` each time it changes. + console.log( `Is open: ${ isOpen }` ); + }, + }, +}; + +type Store = ServerState & typeof storeDef; + +const { state } = store< Store >( '{{namespace}}', storeDef ); +{{/isTypescriptVariant}} diff --git a/packages/create-block-interactive-template/index.js b/packages/create-block-interactive-template/index.js index bb203b7023e281..94f615df2747f2 100644 --- a/packages/create-block-interactive-template/index.js +++ b/packages/create-block-interactive-template/index.js @@ -7,7 +7,7 @@ module.exports = { defaultValues: { slug: 'example-interactive', title: 'Example Interactive', - description: 'An interactive block with the Interactivity API', + description: 'An interactive block with the Interactivity API.', dashicon: 'media-interactive', npmDependencies: [ '@wordpress/interactivity' ], customPackageJSON: { files: [ '[^.]*' ] }, @@ -24,7 +24,14 @@ module.exports = { }, }, variants: { - basic: {}, + default: {}, + typescript: { + slug: 'example-interactive-typescript', + title: 'Example Interactive TypeScript', + description: + 'An interactive block with the Interactivity API using TypeScript.', + viewScriptModule: 'file:./view.ts', + }, }, pluginTemplatesPath: join( __dirname, 'plugin-templates' ), blockTemplatesPath: join( __dirname, 'block-templates' ), diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 6989bcdc0c802c..42f311973709dd 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -6,6 +6,7 @@ ### Enhancements +- Improve TypeScript support for generators ([#64577](https://github.com/WordPress/gutenberg/pull/64577)). - Refactor internal context proxies implementation ([#64713](https://github.com/WordPress/gutenberg/pull/64713)). ### Bug Fixes diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 44dc2645da2c80..6b55ec014aa799 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -190,9 +190,13 @@ const resolve = ( path: string, namespace: string ) => { } let resolvedStore = stores.get( namespace ); if ( typeof resolvedStore === 'undefined' ) { - resolvedStore = store( namespace, undefined, { - lock: universalUnlock, - } ); + resolvedStore = store( + namespace, + {}, + { + lock: universalUnlock, + } + ); } const current = { ...resolvedStore, diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index b1ad07459c62c0..b147e0f61163bf 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -84,6 +84,42 @@ interface StoreOptions { lock?: boolean | string; } +type Prettify< T > = { [ K in keyof T ]: T[ K ] } & {}; +type DeepPartial< T > = T extends object + ? { [ P in keyof T ]?: DeepPartial< T[ P ] > } + : T; +type DeepPartialState< T extends { state: object } > = Omit< T, 'state' > & { + state?: DeepPartial< T[ 'state' ] >; +}; +type ConvertGeneratorToPromise< T > = T extends ( + ...args: infer A +) => Generator< any, infer R, any > + ? ( ...args: A ) => Promise< R > + : never; +type ConvertGeneratorsToPromises< T > = { + [ K in keyof T ]: T[ K ] extends ( ...args: any[] ) => any + ? ConvertGeneratorToPromise< T[ K ] > extends never + ? T[ K ] + : ConvertGeneratorToPromise< T[ K ] > + : T[ K ] extends object + ? Prettify< ConvertGeneratorsToPromises< T[ K ] > > + : T[ K ]; +}; +type ConvertPromiseToGenerator< T > = T extends ( + ...args: infer A +) => Promise< infer R > + ? ( ...args: A ) => Generator< any, R, any > + : never; +type ConvertPromisesToGenerators< T > = { + [ K in keyof T ]: T[ K ] extends ( ...args: any[] ) => any + ? ConvertPromiseToGenerator< T[ K ] > extends never + ? T[ K ] + : ConvertPromiseToGenerator< T[ K ] > + : T[ K ] extends object + ? Prettify< ConvertPromisesToGenerators< T[ K ] > > + : T[ K ]; +}; + export const universalUnlock = 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; @@ -132,17 +168,34 @@ export const universalUnlock = * * @return A reference to the namespace content. */ -export function store< S extends object = {} >( + +// Overload for when the types are inferred. +export function store< T extends object >( + namespace: string, + storePart: T, + options?: StoreOptions +): Prettify< ConvertGeneratorsToPromises< T > >; + +// Overload for when types are passed via generics and they contain state. +export function store< T extends { state: object } >( + namespace: string, + storePart: ConvertPromisesToGenerators< DeepPartialState< T > >, + options?: StoreOptions +): Prettify< ConvertGeneratorsToPromises< T > >; + +// Overload for when types are passed via generics and they don't contain state. +export function store< T extends object >( namespace: string, - storePart?: S, + storePart: ConvertPromisesToGenerators< T >, options?: StoreOptions -): S; +): Prettify< ConvertGeneratorsToPromises< T > >; +// Overload for when types are divided into multiple parts. export function store< T extends object >( namespace: string, - storePart?: T, + storePart: ConvertPromisesToGenerators< DeepPartial< T > >, options?: StoreOptions -): T; +): Prettify< ConvertGeneratorsToPromises< T > >; export function store( namespace: string, diff --git a/packages/interactivity/src/test/store.ts b/packages/interactivity/src/test/store.ts new file mode 100644 index 00000000000000..1092001db03143 --- /dev/null +++ b/packages/interactivity/src/test/store.ts @@ -0,0 +1,286 @@ +/** + * Internal dependencies + */ +import { store } from '../store'; + +describe( 'Interactivity API', () => { + describe( 'store', () => { + it( 'dummy test', () => { + expect( true ).toBe( true ); + } ); + + describe( 'types', () => { + describe( 'the whole store can be inferred', () => { + // eslint-disable-next-line no-unused-expressions + async () => { + const myStore = store( 'test', { + state: { + clientValue: 1, + get derived(): number { + return myStore.state.clientValue; + }, + }, + actions: { + sync( n: number ) { + return n; + }, + *async( n: number ) { + const n1: number = + yield myStore.actions.sync( n ); + return myStore.state.derived + n1 + n; + }, + }, + } ); + + myStore.state.clientValue satisfies number; + myStore.state.derived satisfies number; + + // @ts-expect-error + myStore.state.nonExistent satisfies number; + myStore.actions.sync( 1 ) satisfies number; + myStore.actions.async( 1 ) satisfies Promise< number >; + ( await myStore.actions.async( 1 ) ) satisfies number; + + // @ts-expect-error + myStore.actions.nonExistent() satisfies {}; + }; + } ); + + describe( 'the whole store can be manually typed', () => { + // eslint-disable-next-line no-unused-expressions + async () => { + interface Store { + state: { + clientValue: number; + serverValue: number; + readonly derived: number; + }; + actions: { + sync: ( n: number ) => number; + async: ( n: number ) => Promise< number >; + }; + } + + const myStore = store< Store >( 'test', { + state: { + clientValue: 1, + // @ts-expect-error + nonExistent: 2, + get derived(): number { + return myStore.state.serverValue; + }, + }, + actions: { + sync( n ) { + return n; + }, + *async( n ): Generator< unknown, number, unknown > { + const n1 = myStore.actions.sync( n ); + return myStore.state.derived + n1 + n; + }, + }, + } ); + + myStore.state.clientValue satisfies number; + myStore.state.serverValue satisfies number; + myStore.state.derived satisfies number; + // @ts-expect-error + myStore.state.nonExistent satisfies number; + myStore.actions.sync( 1 ) satisfies number; + myStore.actions.async( 1 ) satisfies Promise< number >; + ( await myStore.actions.async( 1 ) ) satisfies number; + // @ts-expect-error + myStore.actions.nonExistent(); + }; + } ); + + describe( 'the server state can be typed and the rest inferred', () => { + // eslint-disable-next-line no-unused-expressions + async () => { + type ServerStore = { + state: { + serverValue: number; + }; + }; + + const clientStore = { + state: { + clientValue: 1, + get derived(): number { + return myStore.state.serverValue; + }, + }, + actions: { + sync( n: number ) { + return n; + }, + *async( + n: number + ): Generator< unknown, number, number > { + const n1: number = + yield myStore.actions.sync( n ); + return myStore.state.derived + n1 + n; + }, + }, + }; + + type Store = ServerStore & typeof clientStore; + + const myStore = store< Store >( 'test', clientStore ); + + myStore.state.clientValue satisfies number; + myStore.state.serverValue satisfies number; + myStore.state.derived satisfies number; + // @ts-expect-error + myStore.state.nonExistent satisfies number; + myStore.actions.sync( 1 ) satisfies number; + myStore.actions.async( 1 ) satisfies Promise< number >; + ( await myStore.actions.async( 1 ) ) satisfies number; + // @ts-expect-error + myStore.actions.nonExistent(); + }; + } ); + + describe( 'the state can be casted and the rest inferred', () => { + // eslint-disable-next-line no-unused-expressions + async () => { + type State = { + clientValue: number; + serverValue: number; + derived: number; + }; + + const myStore = store( 'test', { + state: { + clientValue: 1, + get derived(): number { + return myStore.state.serverValue; + }, + } as State, + actions: { + sync( n: number ) { + return n; + }, + *async( + n: number + ): Generator< unknown, number, number > { + const n1: number = + yield myStore.actions.sync( n ); + return myStore.state.derived + n1 + n; + }, + }, + } ); + + myStore.state.clientValue satisfies number; + myStore.state.serverValue satisfies number; + myStore.state.derived satisfies number; + // @ts-expect-error + myStore.state.nonExistent satisfies number; + myStore.actions.sync( 1 ) satisfies number; + myStore.actions.async( 1 ) satisfies Promise< number >; + ( await myStore.actions.async( 1 ) ) satisfies number; + // @ts-expect-error + myStore.actions.nonExistent() satisfies {}; + }; + } ); + + describe( 'the whole store can be manually typed even if doesnt contain state', () => { + // eslint-disable-next-line no-unused-expressions + async () => { + interface Store { + actions: { + sync: ( n: number ) => number; + async: ( n: number ) => Promise< number >; + }; + callbacks: { + existent: number; + }; + } + + const myStore = store< Store >( 'test', { + actions: { + sync( n ) { + return n; + }, + *async( n ): Generator< unknown, number, number > { + const n1: number = + yield myStore.actions.sync( n ); + return n1 + n; + }, + }, + callbacks: { + existent: 1, + // @ts-expect-error + nonExistent: 1, + }, + } ); + + // @ts-expect-error + myStore.state.nonExistent satisfies number; + myStore.actions.sync( 1 ) satisfies number; + myStore.actions.async( 1 ) satisfies Promise< number >; + ( await myStore.actions.async( 1 ) ) satisfies number; + myStore.callbacks.existent satisfies number; + // @ts-expect-error + myStore.callbacks.nonExistent satisfies number; + // @ts-expect-error + myStore.actions.nonExistent() satisfies {}; + }; + } ); + + describe( 'the store can be divided into multiple parts', () => { + // eslint-disable-next-line no-unused-expressions + async () => { + type ServerState = { + state: { + serverValue: number; + }; + }; + + const firstStorePart = { + state: { + clientValue1: 1, + }, + actions: { + incrementValue1( n = 1 ) { + myStore.state.clientValue1 += n; + }, + }, + }; + + type FirstStorePart = typeof firstStorePart; + + const secondStorePart = { + state: { + clientValue2: 'test', + }, + actions: { + *asyncAction() { + return ( + myStore.state.clientValue1 + + myStore.state.serverValue + ); + }, + }, + }; + + type Store = ServerState & + FirstStorePart & + typeof secondStorePart; + + const myStore = store< Store >( 'test', firstStorePart ); + store( 'test', secondStorePart ); + + myStore.state.clientValue1 satisfies number; + myStore.state.clientValue2 satisfies string; + myStore.actions.incrementValue1( 1 ); + myStore.actions.asyncAction() satisfies Promise< number >; + ( await myStore.actions.asyncAction() ) satisfies number; + + // @ts-expect-error + myStore.state.nonExistent satisfies {}; + }; + } ); + } ); + } ); +} ); diff --git a/packages/interactivity/tsconfig.test.json b/packages/interactivity/tsconfig.test.json new file mode 100644 index 00000000000000..6a90abc2ba2210 --- /dev/null +++ b/packages/interactivity/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "noEmit": true, + "emitDeclarationOnly": false, + "types": [ "jest" ] + }, + "references": [ { "path": "./tsconfig.json" } ], + "files": [ "src/test/store.ts" ], + "exclude": [] +} diff --git a/tsconfig.json b/tsconfig.json index 3ab54f66019bca..8821ef4404e3b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ { "path": "packages/i18n" }, { "path": "packages/icons" }, { "path": "packages/interactivity" }, + { "path": "packages/interactivity/tsconfig.test.json" }, { "path": "packages/interactivity-router" }, { "path": "packages/is-shallow-equal" }, { "path": "packages/keycodes" }, From 7f87eef0ea040be05a02e03b1b5a67f3f77af892 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Mon, 23 Sep 2024 12:43:12 +0200 Subject: [PATCH 15/53] Global Styles: remove navigator screen overrides (#65522) --- .../src/components/global-styles-sidebar/style.scss | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/edit-site/src/components/global-styles-sidebar/style.scss b/packages/edit-site/src/components/global-styles-sidebar/style.scss index b76192ddfcb5ca..4ca87bf200f178 100644 --- a/packages/edit-site/src/components/global-styles-sidebar/style.scss +++ b/packages/edit-site/src/components/global-styles-sidebar/style.scss @@ -22,14 +22,7 @@ flex-direction: column; min-height: 100%; - &__panel, - &__navigator-provider { - display: flex; - flex-direction: column; - flex: 1; - } - - &__navigator-screen { + &__panel { flex: 1; } } From 51d0f20679f22474cea3b4742e1b83d63508a2bb Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 23 Sep 2024 13:46:16 +0100 Subject: [PATCH 16/53] Select Mode: Updates to the block toolbar (#65485) Co-authored-by: youknowriad Co-authored-by: talldan Co-authored-by: andrewserong Co-authored-by: jorgefilipecosta Co-authored-by: noisysocks Co-authored-by: jameskoster --- .../block-controls/use-has-block-controls.js | 35 --- .../components/block-parent-selector/index.js | 23 +- .../block-settings-dropdown.js | 218 ++++++++++-------- .../src/components/block-switcher/index.js | 67 +++--- .../src/components/block-toolbar/index.js | 56 ++--- .../src/components/block-toolbar/style.scss | 11 +- .../block-toolbar/use-has-block-toolbar.js | 50 ++-- .../src/store/private-selectors.js | 1 + packages/block-editor/src/store/utils.js | 2 + 9 files changed, 224 insertions(+), 239 deletions(-) delete mode 100644 packages/block-editor/src/components/block-controls/use-has-block-controls.js diff --git a/packages/block-editor/src/components/block-controls/use-has-block-controls.js b/packages/block-editor/src/components/block-controls/use-has-block-controls.js deleted file mode 100644 index f7884cc1882ed5..00000000000000 --- a/packages/block-editor/src/components/block-controls/use-has-block-controls.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { __experimentalUseSlotFills as useSlotFills } from '@wordpress/components'; -import warning from '@wordpress/warning'; - -/** - * Internal dependencies - */ -import groups from './groups'; - -export function useHasAnyBlockControls() { - let hasAnyBlockControls = false; - for ( const group in groups ) { - // It is safe to violate the rules of hooks here as the `groups` object - // is static and will not change length between renders. Do not return - // early as that will cause the hook to be called a different number of - // times between renders. - // eslint-disable-next-line react-hooks/rules-of-hooks - if ( useHasBlockControls( group ) ) { - hasAnyBlockControls = true; - } - } - return hasAnyBlockControls; -} - -export function useHasBlockControls( group = 'default' ) { - const Slot = groups[ group ]?.Slot; - const fills = useSlotFills( Slot?.__unstableName ); - if ( ! Slot ) { - warning( `Unknown BlockControls group "${ group }" provided.` ); - return null; - } - return !! fills?.length; -} diff --git a/packages/block-editor/src/components/block-parent-selector/index.js b/packages/block-editor/src/components/block-parent-selector/index.js index 80b314eeb42e5c..9090de42f8b7d7 100644 --- a/packages/block-editor/src/components/block-parent-selector/index.js +++ b/packages/block-editor/src/components/block-parent-selector/index.js @@ -14,6 +14,7 @@ import useBlockDisplayInformation from '../use-block-display-information'; import BlockIcon from '../block-icon'; import { useShowHoveredOrFocusedGestures } from '../block-toolbar/utils'; import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; /** * Block parent selector component, displaying the hierarchy of the @@ -23,24 +24,26 @@ import { store as blockEditorStore } from '../../store'; */ export default function BlockParentSelector() { const { selectBlock } = useDispatch( blockEditorStore ); - const { firstParentClientId, isVisible } = useSelect( ( select ) => { + const { parentClientId, isVisible } = useSelect( ( select ) => { const { getBlockName, getBlockParents, getSelectedBlockClientId, getBlockEditingMode, - } = select( blockEditorStore ); + getParentSectionBlock, + } = unlock( select( blockEditorStore ) ); const { hasBlockSupport } = select( blocksStore ); const selectedBlockClientId = getSelectedBlockClientId(); + const parentSection = getParentSectionBlock( selectedBlockClientId ); const parents = getBlockParents( selectedBlockClientId ); - const _firstParentClientId = parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( _firstParentClientId ); + const _parentClientId = parentSection ?? parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( _parentClientId ); const _parentBlockType = getBlockType( parentBlockName ); return { - firstParentClientId: _firstParentClientId, + parentClientId: _parentClientId, isVisible: - _firstParentClientId && - getBlockEditingMode( _firstParentClientId ) === 'default' && + _parentClientId && + getBlockEditingMode( _parentClientId ) !== 'disabled' && hasBlockSupport( _parentBlockType, '__experimentalParentSelector', @@ -48,7 +51,7 @@ export default function BlockParentSelector() { ), }; }, [] ); - const blockInformation = useBlockDisplayInformation( firstParentClientId ); + const blockInformation = useBlockDisplayInformation( parentClientId ); // Allows highlighting the parent block outline when focusing or hovering // the parent block selector within the child. @@ -65,13 +68,13 @@ export default function BlockParentSelector() { return (
selectBlock( firstParentClientId ) } + onClick={ () => selectBlock( parentClientId ) } label={ sprintf( /* translators: %s: Name of the block's parent. */ __( 'Select parent block: %s' ), diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index fff5acc7b79c46..ac2b99ac2bb620 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -183,6 +183,9 @@ export function BlockSettingsDropdown( { } } + const shouldShowBlockParentMenuItem = + ! parentBlockIsSelected && !! firstParentClientId; + return ( ( - - { ( { onClose } ) => ( - <> - - <__unstableBlockSettingsMenuFirstItem.Slot - fillProps={ { onClose } } - /> - { ! parentBlockIsSelected && - !! firstParentClientId && ( + } ) => { + // It is possible that some plugins register fills for this menu + // even if Core doesn't render anything in the block settings menu. + // in which case, we may want to render the menu anyway. + // That said for now, we can start more conservative. + const isEmpty = + ! canRemove && + ! canDuplicate && + ! canInsertBlock && + isContentOnly; + + if ( isEmpty ) { + return null; + } + + return ( + + { ( { onClose } ) => ( + <> + + <__unstableBlockSettingsMenuFirstItem.Slot + fillProps={ { onClose } } + /> + { shouldShowBlockParentMenuItem && ( ) } - { count === 1 && ( - - ) } - { ! isContentOnly && ( - - ) } - { canDuplicate && ( - - { __( 'Duplicate' ) } - - ) } - { canInsertBlock && ! isContentOnly && ( - <> + { count === 1 && ( + + ) } + { ! isContentOnly && ( + + ) } + { canDuplicate && ( - { __( 'Add before' ) } + { __( 'Duplicate' ) } + + ) } + { canInsertBlock && ! isContentOnly && ( + <> + + { __( 'Add before' ) } + + + { __( 'Add after' ) } + + + ) } + + { canCopyStyles && ! isContentOnly && ( + + + + { __( 'Paste styles' ) } + + ) } + + { typeof children === 'function' + ? children( { onClose } ) + : Children.map( ( child ) => + cloneElement( child, { onClose } ) + ) } + { canRemove && ( + - { __( 'Add after' ) } + { __( 'Delete' ) } - + ) } - - { canCopyStyles && ! isContentOnly && ( - - - - { __( 'Paste styles' ) } - - - ) } - - { typeof children === 'function' - ? children( { onClose } ) - : Children.map( ( child ) => - cloneElement( child, { onClose } ) - ) } - { canRemove && ( - - - { __( 'Delete' ) } - - - ) } - - ) } - - ) } + + ) } + + ); + } } ); } diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 98e7f7b2d21420..79f33bd30d7537 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -35,36 +35,40 @@ function BlockSwitcherDropdownMenuContents( { clientIds, hasBlockStyles, canRemove, - isUsingBindings, } ) { const { replaceBlocks, multiSelect, updateBlockAttributes } = useDispatch( blockEditorStore ); - const { possibleBlockTransformations, patterns, blocks } = useSelect( - ( select ) => { - const { - getBlocksByClientId, - getBlockRootClientId, - getBlockTransformItems, - __experimentalGetPatternTransformItems, - } = select( blockEditorStore ); - const rootClientId = getBlockRootClientId( - Array.isArray( clientIds ) ? clientIds[ 0 ] : clientIds - ); - const _blocks = getBlocksByClientId( clientIds ); - return { - blocks: _blocks, - possibleBlockTransformations: getBlockTransformItems( - _blocks, - rootClientId - ), - patterns: __experimentalGetPatternTransformItems( - _blocks, - rootClientId - ), - }; - }, - [ clientIds ] - ); + const { possibleBlockTransformations, patterns, blocks, isUsingBindings } = + useSelect( + ( select ) => { + const { + getBlockAttributes, + getBlocksByClientId, + getBlockRootClientId, + getBlockTransformItems, + __experimentalGetPatternTransformItems, + } = select( blockEditorStore ); + const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); + const _blocks = getBlocksByClientId( clientIds ); + return { + blocks: _blocks, + possibleBlockTransformations: getBlockTransformItems( + _blocks, + rootClientId + ), + patterns: __experimentalGetPatternTransformItems( + _blocks, + rootClientId + ), + isUsingBindings: clientIds.every( + ( clientId ) => + !! getBlockAttributes( clientId )?.metadata + ?.bindings + ), + }; + }, + [ clientIds ] + ); const blockVariationTransformations = useBlockVariationTransforms( { clientIds, blocks, @@ -196,7 +200,7 @@ const BlockIndicator = ( { icon, showTitle, blockTitle } ) => ( ); -export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { +export const BlockSwitcher = ( { clientIds } ) => { const { hasContentOnlyLocking, canRemove, @@ -205,6 +209,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { invalidBlocks, isReusable, isTemplate, + isDisabled, } = useSelect( ( select ) => { const { @@ -212,6 +217,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { getBlocksByClientId, getBlockAttributes, canRemoveBlocks, + getBlockEditingMode, } = select( blockEditorStore ); const { getBlockStyles, getBlockType, getActiveBlockVariation } = select( blocksStore ); @@ -222,6 +228,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { const [ { name: firstBlockName } ] = _blocks; const _isSingleBlockSelected = _blocks.length === 1; const blockType = getBlockType( firstBlockName ); + const editingMode = getBlockEditingMode( clientIds[ 0 ] ); let _icon; let _hasTemplateLock; @@ -256,6 +263,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { isTemplate: _isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ), hasContentOnlyLocking: _hasTemplateLock, + isDisabled: editingMode !== 'default', }; }, [ clientIds ] @@ -275,7 +283,7 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { : __( 'Multiple blocks selected' ); const hideDropdown = - disabled || + isDisabled || ( ! hasBlockStyles && ! canRemove ) || hasContentOnlyLocking; @@ -339,7 +347,6 @@ export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { clientIds={ clientIds } hasBlockStyles={ hasBlockStyles } canRemove={ canRemove } - isUsingBindings={ isUsingBindings } /> ) } diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 6c4789cb2924f2..2ac2cbb12ff352 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -35,6 +35,7 @@ import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; import NavigableToolbar from '../navigable-toolbar'; import { useHasBlockToolbar } from './use-has-block-toolbar'; +import { unlock } from '../../lock-unlock'; /** * Renders the block toolbar. @@ -58,7 +59,6 @@ export function PrivateBlockToolbar( { const { blockClientId, blockClientIds, - isContentOnlyEditingMode, isDefaultEditingMode, blockType, toolbarKey, @@ -78,12 +78,14 @@ export function PrivateBlockToolbar( { getBlockAttributes, getBlockParentsByBlockName, getTemplateLock, - } = select( blockEditorStore ); + getParentSectionBlock, + } = unlock( select( blockEditorStore ) ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; const parents = getBlockParents( selectedBlockClientId ); - const firstParentClientId = parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( firstParentClientId ); + const parentSection = getParentSectionBlock( selectedBlockClientId ); + const parentClientId = parentSection ?? parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( parentClientId ); const parentBlockType = getBlockType( parentBlockName ); const editingMode = getBlockEditingMode( selectedBlockClientId ); const _isDefaultEditingMode = editingMode === 'default'; @@ -112,21 +114,19 @@ export function PrivateBlockToolbar( { return { blockClientId: selectedBlockClientId, blockClientIds: selectedBlockClientIds, - isContentOnlyEditingMode: editingMode === 'contentOnly', isDefaultEditingMode: _isDefaultEditingMode, blockType: selectedBlockClientId && getBlockType( _blockName ), shouldShowVisualToolbar: isValid && isVisual, - toolbarKey: `${ selectedBlockClientId }${ firstParentClientId }`, + toolbarKey: `${ selectedBlockClientId }${ parentClientId }`, showParentSelector: parentBlockType && - getBlockEditingMode( firstParentClientId ) === 'default' && + getBlockEditingMode( parentClientId ) !== 'disabled' && hasBlockSupport( parentBlockType, '__experimentalParentSelector', true ) && - selectedBlockClientIds.length === 1 && - _isDefaultEditingMode, + selectedBlockClientIds.length === 1, isUsingBindings: _isUsingBindings, hasParentPattern: _hasParentPattern, hasContentOnlyLocking: _hasTemplateLock, @@ -179,36 +179,26 @@ export function PrivateBlockToolbar( { key={ toolbarKey } >
- { ! isMultiToolbar && - isLargeViewport && - isDefaultEditingMode && } + { ! isMultiToolbar && isLargeViewport && ( + + ) } { ( shouldShowVisualToolbar || isMultiToolbar ) && - ( isDefaultEditingMode || - ( isContentOnlyEditingMode && ! hasParentPattern ) || - isSynced ) && ( + ! hasParentPattern && (
- + { ! isMultiToolbar && isDefaultEditingMode && ( + + ) } + - { isDefaultEditingMode && ( - <> - { ! isMultiToolbar && ( - - ) } - - - ) }
) } @@ -242,9 +232,7 @@ export function PrivateBlockToolbar( { ) } - { isDefaultEditingMode && ( - - ) } +
); diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 40d748dd0a1568..2a0f68a6976686 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -52,9 +52,18 @@ > :last-child, > :last-child .components-toolbar-group, - > :last-child .components-toolbar { + > :last-child .components-toolbar, + // If the last toolbar group is empty, + // we need to remove the double border from the penultimate one. + &:has(> :last-child:empty) > :nth-last-child(2), + &:has(> :last-child:empty) > :nth-last-child(2) .components-toolbar-group, + &:has(> :last-child:empty) > :nth-last-child(2) .components-toolbar { border-right: none; } + + .components-toolbar-group:empty { + display: none; + } } .block-editor-block-contextual-toolbar { diff --git a/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js b/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js index c4e228f8a3c07b..80ce3691147834 100644 --- a/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js +++ b/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js @@ -7,7 +7,6 @@ import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; /** * Returns true if the block toolbar should be shown. @@ -15,40 +14,29 @@ import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls * @return {boolean} Whether the block toolbar component will be rendered. */ export function useHasBlockToolbar() { - const { isToolbarEnabled, isDefaultEditingMode } = useSelect( - ( select ) => { - const { - getBlockEditingMode, - getBlockName, - getBlockSelectionStart, - } = select( blockEditorStore ); + const { isToolbarEnabled, isBlockDisabled } = useSelect( ( select ) => { + const { getBlockEditingMode, getBlockName, getBlockSelectionStart } = + select( blockEditorStore ); - // we only care about the 1st selected block - // for the toolbar, so we use getBlockSelectionStart - // instead of getSelectedBlockClientIds - const selectedBlockClientId = getBlockSelectionStart(); + // we only care about the 1st selected block + // for the toolbar, so we use getBlockSelectionStart + // instead of getSelectedBlockClientIds + const selectedBlockClientId = getBlockSelectionStart(); - const blockType = - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ); + const blockType = + selectedBlockClientId && + getBlockType( getBlockName( selectedBlockClientId ) ); - return { - isToolbarEnabled: - blockType && - hasBlockSupport( blockType, '__experimentalToolbar', true ), - isDefaultEditingMode: - getBlockEditingMode( selectedBlockClientId ) === 'default', - }; - }, - [] - ); + return { + isToolbarEnabled: + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ), + isBlockDisabled: + getBlockEditingMode( selectedBlockClientId ) === 'disabled', + }; + }, [] ); - const hasAnyBlockControls = useHasAnyBlockControls(); - - if ( - ! isToolbarEnabled || - ( ! isDefaultEditingMode && ! hasAnyBlockControls ) - ) { + if ( ! isToolbarEnabled || isBlockDisabled ) { return false; } diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index d8955bd6342c4c..9e99176819ae89 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -116,6 +116,7 @@ export const getEnabledClientIdsTree = createSelector( state.settings.templateLock, state.blockListSettings, state.editorMode, + getSectionRootClientId( state ), ] ); diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index af991608238e2e..79e15255e6cc15 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -10,6 +10,7 @@ import { parse as grammarParse } from '@wordpress/block-serialization-default-pa import { selectBlockPatternsKey } from './private-keys'; import { unlock } from '../lock-unlock'; import { STORE_NAME } from './constants'; +import { getSectionRootClientId } from './private-selectors'; export const withRootClientIdOptionKey = Symbol( 'withRootClientId' ); @@ -118,5 +119,6 @@ export function getInsertBlockTypeDependants( state, rootClientId ) { state.settings.templateLock, state.blockEditingModes, state.editorMode, + getSectionRootClientId( state ), ]; } From 058cc63c53daaa794618ec1a1dd2fa4819cde1da Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 23 Sep 2024 13:47:18 +0100 Subject: [PATCH 17/53] Editor: Remove edit template menu item from block settings menu in blocks outside template. (#65560) Co-authored-by: youknowriad Co-authored-by: jorgefilipecosta Co-authored-by: t-hamano Co-authored-by: kevin940726 --- .../content-only-settings-menu.js | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js index c772a062b9e3be..af0e9b30ae83b4 100644 --- a/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js +++ b/packages/editor/src/components/block-settings-menu/content-only-settings-menu.js @@ -27,7 +27,10 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { getBlockParentsByBlockName, getSettings, getBlockAttributes, + getBlockParents, } = select( blockEditorStore ); + const { getCurrentTemplateId, getRenderingMode } = + select( editorStore ); const patternParent = getBlockParentsByBlockName( clientId, 'core/block', @@ -41,23 +44,17 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { 'wp_block', getBlockAttributes( patternParent ).ref ); - } else { - const { getCurrentTemplateId } = select( editorStore ); - const templateId = getCurrentTemplateId(); - const { getBlockParents } = unlock( - select( blockEditorStore ) + } else if ( + getRenderingMode() === 'template-locked' && + ! getBlockParents( clientId ).some( ( parent ) => + postContentBlocks.includes( parent ) + ) + ) { + record = select( coreStore ).getEntityRecord( + 'postType', + 'wp_template', + getCurrentTemplateId() ); - if ( - ! getBlockParents( clientId ).some( ( parent ) => - postContentBlocks.includes( parent ) - ) - ) { - record = select( coreStore ).getEntityRecord( - 'postType', - 'wp_template', - templateId - ); - } } if ( ! record ) { return {}; From 4a91d3263ef4061c9477529ad71700e8cabc8b63 Mon Sep 17 00:00:00 2001 From: dhruvang21 <105810308+dhruvang21@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:19:13 +0530 Subject: [PATCH 18/53] Fixed the focus cutoff of the editor buttons in the widgets editor (#65395) * Fix the focus cutoff of the editor buttons * Add height to fix focus cutoff * Use header height variable Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --------- Co-authored-by: dhruvang21 Co-authored-by: t-hamano --- packages/edit-widgets/src/components/header/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index 6e5d8de8142f42..7bd3c41a6a22ad 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -82,6 +82,7 @@ padding-right: $grid-unit-10; padding-left: $grid-unit-20; overflow: hidden; + height: $header-height; } .edit-widgets-header__title { From 7493185addae2ba9bb1506a24e5b6db47e27f4a1 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:38:39 +1000 Subject: [PATCH 19/53] Comment Content: Add block example (#65559) Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd --- packages/block-library/src/comment-content/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/comment-content/index.js b/packages/block-library/src/comment-content/index.js index 130f1d30125559..aefcef75acf8ae 100644 --- a/packages/block-library/src/comment-content/index.js +++ b/packages/block-library/src/comment-content/index.js @@ -16,6 +16,7 @@ export { metadata, name }; export const settings = { icon, edit, + example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); From 096c883495ab41876b8d2c8252e88575e8900e32 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 24 Sep 2024 03:00:16 +0200 Subject: [PATCH 20/53] Global styles: do not navigate twice to home screen when opening the sidebar (#65523) * Global styles: do not navigate twice to home screen on sidebar load * Remove whole default block Co-authored-by: ciampo Co-authored-by: ramonjd Co-authored-by: andrewserong --- .../edit-site/src/components/global-styles/ui.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 60d7e314d7776a..bc6906a769af48 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -272,19 +272,6 @@ function GlobalStylesEditorCanvasContainerLink() { goTo( '/' ); } break; - default: - /* - * Example: the user has navigated to "Browse styles" or elsewhere - * and changes the editorCanvasContainerView, e.g., closes the style book. - * The panel should not be affected. - * Exclude revisions panel from this behavior, - * as it should close when the editorCanvasContainerView doesn't correspond. - */ - if ( path !== '/' && ! isRevisionsOpen ) { - return; - } - goTo( '/' ); - break; } }, [ editorCanvasContainerView, isRevisionsOpen, goTo ] ); } From 69d77a12f5650f1804bc206acc3be862e5029a6f Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 24 Sep 2024 13:42:48 +1000 Subject: [PATCH 21/53] Link autocompleter: decode post title HTML entities (#65589) Co-authored-by: ramonjd Co-authored-by: Mamaduka Co-authored-by: MadtownLems --- packages/block-editor/src/autocompleters/link.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/autocompleters/link.js b/packages/block-editor/src/autocompleters/link.js index ce9af28f19d000..fb64cb151294d6 100644 --- a/packages/block-editor/src/autocompleters/link.js +++ b/packages/block-editor/src/autocompleters/link.js @@ -6,6 +6,7 @@ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; import { Icon, page, post } from '@wordpress/icons'; +import { decodeEntities } from '@wordpress/html-entities'; const SHOWN_SUGGESTIONS = 10; @@ -46,7 +47,7 @@ function createLinkCompleter() { key="icon" icon={ item.subtype === 'page' ? page : post } /> - { item.title } + { decodeEntities( item.title ) } ); }, From 842f67ddbadc21e1400fbada663ed4136f8e28ed Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 24 Sep 2024 08:30:52 +0200 Subject: [PATCH 22/53] useToolsPanel: calculate derived state in reducer to prevent too many renders (#65564) Co-authored-by: jsnajdr Co-authored-by: youknowriad Co-authored-by: ciampo Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong --- packages/components/CHANGELOG.md | 4 + .../src/tools-panel/tools-panel/hook.ts | 384 ++++++++++-------- 2 files changed, 225 insertions(+), 163 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 90481e58edd7bb..001b2d0b4230e9 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- `ToolsPanel`: atomic one-step state update when (un)registering panels ([#65564](https://github.com/WordPress/gutenberg/pull/65564)). + ## 28.8.0 (2024-09-19) ### Bug Fixes diff --git a/packages/components/src/tools-panel/tools-panel/hook.ts b/packages/components/src/tools-panel/tools-panel/hook.ts index 931bf2494e6e34..583a079ab20026 100644 --- a/packages/components/src/tools-panel/tools-panel/hook.ts +++ b/packages/components/src/tools-panel/tools-panel/hook.ts @@ -5,8 +5,8 @@ import { useCallback, useEffect, useMemo, + useReducer, useRef, - useState, } from '@wordpress/element'; /** @@ -27,14 +27,40 @@ import type { const DEFAULT_COLUMNS = 2; +type PanelItemsState = { + panelItems: ToolsPanelItem[]; + menuItemOrder: string[]; + menuItems: ToolsPanelMenuItems; +}; + +type PanelItemsAction = + | { type: 'REGISTER_PANEL'; item: ToolsPanelItem } + | { type: 'UNREGISTER_PANEL'; label: string } + | { + type: 'UPDATE_VALUE'; + group: ToolsPanelMenuItemKey; + label: string; + value: boolean; + } + | { type: 'TOGGLE_VALUE'; label: string } + | { type: 'RESET_ALL' }; + +function emptyMenuItems(): ToolsPanelMenuItems { + return { default: {}, optional: {} }; +} + +function emptyState(): PanelItemsState { + return { panelItems: [], menuItemOrder: [], menuItems: emptyMenuItems() }; +} + const generateMenuItems = ( { panelItems, shouldReset, currentMenuItems, menuItemOrder, }: ToolsPanelMenuItemsConfig ) => { - const newMenuItems: ToolsPanelMenuItems = { default: {}, optional: {} }; - const menuItems: ToolsPanelMenuItems = { default: {}, optional: {} }; + const newMenuItems: ToolsPanelMenuItems = emptyMenuItems(); + const menuItems: ToolsPanelMenuItems = emptyMenuItems(); panelItems.forEach( ( { hasValue, isShownByDefault, label } ) => { const group = isShownByDefault ? 'default' : 'optional'; @@ -75,9 +101,149 @@ const generateMenuItems = ( { return menuItems; }; +function panelItemsReducer( + panelItems: ToolsPanelItem[], + action: PanelItemsAction +) { + switch ( action.type ) { + case 'REGISTER_PANEL': { + const newItems = [ ...panelItems ]; + // If an item with this label has already been registered, remove it + // first. This can happen when an item is moved between the default + // and optional groups. + const existingIndex = newItems.findIndex( + ( oldItem ) => oldItem.label === action.item.label + ); + if ( existingIndex !== -1 ) { + newItems.splice( existingIndex, 1 ); + } + newItems.push( action.item ); + return newItems; + } + case 'UNREGISTER_PANEL': { + const index = panelItems.findIndex( + ( item ) => item.label === action.label + ); + if ( index !== -1 ) { + const newItems = [ ...panelItems ]; + newItems.splice( index, 1 ); + return newItems; + } + return panelItems; + } + default: + return panelItems; + } +} + +function menuItemOrderReducer( + menuItemOrder: string[], + action: PanelItemsAction +) { + switch ( action.type ) { + case 'REGISTER_PANEL': { + // Track the initial order of item registration. This is used for + // maintaining menu item order later. + if ( menuItemOrder.includes( action.item.label ) ) { + return menuItemOrder; + } + + return [ ...menuItemOrder, action.item.label ]; + } + default: + return menuItemOrder; + } +} + +function menuItemsReducer( state: PanelItemsState, action: PanelItemsAction ) { + switch ( action.type ) { + case 'REGISTER_PANEL': + case 'UNREGISTER_PANEL': + // generate new menu items from original `menuItems` and updated `panelItems` and `menuItemOrder` + return generateMenuItems( { + currentMenuItems: state.menuItems, + panelItems: state.panelItems, + menuItemOrder: state.menuItemOrder, + shouldReset: false, + } ); + case 'RESET_ALL': + return generateMenuItems( { + panelItems: state.panelItems, + menuItemOrder: state.menuItemOrder, + shouldReset: true, + } ); + case 'UPDATE_VALUE': { + const oldValue = state.menuItems[ action.group ][ action.label ]; + if ( action.value === oldValue ) { + return state.menuItems; + } + return { + ...state.menuItems, + [ action.group ]: { + ...state.menuItems[ action.group ], + [ action.label ]: action.value, + }, + }; + } + case 'TOGGLE_VALUE': { + const currentItem = state.panelItems.find( + ( item ) => item.label === action.label + ); + + if ( ! currentItem ) { + return state.menuItems; + } + + const menuGroup = currentItem.isShownByDefault + ? 'default' + : 'optional'; + + const newMenuItems = { + ...state.menuItems, + [ menuGroup ]: { + ...state.menuItems[ menuGroup ], + [ action.label ]: + ! state.menuItems[ menuGroup ][ action.label ], + }, + }; + return newMenuItems; + } + + default: + return state.menuItems; + } +} + +function panelReducer( state: PanelItemsState, action: PanelItemsAction ) { + const panelItems = panelItemsReducer( state.panelItems, action ); + const menuItemOrder = menuItemOrderReducer( state.menuItemOrder, action ); + // `menuItemsReducer` is a bit unusual because it generates new state from original `menuItems` + // and the updated `panelItems` and `menuItemOrder`. + const menuItems = menuItemsReducer( + { panelItems, menuItemOrder, menuItems: state.menuItems }, + action + ); + + return { panelItems, menuItemOrder, menuItems }; +} + +function resetAllFiltersReducer( + filters: ResetAllFilter[], + action: { type: 'REGISTER' | 'UNREGISTER'; filter: ResetAllFilter } +) { + switch ( action.type ) { + case 'REGISTER': + return [ ...filters, action.filter ]; + case 'UNREGISTER': + return filters.filter( ( f ) => f !== action.filter ); + default: + return filters; + } +} + const isMenuItemTypeEmpty = ( - obj?: ToolsPanelMenuItems[ ToolsPanelMenuItemKey ] -) => obj && Object.keys( obj ).length === 0; + obj: ToolsPanelMenuItems[ ToolsPanelMenuItemKey ] +) => Object.keys( obj ).length === 0; export function useToolsPanel( props: WordPressComponentProps< ToolsPanelProps, 'div' > @@ -108,103 +274,43 @@ export function useToolsPanel( }, [ wasResetting ] ); // Allow panel items to register themselves. - const [ panelItems, setPanelItems ] = useState< ToolsPanelItem[] >( [] ); - const [ menuItemOrder, setMenuItemOrder ] = useState< string[] >( [] ); - const [ resetAllFilters, setResetAllFilters ] = useState< - ResetAllFilter[] - >( [] ); - - const registerPanelItem = useCallback( - ( item: ToolsPanelItem ) => { - // Add item to panel items. - setPanelItems( ( items ) => { - const newItems = [ ...items ]; - // If an item with this label has already been registered, remove it - // first. This can happen when an item is moved between the default - // and optional groups. - const existingIndex = newItems.findIndex( - ( oldItem ) => oldItem.label === item.label - ); - if ( existingIndex !== -1 ) { - newItems.splice( existingIndex, 1 ); - } - return [ ...newItems, item ]; - } ); - - // Track the initial order of item registration. This is used for - // maintaining menu item order later. - setMenuItemOrder( ( items ) => { - if ( items.includes( item.label ) ) { - return items; - } + const [ { panelItems, menuItems }, panelDispatch ] = useReducer( + panelReducer, + undefined, + emptyState + ); - return [ ...items, item.label ]; - } ); - }, - [ setPanelItems, setMenuItemOrder ] + const [ resetAllFilters, dispatchResetAllFilters ] = useReducer( + resetAllFiltersReducer, + [] ); + const registerPanelItem = useCallback( ( item: ToolsPanelItem ) => { + // Add item to panel items. + panelDispatch( { type: 'REGISTER_PANEL', item } ); + }, [] ); + // Panels need to deregister on unmount to avoid orphans in menu state. // This is an issue when panel items are being injected via SlotFills. - const deregisterPanelItem = useCallback( - ( label: string ) => { - // When switching selections between components injecting matching - // controls, e.g. both panels have a "padding" control, the - // deregistration of the first panel doesn't occur until after the - // registration of the next. - setPanelItems( ( items ) => { - const newItems = [ ...items ]; - const index = newItems.findIndex( - ( item ) => item.label === label - ); - if ( index !== -1 ) { - newItems.splice( index, 1 ); - } - return newItems; - } ); - }, - [ setPanelItems ] - ); - - const registerResetAllFilter = useCallback( - ( newFilter: ResetAllFilter ) => { - setResetAllFilters( ( filters ) => { - return [ ...filters, newFilter ]; - } ); - }, - [ setResetAllFilters ] - ); + const deregisterPanelItem = useCallback( ( label: string ) => { + // When switching selections between components injecting matching + // controls, e.g. both panels have a "padding" control, the + // deregistration of the first panel doesn't occur until after the + // registration of the next. + panelDispatch( { type: 'UNREGISTER_PANEL', label } ); + }, [] ); + + const registerResetAllFilter = useCallback( ( filter: ResetAllFilter ) => { + dispatchResetAllFilters( { type: 'REGISTER', filter } ); + }, [] ); const deregisterResetAllFilter = useCallback( - ( filterToRemove: ResetAllFilter ) => { - setResetAllFilters( ( filters ) => { - return filters.filter( - ( filter ) => filter !== filterToRemove - ); - } ); + ( filter: ResetAllFilter ) => { + dispatchResetAllFilters( { type: 'UNREGISTER', filter } ); }, - [ setResetAllFilters ] + [] ); - // Manage and share display state of menu items representing child controls. - const [ menuItems, setMenuItems ] = useState< ToolsPanelMenuItems >( { - default: {}, - optional: {}, - } ); - - // Setup menuItems state as panel items register themselves. - useEffect( () => { - setMenuItems( ( prevState ) => { - const items = generateMenuItems( { - panelItems, - shouldReset: false, - currentMenuItems: prevState, - menuItemOrder, - } ); - return items; - } ); - }, [ panelItems, setMenuItems, menuItemOrder ] ); - // Updates the status of the panel’s menu items. For default items the // value represents whether it differs from the default and for optional // items whether the item is shown. @@ -214,38 +320,24 @@ export function useToolsPanel( label: string, group: ToolsPanelMenuItemKey = 'default' ) => { - setMenuItems( ( items ) => { - const newState = { - ...items, - [ group ]: { - ...items[ group ], - [ label ]: value, - }, - }; - return newState; - } ); + panelDispatch( { type: 'UPDATE_VALUE', group, label, value } ); }, - [ setMenuItems ] + [] ); // Whether all optional menu items are hidden or not must be tracked // in order to later determine if the panel display is empty and handle // conditional display of a plus icon to indicate the presence of further // menu items. - const [ areAllOptionalControlsHidden, setAreAllOptionalControlsHidden ] = - useState( false ); - - useEffect( () => { - if ( - isMenuItemTypeEmpty( menuItems?.default ) && - ! isMenuItemTypeEmpty( menuItems?.optional ) - ) { - const allControlsHidden = ! Object.entries( - menuItems.optional - ).some( ( [ , isSelected ] ) => isSelected ); - setAreAllOptionalControlsHidden( allControlsHidden ); - } - }, [ menuItems, setAreAllOptionalControlsHidden ] ); + const areAllOptionalControlsHidden = useMemo( () => { + return ( + isMenuItemTypeEmpty( menuItems.default ) && + ! isMenuItemTypeEmpty( menuItems.optional ) && + Object.values( menuItems.optional ).every( + ( isSelected ) => ! isSelected + ) + ); + }, [ menuItems ] ); const cx = useCx(); const classes = useMemo( () => { @@ -253,9 +345,7 @@ export function useToolsPanel( hasInnerWrapper && styles.ToolsPanelWithInnerWrapper( DEFAULT_COLUMNS ); const emptyStyle = - isMenuItemTypeEmpty( menuItems?.default ) && - areAllOptionalControlsHidden && - styles.ToolsPanelHiddenInnerWrapper; + areAllOptionalControlsHidden && styles.ToolsPanelHiddenInnerWrapper; return cx( styles.ToolsPanel( DEFAULT_COLUMNS ), @@ -263,42 +353,13 @@ export function useToolsPanel( emptyStyle, className ); - }, [ - areAllOptionalControlsHidden, - className, - cx, - hasInnerWrapper, - menuItems, - ] ); + }, [ areAllOptionalControlsHidden, className, cx, hasInnerWrapper ] ); // Toggle the checked state of a menu item which is then used to determine // display of the item within the panel. - const toggleItem = useCallback( - ( label: string ) => { - const currentItem = panelItems.find( - ( item ) => item.label === label - ); - - if ( ! currentItem ) { - return; - } - - const menuGroup = currentItem.isShownByDefault - ? 'default' - : 'optional'; - - const newMenuItems = { - ...menuItems, - [ menuGroup ]: { - ...menuItems[ menuGroup ], - [ label ]: ! menuItems[ menuGroup ][ label ], - }, - }; - - setMenuItems( newMenuItems ); - }, - [ menuItems, panelItems, setMenuItems ] - ); + const toggleItem = useCallback( ( label: string ) => { + panelDispatch( { type: 'TOGGLE_VALUE', label } ); + }, [] ); // Resets display of children and executes resetAll callback if available. const resetAllItems = useCallback( () => { @@ -308,20 +369,15 @@ export function useToolsPanel( } // Turn off display of all non-default items. - const resetMenuItems = generateMenuItems( { - panelItems, - menuItemOrder, - shouldReset: true, - } ); - setMenuItems( resetMenuItems ); - }, [ panelItems, resetAllFilters, resetAll, setMenuItems, menuItemOrder ] ); + panelDispatch( { type: 'RESET_ALL' } ); + }, [ resetAllFilters, resetAll ] ); // Assist ItemGroup styling when there are potentially hidden placeholder // items by identifying first & last items that are toggled on for display. const getFirstVisibleItemLabel = ( items: ToolsPanelItem[] ) => { const optionalItems = menuItems.optional || {}; const firstItem = items.find( - ( item ) => item.isShownByDefault || !! optionalItems[ item.label ] + ( item ) => item.isShownByDefault || optionalItems[ item.label ] ); return firstItem?.label; @@ -332,6 +388,8 @@ export function useToolsPanel( [ ...panelItems ].reverse() ); + const hasMenuItems = panelItems.length > 0; + const panelContext = useMemo( () => ( { areAllOptionalControlsHidden, @@ -339,7 +397,7 @@ export function useToolsPanel( deregisterResetAllFilter, firstDisplayedItem, flagItemCustomization, - hasMenuItems: !! panelItems.length, + hasMenuItems, isResetting: isResettingRef.current, lastDisplayedItem, menuItems, @@ -359,7 +417,7 @@ export function useToolsPanel( lastDisplayedItem, menuItems, panelId, - panelItems, + hasMenuItems, registerResetAllFilter, registerPanelItem, shouldRenderPlaceholderItems, From 0844af0547d6853ea9184b85f3908f8a7e9802a0 Mon Sep 17 00:00:00 2001 From: "Joen A." <1204802+jasmussen@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:55:47 +0200 Subject: [PATCH 23/53] Connected blocks, add backdrop-color. (#65233) Co-authored-by: jasmussen Co-authored-by: gziolo Co-authored-by: t-hamano Co-authored-by: mtias Co-authored-by: jameskoster Co-authored-by: SantosGuillamot --- .../block-editor/src/components/block-toolbar/style.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 2a0f68a6976686..ae03eeed1a817c 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -37,6 +37,13 @@ border-right: $border-width solid $gray-300; } + &.is-connected { + .block-editor-block-switcher .components-button::before { + background: color-mix(in srgb, var(--wp-block-synced-color) 10%, transparent); + border-radius: $radius-small; + } + } + &.is-synced, &.is-connected { .block-editor-block-switcher .components-button .block-editor-block-icon { From 2a80118931756c72b5cb5d1b728117ddb6749ffc Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:06:11 +0200 Subject: [PATCH 24/53] Block Bindings: Fix passing bindings context to `canUserEditValue` (#65599) * Pass updated context as `context` prop * Use `updatedContext` in pattern overrides Co-authored-by: SantosGuillamot Co-authored-by: gziolo --- .../src/hooks/use-bindings-attributes.js | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index ac045004cc654b..9f9234ad47d103 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -103,11 +103,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( const sources = useSelect( ( select ) => unlock( select( blocksStore ) ).getAllBlockBindingsSources() ); - const { name, clientId } = props; - const hasParentPattern = !! props.context[ 'pattern/overrides' ]; - const hasPatternOverridesDefaultBinding = - props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] - ?.source === 'core/pattern-overrides'; + const { name, clientId, context, setAttributes } = props; const blockBindings = useMemo( () => replacePatternOverrideDefaultBindings( @@ -121,6 +117,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( // used purposely here to ensure `boundAttributes` is updated whenever // there are attribute updates. // `source.getValues` may also call a selector via `registry.select`. + const updatedContext = { ...context }; const boundAttributes = useSelect( () => { if ( ! blockBindings ) { return; @@ -139,6 +136,11 @@ export const withBlockBindingSupport = createHigherOrderComponent( continue; } + // Populate context. + for ( const key of source.usesContext || [] ) { + updatedContext[ key ] = blockContext[ key ]; + } + blockBindingsBySource.set( source, { ...blockBindingsBySource.get( source ), [ attributeName ]: { @@ -149,15 +151,6 @@ export const withBlockBindingSupport = createHigherOrderComponent( if ( blockBindingsBySource.size ) { for ( const [ source, bindings ] of blockBindingsBySource ) { - // Populate context. - const context = {}; - - if ( source.usesContext?.length ) { - for ( const key of source.usesContext ) { - context[ key ] = blockContext[ key ]; - } - } - // Get values in batch if the source supports it. let values = {}; if ( ! source.getValues ) { @@ -168,7 +161,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( } else { values = source.getValues( { registry, - context, + context: updatedContext, clientId, bindings, } ); @@ -190,9 +183,19 @@ export const withBlockBindingSupport = createHigherOrderComponent( } return attributes; - }, [ blockBindings, name, clientId, blockContext, registry, sources ] ); - - const { setAttributes } = props; + }, [ + blockBindings, + name, + clientId, + updatedContext, + registry, + sources, + ] ); + + const hasParentPattern = !! updatedContext[ 'pattern/overrides' ]; + const hasPatternOverridesDefaultBinding = + props.attributes.metadata?.bindings?.[ DEFAULT_ATTRIBUTE ] + ?.source === 'core/pattern-overrides'; const _setAttributes = useCallback( ( nextAttributes ) => { @@ -236,18 +239,9 @@ export const withBlockBindingSupport = createHigherOrderComponent( source, bindings, ] of blockBindingsBySource ) { - // Populate context. - const context = {}; - - if ( source.usesContext?.length ) { - for ( const key of source.usesContext ) { - context[ key ] = blockContext[ key ]; - } - } - source.setValues( { registry, - context, + context: updatedContext, clientId, bindings, } ); @@ -277,7 +271,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( blockBindings, name, clientId, - blockContext, + updatedContext, setAttributes, sources, hasPatternOverridesDefaultBinding, @@ -291,6 +285,7 @@ export const withBlockBindingSupport = createHigherOrderComponent( { ...props } attributes={ { ...props.attributes, ...boundAttributes } } setAttributes={ _setAttributes } + context={ updatedContext } /> ); From efd622d95ebd6721c844949d57c559de67b17101 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 24 Sep 2024 11:43:32 +0200 Subject: [PATCH 25/53] Navigator: fix isInitial logic (#65527) * Navigator: fix isInitial logic * CHANGELOG --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + .../src/navigator/navigator-provider/component.tsx | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 001b2d0b4230e9..34522f79572652 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug Fixes - `ToolsPanel`: atomic one-step state update when (un)registering panels ([#65564](https://github.com/WordPress/gutenberg/pull/65564)). +- `Navigator`: fix `isInitial` logic ([#65527](https://github.com/WordPress/gutenberg/pull/65527)). ## 28.8.0 (2024-09-19) diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index ebcb247c574830..01254b743f87d0 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -66,7 +66,7 @@ function goTo( options: NavigateOptions = {} ) { const { focusSelectors } = state; - const currentLocation = { ...state.currentLocation, isInitial: false }; + const currentLocation = { ...state.currentLocation }; const { // Default assignments @@ -114,6 +114,7 @@ function goTo( return { currentLocation: { ...restOptions, + isInitial: false, path, isBack, hasRestoredFocus: false, @@ -129,7 +130,7 @@ function goToParent( options: NavigateToParentOptions = {} ) { const { screens, focusSelectors } = state; - const currentLocation = { ...state.currentLocation, isInitial: false }; + const currentLocation = { ...state.currentLocation }; const currentPath = currentLocation.path; if ( currentPath === undefined ) { return { currentLocation, focusSelectors }; From 8357fceaf5acf26777770741975a1f5ad67619b3 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:26:48 +0900 Subject: [PATCH 26/53] Site Editor: Make resizable frame compatible with RTL languages (#65545) * Site Editor: Make resizable frame compatible with RTL languages * Update packages/edit-site/src/components/resizable-frame/index.js Co-authored-by: Mitchell Austin * Fix dragging behaviour --------- Co-authored-by: t-hamano Co-authored-by: stokesman --- .../src/components/resizable-frame/index.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/edit-site/src/components/resizable-frame/index.js b/packages/edit-site/src/components/resizable-frame/index.js index b589a3861c75d3..b5aae01918e691 100644 --- a/packages/edit-site/src/components/resizable-frame/index.js +++ b/packages/edit-site/src/components/resizable-frame/index.js @@ -14,7 +14,7 @@ import { } from '@wordpress/components'; import { useInstanceId, useReducedMotion } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; +import { __, isRTL } from '@wordpress/i18n'; /** * Internal dependencies @@ -171,7 +171,10 @@ function ResizableFrame( { event.preventDefault(); const step = 20 * ( event.shiftKey ? 5 : 1 ); - const delta = step * ( event.key === 'ArrowLeft' ? 1 : -1 ); + const delta = + step * + ( event.key === 'ArrowLeft' ? 1 : -1 ) * + ( isRTL() ? -1 : 1 ); const newWidth = Math.min( Math.max( FRAME_MIN_WIDTH, @@ -200,15 +203,17 @@ function ResizableFrame( { const resizeHandleVariants = { hidden: { opacity: 0, - left: 0, + ...( isRTL() ? { right: 0 } : { left: 0 } ), }, visible: { opacity: 1, - left: -14, // Account for the handle's width. + // Account for the handle's width. + ...( isRTL() ? { right: -14 } : { left: -14 } ), }, active: { opacity: 1, - left: -14, // Account for the handle's width. + // Account for the handle's width. + ...( isRTL() ? { right: -14 } : { left: -14 } ), scaleY: 1.3, }, }; @@ -246,10 +251,11 @@ function ResizableFrame( { size={ frameSize } enable={ { top: false, - right: false, bottom: false, // Resizing will be disabled until the editor content is loaded. - left: isReady, + ...( isRTL() + ? { right: isReady, left: false } + : { left: isReady, right: false } ), topRight: false, bottomRight: false, bottomLeft: false, @@ -269,7 +275,7 @@ function ResizableFrame( { onMouseOver={ () => setShouldShowHandle( true ) } onMouseOut={ () => setShouldShowHandle( false ) } handleComponent={ { - left: canvasMode === 'view' && ( + [ isRTL() ? 'right' : 'left' ]: canvasMode === 'view' && ( <> { /* Disable reason: role="separator" does in fact support aria-valuenow */ } From 82dff1321f83329e1ba00b945a67d97edc291870 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:27:28 +0900 Subject: [PATCH 27/53] Don't show tooltip in zoom out toggle button when showIconLabels is true (#65573) Co-authored-by: t-hamano Co-authored-by: afercia Co-authored-by: draganescu --- packages/editor/src/components/zoom-out-toggle/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/components/zoom-out-toggle/index.js b/packages/editor/src/components/zoom-out-toggle/index.js index 214b1c51fd6255..dd4669d04bec18 100644 --- a/packages/editor/src/components/zoom-out-toggle/index.js +++ b/packages/editor/src/components/zoom-out-toggle/index.js @@ -7,6 +7,7 @@ import { __ } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { square as zoomOutIcon } from '@wordpress/icons'; +import { store as preferencesStore } from '@wordpress/preferences'; /** * Internal dependencies @@ -14,8 +15,12 @@ import { square as zoomOutIcon } from '@wordpress/icons'; import { unlock } from '../../lock-unlock'; const ZoomOutToggle = () => { - const { isZoomOut } = useSelect( ( select ) => ( { + const { isZoomOut, showIconLabels } = useSelect( ( select ) => ( { isZoomOut: unlock( select( blockEditorStore ) ).isZoomOut(), + showIconLabels: select( preferencesStore ).get( + 'core', + 'showIconLabels' + ), } ) ); const { resetZoomLevel, setZoomLevel, __unstableSetEditorMode } = unlock( @@ -38,6 +43,7 @@ const ZoomOutToggle = () => { label={ __( 'Toggle Zoom Out' ) } isPressed={ isZoomOut } size="compact" + showTooltip={ ! showIconLabels } /> ); }; From 228ab3aa8ce5ae1578ddd6980ecceab63ce2ae3f Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:28:36 +0900 Subject: [PATCH 28/53] Resizable Editor: Make the editor resizable with arrow keys (#65546) Co-authored-by: t-hamano Co-authored-by: kevin940726 --- .../editor/src/components/resizable-editor/resize-handle.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/editor/src/components/resizable-editor/resize-handle.js b/packages/editor/src/components/resizable-editor/resize-handle.js index dbba31f6f998ba..ccd903d0f3a172 100644 --- a/packages/editor/src/components/resizable-editor/resize-handle.js +++ b/packages/editor/src/components/resizable-editor/resize-handle.js @@ -15,6 +15,11 @@ export default function ResizeHandle( { direction, resizeWidthBy } ) { function handleKeyDown( event ) { const { keyCode } = event; + if ( keyCode !== LEFT && keyCode !== RIGHT ) { + return; + } + event.preventDefault(); + if ( ( direction === 'left' && keyCode === LEFT ) || ( direction === 'right' && keyCode === RIGHT ) From 8a658c85107e5811c55506bbe85b9480992bfd27 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 24 Sep 2024 12:24:18 +0100 Subject: [PATCH 29/53] Components: Simplify MenuGroup component styles (#65561) Co-authored-by: youknowriad Co-authored-by: ciampo Co-authored-by: andrewserong --- packages/components/CHANGELOG.md | 4 ++++ .../src/dropdown-menu/stories/index.story.tsx | 3 +++ .../src/dropdown/stories/index.story.tsx | 1 + packages/components/src/dropdown/style.scss | 23 ++++++++----------- packages/components/src/menu-group/style.scss | 5 +++- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 34522f79572652..71aa08d6934034 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,6 +7,10 @@ - `ToolsPanel`: atomic one-step state update when (un)registering panels ([#65564](https://github.com/WordPress/gutenberg/pull/65564)). - `Navigator`: fix `isInitial` logic ([#65527](https://github.com/WordPress/gutenberg/pull/65527)). +### Enhancements + +- `MenuGroup`: Simplify the MenuGroup styles within dropdown menus. ([#65561](https://github.com/WordPress/gutenberg/pull/65561)). + ## 28.8.0 (2024-09-19) ### Bug Fixes diff --git a/packages/components/src/dropdown-menu/stories/index.story.tsx b/packages/components/src/dropdown-menu/stories/index.story.tsx index b6bc11ddec9abf..dd4907bd0b96b1 100644 --- a/packages/components/src/dropdown-menu/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu/stories/index.story.tsx @@ -96,6 +96,9 @@ export const WithChildren: StoryObj< typeof DropdownMenu > = { icon: more, children: ( { onClose } ) => ( <> + + Standalone Item + Move Up diff --git a/packages/components/src/dropdown/stories/index.story.tsx b/packages/components/src/dropdown/stories/index.story.tsx index c6fe5014ffdc2a..0f07664787cc33 100644 --- a/packages/components/src/dropdown/stories/index.story.tsx +++ b/packages/components/src/dropdown/stories/index.story.tsx @@ -99,6 +99,7 @@ export const WithMenuItems: StoryObj< typeof Dropdown > = { ...Default.args, renderContent: () => ( <> + Standalone Item Item 1 Item 2 diff --git a/packages/components/src/dropdown/style.scss b/packages/components/src/dropdown/style.scss index 8a5b0e0a0a6a28..d7ae7963f7ed8c 100644 --- a/packages/components/src/dropdown/style.scss +++ b/packages/components/src/dropdown/style.scss @@ -5,6 +5,16 @@ .components-dropdown__content { .components-popover__content { padding: $grid-unit-10; + + &:has(.components-menu-group) { + padding: 0; + + .components-dropdown-menu__menu > .components-menu-item__button, + > .components-menu-item__button { + margin: $grid-unit-10; + width: auto; + } + } } [role="menuitem"] { @@ -13,22 +23,9 @@ .components-menu-group { padding: $grid-unit-10; - margin-top: 0; - margin-bottom: 0; - margin-left: -$grid-unit-10; - margin-right: -$grid-unit-10; - - &:first-child { - margin-top: -$grid-unit-10; - } - - &:last-child { - margin-bottom: -$grid-unit-10; - } } .components-menu-group + .components-menu-group { - margin-top: 0; border-top: $border-width solid $gray-400; padding: $grid-unit-10; } diff --git a/packages/components/src/menu-group/style.scss b/packages/components/src/menu-group/style.scss index d9412c504940b3..744e3f74c5b955 100644 --- a/packages/components/src/menu-group/style.scss +++ b/packages/components/src/menu-group/style.scss @@ -1,5 +1,4 @@ .components-menu-group + .components-menu-group { - margin-top: $grid-unit-10; padding-top: $grid-unit-10; border-top: $border-width solid $gray-900; @@ -10,6 +9,10 @@ } } +.components-menu-group:has(> div:empty) { + display: none; +} + .components-menu-group__label { padding: 0 $grid-unit-10; margin-top: $grid-unit-05; From cf78cf014d766b32cbb7326f948cc7381c411289 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 24 Sep 2024 12:26:47 +0100 Subject: [PATCH 30/53] Inserter: Update how we compute the actual insertion point for blocks (#65490) Co-authored-by: youknowriad Co-authored-by: andrewserong Co-authored-by: jorgefilipecosta Co-authored-by: jasmussen --- .../components/inserter/block-types-tab.js | 2 +- .../inserter/hooks/use-block-types-state.js | 48 +++++++++---- .../inserter/hooks/use-insertion-point.js | 35 +++++++--- .../inserter/hooks/use-patterns-state.js | 2 +- .../inserter/media-tab/media-preview.js | 8 ++- .../inserter/test/block-types-tab.native.js | 67 ------------------ .../src/store/private-selectors.js | 38 ++++++++++ packages/block-editor/src/store/selectors.js | 70 +++++-------------- packages/block-editor/src/store/utils.js | 3 +- 9 files changed, 122 insertions(+), 151 deletions(-) delete mode 100644 packages/block-editor/src/components/inserter/test/block-types-tab.native.js diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js index 50a8b46b46427c..844d5dd341437e 100644 --- a/packages/block-editor/src/components/inserter/block-types-tab.js +++ b/packages/block-editor/src/components/inserter/block-types-tab.js @@ -186,7 +186,7 @@ export function BlockTypesTab( continue; } - if ( rootClientId && item.rootClientId === rootClientId ) { + if ( rootClientId && item.isAllowedInCurrentRoot ) { itemsForCurrentRoot.push( item ); } else { itemsRemaining.push( item ); diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 8db23267eee8f4..6f11060c75c494 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -2,19 +2,23 @@ * WordPress dependencies */ import { + getBlockType, createBlock, createBlocksFromInnerBlocksTemplate, store as blocksStore, parse, } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useMemo } from '@wordpress/element'; +import { store as noticesStore } from '@wordpress/notices'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; -import { withRootClientIdOptionKey } from '../../../store/utils'; +import { isFiltered } from '../../../store/utils'; +import { unlock } from '../../../lock-unlock'; /** * Retrieves the block types inserter state. @@ -26,7 +30,7 @@ import { withRootClientIdOptionKey } from '../../../store/utils'; */ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { const options = useMemo( - () => ( { [ withRootClientIdOptionKey ]: ! isQuick } ), + () => ( { [ isFiltered ]: !! isQuick } ), [ isQuick ] ); const [ items ] = useSelect( @@ -38,6 +42,10 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { ], [ rootClientId, options ] ); + const { getClosestAllowedInsertionPoint } = unlock( + useSelect( blockEditorStore ) + ); + const { createErrorNotice } = useDispatch( noticesStore ); const [ categories, collections ] = useSelect( ( select ) => { const { getCategories, getCollections } = select( blocksStore ); @@ -46,16 +54,29 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { const onSelectItem = useCallback( ( - { - name, - initialAttributes, - innerBlocks, - syncStatus, - content, - rootClientId: _rootClientId, - }, + { name, initialAttributes, innerBlocks, syncStatus, content }, shouldFocusBlock ) => { + const destinationClientId = getClosestAllowedInsertionPoint( + name, + rootClientId + ); + if ( destinationClientId === null ) { + const title = getBlockType( name )?.title ?? name; + createErrorNotice( + sprintf( + /* translators: %s: block pattern title. */ + __( 'Block "%s" can\'t be inserted.' ), + title + ), + { + type: 'snackbar', + id: 'inserter-notice', + } + ); + return; + } + const insertedBlock = syncStatus === 'unsynced' ? parse( content, { @@ -66,15 +87,14 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => { initialAttributes, createBlocksFromInnerBlocksTemplate( innerBlocks ) ); - onInsert( insertedBlock, undefined, shouldFocusBlock, - _rootClientId + destinationClientId ); }, - [ onInsert ] + [ onInsert, getClosestAllowedInsertionPoint, rootClientId ] ); return [ items, categories, collections, onSelectItem ]; diff --git a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js index 24074ec5004565..0cd71bf77b9830 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js +++ b/packages/block-editor/src/components/inserter/hooks/use-insertion-point.js @@ -71,7 +71,11 @@ function useInsertionPoint( { selectBlockOnInsert = true, } ) { const registry = useRegistry(); - const { getSelectedBlock } = useSelect( blockEditorStore ); + const { + getSelectedBlock, + getClosestAllowedInsertionPoint, + isBlockInsertionPointVisible, + } = unlock( useSelect( blockEditorStore ) ); const { destinationRootClientId, destinationIndex } = useSelect( ( select ) => { const { @@ -193,21 +197,30 @@ function useInsertionPoint( { const onToggleInsertionPoint = useCallback( ( item ) => { - if ( item?.hasOwnProperty( 'rootClientId' ) ) { - showInsertionPoint( - item.rootClientId, - getIndex( { - destinationRootClientId, - destinationIndex, - rootClientId: item.rootClientId, - registry, - } ) - ); + if ( item && ! isBlockInsertionPointVisible() ) { + const allowedDestinationRootClientId = + getClosestAllowedInsertionPoint( + item.name, + destinationRootClientId + ); + if ( allowedDestinationRootClientId !== null ) { + showInsertionPoint( + allowedDestinationRootClientId, + getIndex( { + destinationRootClientId, + destinationIndex, + rootClientId: allowedDestinationRootClientId, + registry, + } ) + ); + } } else { hideInsertionPoint(); } }, [ + getClosestAllowedInsertionPoint, + isBlockInsertionPointVisible, showInsertionPoint, hideInsertionPoint, destinationRootClientId, diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js index 6483dc58ae8b97..f8b083d4eedf19 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -87,7 +87,7 @@ const usePatternsState = ( onInsert, rootClientId, selectedCategory ) => { ), { type: 'snackbar', - id: 'block-pattern-inserted-notice', + id: 'inserter-notice', } ); }, diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js index 64088f45fa1c39..a890e5fe8dc132 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -184,13 +184,16 @@ export function MediaPreview( { media, onClick, category } ) { } ); createSuccessNotice( __( 'Image uploaded and inserted.' ), - { type: 'snackbar' } + { type: 'snackbar', id: 'inserter-notice' } ); setIsInserting( false ); }, allowedTypes: ALLOWED_MEDIA_TYPES, onError( message ) { - createErrorNotice( message, { type: 'snackbar' } ); + createErrorNotice( message, { + type: 'snackbar', + id: 'inserter-notice', + } ); setIsInserting( false ); }, } ); @@ -281,6 +284,7 @@ export function MediaPreview( { media, onClick, category } ) { onClick( cloneBlock( block ) ); createSuccessNotice( __( 'Image inserted.' ), { type: 'snackbar', + id: 'inserter-notice', } ); setShowExternalUploadModal( false ); } } diff --git a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js b/packages/block-editor/src/components/inserter/test/block-types-tab.native.js deleted file mode 100644 index 925570130359a6..00000000000000 --- a/packages/block-editor/src/components/inserter/test/block-types-tab.native.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * External dependencies - */ -import { render } from 'test/helpers'; - -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import items from './fixtures'; -import BlockTypesTab from '../block-types-tab'; - -jest.mock( '../hooks/use-clipboard-block' ); -jest.mock( '@wordpress/data/src/components/use-select' ); - -const selectMock = { - getCategories: jest.fn().mockReturnValue( [] ), - getCollections: jest.fn().mockReturnValue( [] ), - getInserterItems: jest.fn().mockReturnValue( [] ), - canInsertBlockType: jest.fn(), - getBlockType: jest.fn(), - getClipboard: jest.fn(), - getSettings: jest.fn( () => ( { impressions: {} } ) ), -}; - -describe( 'BlockTypesTab component', () => { - beforeEach( () => { - useSelect.mockImplementation( ( callback ) => - callback( () => selectMock ) - ); - } ); - - it( 'renders without crashing', () => { - const component = render( - - ); - expect( component ).toBeTruthy(); - } ); - - it( 'shows block items', () => { - selectMock.getInserterItems.mockReturnValue( items ); - - const blockItems = items.filter( - ( { id, category } ) => - category !== 'reusable' && id !== 'core-embed/a-paragraph-embed' - ); - const component = render( - - ); - - blockItems.forEach( ( item ) => { - expect( component.getByText( item.title ) ).toBeTruthy(); - } ); - } ); -} ); diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 9e99176819ae89..02a37b94ec27ff 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -16,6 +16,7 @@ import { getTemplateLock, getClientIdsWithDescendants, isNavigationMode, + getBlockRootClientId, } from './selectors'; import { checkAllowListRecursive, @@ -637,3 +638,40 @@ export function getZoomLevel( state ) { export function isZoomOut( state ) { return getZoomLevel( state ) < 100; } + +/** + * Finds the closest block where the block is allowed to be inserted. + * + * @param {Object} state Editor state. + * @param {string} name Block name. + * @param {string} clientId Default insertion point. + * + * @return {string} clientID of the closest container when the block name can be inserted. + */ +export function getClosestAllowedInsertionPoint( state, name, clientId = '' ) { + // If we're trying to insert at the root level and it's not allowed + // Try the section root instead. + if ( ! clientId ) { + if ( canInsertBlockType( state, name, clientId ) ) { + return clientId; + } + + const sectionRootClientId = getSectionRootClientId( state ); + if ( + sectionRootClientId && + canInsertBlockType( state, name, sectionRootClientId ) + ) { + return sectionRootClientId; + } + return null; + } + + // Traverse the block tree up until we find a place where we can insert. + let current = clientId; + while ( current !== null && ! canInsertBlockType( state, name, current ) ) { + const parentClientId = getBlockRootClientId( state, current ); + current = parentClientId; + } + + return current; +} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 20d6627398886c..3163bb5257a9ad 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -21,7 +21,7 @@ import { createSelector, createRegistrySelector } from '@wordpress/data'; * Internal dependencies */ import { - withRootClientIdOptionKey, + isFiltered, checkAllowListRecursive, checkAllowList, getAllPatternsDependants, @@ -80,7 +80,9 @@ const EMPTY_ARRAY = []; */ const EMPTY_SET = new Set(); -const EMPTY_OBJECT = {}; +const DEFAULT_INSERTER_OPTIONS = { + [ isFiltered ]: true, +}; /** * Returns a block's name given its client ID, or null if no block exists with @@ -2008,7 +2010,7 @@ const buildBlockTypeItem = */ export const getInserterItems = createRegistrySelector( ( select ) => createSelector( - ( state, rootClientId = null, options = EMPTY_OBJECT ) => { + ( state, rootClientId = null, options = DEFAULT_INSERTER_OPTIONS ) => { const buildReusableBlockInserterItem = ( reusableBlock ) => { const icon = ! reusableBlock.wp_pattern_sync_status ? { @@ -2056,56 +2058,7 @@ export const getInserterItems = createRegistrySelector( ( select ) => ) .map( buildBlockTypeInserterItem ); - if ( options[ withRootClientIdOptionKey ] ) { - blockTypeInserterItems = blockTypeInserterItems.reduce( - ( accumulator, item ) => { - item.rootClientId = rootClientId ?? ''; - - while ( - ! canInsertBlockTypeUnmemoized( - state, - item.name, - item.rootClientId - ) - ) { - if ( ! item.rootClientId ) { - let sectionRootClientId; - try { - sectionRootClientId = - getSectionRootClientId( state ); - } catch ( e ) {} - if ( - sectionRootClientId && - canInsertBlockTypeUnmemoized( - state, - item.name, - sectionRootClientId - ) - ) { - item.rootClientId = sectionRootClientId; - } else { - delete item.rootClientId; - } - break; - } else { - const parentClientId = getBlockRootClientId( - state, - item.rootClientId - ); - item.rootClientId = parentClientId; - } - } - - // We could also add non insertable items and gray them out. - if ( item.hasOwnProperty( 'rootClientId' ) ) { - accumulator.push( item ); - } - - return accumulator; - }, - [] - ); - } else { + if ( options[ isFiltered ] !== false ) { blockTypeInserterItems = blockTypeInserterItems.filter( ( blockType ) => canIncludeBlockTypeInInserter( @@ -2114,6 +2067,17 @@ export const getInserterItems = createRegistrySelector( ( select ) => rootClientId ) ); + } else { + blockTypeInserterItems = blockTypeInserterItems.map( + ( blockType ) => ( { + ...blockType, + isAllowedInCurrentRoot: canIncludeBlockTypeInInserter( + state, + blockType, + rootClientId + ), + } ) + ); } const items = blockTypeInserterItems.reduce( diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 79e15255e6cc15..9b83a8f74cf9aa 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -12,8 +12,7 @@ import { unlock } from '../lock-unlock'; import { STORE_NAME } from './constants'; import { getSectionRootClientId } from './private-selectors'; -export const withRootClientIdOptionKey = Symbol( 'withRootClientId' ); - +export const isFiltered = Symbol( 'isFiltered' ); const parsedPatternCache = new WeakMap(); const grammarMapCache = new WeakMap(); From df5eaed851c7effe459aa2a5f6cdf41cd3648104 Mon Sep 17 00:00:00 2001 From: Vipul Gupta <55375170+vipul0425@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:15:19 +0530 Subject: [PATCH 31/53] Fix: Button Replace remaining 40px default size violations [Block Editor 3] (#65225) * Fix: Global Styles component to use 40px default size. * Fix: Block variation picket to use 40px default size. * Fix: Block variation transform to use 40px default size. * Fix: color gradient dropdown and block appender button to use 40px default size. * fix: shadowpanel clear button * fix: Button Block appender issues. * fix: Coverts shadow panel Buttons to normal html buttons. * Update packages/block-library/src/group/editor.scss Co-authored-by: Lena Morita * Update packages/block-library/src/group/editor.scss Co-authored-by: Lena Morita * feat: Add tootlip tu shadow button. --------- Co-authored-by: vipul0425 Co-authored-by: DaniGuardiola Co-authored-by: t-hamano Co-authored-by: jasmussen Co-authored-by: jameskoster Co-authored-by: tyxla Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: ciampo --- .../block-variation-picker/index.js | 3 +- .../block-variation-transforms/index.js | 4 +- .../button-block-appender/content.scss | 5 -- .../components/button-block-appender/index.js | 3 +- .../components/colors-gradients/dropdown.js | 6 +- .../components/global-styles/color-panel.js | 6 +- .../components/global-styles/filters-panel.js | 3 +- .../global-styles/shadow-panel-components.js | 61 +++++++++---------- .../src/components/global-styles/style.scss | 5 ++ packages/block-library/src/group/editor.scss | 4 +- 10 files changed, 42 insertions(+), 58 deletions(-) diff --git a/packages/block-editor/src/components/block-variation-picker/index.js b/packages/block-editor/src/components/block-variation-picker/index.js index ecdf8b23bec3fe..f3687a305e84fd 100644 --- a/packages/block-editor/src/components/block-variation-picker/index.js +++ b/packages/block-editor/src/components/block-variation-picker/index.js @@ -64,8 +64,7 @@ function BlockVariationPicker( { { allowSkip && (
- } - /> + + + { isActive && } + + } + /> + ); } @@ -143,11 +142,7 @@ function renderShadowToggle() { }; return ( -