diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b80804b824e3cd..c0f70070908c1c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -135,9 +135,7 @@ jobs: uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: build-assets - path: | - ./build/ - ./build-module/ + path: ./build/ test-php: name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on ubuntu-latest @@ -214,6 +212,7 @@ jobs: uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: build-assets + path: ./build - name: Docker debug information run: | diff --git a/backport-changelog/6.7/7360.md b/backport-changelog/6.7/7360.md deleted file mode 100644 index b2fb8efd624b93..00000000000000 --- a/backport-changelog/6.7/7360.md +++ /dev/null @@ -1,3 +0,0 @@ -https://github.com/WordPress/wordpress-develop/pull/7360 - -* https://github.com/WordPress/gutenberg/pull/65460 diff --git a/changelog.txt b/changelog.txt index b04fa0e9bbf8e2..dca31f9afc622e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,280 +1,5 @@ == 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 diff --git a/docs/manifest.json b/docs/manifest.json index d76717fbdedfc1..d7f74d47995b63 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -521,12 +521,6 @@ "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/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 4b3ca78f74d299..c6552ef431cef8 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -857,10 +857,16 @@ _Returns_ ### hasBlockMovingClientId -> **Deprecated** - Returns whether block moving mode is enabled. +_Parameters_ + +- _state_ `Object`: Editor state. + +_Returns_ + +- `string`: Client Id of moving block. + ### hasDraggedInnerBlock Returns true if one of the block's inner blocks is dragged. @@ -1655,13 +1661,11 @@ _Returns_ ### setBlockMovingClientId -> **Deprecated** - -Set the block moving client ID. +Action that enables or disables the block moving mode. -_Returns_ +_Parameters_ -- `Object`: Action object. +- _hasBlockMovingClientId_ `string|null`: Enable/Disable block moving mode. ### setBlockVisibility diff --git a/docs/reference-guides/interactivity-api/core-concepts/README.md b/docs/reference-guides/interactivity-api/core-concepts/README.md index 695a4d622f6c52..f4e6891c4ff165 100644 --- a/docs/reference-guides/interactivity-api/core-concepts/README.md +++ b/docs/reference-guides/interactivity-api/core-concepts/README.md @@ -7,5 +7,3 @@ 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 deleted file mode 100644 index ed0bdd88211d11..00000000000000 --- a/docs/reference-guides/interactivity-api/core-concepts/using-typescript.md +++ /dev/null @@ -1,746 +0,0 @@ -# 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 0d4689811b26ec..719ffa344e3744 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -214,9 +214,6 @@ }, { "docs/reference-guides/interactivity-api/core-concepts/server-side-rendering.md": [] - }, - { - "docs/reference-guides/interactivity-api/core-concepts/using-typescript.md": [] } ] }, diff --git a/gutenberg.php b/gutenberg.php index e15cb113ea65a1..8dddcfeccd5282 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.2 + * Version: 19.3.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/client-assets.php b/lib/client-assets.php index 2343530e5595a7..62e874d6b06c82 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -601,56 +601,6 @@ function gutenberg_register_vendor_scripts( $scripts ) { } add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); -/** - * Registers or re-registers Gutenberg Script Modules. - * - * Script modules that are registered by Core will be re-registered by Gutenberg. - * - * @since 19.3.0 - */ -function gutenberg_default_script_modules() { - /* - * Expects multidimensional array like: - * - * 'interactivity/index.min.js' => array('dependencies' => array(…), 'version' => '…'), - * 'interactivity/debug.min.js' => array('dependencies' => array(…), 'version' => '…'), - * 'interactivity-router/index.min.js' => … - */ - $assets = include gutenberg_dir_path() . '/build-module/assets.php'; - - foreach ( $assets as $file_name => $script_module_data ) { - /* - * Build the WordPress Script Module ID from the file name. - * Prepend `@wordpress/` and remove extensions and `/index` if present: - * - interactivity/index.min.js => @wordpress/interactivity - * - interactivity/debug.min.js => @wordpress/interactivity/debug - * - block-library/query/view.js => @wordpress/block-library/query/view - */ - $script_module_id = '@wordpress/' . preg_replace( '~(?:/index)?\.min\.js$~D', '', $file_name, 1 ); - switch ( $script_module_id ) { - /* - * Interactivity exposes two entrypoints, "/index" and "/debug". - * "/debug" should replalce "/index" in devlopment. - */ - case '@wordpress/interactivity/debug': - if ( ! SCRIPT_DEBUG ) { - continue 2; - } - $script_module_id = '@wordpress/interactivity'; - break; - case '@wordpress/interactivity': - if ( SCRIPT_DEBUG ) { - continue 2; - } - break; - } - - $path = gutenberg_url( "build-module/{$file_name}" ); - wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'] ); - } -} -remove_action( 'wp_default_scripts', 'wp_default_script_modules' ); -add_action( 'wp_default_scripts', 'gutenberg_default_script_modules' ); /* * Always remove the Core action hook while gutenberg_enqueue_stored_styles() exists to avoid styles being printed twice. diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php index fe23786fc03621..f65bc1704dd890 100644 --- a/lib/experimental/script-modules.php +++ b/lib/experimental/script-modules.php @@ -239,5 +239,26 @@ function gutenberg_a11y_script_module_html() { . '
' . ''; } -add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' ); -add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' ); + +/** + * Registers Gutenberg Script Modules. + * + * @since 19.3 + */ +function gutenberg_register_script_modules() { + // When in production, use the plugin's version as the default asset version; + // else (for development or test) default to use the current time. + $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); + + wp_deregister_script_module( '@wordpress/a11y' ); + wp_register_script_module( + '@wordpress/a11y', + gutenberg_url( 'build-module/a11y/index.min.js' ), + array(), + $default_version + ); + + add_action( 'wp_footer', 'gutenberg_a11y_script_module_html' ); + add_action( 'admin_footer', 'gutenberg_a11y_script_module_html' ); +} +add_action( 'init', 'gutenberg_register_script_modules' ); diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index ff68936f054a7e..c00d68bc70e8e2 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -5,6 +5,37 @@ * @package gutenberg */ +/** + * Deregisters the Core Interactivity API Modules and replace them + * with the ones from the Gutenberg plugin. + */ +function gutenberg_reregister_interactivity_script_modules() { + $default_version = defined( 'GUTENBERG_VERSION' ) && ! SCRIPT_DEBUG ? GUTENBERG_VERSION : time(); + wp_deregister_script_module( '@wordpress/interactivity' ); + wp_deregister_script_module( '@wordpress/interactivity-router' ); + + wp_register_script_module( + '@wordpress/interactivity', + gutenberg_url( '/build-module/' . ( SCRIPT_DEBUG ? 'interactivity/debug.min.js' : 'interactivity/index.min.js' ) ), + array(), + $default_version + ); + + wp_register_script_module( + '@wordpress/interactivity-router', + gutenberg_url( '/build-module/interactivity-router/index.min.js' ), + array( + array( + 'id' => '@wordpress/a11y', + 'import' => 'dynamic', + ), + '@wordpress/interactivity', + ), + $default_version + ); +} +add_action( 'init', 'gutenberg_reregister_interactivity_script_modules' ); + /** * Adds script data to the interactivity-router script module. * diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index b31be6ffd8d56d..c8c3fdb66ecb0e 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -1,16 +1,8 @@ -## Unreleased +## Internal -### Bug Fixes - -- Fix a bug in 8.8.1 due to missing files in the published package ([#65481](https://github.com/WordPress/gutenberg/pull/65481)). - -## 8.8.0 (2024-09-19) - -### Internal - -- Added `addPolyfillComments` option. When used, it will automatically add magic comments to mark files that need `wp-polyfill` ([#65292](https://github.com/WordPress/gutenberg/pull/65292)). +- Added `addPolyfillComments` option. When used, it will automatically add magic comments to mark files that need `wp-polyfill`. ## 8.7.0 (2024-09-05) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 1203586ec20292..f0f015cb2203f7 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -26,9 +26,7 @@ }, "files": [ "build", - "index.js", - "polyfill-exclusions.js", - "replace-polyfills.js" + "index.js" ], "main": "index.js", "dependencies": { diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 77238c6f386084..cc99df6dbeaafc 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -8,6 +8,7 @@ $z-layers: ( ".block-editor-block-switcher__arrow": 1, ".block-editor-block-list__block {core/image aligned wide or fullwide}": 20, ".block-library-classic__toolbar": 31, // When scrolled to top this toolbar needs to sit over block-editor-block-toolbar + ".block-editor-block-list__block-selection-button": 22, ".components-form-toggle__input": 1, ".editor-text-editor__toolbar": 1, @@ -69,6 +70,10 @@ $z-layers: ( // Below the media library backdrop (.media-modal-backdrop), which has a z-index of 159900. ".block-editor-global-styles-background-panel__popover": 159900 - 10, + // Small screen inner blocks overlay must be displayed above drop zone, + // settings menu, and movers. + ".block-editor-block-list__layout.has-overlay::after": 60, + // The toolbar, when contextual, should be above any adjacent nested block click overlays. ".block-editor-block-contextual-toolbar": 61, diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 75d7e8879572d4..a474d1df7e56d0 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -1027,11 +1027,11 @@ _Parameters_ ### useZoomOut -A hook used to set the zoomed out view, invoking the hook sets the mode. +A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. _Parameters_ -- _zoomOut_ `boolean`: If we should zoom out or not. +- _zoomOut_ `boolean`: If we should enter into zoomOut mode or not ### Warning diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index f06c8addedad50..5e12165c679424 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -29,6 +29,7 @@ export default function BlockActions( { getBlockRootClientId, getBlocksByClientId, getDirectInsertBlock, + canMoveBlocks, canRemoveBlocks, } = select( blockEditorStore ); @@ -43,6 +44,7 @@ export default function BlockActions( { : null; return { + canMove: canMoveBlocks( clientIds ), canRemove: canRemoveBlocks( clientIds ), canInsertBlock: canInsertDefaultBlock || !! directInsertBlock, canCopyStyles: blocks.every( ( block ) => { @@ -65,7 +67,8 @@ export default function BlockActions( { ); const { getBlocksByClientId, getBlocks } = useSelect( blockEditorStore ); - const { canRemove, canInsertBlock, canCopyStyles, canDuplicate } = selected; + const { canMove, canRemove, canInsertBlock, canCopyStyles, canDuplicate } = + selected; const { removeBlocks, @@ -74,6 +77,9 @@ export default function BlockActions( { insertAfterBlock, insertBeforeBlock, flashBlock, + setBlockMovingClientId, + setNavigationMode, + selectBlock, } = useDispatch( blockEditorStore ); const notifyCopy = useNotifyCopy(); @@ -83,6 +89,7 @@ export default function BlockActions( { canCopyStyles, canDuplicate, canInsertBlock, + canMove, canRemove, onDuplicate() { return duplicateBlocks( clientIds, updateSelection ); @@ -96,6 +103,11 @@ export default function BlockActions( { onInsertAfter() { insertAfterBlock( clientIds[ clientIds.length - 1 ] ); }, + onMoveTo() { + setNavigationMode( true ); + selectBlock( clientIds[ 0 ] ); + setBlockMovingClientId( clientIds[ 0 ] ); + }, onGroup() { if ( ! clientIds.length ) { return; 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 new file mode 100644 index 00000000000000..f7884cc1882ed5 --- /dev/null +++ b/packages/block-editor/src/components/block-controls/use-has-block-controls.js @@ -0,0 +1,35 @@ +/** + * 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-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 475d4f6a4b8c2e..a18556f2fa5bd9 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -40,36 +40,83 @@ function BlockStylesPanel( { clientId } ) { ); } +function BlockInspectorLockedBlocks( { topLevelLockedBlock } ) { + const contentClientIds = useSelect( + ( select ) => { + const { + getClientIdsOfDescendants, + getBlockName, + getBlockEditingMode, + } = select( blockEditorStore ); + return getClientIdsOfDescendants( topLevelLockedBlock ).filter( + ( clientId ) => + getBlockName( clientId ) !== 'core/list-item' && + getBlockEditingMode( clientId ) === 'contentOnly' + ); + }, + [ topLevelLockedBlock ] + ); + const hasBlockStyles = useSelect( + ( select ) => { + const { getBlockName } = select( blockEditorStore ); + const { getBlockStyles } = select( blocksStore ); + return !! getBlockStyles( getBlockName( topLevelLockedBlock ) ) + ?.length; + }, + [ topLevelLockedBlock ] + ); + const blockInformation = useBlockDisplayInformation( topLevelLockedBlock ); + return ( +
+ + + { hasBlockStyles && ( + + ) } + { contentClientIds.length > 0 && ( + + + + ) } +
+ ); +} + const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { const { count, selectedBlockName, selectedBlockClientId, blockType, - isSectionBlock, + topLevelLockedBlock, } = useSelect( ( select ) => { const { getSelectedBlockClientId, getSelectedBlockCount, getBlockName, - getParentSectionBlock, - isSectionBlock: _isSectionBlock, + getContentLockingParent, + getTemplateLock, } = unlock( select( blockEditorStore ) ); const _selectedBlockClientId = getSelectedBlockClientId(); - const renderedBlockClientId = - getParentSectionBlock( _selectedBlockClientId ) || - getSelectedBlockClientId(); const _selectedBlockName = - renderedBlockClientId && getBlockName( renderedBlockClientId ); + _selectedBlockClientId && getBlockName( _selectedBlockClientId ); const _blockType = _selectedBlockName && getBlockType( _selectedBlockName ); return { count: getSelectedBlockCount(), - selectedBlockClientId: renderedBlockClientId, + selectedBlockClientId: _selectedBlockClientId, selectedBlockName: _selectedBlockName, blockType: _blockType, - isSectionBlock: _isSectionBlock( renderedBlockClientId ), + topLevelLockedBlock: + getContentLockingParent( _selectedBlockClientId ) || + ( getTemplateLock( _selectedBlockClientId ) === 'contentOnly' || + _selectedBlockName === 'core/block' + ? _selectedBlockClientId + : undefined ), }; }, [] ); @@ -89,7 +136,7 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { blockName: selectedBlockName, } ); - if ( count > 1 && ! isSectionBlock ) { + if ( count > 1 ) { return (
@@ -147,6 +194,13 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { } return null; } + if ( topLevelLockedBlock ) { + return ( + + ); + } return ( { ); @@ -207,13 +260,9 @@ const AnimatedContainer = ( { ); }; -const BlockInspectorSingleBlock = ( { - clientId, - blockName, - isSectionBlock, -} ) => { +const BlockInspectorSingleBlock = ( { clientId, blockName } ) => { const availableTabs = useInspectorControlsTabs( blockName ); - const showTabs = ! isSectionBlock && availableTabs?.length > 1; + const showTabs = availableTabs?.length > 1; const hasBlockStyles = useSelect( ( select ) => { @@ -225,26 +274,6 @@ const BlockInspectorSingleBlock = ( { ); const blockInformation = useBlockDisplayInformation( clientId ); const borderPanelLabel = useBorderPanelLabel( { blockName } ); - const contentClientIds = useSelect( - ( select ) => { - // Avoid unnecessary subscription. - if ( ! isSectionBlock ) { - return; - } - - const { - getClientIdsOfDescendants, - getBlockName, - getBlockEditingMode, - } = select( blockEditorStore ); - return getClientIdsOfDescendants( clientId ).filter( - ( current ) => - getBlockName( current ) !== 'core/list-item' && - getBlockEditingMode( current ) === 'contentOnly' - ); - }, - [ isSectionBlock, clientId ] - ); return (
@@ -267,48 +296,35 @@ const BlockInspectorSingleBlock = ( { { hasBlockStyles && ( ) } - - { contentClientIds && contentClientIds?.length > 0 && ( - - - - ) } - - { ! isSectionBlock && ( - <> - - - - - - - - - - -
- -
- - ) } + + + + + + + + + + +
+ +
) } diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 90c39649319dc8..deda4e3b9d0897 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -549,7 +549,6 @@ function BlockListBlockProvider( props ) { getBlockMode, isSelectionEnabled, getTemplateLock, - isSectionBlock: _isSectionBlock, getBlockWithoutAttributes, getBlockAttributes, canRemoveBlock, @@ -572,6 +571,8 @@ function BlockListBlockProvider( props ) { __unstableSelectionHasUnmergeableBlock, isBlockBeingDragged, isDragging, + hasBlockMovingClientId, + canInsertBlockType, __unstableHasActiveBlockOverlayActive, __unstableGetEditorMode, getSelectedBlocksInitialCaretPosition, @@ -631,6 +632,7 @@ function BlockListBlockProvider( props ) { clientId, checkDeep ); + const movingClientId = hasBlockMovingClientId(); const blockEditingMode = getBlockEditingMode( clientId ); const multiple = hasBlockSupport( blockName, 'multiple', true ); @@ -651,7 +653,7 @@ function BlockListBlockProvider( props ) { mode: getBlockMode( clientId ), isSelectionEnabled: isSelectionEnabled(), isLocked: !! getTemplateLock( rootClientId ), - isSectionBlock: _isSectionBlock( clientId ), + templateLock: getTemplateLock( clientId ), canRemove, canMove, isSelected: _isSelected, @@ -679,9 +681,11 @@ function BlockListBlockProvider( props ) { hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ) && ! isDragging(), - initialPosition: _isSelected - ? getSelectedBlocksInitialCaretPosition() - : undefined, + initialPosition: + _isSelected && + ( editorMode === 'edit' || editorMode === 'zoom-out' ) // Don't recalculate the initialPosition when toggling in/out of zoom-out mode + ? getSelectedBlocksInitialCaretPosition() + : undefined, isHighlighted: isBlockHighlighted( clientId ), isMultiSelected, isPartiallySelected: @@ -690,6 +694,13 @@ function BlockListBlockProvider( props ) { ! __unstableSelectionHasUnmergeableBlock(), isDragging: isBlockBeingDragged( clientId ), hasChildSelected: isAncestorOfSelectedBlock, + isBlockMovingMode: !! movingClientId, + canInsertMovingBlock: + movingClientId && + canInsertBlockType( + getBlockName( movingClientId ), + rootClientId + ), isEditingDisabled: blockEditingMode === 'disabled', hasEditableOutline: blockEditingMode !== 'disabled' && @@ -734,7 +745,9 @@ function BlockListBlockProvider( props ) { isReusable, isDragging, hasChildSelected, - isSectionBlock, + isBlockMovingMode, + canInsertMovingBlock, + templateLock, isEditingDisabled, hasEditableOutline, className, @@ -779,7 +792,9 @@ function BlockListBlockProvider( props ) { isReusable, isDragging, hasChildSelected, - isSectionBlock, + isBlockMovingMode, + canInsertMovingBlock, + templateLock, isEditingDisabled, hasEditableOutline, isTemporarilyEditingAsBlocks, diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index 3e3865e689beac..3f4b4c508aeb02 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -80,6 +80,7 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b // since things like border-radius need to be able to be set on the block itself. .block-editor-block-list__block.is-highlighted, .block-editor-block-list__block.is-highlighted ~ .is-multi-selected, + &.is-navigate-mode .block-editor-block-list__block.is-selected, .block-editor-block-list__block:not([contenteditable="true"]):focus { outline: none; @@ -91,6 +92,34 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b } } + // Moving blocks using keyboard (Ellipsis > Move). + & .is-block-moving-mode.block-editor-block-list__block.is-selected { + + &::after { + content: ""; + position: absolute; + z-index: 0; + pointer-events: none; + transition: + border-color 0.1s linear, + border-style 0.1s linear, + box-shadow 0.1s linear; + right: 0; + left: 0; + top: -$default-block-margin * 0.5; + border-radius: $radius-small; + border-top: 4px solid $gray-400; + bottom: auto; + box-shadow: none; + } + } + + & .is-block-moving-mode.can-insert-moving-block.block-editor-block-list__block.is-selected { + &::after { + border-color: var(--wp-admin-theme-color); + } + } + // Ensure an accurate partial text selection. // To do this, we disable text selection on the main container, then re-enable it only on the // elements that actually get selected. @@ -105,6 +134,14 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b } } +.is-block-moving-mode.block-editor-block-list__block-selection-button { + // Should be invisible but not unfocusable. + opacity: 0; + font-size: 1px; + height: 1px; + padding: 0; +} + .block-editor-block-list__layout .block-editor-block-list__block { // With `position: static`, Safari marks a full-width selection rectangle, including margins. // With `position: relative`, Safari marks an inline selection rectangle, similar to that of @@ -186,6 +223,19 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b background-color: transparent; } + // Reusable blocks clickthrough overlays. + &.is-reusable > .block-editor-inner-blocks > .block-editor-block-list__layout.has-overlay { + // Remove only the top click overlay. + &::after { + display: none; + } + + // Restore it for subsequent. + .block-editor-block-list__layout.has-overlay::after { + display: block; + } + } + // Reusable blocks parent border. &.is-reusable.has-child-selected::after { box-shadow: 0 0 0 1px var(--wp-admin-theme-color); @@ -257,7 +307,7 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b } } -.is-root-container:not([inert]) .block-editor-block-list__block.is-selected .block-editor-block-list__block.has-editable-outline::after { +.is-root-container:not([inert]) .block-editor-block-list__block.is-reusable.is-selected .block-editor-block-list__block.has-editable-outline::after { animation-name: block-editor-is-editable__animation; animation-duration: 0.8s; animation-timing-function: ease-out; diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index e2e019d4a9bf69..ea6128f1534642 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -47,17 +47,26 @@ const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap(); function Root( { className, ...settings } ) { const isLargeViewport = useViewportMatch( 'medium' ); - const { isOutlineMode, isFocusMode, temporarilyEditingAsBlocks } = - useSelect( ( select ) => { - const { getSettings, getTemporarilyEditingAsBlocks, isTyping } = - unlock( select( blockEditorStore ) ); - const { outlineMode, focusMode } = getSettings(); - return { - isOutlineMode: outlineMode && ! isTyping(), - isFocusMode: focusMode, - temporarilyEditingAsBlocks: getTemporarilyEditingAsBlocks(), - }; - }, [] ); + const { + isOutlineMode, + isFocusMode, + editorMode, + temporarilyEditingAsBlocks, + } = useSelect( ( select ) => { + const { + getSettings, + __unstableGetEditorMode, + getTemporarilyEditingAsBlocks, + isTyping, + } = unlock( select( blockEditorStore ) ); + const { outlineMode, focusMode } = getSettings(); + return { + isOutlineMode: outlineMode && ! isTyping(), + isFocusMode: focusMode, + editorMode: __unstableGetEditorMode(), + temporarilyEditingAsBlocks: getTemporarilyEditingAsBlocks(), + }; + }, [] ); const registry = useRegistry(); const { setBlockVisibility } = useDispatch( blockEditorStore ); @@ -106,6 +115,7 @@ function Root( { className, ...settings } ) { className: clsx( 'is-root-container', className, { 'is-outline-mode': isOutlineMode, 'is-focus-mode': isFocusMode && isLargeViewport, + 'is-navigate-mode': editorMode === 'navigation', } ), }, settings @@ -182,8 +192,7 @@ function Items( { getTemplateLock, getBlockEditingMode, __unstableGetEditorMode, - isSectionBlock, - } = unlock( select( blockEditorStore ) ); + } = select( blockEditorStore ); const _order = getBlockOrder( rootClientId ); @@ -202,16 +211,15 @@ function Items( { visibleBlocks: __unstableGetVisibleBlocks(), isZoomOut: __unstableGetEditorMode() === 'zoom-out', shouldRenderAppender: - ! isSectionBlock( rootClientId ) && - getBlockEditingMode( rootClientId ) !== 'disabled' && - ! getTemplateLock( rootClientId ) && hasAppender && __unstableGetEditorMode() !== 'zoom-out' && - ( hasCustomAppender || - rootClientId === selectedBlockClientId || - ( ! rootClientId && - ! selectedBlockClientId && - ! _order.length ) ), + ( hasCustomAppender + ? ! getTemplateLock( rootClientId ) && + getBlockEditingMode( rootClientId ) !== 'disabled' + : rootClientId === selectedBlockClientId || + ( ! rootClientId && + ! selectedBlockClientId && + ! _order.length ) ), }; }, [ rootClientId, hasAppender, hasCustomAppender ] diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 1cf1d4908b0768..c3a279a618b5da 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -25,6 +25,7 @@ import { } from '../../block-edit/context'; import { useFocusHandler } from './use-focus-handler'; import { useEventHandlers } from './use-selected-block-event-handlers'; +import { useNavModeExit } from './use-nav-mode-exit'; import { useZoomOutModeExit } from './use-zoom-out-mode-exit'; import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; @@ -97,11 +98,13 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isReusable, isDragging, hasChildSelected, + isBlockMovingMode, + canInsertMovingBlock, isEditingDisabled, hasEditableOutline, isTemporarilyEditingAsBlocks, defaultClassName, - isSectionBlock, + templateLock, } = useContext( PrivateBlockContext ); // translators: %s: Type of block (i.e. Text, Image etc) @@ -113,6 +116,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { useBlockRefProvider( clientId ), useFocusHandler( clientId ), useEventHandlers( { clientId, isSelected } ), + useNavModeExit( clientId ), useZoomOutModeExit( { editorMode } ), useIsHovered( { clientId } ), useIntersectionObserver(), @@ -120,7 +124,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { useDisabled( { isDisabled: ! hasOverlay } ), useFlashEditableBlocks( { clientId, - isEnabled: isSectionBlock, + isEnabled: name === 'core/block' || templateLock === 'contentOnly', } ), useScrollIntoView( { isSelected } ), ] ); @@ -178,6 +182,8 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { 'is-reusable': isReusable, 'is-dragging': isDragging, 'has-child-selected': hasChildSelected, + 'is-block-moving-mode': isBlockMovingMode, + 'can-insert-moving-block': canInsertMovingBlock, 'is-editing-disabled': isEditingDisabled, 'has-editable-outline': hasEditableOutline, 'has-negative-margin': hasNegativeMargin, diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-nav-mode-exit.js b/packages/block-editor/src/components/block-list/use-block-props/use-nav-mode-exit.js new file mode 100644 index 00000000000000..aa9c0a630c5bd7 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-nav-mode-exit.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useRefEffect } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../../store'; + +/** + * Allows navigation mode to be exited by clicking in the selected block. + * + * @param {string} clientId Block client ID. + */ +export function useNavModeExit( clientId ) { + const { isNavigationMode, isBlockSelected } = useSelect( blockEditorStore ); + const { setNavigationMode, selectBlock } = useDispatch( blockEditorStore ); + return useRefEffect( + ( node ) => { + function onMouseDown( event ) { + // Don't select a block if it's already handled by a child + // block. + if ( isNavigationMode() && ! event.defaultPrevented ) { + // Prevent focus from moving to the block. + event.preventDefault(); + + // When clicking on a selected block, exit navigation mode. + if ( isBlockSelected( clientId ) ) { + setNavigationMode( false ); + } else { + selectBlock( clientId ); + } + } + } + + node.addEventListener( 'mousedown', onMouseDown ); + + return () => { + node.removeEventListener( 'mousedown', onMouseDown ); + }; + }, + [ clientId, isNavigationMode, isBlockSelected, setNavigationMode ] + ); +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js index 92c54bac9b806d..d0001bd3b33c68 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-zoom-out-mode-exit.js @@ -16,17 +16,14 @@ import { unlock } from '../../../lock-unlock'; * @param {string} clientId Block client ID. */ export function useZoomOutModeExit( { editorMode } ) { - const { getSettings, isZoomOut } = unlock( useSelect( blockEditorStore ) ); - const { __unstableSetEditorMode, resetZoomLevel } = unlock( + const { getSettings } = useSelect( blockEditorStore ); + const { __unstableSetEditorMode } = unlock( useDispatch( blockEditorStore ) ); return useRefEffect( ( node ) => { - // In "compose" mode. - const composeMode = editorMode === 'zoom-out' && isZoomOut(); - - if ( ! composeMode ) { + if ( editorMode !== 'zoom-out' ) { return; } @@ -42,7 +39,6 @@ export function useZoomOutModeExit( { editorMode } ) { __experimentalSetIsInserterOpened( false ); } __unstableSetEditorMode( 'edit' ); - resetZoomLevel(); } } diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index 2b76804785a576..bb307816fd1501 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -11,7 +11,6 @@ import { isRTL } from '@wordpress/i18n'; */ import { store as blockEditorStore } from '../../store'; import { InsertionPointOpenRef } from '../block-tools/insertion-point'; -import { unlock } from '../../lock-unlock'; export function useInBetweenInserter() { const openRef = useContext( InsertionPointOpenRef ); @@ -32,8 +31,7 @@ export function useInBetweenInserter() { getBlockEditingMode, getBlockName, getBlockAttributes, - getParentSectionBlock, - } = unlock( useSelect( blockEditorStore ) ); + } = useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); @@ -135,8 +133,7 @@ export function useInBetweenInserter() { const clientId = element.id.slice( 'block-'.length ); if ( ! clientId || - __unstableIsWithinBlockOverlay( clientId ) || - !! getParentSectionBlock( clientId ) + __unstableIsWithinBlockOverlay( clientId ) ) { return; } 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 9090de42f8b7d7..80b314eeb42e5c 100644 --- a/packages/block-editor/src/components/block-parent-selector/index.js +++ b/packages/block-editor/src/components/block-parent-selector/index.js @@ -14,7 +14,6 @@ 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 @@ -24,26 +23,24 @@ import { unlock } from '../../lock-unlock'; */ export default function BlockParentSelector() { const { selectBlock } = useDispatch( blockEditorStore ); - const { parentClientId, isVisible } = useSelect( ( select ) => { + const { firstParentClientId, isVisible } = useSelect( ( select ) => { const { getBlockName, getBlockParents, getSelectedBlockClientId, getBlockEditingMode, - getParentSectionBlock, - } = unlock( select( blockEditorStore ) ); + } = select( blockEditorStore ); const { hasBlockSupport } = select( blocksStore ); const selectedBlockClientId = getSelectedBlockClientId(); - const parentSection = getParentSectionBlock( selectedBlockClientId ); const parents = getBlockParents( selectedBlockClientId ); - const _parentClientId = parentSection ?? parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( _parentClientId ); + const _firstParentClientId = parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( _firstParentClientId ); const _parentBlockType = getBlockType( parentBlockName ); return { - parentClientId: _parentClientId, + firstParentClientId: _firstParentClientId, isVisible: - _parentClientId && - getBlockEditingMode( _parentClientId ) !== 'disabled' && + _firstParentClientId && + getBlockEditingMode( _firstParentClientId ) === 'default' && hasBlockSupport( _parentBlockType, '__experimentalParentSelector', @@ -51,7 +48,7 @@ export default function BlockParentSelector() { ), }; }, [] ); - const blockInformation = useBlockDisplayInformation( parentClientId ); + const blockInformation = useBlockDisplayInformation( firstParentClientId ); // Allows highlighting the parent block outline when focusing or hovering // the parent block selector within the child. @@ -68,13 +65,13 @@ export default function BlockParentSelector() { return (
selectBlock( parentClientId ) } + onClick={ () => selectBlock( firstParentClientId ) } 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-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js index 4ebce4172e9b37..39063db4f52e02 100644 --- a/packages/block-editor/src/components/block-settings-menu-controls/index.js +++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js @@ -4,9 +4,12 @@ import { createSlotFill, MenuGroup, + MenuItem, __experimentalStyleProvider as StyleProvider, } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; +import { pipe } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -93,6 +96,18 @@ const BlockSettingsMenuControlsSlot = ( { fillProps, clientIds = null } ) => { /> ) } { fills } + { fillProps?.canMove && + ! fillProps?.onlyBlock && + ! isContentOnly && ( + + { __( 'Move to' ) } + + ) } { selectedClientIds.length === 1 && ( { const { + getBlockCount, getBlockName, getBlockRootClientId, getPreviousBlockClientId, @@ -84,6 +86,7 @@ export function BlockSettingsDropdown( { return { firstParentClientId: _firstParentClientId, + onlyBlock: 1 === getBlockCount( _firstParentClientId ), parentBlockType: _firstParentClientId && ( getActiveBlockVariation( @@ -183,9 +186,6 @@ export function BlockSettingsDropdown( { } } - const shouldShowBlockParentMenuItem = - ! parentBlockIsSelected && !! firstParentClientId; - return ( { - // 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 && ( + onMoveTo, + } ) => ( + + { ( { onClose } ) => ( + <> + + <__unstableBlockSettingsMenuFirstItem.Slot + fillProps={ { onClose } } + /> + { ! parentBlockIsSelected && + !! firstParentClientId && ( ) } - { count === 1 && ( - - ) } - { ! isContentOnly && ( - - ) } - { canDuplicate && ( + { count === 1 && ( + + ) } + { ! isContentOnly && ( + + ) } + { canDuplicate && ( + + { __( 'Duplicate' ) } + + ) } + { canInsertBlock && ! isContentOnly && ( + <> - { __( 'Duplicate' ) } - - ) } - { canInsertBlock && ! isContentOnly && ( - <> - - { __( 'Add before' ) } - - - { __( 'Add after' ) } - - - ) } - - { canCopyStyles && ! isContentOnly && ( - - - - { __( 'Paste styles' ) } + { __( 'Add before' ) } - - ) } - - { typeof children === 'function' - ? children( { onClose } ) - : Children.map( ( child ) => - cloneElement( child, { onClose } ) - ) } - { canRemove && ( - - { __( 'Delete' ) } + { __( 'Add after' ) } - + ) } - - ) } - - ); - } } + + { 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 79f33bd30d7537..98e7f7b2d21420 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -35,40 +35,36 @@ function BlockSwitcherDropdownMenuContents( { clientIds, hasBlockStyles, canRemove, + isUsingBindings, } ) { const { replaceBlocks, multiSelect, updateBlockAttributes } = useDispatch( blockEditorStore ); - 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 { 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 blockVariationTransformations = useBlockVariationTransforms( { clientIds, blocks, @@ -200,7 +196,7 @@ const BlockIndicator = ( { icon, showTitle, blockTitle } ) => ( ); -export const BlockSwitcher = ( { clientIds } ) => { +export const BlockSwitcher = ( { clientIds, disabled, isUsingBindings } ) => { const { hasContentOnlyLocking, canRemove, @@ -209,7 +205,6 @@ export const BlockSwitcher = ( { clientIds } ) => { invalidBlocks, isReusable, isTemplate, - isDisabled, } = useSelect( ( select ) => { const { @@ -217,7 +212,6 @@ export const BlockSwitcher = ( { clientIds } ) => { getBlocksByClientId, getBlockAttributes, canRemoveBlocks, - getBlockEditingMode, } = select( blockEditorStore ); const { getBlockStyles, getBlockType, getActiveBlockVariation } = select( blocksStore ); @@ -228,7 +222,6 @@ export const BlockSwitcher = ( { clientIds } ) => { const [ { name: firstBlockName } ] = _blocks; const _isSingleBlockSelected = _blocks.length === 1; const blockType = getBlockType( firstBlockName ); - const editingMode = getBlockEditingMode( clientIds[ 0 ] ); let _icon; let _hasTemplateLock; @@ -263,7 +256,6 @@ export const BlockSwitcher = ( { clientIds } ) => { isTemplate: _isSingleBlockSelected && isTemplatePart( _blocks[ 0 ] ), hasContentOnlyLocking: _hasTemplateLock, - isDisabled: editingMode !== 'default', }; }, [ clientIds ] @@ -283,7 +275,7 @@ export const BlockSwitcher = ( { clientIds } ) => { : __( 'Multiple blocks selected' ); const hideDropdown = - isDisabled || + disabled || ( ! hasBlockStyles && ! canRemove ) || hasContentOnlyLocking; @@ -347,6 +339,7 @@ export const BlockSwitcher = ( { clientIds } ) => { 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 2ac2cbb12ff352..6c4789cb2924f2 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -35,7 +35,6 @@ 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. @@ -59,6 +58,7 @@ export function PrivateBlockToolbar( { const { blockClientId, blockClientIds, + isContentOnlyEditingMode, isDefaultEditingMode, blockType, toolbarKey, @@ -78,14 +78,12 @@ export function PrivateBlockToolbar( { getBlockAttributes, getBlockParentsByBlockName, getTemplateLock, - getParentSectionBlock, - } = unlock( select( blockEditorStore ) ); + } = select( blockEditorStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; const parents = getBlockParents( selectedBlockClientId ); - const parentSection = getParentSectionBlock( selectedBlockClientId ); - const parentClientId = parentSection ?? parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( parentClientId ); + const firstParentClientId = parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( firstParentClientId ); const parentBlockType = getBlockType( parentBlockName ); const editingMode = getBlockEditingMode( selectedBlockClientId ); const _isDefaultEditingMode = editingMode === 'default'; @@ -114,19 +112,21 @@ export function PrivateBlockToolbar( { return { blockClientId: selectedBlockClientId, blockClientIds: selectedBlockClientIds, + isContentOnlyEditingMode: editingMode === 'contentOnly', isDefaultEditingMode: _isDefaultEditingMode, blockType: selectedBlockClientId && getBlockType( _blockName ), shouldShowVisualToolbar: isValid && isVisual, - toolbarKey: `${ selectedBlockClientId }${ parentClientId }`, + toolbarKey: `${ selectedBlockClientId }${ firstParentClientId }`, showParentSelector: parentBlockType && - getBlockEditingMode( parentClientId ) !== 'disabled' && + getBlockEditingMode( firstParentClientId ) === 'default' && hasBlockSupport( parentBlockType, '__experimentalParentSelector', true ) && - selectedBlockClientIds.length === 1, + selectedBlockClientIds.length === 1 && + _isDefaultEditingMode, isUsingBindings: _isUsingBindings, hasParentPattern: _hasParentPattern, hasContentOnlyLocking: _hasTemplateLock, @@ -179,26 +179,36 @@ export function PrivateBlockToolbar( { key={ toolbarKey } >
- { ! isMultiToolbar && isLargeViewport && ( - - ) } + { ! isMultiToolbar && + isLargeViewport && + isDefaultEditingMode && } { ( shouldShowVisualToolbar || isMultiToolbar ) && - ! hasParentPattern && ( + ( isDefaultEditingMode || + ( isContentOnlyEditingMode && ! hasParentPattern ) || + isSynced ) && (
- - { ! isMultiToolbar && isDefaultEditingMode && ( - - ) } - + { isDefaultEditingMode && ( + <> + { ! isMultiToolbar && ( + + ) } + + + ) }
) } @@ -232,7 +242,9 @@ 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 2a0f68a6976686..40d748dd0a1568 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -52,18 +52,9 @@ > :last-child, > :last-child .components-toolbar-group, - > :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 { + > :last-child .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 80ce3691147834..c4e228f8a3c07b 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,6 +7,7 @@ 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. @@ -14,29 +15,40 @@ import { store as blockEditorStore } from '../../store'; * @return {boolean} Whether the block toolbar component will be rendered. */ export function useHasBlockToolbar() { - const { isToolbarEnabled, isBlockDisabled } = useSelect( ( select ) => { - const { getBlockEditingMode, getBlockName, getBlockSelectionStart } = - select( blockEditorStore ); + const { isToolbarEnabled, isDefaultEditingMode } = 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 ), - isBlockDisabled: - getBlockEditingMode( selectedBlockClientId ) === 'disabled', - }; - }, [] ); + return { + isToolbarEnabled: + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ), + isDefaultEditingMode: + getBlockEditingMode( selectedBlockClientId ) === 'default', + }; + }, + [] + ); - if ( ! isToolbarEnabled || isBlockDisabled ) { + const hasAnyBlockControls = useHasAnyBlockControls(); + + if ( + ! isToolbarEnabled || + ( ! isDefaultEditingMode && ! hasAnyBlockControls ) + ) { return false; } diff --git a/packages/block-editor/src/components/block-tools/block-selection-button.js b/packages/block-editor/src/components/block-tools/block-selection-button.js new file mode 100644 index 00000000000000..9c6c22181ef2ac --- /dev/null +++ b/packages/block-editor/src/components/block-tools/block-selection-button.js @@ -0,0 +1,302 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { dragHandle } from '@wordpress/icons'; +import { Button, Flex, FlexItem } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { forwardRef, useEffect } from '@wordpress/element'; +import { + BACKSPACE, + DELETE, + UP, + DOWN, + LEFT, + RIGHT, + TAB, + ESCAPE, + ENTER, + SPACE, +} from '@wordpress/keycodes'; +import { + __experimentalGetAccessibleBlockLabel as getAccessibleBlockLabel, + store as blocksStore, +} from '@wordpress/blocks'; +import { speak } from '@wordpress/a11y'; +import { focus } from '@wordpress/dom'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BlockTitle from '../block-title'; +import BlockIcon from '../block-icon'; +import { store as blockEditorStore } from '../../store'; +import BlockDraggable from '../block-draggable'; +import { useBlockElement } from '../block-list/use-block-props/use-block-refs'; + +/** + * Block selection button component, displaying the label of the block. If the block + * descends from a root block, a button is displayed enabling the user to select + * the root block. + * + * @param {string} props Component props. + * @param {string} props.clientId Client ID of block. + * @param {Object} ref Reference to the component. + * + * @return {Component} The component to be rendered. + */ +function BlockSelectionButton( { clientId, rootClientId }, ref ) { + const selected = useSelect( + ( select ) => { + const { + getBlock, + getBlockIndex, + hasBlockMovingClientId, + getBlockListSettings, + __unstableGetEditorMode, + getNextBlockClientId, + getPreviousBlockClientId, + canMoveBlock, + } = select( blockEditorStore ); + const { getActiveBlockVariation, getBlockType } = + select( blocksStore ); + const index = getBlockIndex( clientId ); + const { name, attributes } = getBlock( clientId ); + const blockType = getBlockType( name ); + const orientation = + getBlockListSettings( rootClientId )?.orientation; + const match = getActiveBlockVariation( name, attributes ); + + return { + blockMovingMode: hasBlockMovingClientId(), + editorMode: __unstableGetEditorMode(), + icon: match?.icon || blockType.icon, + label: getAccessibleBlockLabel( + blockType, + attributes, + index + 1, + orientation + ), + canMove: canMoveBlock( clientId, rootClientId ), + getNextBlockClientId, + getPreviousBlockClientId, + }; + }, + [ clientId, rootClientId ] + ); + const { label, icon, blockMovingMode, editorMode, canMove } = selected; + const { setNavigationMode, removeBlock } = useDispatch( blockEditorStore ); + + // Focus the breadcrumb in navigation mode. + useEffect( () => { + if ( editorMode === 'navigation' ) { + ref.current.focus(); + speak( label ); + } + }, [ label, editorMode ] ); + const blockElement = useBlockElement( clientId ); + + const { + hasBlockMovingClientId, + getBlockIndex, + getBlockRootClientId, + getClientIdsOfDescendants, + getSelectedBlockClientId, + getMultiSelectedBlocksEndClientId, + getPreviousBlockClientId, + getNextBlockClientId, + } = useSelect( blockEditorStore ); + const { + selectBlock, + clearSelectedBlock, + setBlockMovingClientId, + moveBlockToPosition, + } = useDispatch( blockEditorStore ); + + function onKeyDown( event ) { + const { keyCode } = event; + const isUp = keyCode === UP; + const isDown = keyCode === DOWN; + const isLeft = keyCode === LEFT; + const isRight = keyCode === RIGHT; + const isTab = keyCode === TAB; + const isEscape = keyCode === ESCAPE; + const isEnter = keyCode === ENTER; + const isSpace = keyCode === SPACE; + const isShift = event.shiftKey; + + if ( keyCode === BACKSPACE || keyCode === DELETE ) { + removeBlock( clientId ); + event.preventDefault(); + return; + } + + const selectedBlockClientId = getSelectedBlockClientId(); + const selectionEndClientId = getMultiSelectedBlocksEndClientId(); + const selectionBeforeEndClientId = getPreviousBlockClientId( + selectionEndClientId || selectedBlockClientId + ); + const selectionAfterEndClientId = getNextBlockClientId( + selectionEndClientId || selectedBlockClientId + ); + + const navigateUp = ( isTab && isShift ) || isUp; + const navigateDown = ( isTab && ! isShift ) || isDown; + // Move out of current nesting level (no effect if at root level). + const navigateOut = isLeft; + // Move into next nesting level (no effect if the current block has no innerBlocks). + const navigateIn = isRight; + + let focusedBlockUid; + if ( navigateUp ) { + focusedBlockUid = selectionBeforeEndClientId; + } else if ( navigateDown ) { + focusedBlockUid = selectionAfterEndClientId; + } else if ( navigateOut ) { + focusedBlockUid = + getBlockRootClientId( selectedBlockClientId ) ?? + selectedBlockClientId; + } else if ( navigateIn ) { + focusedBlockUid = + getClientIdsOfDescendants( selectedBlockClientId )[ 0 ] ?? + selectedBlockClientId; + } + const startingBlockClientId = hasBlockMovingClientId(); + if ( isEscape && startingBlockClientId && ! event.defaultPrevented ) { + setBlockMovingClientId( null ); + event.preventDefault(); + } + if ( ( isEnter || isSpace ) && startingBlockClientId ) { + const sourceRoot = getBlockRootClientId( startingBlockClientId ); + const destRoot = getBlockRootClientId( selectedBlockClientId ); + const sourceBlockIndex = getBlockIndex( startingBlockClientId ); + let destinationBlockIndex = getBlockIndex( selectedBlockClientId ); + if ( + sourceBlockIndex < destinationBlockIndex && + sourceRoot === destRoot + ) { + destinationBlockIndex -= 1; + } + moveBlockToPosition( + startingBlockClientId, + sourceRoot, + destRoot, + destinationBlockIndex + ); + selectBlock( startingBlockClientId ); + setBlockMovingClientId( null ); + } + // Prevent the block from being moved into itself. + if ( + startingBlockClientId && + selectedBlockClientId === startingBlockClientId && + navigateIn + ) { + return; + } + if ( navigateDown || navigateUp || navigateOut || navigateIn ) { + if ( focusedBlockUid ) { + event.preventDefault(); + selectBlock( focusedBlockUid ); + } else if ( isTab && selectedBlockClientId ) { + let nextTabbable; + + if ( navigateDown ) { + nextTabbable = blockElement; + do { + nextTabbable = focus.tabbable.findNext( nextTabbable ); + } while ( + nextTabbable && + blockElement.contains( nextTabbable ) + ); + + if ( ! nextTabbable ) { + nextTabbable = + blockElement.ownerDocument.defaultView.frameElement; + nextTabbable = focus.tabbable.findNext( nextTabbable ); + } + } else { + nextTabbable = focus.tabbable.findPrevious( blockElement ); + } + + if ( nextTabbable ) { + event.preventDefault(); + nextTabbable.focus(); + clearSelectedBlock(); + } + } + } + } + + const classNames = clsx( + 'block-editor-block-list__block-selection-button', + { + 'is-block-moving-mode': !! blockMovingMode, + } + ); + + const dragHandleLabel = __( 'Drag' ); + const showBlockDraggable = canMove && editorMode === 'navigation'; + + return ( +
+ + + + + { showBlockDraggable && ( + + + { ( draggableProps ) => ( + + + ) } + +
+ ); +} + +export default forwardRef( BlockSelectionButton ); diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js new file mode 100644 index 00000000000000..ae03bdb4f51647 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import BlockSelectionButton from './block-selection-button'; +import { PrivateBlockPopover } from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; + +function BlockToolbarBreadcrumb( { clientId, __unstableContentRef }, ref ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + return ( + + + + ); +} + +export default forwardRef( BlockToolbarBreadcrumb ); diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 099323925384b8..24f60dbbf970aa 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -19,6 +19,7 @@ import { default as InsertionPoint, } from './insertion-point'; import BlockToolbarPopover from './block-toolbar-popover'; +import BlockToolbarBreadcrumb from './block-toolbar-breadcrumb'; import ZoomOutPopover from './zoom-out-popover'; import { store as blockEditorStore } from '../../store'; import usePopoverScroll from '../block-popover/use-popover-scroll'; @@ -77,6 +78,7 @@ export default function BlockTools( { const { getGroupingBlockName } = useSelect( blocksStore ); const { showEmptyBlockSideInserter, + showBreadcrumb, showBlockToolbarPopover, showZoomOutToolbar, } = useShowBlockTools(); @@ -221,6 +223,14 @@ export default function BlockTools( { /> ) } + { showBreadcrumb && ( + + ) } + { showZoomOutToolbar && ( { const { @@ -47,6 +48,7 @@ function InbetweenInsertionPointPopover( { getPreviousBlockClientId, getNextBlockClientId, getSettings, + isNavigationMode: _isNavigationMode, __unstableGetEditorMode, } = select( blockEditorStore ); const insertionPoint = getBlockInsertionPoint(); @@ -76,6 +78,7 @@ function InbetweenInsertionPointPopover( { getBlockListSettings( insertionPoint.rootClientId ) ?.orientation || 'vertical', rootClientId: insertionPoint.rootClientId, + isNavigationMode: _isNavigationMode(), isDistractionFree: settings.isDistractionFree, isInserterShown: insertionPoint?.__unstableWithInserter, isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', @@ -141,7 +144,7 @@ function InbetweenInsertionPointPopover( { }, }; - if ( isDistractionFree ) { + if ( isDistractionFree && ! isNavigationMode ) { return null; } diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index a3d9153273e983..9f1325d7f95a1a 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -84,6 +84,84 @@ } } +/** + * Block Label for Navigation/Selection Mode + */ + +.block-editor-block-list__block-selection-button { + display: inline-flex; + padding: 0 $grid-unit-15; + z-index: z-index(".block-editor-block-list__block-selection-button"); + + // Dark block UI appearance. + border-radius: $radius-small; + background-color: $gray-900; + + font-size: $default-font-size; + height: $block-toolbar-height; + + .block-editor-block-list__block-selection-button__content { + margin: auto; + display: inline-flex; + align-items: center; + + > .components-flex__item { + margin-right: $grid-unit-15 * 0.5; + } + } + .components-button.has-icon.block-selection-button_drag-handle { + cursor: grab; + padding: 0; + height: $grid-unit-30; + min-width: $grid-unit-30; + margin-left: -2px; + + // Drag handle is smaller than the others. + svg { + min-width: 18px; + min-height: 18px; + } + } + + .block-editor-block-icon { + font-size: $default-font-size; + color: $white; + height: $block-toolbar-height; + } + + // The button here has a special style to appear as a toolbar. + .components-button { + min-width: $button-size; + color: $white; + height: $block-toolbar-height; + + // When button is focused, it receives a box-shadow instead of the border. + &:focus { + box-shadow: none; + border: none; + } + + &:active { + color: $white; + } + + // Make sure the button has no hover style when it's disabled. + &[aria-disabled="true"]:hover { + color: $white; + } + + display: flex; + } + .block-selection-button_select-button.components-button { + padding: 0; + } + + .block-editor-block-mover { + background: unset; + border: none; + } +} + // Hide the popover block editor list while dragging. // Using a hacky animation to delay hiding the element. // It's needed because if we hide the element immediately upon dragging, @@ -100,10 +178,14 @@ .components-popover.block-editor-block-list__block-popover { // Position the block toolbar. + .block-editor-block-list__block-selection-button, .block-editor-block-contextual-toolbar { pointer-events: all; margin-top: $grid-unit-10; margin-bottom: $grid-unit-10; + } + + .block-editor-block-contextual-toolbar { border: $border-width solid $gray-900; border-radius: $radius-small; overflow: visible; // allow the parent selector to be visible diff --git a/packages/block-editor/src/components/block-tools/use-show-block-tools.js b/packages/block-editor/src/components/block-tools/use-show-block-tools.js index 02a8f0583bcddf..07e0ebd16a64b0 100644 --- a/packages/block-editor/src/components/block-tools/use-show-block-tools.js +++ b/packages/block-editor/src/components/block-tools/use-show-block-tools.js @@ -22,6 +22,7 @@ export function useShowBlockTools() { getBlock, getBlockMode, getSettings, + hasMultiSelection, __unstableGetEditorMode, isTyping, } = select( blockEditorStore ); @@ -41,20 +42,29 @@ export function useShowBlockTools() { ! isTyping() && editorMode === 'edit' && isEmptyDefaultBlock; + const maybeShowBreadcrumb = + hasSelectedBlock && + ! hasMultiSelection() && + editorMode === 'navigation'; + const isZoomOut = editorMode === 'zoom-out'; const _showZoomOutToolbar = isZoomOut && block?.attributes?.align === 'full' && - ! _showEmptyBlockSideInserter; + ! _showEmptyBlockSideInserter && + ! maybeShowBreadcrumb; const _showBlockToolbarPopover = ! _showZoomOutToolbar && ! getSettings().hasFixedToolbar && ! _showEmptyBlockSideInserter && hasSelectedBlock && - ! isEmptyDefaultBlock; + ! isEmptyDefaultBlock && + ! maybeShowBreadcrumb; return { showEmptyBlockSideInserter: _showEmptyBlockSideInserter, + showBreadcrumb: + ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, showBlockToolbarPopover: _showBlockToolbarPopover, showZoomOutToolbar: _showZoomOutToolbar, }; diff --git a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js index b8736de11481a0..a3c46c4b4c970a 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-toolbar.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + /** * WordPress dependencies */ @@ -15,13 +20,13 @@ import BlockDraggable from '../block-draggable'; import BlockMover from '../block-mover'; import Shuffle from '../block-toolbar/shuffle'; import NavigableToolbar from '../navigable-toolbar'; -import { unlock } from '../../lock-unlock'; export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { const selected = useSelect( ( select ) => { const { getBlock, + hasBlockMovingClientId, getNextBlockClientId, getPreviousBlockClientId, canRemoveBlock, @@ -57,6 +62,7 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { } return { + blockMovingMode: hasBlockMovingClientId(), isBlockTemplatePart, isNextBlockTemplatePart, isPrevBlockTemplatePart, @@ -69,6 +75,7 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { ); const { + blockMovingMode, isBlockTemplatePart, isNextBlockTemplatePart, isPrevBlockTemplatePart, @@ -77,15 +84,18 @@ export default function ZoomOutToolbar( { clientId, __unstableContentRef } ) { setIsInserterOpened, } = selected; - const { removeBlock, __unstableSetEditorMode, resetZoomLevel } = unlock( - useDispatch( blockEditorStore ) - ); + const { removeBlock, __unstableSetEditorMode } = + useDispatch( blockEditorStore ); + + const classNames = clsx( 'zoom-out-toolbar', { + 'is-block-moving-mode': !! blockMovingMode, + } ); const showBlockDraggable = canMove && ! isBlockTemplatePart; return ( diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js index 8f29effb14e679..97a3f980541842 100644 --- a/packages/block-editor/src/components/block-variation-transforms/index.js +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -140,30 +140,19 @@ function VariationsToggleGroupControl( { function __experimentalBlockVariationTransforms( { blockClientId } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const { activeBlockVariation, variations, isContentOnly } = useSelect( + const { activeBlockVariation, variations } = useSelect( ( select ) => { - const { - getActiveBlockVariation, - getBlockVariations, - __experimentalHasContentRoleAttribute, - } = select( blocksStore ); - const { getBlockName, getBlockAttributes, getBlockEditingMode } = + const { getActiveBlockVariation, getBlockVariations } = + select( blocksStore ); + const { getBlockName, getBlockAttributes } = select( blockEditorStore ); - const name = blockClientId && getBlockName( blockClientId ); - - const isContentBlock = - __experimentalHasContentRoleAttribute( name ); - return { activeBlockVariation: getActiveBlockVariation( name, getBlockAttributes( blockClientId ) ), variations: name && getBlockVariations( name, 'transform' ), - isContentOnly: - getBlockEditingMode( blockClientId ) === 'contentOnly' && - ! isContentBlock, }; }, [ blockClientId ] @@ -192,7 +181,8 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) { } ); }; - if ( ! variations?.length || isContentOnly ) { + // Skip rendering if there are no variations + if ( ! variations?.length ) { return null; } 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 4c52de6a3d7d11..ce508a5ebc89e5 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, - BoxControl, + __experimentalBoxControl as BoxControl, __experimentalUnitControl as UnitControl, __experimentalUseCustomUnits as useCustomUnits, __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, diff --git a/packages/block-editor/src/components/inner-blocks/content.scss b/packages/block-editor/src/components/inner-blocks/content.scss new file mode 100644 index 00000000000000..05259ede35daaa --- /dev/null +++ b/packages/block-editor/src/components/inner-blocks/content.scss @@ -0,0 +1,13 @@ +// Add clickable overlay to blocks with nesting. +// This makes it easy to select all layers of the block. +.block-editor-block-list__layout.has-overlay { + &::after { + content: ""; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: z-index(".block-editor-block-list__layout.has-overlay::after"); + } +} diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 1583031a8ea18d..0d35b459190fef 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -195,11 +195,14 @@ export function useInnerBlocksProps( props = {}, options = {} ) { ( select ) => { const { getBlockName, + isBlockSelected, + hasSelectedInnerBlock, __unstableGetEditorMode, getTemplateLock, getBlockRootClientId, getBlockEditingMode, getBlockSettings, + isDragging, getSectionRootClientId, } = unlock( select( blockEditorStore ) ); let _isDropZoneDisabled; @@ -210,6 +213,8 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const { hasBlockSupport, getBlockType } = select( blocksStore ); const blockName = getBlockName( clientId ); + const enableClickThrough = + __unstableGetEditorMode() === 'navigation'; const blockEditingMode = getBlockEditingMode( clientId ); const parentClientId = getBlockRootClientId( clientId ); const [ defaultLayout ] = getBlockSettings( clientId, 'layout' ); @@ -231,6 +236,12 @@ export function useInnerBlocksProps( props = {}, options = {} ) { '__experimentalExposeControlsToChildren', false ), + hasOverlay: + blockName !== 'core/template' && + ! isBlockSelected( clientId ) && + ! hasSelectedInnerBlock( clientId, true ) && + enableClickThrough && + ! isDragging(), name: blockName, blockType: getBlockType( blockName ), parentLock: getTemplateLock( parentClientId ), @@ -243,6 +254,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { ); const { __experimentalCaptureToolbars, + hasOverlay, name, blockType, parentLock, @@ -287,7 +299,10 @@ export function useInnerBlocksProps( props = {}, options = {} ) { className: clsx( props.className, 'block-editor-block-list__layout', - __unstableDisableLayoutClassNames ? '' : layoutClassNames + __unstableDisableLayoutClassNames ? '' : layoutClassNames, + { + 'has-overlay': hasOverlay, + } ), children: clientId ? ( diff --git a/packages/block-editor/src/components/list-view/block-contents.js b/packages/block-editor/src/components/list-view/block-contents.js index 0ed2b162b127b8..91bfbd7eddaa03 100644 --- a/packages/block-editor/src/components/list-view/block-contents.js +++ b/packages/block-editor/src/components/list-view/block-contents.js @@ -1,6 +1,12 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + /** * WordPress dependencies */ +import { useSelect } from '@wordpress/data'; import { forwardRef } from '@wordpress/element'; /** @@ -8,6 +14,7 @@ import { forwardRef } from '@wordpress/element'; */ import ListViewBlockSelectButton from './block-select-button'; import BlockDraggable from '../block-draggable'; +import { store as blockEditorStore } from '../../store'; import { useListViewContext } from './context'; const ListViewBlockContents = forwardRef( @@ -27,9 +34,29 @@ const ListViewBlockContents = forwardRef( ref ) => { const { clientId } = block; + + const { blockMovingClientId, selectedBlockInBlockEditor } = useSelect( + ( select ) => { + const { hasBlockMovingClientId, getSelectedBlockClientId } = + select( blockEditorStore ); + return { + blockMovingClientId: hasBlockMovingClientId(), + selectedBlockInBlockEditor: getSelectedBlockClientId(), + }; + }, + [] + ); + const { AdditionalBlockContent, insertedBlock, setInsertedBlock } = useListViewContext(); + const isBlockMoveTarget = + blockMovingClientId && selectedBlockInBlockEditor === clientId; + + const className = clsx( 'block-editor-list-view-block-contents', { + 'is-dropping-before': isBlockMoveTarget, + } ); + // Only include all selected blocks if the currently clicked on block // is one of the selected blocks. This ensures that if a user attempts // to drag a block that isn't part of the selection, they're still able @@ -55,7 +82,7 @@ const ListViewBlockContents = forwardRef( { ( { draggable, onDragStart, onDragEnd } ) => ( select( blockEditorStore ).__unstableGetEditorMode(), [] ); - const { __unstableSetEditorMode } = unlock( - useDispatch( blockEditorStore ) - ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); return ( ( <> - + - { selectIcon } - { __( 'Design' ) } + + { __( 'Edit' ) } ), - info: __( - 'Full control over layout and styling.' - ), }, { value: 'navigation', label: ( <> - - { __( 'Edit' ) } + { selectIcon } + { __( 'Select' ) } ), - info: __( 'Focus on content.' ), }, ] } /> diff --git a/packages/block-editor/src/components/tool-selector/style.scss b/packages/block-editor/src/components/tool-selector/style.scss index 07ca91d346d907..03774fe0f6b9d3 100644 --- a/packages/block-editor/src/components/tool-selector/style.scss +++ b/packages/block-editor/src/components/tool-selector/style.scss @@ -8,8 +8,3 @@ color: $gray-700; min-width: 280px; } - -.block-editor-tool-selector__menu .components-menu-item__info { - margin-left: $grid-unit-30 + $grid-unit-15; // icon size + margin - text-align: left; -} diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index c88ec4e5378926..8a09999ecdff3c 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -16,6 +16,7 @@ import { plus as add, group, ungroup, + moveTo as move, } from '@wordpress/icons'; /** @@ -126,6 +127,59 @@ export const useTransformCommands = () => { return { isLoading: false, commands }; }; +const useActionsCommands = () => { + const { clientIds } = useSelect( ( select ) => { + const { getSelectedBlockClientIds } = select( blockEditorStore ); + const selectedBlockClientIds = getSelectedBlockClientIds(); + + return { + clientIds: selectedBlockClientIds, + }; + }, [] ); + + const { getBlockRootClientId, canMoveBlocks, getBlockCount } = + useSelect( blockEditorStore ); + + const { setBlockMovingClientId, setNavigationMode, selectBlock } = + useDispatch( blockEditorStore ); + + if ( ! clientIds || clientIds.length < 1 ) { + return { isLoading: false, commands: [] }; + } + + const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); + + const canMove = + canMoveBlocks( clientIds ) && getBlockCount( rootClientId ) !== 1; + + const commands = []; + + if ( canMove ) { + commands.push( { + name: 'move-to', + label: __( 'Move to' ), + callback: () => { + setNavigationMode( true ); + selectBlock( clientIds[ 0 ] ); + setBlockMovingClientId( clientIds[ 0 ] ); + }, + icon: move, + } ); + } + + return { + isLoading: false, + commands: commands.map( ( command ) => ( { + ...command, + name: 'core/block-editor/action-' + command.name, + callback: ( { close } ) => { + command.callback(); + close(); + }, + } ) ), + }; +}; + const useQuickActionsCommands = () => { const { clientIds, isUngroupable, isGroupable } = useSelect( ( select ) => { const { @@ -290,6 +344,10 @@ export const useBlockCommands = () => { name: 'core/block-editor/blockTransforms', hook: useTransformCommands, } ); + useCommandLoader( { + name: 'core/block-editor/blockActions', + hook: useActionsCommands, + } ); useCommandLoader( { name: 'core/block-editor/blockQuickActions', hook: useQuickActionsCommands, diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index 3788c7021fd664..b321d7c8d29957 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { focus, isFormElement } from '@wordpress/dom'; -import { TAB } from '@wordpress/keycodes'; +import { TAB, ESCAPE } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect, useMergeRefs } from '@wordpress/compose'; import { useRef } from '@wordpress/element'; @@ -21,9 +21,19 @@ export default function useTabNav() { const { hasMultiSelection, getSelectedBlockClientId, getBlockCount } = useSelect( blockEditorStore ); - const { setLastFocus } = unlock( useDispatch( blockEditorStore ) ); + const { setNavigationMode, setLastFocus } = unlock( + useDispatch( blockEditorStore ) + ); + const isNavigationMode = useSelect( + ( select ) => select( blockEditorStore ).isNavigationMode(), + [] + ); + const { getLastFocus } = unlock( useSelect( blockEditorStore ) ); + // Don't allow tabbing to this element in Navigation mode. + const focusCaptureTabIndex = ! isNavigationMode ? '0' : undefined; + // Reference that holds the a flag for enabling or disabling // capturing on the focus capture elements. const noCaptureRef = useRef(); @@ -46,6 +56,8 @@ export default function useTabNav() { .focus(); } } else { + setNavigationMode( true ); + const canvasElement = container.current.ownerDocument === event.target.ownerDocument ? container.current @@ -70,7 +82,7 @@ export default function useTabNav() { const before = (
); @@ -78,7 +90,7 @@ export default function useTabNav() { const after = (
); @@ -89,6 +101,12 @@ export default function useTabNav() { return; } + if ( event.keyCode === ESCAPE && ! hasMultiSelection() ) { + event.preventDefault(); + setNavigationMode( true ); + return; + } + // In Edit mode, Tab should focus the first tabbable element after // the content, which is normally the sidebar (with block controls) // and Shift+Tab should focus the first tabbable element before the @@ -101,6 +119,20 @@ export default function useTabNav() { const isShift = event.shiftKey; const direction = isShift ? 'findPrevious' : 'findNext'; + + if ( ! hasMultiSelection() && ! getSelectedBlockClientId() ) { + // Preserve the behaviour of entering navigation mode when + // tabbing into the content without a block selection. + // `onFocusCapture` already did this previously, but we need to + // do it again here because after clearing block selection, + // focus land on the writing flow container and pressing Tab + // will no longer send focus through the focus capture element. + if ( event.target === node ) { + setNavigationMode( true ); + } + return; + } + const nextTabbable = focus.tabbable[ direction ]( event.target ); // We want to constrain the tabbing to the block and its child blocks. diff --git a/packages/block-editor/src/content.scss b/packages/block-editor/src/content.scss index 1ef4e118fb1bbe..36d428dca6b762 100644 --- a/packages/block-editor/src/content.scss +++ b/packages/block-editor/src/content.scss @@ -8,6 +8,7 @@ @import "./components/button-block-appender/content.scss"; @import "./components/default-block-appender/content.scss"; @import "./components/iframe/content.scss"; +@import "./components/inner-blocks/content.scss"; @import "./components/media-placeholder/content.scss"; @import "./components/plain-text/content.scss"; @import "./components/rich-text/content.scss"; diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index ac045004cc654b..e1ebf5fda6b8ee 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -162,8 +162,9 @@ export const withBlockBindingSupport = createHigherOrderComponent( let values = {}; if ( ! source.getValues ) { Object.keys( bindings ).forEach( ( attr ) => { - // Default to the the source label when `getValues` doesn't exist. - values[ attr ] = source.label; + // Default to the `key` or the source label when `getValues` doesn't exist + values[ attr ] = + bindings[ attr ].args?.key || source.label; } ); } else { values = source.getValues( { diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index 2a1b43060c00a9..d7e21ec1be0578 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -8,40 +8,46 @@ import { useEffect, useRef } from '@wordpress/element'; * Internal dependencies */ import { store as blockEditorStore } from '../store'; -import { unlock } from '../lock-unlock'; /** - * A hook used to set the zoomed out view, invoking the hook sets the mode. + * A hook used to set the editor mode to zoomed out mode, invoking the hook sets the mode. * - * @param {boolean} zoomOut If we should zoom out or not. + * @param {boolean} zoomOut If we should enter into zoomOut mode or not */ export function useZoomOut( zoomOut = true ) { - const { setZoomLevel } = unlock( useDispatch( blockEditorStore ) ); - const { isZoomOut } = unlock( useSelect( blockEditorStore ) ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { __unstableGetEditorMode } = useSelect( blockEditorStore ); - const originalIsZoomOutRef = useRef( null ); + const originalEditingModeRef = useRef( null ); + const mode = __unstableGetEditorMode(); useEffect( () => { // Only set this on mount so we know what to return to when we unmount. - if ( ! originalIsZoomOutRef.current ) { - originalIsZoomOutRef.current = isZoomOut(); + if ( ! originalEditingModeRef.current ) { + originalEditingModeRef.current = mode; } - // The effect opens the zoom-out view if we want it open and the canvas is not currently zoomed-out. - if ( zoomOut && isZoomOut() === false ) { - setZoomLevel( 50 ); + return () => { + // We need to use __unstableGetEditorMode() here and not `mode`, as mode may not update on unmount + if ( + __unstableGetEditorMode() === 'zoom-out' && + __unstableGetEditorMode() !== originalEditingModeRef.current + ) { + __unstableSetEditorMode( originalEditingModeRef.current ); + } + }; + }, [] ); + + // The effect opens the zoom-out view if we want it open and it's not currently in zoom-out mode. + useEffect( () => { + if ( zoomOut && mode !== 'zoom-out' ) { + __unstableSetEditorMode( 'zoom-out' ); } else if ( ! zoomOut && - isZoomOut() && - originalIsZoomOutRef.current !== isZoomOut() + __unstableGetEditorMode() === 'zoom-out' && + originalEditingModeRef.current !== mode ) { - setZoomLevel( originalIsZoomOutRef.current ? 50 : 100 ); + __unstableSetEditorMode( originalEditingModeRef.current ); } - - return () => { - if ( isZoomOut() && isZoomOut() !== originalIsZoomOutRef.current ) { - setZoomLevel( originalIsZoomOutRef.current ? 50 : 100 ); - } - }; - }, [ isZoomOut, setZoomLevel, zoomOut ] ); + }, [ __unstableGetEditorMode, __unstableSetEditorMode, zoomOut ] ); // Mode is deliberately excluded from the dependencies so that the effect does not run when mode changes. } diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index ee11838395ec5c..e91f997ca67837 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1728,24 +1728,23 @@ export const __unstableSetEditorMode = }; /** - * Set the block moving client ID. + * Action that enables or disables the block moving mode. * - * @deprecated - * - * @return {Object} Action object. + * @param {string|null} hasBlockMovingClientId Enable/Disable block moving mode. */ -export function setBlockMovingClientId() { - deprecated( - 'wp.data.dispatch( "core/block-editor" ).setBlockMovingClientId', - { - since: '6.7', - hint: 'Block moving mode feature has been removed', +export const setBlockMovingClientId = + ( hasBlockMovingClientId = null ) => + ( { dispatch } ) => { + dispatch( { type: 'SET_BLOCK_MOVING_MODE', hasBlockMovingClientId } ); + + if ( hasBlockMovingClientId ) { + speak( + __( + 'Use the Tab key and Arrow keys to choose new block location. Use Left and Right Arrow keys to move between nesting levels. Once location is selected press Enter or Space to move the block.' + ) + ); } - ); - return { - type: 'DO_NOTHING', }; -} /** * Action that duplicates a list of blocks. diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index 441a07202c42ac..dc57d61fd6b76c 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -383,26 +383,3 @@ export const modifyContentLockBlock = focusModeToRevert ); }; - -/** - * Sets the zoom level. - * - * @param {number} zoom the new zoom level - * @return {Object} Action object. - */ -export function setZoomLevel( zoom = 100 ) { - return { - type: 'SET_ZOOM_LEVEL', - zoom, - }; -} - -/** - * Resets the Zoom state. - * @return {Object} Action object. - */ -export function resetZoomLevel() { - return { - type: 'RESET_ZOOM_LEVEL', - }; -} diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index 9e99176819ae89..01ad8f69febc9e 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -15,7 +15,6 @@ import { getBlockName, getTemplateLock, getClientIdsWithDescendants, - isNavigationMode, } from './selectors'; import { checkAllowListRecursive, @@ -116,7 +115,6 @@ export const getEnabledClientIdsTree = createSelector( state.settings.templateLock, state.blockListSettings, state.editorMode, - getSectionRootClientId( state ), ] ); @@ -481,74 +479,19 @@ export const getContentLockingParent = createSelector( ( state, clientId ) => { let current = clientId; let result; - while ( - ! result && - ( current = state.blocks.parents.get( current ) ) - ) { - if ( getTemplateLock( state, current ) === 'contentOnly' ) { + while ( ( current = state.blocks.parents.get( current ) ) ) { + if ( + getBlockName( state, current ) === 'core/block' || + getTemplateLock( state, current ) === 'contentOnly' + ) { result = current; } } return result; }, - ( state ) => [ - state.blocks.parents, - state.blockListSettings, - state.settings.templateLock, - ] + ( state ) => [ state.blocks.parents, state.blockListSettings ] ); -/** - * Retrieves the client ID of the parent section block. - * - * @param {Object} state Global application state. - * @param {Object} clientId Client Id of the block. - * - * @return {?string} Client ID of the ancestor block that is content locking the block. - */ -export const getParentSectionBlock = createSelector( - ( state, clientId ) => { - let current = clientId; - let result; - while ( - ! result && - ( current = state.blocks.parents.get( current ) ) - ) { - if ( isSectionBlock( state, current ) ) { - result = current; - } - } - return result; - }, - ( state ) => [ - state.blocks.parents, - state.blocks.order, - state.blockListSettings, - state.editorMode, - state.settings.templateLock, - state.blocks.byClientId, - getSectionRootClientId( state ), - ] -); - -/** - * Retrieves the client ID is a content locking parent - * - * @param {Object} state Global application state. - * @param {Object} clientId Client Id of the block. - * - * @return {boolean} Whether the block is a content locking parent. - */ -export function isSectionBlock( state, clientId ) { - const sectionRootClientId = getSectionRootClientId( state ); - const sectionClientIds = getBlockOrder( state, sectionRootClientId ); - return ( - getBlockName( state, clientId ) === 'core/block' || - getTemplateLock( state, clientId ) === 'contentOnly' || - ( isNavigationMode( state ) && sectionClientIds.includes( clientId ) ) - ); -} - /** * Retrieves the client ID of the block that is content locked but is * currently being temporarily edited as a non-locked block. @@ -617,23 +560,3 @@ export function isZoomOutMode( state ) { export function getSectionRootClientId( state ) { return state.settings?.[ sectionRootClientIdKey ]; } - -/** - * Returns the zoom out state. - * - * @param {Object} state Global application state. - * @return {boolean} The zoom out state. - */ -export function getZoomLevel( state ) { - return state.zoomLevel; -} - -/** - * Returns whether the editor is considered zoomed out. - * - * @param {Object} state Global application state. - * @return {boolean} Whether the editor is zoomed. - */ -export function isZoomOut( state ) { - return getZoomLevel( state ) < 100; -} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 827a2141f44c1c..cd4569c45e5801 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1795,6 +1795,11 @@ export const blockListSettings = ( state = {}, action ) => { * @return {string} Updated state. */ export function editorMode( state = 'edit', action ) { + // Let inserting block in navigation mode always trigger Edit mode. + if ( action.type === 'INSERT_BLOCKS' && state === 'navigation' ) { + return 'edit'; + } + if ( action.type === 'SET_EDITOR_MODE' ) { return action.mode; } @@ -1802,6 +1807,26 @@ export function editorMode( state = 'edit', action ) { return state; } +/** + * Reducer returning whether the block moving mode is enabled or not. + * + * @param {string|null} state Current state. + * @param {Object} action Dispatched action. + * + * @return {string|null} Updated state. + */ +export function hasBlockMovingClientId( state = null, action ) { + if ( action.type === 'SET_BLOCK_MOVING_MODE' ) { + return action.hasBlockMovingClientId; + } + + if ( action.type === 'SET_EDITOR_MODE' ) { + return null; + } + + return state; +} + /** * Reducer return an updated state representing the most recent block attribute * update. The state is structured as an object where the keys represent the @@ -2060,25 +2085,6 @@ export function hoveredBlockClientId( state = false, action ) { return state; } -/** - * Reducer setting zoom out state. - * - * @param {boolean} state Current state. - * @param {Object} action Dispatched action. - * - * @return {boolean} Updated state. - */ -export function zoomLevel( state = 100, action ) { - switch ( action.type ) { - case 'SET_ZOOM_LEVEL': - return action.zoom; - case 'RESET_ZOOM_LEVEL': - return 100; - } - - return state; -} - const combinedReducers = combineReducers( { blocks, isDragging, @@ -2098,6 +2104,7 @@ const combinedReducers = combineReducers( { lastBlockAttributesChange, lastFocus, editorMode, + hasBlockMovingClientId, expandedBlock, highlightedBlock, lastBlockInserted, @@ -2111,7 +2118,6 @@ const combinedReducers = combineReducers( { openedBlockSettingsMenu, registeredInserterMediaCategories, hoveredBlockClientId, - zoomLevel, } ); function withAutomaticChangeReset( reducer ) { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 20d6627398886c..30fdb76bdbe787 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -38,8 +38,6 @@ import { getTemporarilyEditingAsBlocks, getTemporarilyEditingFocusModeToRevert, getSectionRootClientId, - isSectionBlock, - getParentSectionBlock, } from './private-selectors'; /** @@ -1584,11 +1582,6 @@ const canInsertBlockTypeUnmemoized = ( return false; } - const _isSectionBlock = !! isSectionBlock( state, rootClientId ); - if ( _isSectionBlock ) { - return false; - } - if ( getBlockEditingMode( state, rootClientId ?? '' ) === 'disabled' ) { return false; } @@ -1740,11 +1733,6 @@ export function canRemoveBlock( state, clientId ) { return false; } - const isBlockWithinSection = !! getParentSectionBlock( state, clientId ); - if ( isBlockWithinSection ) { - return false; - } - return getBlockEditingMode( state, rootClientId ) !== 'disabled'; } @@ -2686,17 +2674,12 @@ export function __unstableGetEditorMode( state ) { /** * Returns whether block moving mode is enabled. * - * @deprecated + * @param {Object} state Editor state. + * + * @return {string} Client Id of moving block. */ -export function hasBlockMovingClientId() { - deprecated( - 'wp.data.select( "core/block-editor" ).hasBlockMovingClientId', - { - since: '6.7', - hint: 'Block moving mode feature has been removed', - } - ); - return false; +export function hasBlockMovingClientId( state ) { + return state.hasBlockMovingClientId; } /** @@ -2879,9 +2862,11 @@ export function __unstableHasActiveBlockOverlayActive( state, clientId ) { '__experimentalDisableBlockOverlay', false ); - const shouldEnableIfUnselected = blockSupportDisable - ? false - : areInnerBlocksControlled( state, clientId ); + const shouldEnableIfUnselected = + editorMode === 'navigation' || + ( blockSupportDisable + ? false + : areInnerBlocksControlled( state, clientId ) ); return ( shouldEnableIfUnselected && @@ -2901,14 +2886,6 @@ export function __unstableIsWithinBlockOverlay( state, clientId ) { return false; } -function isWithinBlock( state, clientId, parentClientId ) { - let parent = state.blocks.parents.get( clientId ); - while ( !! parent && parent !== parentClientId ) { - parent = state.blocks.parents.get( parent ); - } - return parent === parentClientId; -} - /** * @typedef {import('../components/block-editing-mode').BlockEditingMode} BlockEditingMode */ @@ -2949,7 +2926,6 @@ export const getBlockEditingMode = createRegistrySelector( if ( clientId === null ) { clientId = ''; } - // In zoom-out mode, override the behavior set by // __unstableSetBlockEditingMode to only allow editing the top-level // sections. @@ -2967,66 +2943,20 @@ export const getBlockEditingMode = createRegistrySelector( state, sectionRootClientId ); - - // Sections are always contentOnly. - if ( sectionsClientIds?.includes( clientId ) ) { - return 'contentOnly'; - } - - return 'disabled'; - } - - if ( editorMode === 'navigation' ) { - const sectionRootClientId = getSectionRootClientId( state ); - - // The root section is "default mode" - if ( clientId === sectionRootClientId ) { - return 'default'; - } - - // Sections should always be contentOnly in navigation mode. - const sectionsClientIds = getBlockOrder( - state, - sectionRootClientId - ); - if ( sectionsClientIds.includes( clientId ) ) { - return 'contentOnly'; - } - - // Blocks outside sections should be disabled. - const isWithinSectionRoot = isWithinBlock( - state, - clientId, - sectionRootClientId - ); - if ( ! isWithinSectionRoot ) { + if ( ! sectionsClientIds?.includes( clientId ) ) { return 'disabled'; } - - // The rest of the blocks depend on whether they are content blocks or not. - // This "flattens" the sections tree. - const name = getBlockName( state, clientId ); - const isContent = - select( blocksStore ).__experimentalHasContentRoleAttribute( - name - ); - return isContent ? 'contentOnly' : 'disabled'; } - // In normal mode, consider that an explicitely set editing mode takes over. const blockEditingMode = state.blockEditingModes.get( clientId ); if ( blockEditingMode ) { return blockEditingMode; } - - // In normal mode, top level is default mode. if ( ! clientId ) { return 'default'; } - const rootClientId = getBlockRootClientId( state, clientId ); const templateLock = getTemplateLock( state, rootClientId ); - // If the parent of the block is contentOnly locked, check whether it's a content block. if ( templateLock === 'contentOnly' ) { const name = getBlockName( state, clientId ); const isContent = @@ -3035,7 +2965,6 @@ export const getBlockEditingMode = createRegistrySelector( ); return isContent ? 'contentOnly' : 'disabled'; } - // Otherwise, check if there's an ancestor that is contentOnly const parentMode = getBlockEditingMode( state, rootClientId ); return parentMode === 'contentOnly' ? 'default' : parentMode; } diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index 5f427e79cf6999..45432b750bb9eb 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -394,10 +394,6 @@ describe( 'private selectors', () => { parents: new Map( [ [ '6cf70164-9097-4460-bcbf-200560546988', '' ], ] ), - order: new Map( [ - [ '6cf70164-9097-4460-bcbf-200560546988', [] ], - [ '', [ '6cf70164-9097-4460-bcbf-200560546988' ] ], - ] ), }, blockEditingModes: new Map(), }; @@ -428,21 +424,6 @@ describe( 'private selectors', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', ], ] ), - - order: new Map( [ - [ - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - ], - ], - [ - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - [ '4c2b7140-fffd-44b4-b2a7-820c670a6514' ], - ], - [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], - ] ), }, blockEditingModes: new Map( [ [ '', 'disabled' ], @@ -480,21 +461,6 @@ describe( 'private selectors', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', ], ] ), - order: new Map( [ - [ - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' ], - ], - [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c' ], - ], - [ - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - [ '4c2b7140-fffd-44b4-b2a7-820c670a6514' ], - ], - [ '', [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' ] ], - ] ), }, blockEditingModes: new Map( [ [ '', 'disabled' ], diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 2895f6573dc54f..85006621c4701e 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -15,7 +15,6 @@ import { select, dispatch } from '@wordpress/data'; */ import * as selectors from '../selectors'; import { store } from '../'; -import { sectionRootClientIdKey } from '../private-keys'; const { getBlockName, @@ -2695,7 +2694,6 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: { @@ -2713,7 +2711,6 @@ describe( 'selectors', () => { blocks: { byClientId: new Map(), attributes: new Map(), - order: new Map(), }, blockListSettings: {}, settings: { @@ -2731,7 +2728,6 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2752,7 +2748,6 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2777,7 +2772,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2802,7 +2796,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { block1: {}, @@ -2829,7 +2822,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { block1: {}, @@ -2856,7 +2848,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { block1: { @@ -2885,7 +2876,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { block1: { @@ -2914,7 +2904,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2943,7 +2932,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { block1: { @@ -2972,7 +2960,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: {}, @@ -2989,7 +2976,6 @@ describe( 'selectors', () => { byClientId: new Map(), attributes: new Map(), parents: new Map(), - order: new Map(), }, blockListSettings: {}, settings: {}, @@ -3006,7 +2992,7 @@ describe( 'selectors', () => { byClientId: new Map( Object.entries( { block1: { name: 'core/test-block-ancestor' }, - block2: { name: 'core/block1' }, + block2: { name: 'core/block' }, } ) ), attributes: new Map( @@ -3020,10 +3006,6 @@ describe( 'selectors', () => { block2: 'block1', } ) ), - order: new Map( [ - [ '', [ 'block1' ] ], - [ 'block1', [ 'block2' ] ], - ] ), }, blockListSettings: { block1: {}, @@ -3041,37 +3023,6 @@ describe( 'selectors', () => { ).toBe( true ); } ); - it( 'should prevent blocks from being inserted within sections', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { name: 'core/block' }, // reusable blocks are always sections. - } ) - ), - attributes: new Map( - Object.entries( { - block1: {}, - } ) - ), - parents: new Map( - Object.entries( { - block1: '', - } ) - ), - order: new Map( [ [ '', [ 'block1' ] ] ] ), - }, - blockListSettings: { - block1: {}, - }, - settings: {}, - blockEditingModes: new Map(), - }; - expect( - canInsertBlockType( state, 'core/test-block-a', 'block1' ) - ).toBe( false ); - } ); - it( 'should allow blocks to be inserted if both parent and ancestor restrictions are met', () => { const state = { blocks: { @@ -3095,11 +3046,6 @@ describe( 'selectors', () => { block3: 'block2', } ) ), - order: new Map( [ - [ '', [ 'block1' ] ], - [ 'block1', [ 'block2' ] ], - [ 'block2', [ 'block3' ] ], - ] ), }, blockListSettings: { block1: {}, @@ -3140,11 +3086,6 @@ describe( 'selectors', () => { block3: 'block2', } ) ), - order: new Map( [ - [ '', [ 'block1' ] ], - [ 'block1', [ 'block2' ] ], - [ 'block2', [ 'block3' ] ], - ] ), }, blockListSettings: { block1: {}, @@ -3185,11 +3126,6 @@ describe( 'selectors', () => { block3: 'block2', } ) ), - order: new Map( [ - [ '', [ 'block1' ] ], - [ 'block1', [ 'block2' ] ], - [ 'block2', [ 'block3' ] ], - ] ), }, blockListSettings: { block1: {}, @@ -3223,14 +3159,11 @@ describe( 'selectors', () => { block2: {}, } ) ), - order: new Map( [ - [ '', [ 'block1' ] ], - [ 'block1', [ 'block2' ] ], - ] ), - parents: new Map( [ - [ 'block2', 'block1' ], - [ 'block1', '' ], - ] ), + parents: new Map( + Object.entries( { + block2: 'block1', + } ) + ), }, blockListSettings: { block1: {}, @@ -3270,10 +3203,6 @@ describe( 'selectors', () => { block2: 'block1', } ) ), - order: new Map( [ - [ '', [ 'block1' ] ], - [ 'block1', [ 'block2' ] ], - ] ), }, blockListSettings: { block1: {}, @@ -3311,7 +3240,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { 1: { @@ -3345,7 +3273,6 @@ describe( 'selectors', () => { } ) ), parents: new Map(), - order: new Map(), }, blockListSettings: { 1: { @@ -4383,28 +4310,12 @@ describe( 'getBlockEditingMode', () => { settings: {}, blocks: { byClientId: new Map( [ - [ - '6cf70164-9097-4460-bcbf-200560546988', - { name: 'core/template-part' }, - ], // Header - [ - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', - { name: 'core/group' }, - ], // Group - [ - 'b26fc763-417d-4f01-b81c-2ec61e14a972', - { name: 'core/post-title' }, - ], // | Post Title - [ - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - { name: 'core/group' }, - ], // | Group - [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', { name: 'core/p' } ], // | | Paragraph - [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', { name: 'core/p' } ], // | | Paragraph - [ - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', - { name: 'core/group' }, - ], // | | Group + [ '6cf70164-9097-4460-bcbf-200560546988', {} ], // Header + [ 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', {} ], // Group + [ 'b26fc763-417d-4f01-b81c-2ec61e14a972', {} ], // | Post Title + [ '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', {} ], // | Post Content + [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', {} ], // | | Paragraph + [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', {} ], // | | Paragraph ] ), order: new Map( [ [ @@ -4428,12 +4339,10 @@ describe( 'getBlockEditingMode', () => { [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', ], ], [ 'b3247f75-fd94-4fef-97f9-5bfd162cc416', [] ], [ 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', [] ], - [ '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', [] ], ] ), parents: new Map( [ [ '6cf70164-9097-4460-bcbf-200560546988', '' ], @@ -4454,10 +4363,6 @@ describe( 'getBlockEditingMode', () => { 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c', '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', ], - [ - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s', - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', - ], ] ), }, blockListSettings: { @@ -4467,18 +4372,7 @@ describe( 'getBlockEditingMode', () => { blockEditingModes: new Map( [] ), }; - const navigationModeStateWithRootSection = { - ...baseState, - editorMode: 'navigation', - settings: { - [ sectionRootClientIdKey ]: 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', // The group is the "main" container - }, - }; - - const __experimentalHasContentRoleAttribute = jest.fn( ( name ) => { - // consider paragraphs as content blocks. - return name === 'core/p'; - } ); + const __experimentalHasContentRoleAttribute = jest.fn( () => false ); getBlockEditingMode.registry = { select: jest.fn( () => ( { __experimentalHasContentRoleAttribute, @@ -4607,61 +4501,4 @@ describe( 'getBlockEditingMode', () => { getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' ) ).toBe( 'contentOnly' ); } ); - - it( 'in navigation mode, the root section container is default', () => { - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337' - ) - ).toBe( 'default' ); - } ); - - it( 'in navigation mode, anything outside the section container is disabled', () => { - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - '6cf70164-9097-4460-bcbf-200560546988' - ) - ).toBe( 'disabled' ); - } ); - - it( 'in navigation mode, sections are contentOnly', () => { - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - 'b26fc763-417d-4f01-b81c-2ec61e14a972' - ) - ).toBe( 'contentOnly' ); - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' - ) - ).toBe( 'contentOnly' ); - } ); - - it( 'in navigation mode, blocks with content attributes within sections are contentOnly', () => { - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - 'b3247f75-fd94-4fef-97f9-5bfd162cc416' - ) - ).toBe( 'contentOnly' ); - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c' - ) - ).toBe( 'contentOnly' ); - } ); - - it( 'in navigation mode, blocks without content attributes within sections are disabled', () => { - expect( - getBlockEditingMode( - navigationModeStateWithRootSection, - '9b9c5c3f-2e46-4f02-9e14-9fed515b958s' - ) - ).toBe( 'disabled' ); - } ); } ); diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 79e15255e6cc15..b630912a5163d6 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -10,7 +10,6 @@ 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,7 +117,5 @@ export function getInsertBlockTypeDependants( state, rootClientId ) { state.settings.allowedBlockTypes, state.settings.templateLock, state.blockEditingModes, - state.editorMode, - getSectionRootClientId( state ), ]; } diff --git a/packages/block-library/src/avatar/index.js b/packages/block-library/src/avatar/index.js index 0b3ad9c62c4e30..d318450aec3903 100644 --- a/packages/block-library/src/avatar/index.js +++ b/packages/block-library/src/avatar/index.js @@ -16,7 +16,6 @@ export { metadata, name }; export const settings = { icon, edit, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/comment-author-name/index.js b/packages/block-library/src/comment-author-name/index.js index 5bcb6896564807..4d85bbebe047be 100644 --- a/packages/block-library/src/comment-author-name/index.js +++ b/packages/block-library/src/comment-author-name/index.js @@ -18,7 +18,6 @@ export const settings = { icon, edit, deprecated, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/comment-content/index.js b/packages/block-library/src/comment-content/index.js index aefcef75acf8ae..130f1d30125559 100644 --- a/packages/block-library/src/comment-content/index.js +++ b/packages/block-library/src/comment-content/index.js @@ -16,7 +16,6 @@ export { metadata, name }; export const settings = { icon, edit, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/comments-title/index.js b/packages/block-library/src/comments-title/index.js index 69b8228eab892b..86bdab0dbccbff 100644 --- a/packages/block-library/src/comments-title/index.js +++ b/packages/block-library/src/comments-title/index.js @@ -18,7 +18,6 @@ export const settings = { icon, edit, deprecated, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 8ea668d56d8545..85cc840201da59 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -19,7 +19,18 @@ function render_block_core_file( $attributes, $content ) { // If it's interactive, enqueue the script module and add the directives. if ( ! empty( $attributes['displayPreview'] ) ) { - wp_enqueue_script_module( '@wordpress/block-library/file/view' ); + $suffix = wp_scripts_get_suffix(); + if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { + $module_url = gutenberg_url( '/build-module/block-library/file/view.min.js' ); + } + + wp_register_script_module( + '@wordpress/block-library/file', + isset( $module_url ) ? $module_url : includes_url( "blocks/file/view{$suffix}.js" ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); + wp_enqueue_script_module( '@wordpress/block-library/file' ); $processor = new WP_HTML_Tag_Processor( $content ); $processor->next_tag(); diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 5d7815a1f2f3fb..abbb03c0952452 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -70,7 +70,19 @@ function render_block_core_image( $attributes, $content, $block ) { isset( $lightbox_settings['enabled'] ) && true === $lightbox_settings['enabled'] ) { - wp_enqueue_script_module( '@wordpress/block-library/image/view' ); + $suffix = wp_scripts_get_suffix(); + if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { + $module_url = gutenberg_url( '/build-module/block-library/image/view.min.js' ); + } + + wp_register_script_module( + '@wordpress/block-library/image', + isset( $module_url ) ? $module_url : includes_url( "blocks/image/view{$suffix}.js" ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); + + wp_enqueue_script_module( '@wordpress/block-library/image' ); /* * This render needs to happen in a filter with priority 15 to ensure that diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 10fec84ed59d9c..ec72b03b6906f0 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -622,7 +622,18 @@ private static function get_nav_element_directives( $is_interactive ) { */ private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) { if ( static::is_interactive( $attributes, $inner_blocks ) ) { - wp_enqueue_script_module( '@wordpress/block-library/navigation/view' ); + $suffix = wp_scripts_get_suffix(); + if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { + $module_url = gutenberg_url( '/build-module/block-library/navigation/view.min.js' ); + } + + wp_register_script_module( + '@wordpress/block-library/navigation', + isset( $module_url ) ? $module_url : includes_url( "blocks/navigation/view{$suffix}.js" ), + array( '@wordpress/interactivity' ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); + wp_enqueue_script_module( '@wordpress/block-library/navigation' ); } } diff --git a/packages/block-library/src/post-navigation-link/block.json b/packages/block-library/src/post-navigation-link/block.json index 5f1b295119822a..ce733759846fee 100644 --- a/packages/block-library/src/post-navigation-link/block.json +++ b/packages/block-library/src/post-navigation-link/block.json @@ -34,12 +34,6 @@ "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 4f52b21338af1e..945d6eb550f276 100644 --- a/packages/block-library/src/post-navigation-link/variations.js +++ b/packages/block-library/src/post-navigation-link/variations.js @@ -15,12 +15,6 @@ const variations = [ icon: next, attributes: { type: 'next' }, scope: [ 'inserter', 'transform' ], - example: { - attributes: { - label: 'Next post', - arrow: 'arrow', - }, - }, }, { name: 'post-previous', @@ -31,12 +25,6 @@ const variations = [ icon: previous, attributes: { type: 'previous' }, scope: [ 'inserter', 'transform' ], - example: { - attributes: { - label: 'Previous post', - arrow: 'arrow', - }, - }, }, ]; diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php index 9126355c096a57..64cdd156a54310 100644 --- a/packages/block-library/src/post-template/index.php +++ b/packages/block-library/src/post-template/index.php @@ -64,6 +64,11 @@ function render_block_core_post_template( $attributes, $content, $block ) { if ( in_the_loop() ) { $query = clone $wp_query; $query->rewind_posts(); + + // If in a single post of any post type, default to the 'post' post type. + if ( is_singular() ) { + query_posts( array( 'post_type' => 'post' ) ); + } } else { $query = $wp_query; } 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 039923161ca81d..95b379f55f0b3f 100644 --- a/packages/block-library/src/post-time-to-read/index.js +++ b/packages/block-library/src/post-time-to-read/index.js @@ -12,7 +12,6 @@ export { metadata, name }; export const settings = { icon, edit, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/query-no-results/block.json b/packages/block-library/src/query-no-results/block.json index 2f656594afa306..8f3ba56adcc36a 100644 --- a/packages/block-library/src/query-no-results/block.json +++ b/packages/block-library/src/query-no-results/block.json @@ -8,16 +8,6 @@ "parent": [ "core/query" ], "textdomain": "default", "usesContext": [ "queryId", "query" ], - "example": { - "innerBlocks": [ - { - "name": "core/paragraph", - "attributes": { - "content": "No posts were found." - } - } - ] - }, "supports": { "align": true, "reusable": false, diff --git a/packages/block-library/src/query-pagination/index.js b/packages/block-library/src/query-pagination/index.js index 158106c4ac185a..b113a8384b043b 100644 --- a/packages/block-library/src/query-pagination/index.js +++ b/packages/block-library/src/query-pagination/index.js @@ -20,7 +20,6 @@ export const settings = { edit, save, deprecated, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/query-title/block.json b/packages/block-library/src/query-title/block.json index 5d5c9113bda084..de3e60214685c2 100644 --- a/packages/block-library/src/query-title/block.json +++ b/packages/block-library/src/query-title/block.json @@ -29,11 +29,6 @@ "default": true } }, - "example": { - "attributes": { - "type": "search" - } - }, "supports": { "align": [ "wide", "full" ], "html": false, diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index b2225192c6b218..22bfa7b713801c 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -5,7 +5,6 @@ "title": "Query Loop", "category": "theme", "description": "An advanced block that allows displaying post types based on different query parameters and visual configurations.", - "keywords": [ "posts", "list", "blog", "blogs", "custom post types" ], "textdomain": "default", "attributes": { "queryId": { diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 043f351e11d7f1..d10db26529854e 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -24,7 +24,27 @@ function render_block_core_query( $attributes, $content, $block ) { // Enqueue the script module and add the necessary directives if the block is // interactive. if ( $is_interactive ) { - wp_enqueue_script_module( '@wordpress/block-library/query/view' ); + $suffix = wp_scripts_get_suffix(); + if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { + $module_url = gutenberg_url( '/build-module/block-library/query/view.min.js' ); + } + + wp_register_script_module( + '@wordpress/block-library/query', + isset( $module_url ) ? $module_url : includes_url( "blocks/query/view{$suffix}.js" ), + array( + array( + 'id' => '@wordpress/interactivity', + 'import' => 'static', + ), + array( + 'id' => '@wordpress/interactivity-router', + 'import' => 'dynamic', + ), + ), + defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) + ); + wp_enqueue_script_module( '@wordpress/block-library/query' ); $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag() ) { diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index d4ed5b7e3a4055..e2f3bb3999e42c 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -424,12 +424,13 @@ export default function SearchEdit( { } step={ 1 } onChange={ ( newWidth ) => { - const parsedNewWidth = - newWidth === '' - ? undefined - : parseInt( newWidth, 10 ); + const filteredWidth = + widthUnit === '%' && + parseInt( newWidth, 10 ) > 100 + ? 100 + : newWidth; setAttributes( { - width: parsedNewWidth, + width: parseInt( filteredWidth, 10 ), } ); } } onUnitChange={ ( newUnit ) => { @@ -565,11 +566,7 @@ export default function SearchEdit( { set_attribute( 'data-wp-bind--aria-hidden', '!context.isSearchInputVisible' ); $input->set_attribute( 'data-wp-bind--tabindex', 'state.tabindex' ); diff --git a/packages/block-library/src/table-of-contents/block.json b/packages/block-library/src/table-of-contents/block.json index 5eb6e729d3f03e..451d245d867b07 100644 --- a/packages/block-library/src/table-of-contents/block.json +++ b/packages/block-library/src/table-of-contents/block.json @@ -62,57 +62,6 @@ } } }, - "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 - } - ] - } - }, + "example": {}, "style": "wp-block-table-of-contents" } diff --git a/packages/block-library/src/term-description/index.js b/packages/block-library/src/term-description/index.js index 330ca05bd174e1..0ff710a91f5d50 100644 --- a/packages/block-library/src/term-description/index.js +++ b/packages/block-library/src/term-description/index.js @@ -16,7 +16,6 @@ export { metadata, name }; export const settings = { icon, edit, - example: {}, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 90481e58edd7bb..d7e8b191229893 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,13 +9,11 @@ - `Tabs`: restore vertical indicator ([#65385](https://github.com/WordPress/gutenberg/pull/65385)). - `Tabs`: indicator positioning under RTL direction ([#64926](https://github.com/WordPress/gutenberg/pull/64926)). - `Popover`: Update `toolbar` variant radius to match block toolbar ([#65263](https://github.com/WordPress/gutenberg/pull/65263)). -- `MenuItemsChoice`: Allow menu items height to adapt to its content ([#65204](https://github.com/WordPress/gutenberg/pull/65204)). - `BoxControl`: Unify input filed width whether linked or not ([#65348](https://github.com/WordPress/gutenberg/pull/65348)). ### 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 @@ -36,7 +34,6 @@ - `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 77176b49eeb6d8..b03b03a85466ae 100644 --- a/packages/components/src/box-control/README.md +++ b/packages/components/src/box-control/README.md @@ -1,14 +1,18 @@ # BoxControl -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`. +
+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`. ## Usage ```jsx import { useState } from 'react'; -import { BoxControl } from '@wordpress/components'; +import { __experimentalBoxControl as BoxControl } from '@wordpress/components'; -function Example() { +const Example = () => { const [ values, setValues ] = useState( { top: '50px', left: '10%', @@ -22,24 +26,23 @@ function 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 41e95aa88bea37..9c3452d4ccb806 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 ) { } /** - * 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`. + * 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`. * * ```jsx - * import { BoxControl } from '@wordpress/components'; + * import { __experimentalBoxControl as BoxControl } from '@wordpress/components'; * import { useState } from '@wordpress/element'; * - * function Example() { + * const 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 783f9d047b1bb0..1b6604048f6d52 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/BoxControl', + title: 'Components (Experimental)/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 5f4071aeed88a7..eeb72df14bb9c1 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 a59d258012807d..32195ebc444ce6 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -40,9 +40,7 @@ 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/menu-items-choice/style.scss b/packages/components/src/menu-items-choice/style.scss index 383eb4066ba86b..5de8363be0d6e8 100644 --- a/packages/components/src/menu-items-choice/style.scss +++ b/packages/components/src/menu-items-choice/style.scss @@ -1,7 +1,5 @@ .components-menu-items-choice, .components-menu-items-choice.components-button { - height: auto; - svg { margin-right: $grid-unit-15; } diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts index 020468991225c1..86efc5224077f4 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/styles.ts @@ -70,7 +70,7 @@ export const buttonView = ( { } &:active { - background: ${ CONFIG.controlBackgroundColor }; + background: ${ CONFIG.toggleGroupControlBackgroundColor }; } ${ isDeselectable && deselectable } diff --git a/packages/components/src/tools-panel/tools-panel/README.md b/packages/components/src/tools-panel/tools-panel/README.md index 1daa7537335e1c..df41b623eefb6c 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 { - BoxControl, + __experimentalBoxControl as 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/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index 1bc3945f9b3b16..2040f479a231c2 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -7,13 +7,18 @@ import { COLORS } from './colors-values'; const CONTROL_HEIGHT = '36px'; const CONTROL_PROPS = { + controlSurfaceColor: COLORS.white, + controlTextActiveColor: COLORS.theme.accent, + // These values should be shared with TextControl. controlPaddingX: 12, controlPaddingXSmall: 8, controlPaddingXLarge: 12 * 1.3334, // TODO: Deprecate controlBackgroundColor: COLORS.white, + controlBoxShadow: 'transparent', controlBoxShadowFocus: `0 0 0 0.5px ${ COLORS.theme.accent }`, + controlDestructiveBorderColor: COLORS.alert.red, controlHeight: CONTROL_HEIGHT, controlHeightXSmall: `calc( ${ CONTROL_HEIGHT } * 0.6 )`, controlHeightSmall: `calc( ${ CONTROL_HEIGHT } * 0.8 )`, @@ -21,9 +26,18 @@ const CONTROL_PROPS = { controlHeightXLarge: `calc( ${ CONTROL_HEIGHT } * 1.4 )`, }; +const TOGGLE_GROUP_CONTROL_PROPS = { + toggleGroupControlBackgroundColor: CONTROL_PROPS.controlBackgroundColor, + toggleGroupControlBorderColor: COLORS.ui.border, + toggleGroupControlBackdropBackgroundColor: + CONTROL_PROPS.controlSurfaceColor, + toggleGroupControlBackdropBorderColor: COLORS.ui.border, + toggleGroupControlButtonColorActive: CONTROL_PROPS.controlBackgroundColor, +}; + // Using Object.assign to avoid creating circular references when emitting // TypeScript type declarations. -export default Object.assign( {}, CONTROL_PROPS, { +export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, { colorDivider: 'rgba(0, 0, 0, 0.1)', colorScrollbarThumb: 'rgba(0, 0, 0, 0.2)', colorScrollbarThumbHover: 'rgba(0, 0, 0, 0.5)', diff --git a/packages/components/src/utils/element-rect.ts b/packages/components/src/utils/element-rect.ts index a96c25ecfac949..550ec35b0bc932 100644 --- a/packages/components/src/utils/element-rect.ts +++ b/packages/components/src/utils/element-rect.ts @@ -3,7 +3,11 @@ * WordPress dependencies */ import { useLayoutEffect, useRef, useState } from '@wordpress/element'; -import { useEvent, useResizeObserver } from '@wordpress/compose'; +import { useResizeObserver } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import { useEvent } from './hooks/use-event'; /** * The position and dimensions of an element, relative to its offset parent. diff --git a/packages/components/src/utils/hooks/use-event.ts b/packages/components/src/utils/hooks/use-event.ts new file mode 100644 index 00000000000000..eefac9478a8b4f --- /dev/null +++ b/packages/components/src/utils/hooks/use-event.ts @@ -0,0 +1,38 @@ +/* eslint-disable jsdoc/require-param */ +/** + * WordPress dependencies + */ +import { useRef, useInsertionEffect, useCallback } from '@wordpress/element'; + +/** + * Any function. + */ +export type AnyFunction = ( ...args: any ) => any; + +/** + * Creates a stable callback function that has access to the latest state and + * can be used within event handlers and effect callbacks. Throws when used in + * the render phase. + * + * @example + * + * ```tsx + * function Component(props) { + * const onClick = useEvent(props.onClick); + * React.useEffect(() => {}, [onClick]); + * } + * ``` + */ +export function useEvent< T extends AnyFunction >( callback?: T ) { + const ref = useRef< AnyFunction | undefined >( () => { + throw new Error( 'Cannot call an event handler while rendering.' ); + } ); + useInsertionEffect( () => { + ref.current = callback; + } ); + return useCallback< AnyFunction >( + ( ...args ) => ref.current?.( ...args ), + [] + ) as T; +} +/* eslint-enable jsdoc/require-param */ diff --git a/packages/components/src/utils/hooks/use-on-value-update.ts b/packages/components/src/utils/hooks/use-on-value-update.ts index 05c7173d092fac..5726f3977daf04 100644 --- a/packages/components/src/utils/hooks/use-on-value-update.ts +++ b/packages/components/src/utils/hooks/use-on-value-update.ts @@ -2,8 +2,11 @@ /** * WordPress dependencies */ -import { useEvent } from '@wordpress/compose'; import { useRef, useEffect } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { useEvent } from './use-event'; /** * Context object for the `onUpdate` callback of `useOnValueUpdate`. diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index a2d3c76ebe5d9c..8334ef97c9244d 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -37,7 +37,6 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", - "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", "@wordpress/router": "file:../router", "@wordpress/url": "file:../url" diff --git a/packages/core-commands/src/admin-navigation-commands.js b/packages/core-commands/src/admin-navigation-commands.js index c0d8bb084b46ad..0ffa7ba7eb6285 100644 --- a/packages/core-commands/src/admin-navigation-commands.js +++ b/packages/core-commands/src/admin-navigation-commands.js @@ -1,92 +1,9 @@ /** * WordPress dependencies */ -import { useCommand, useCommandLoader } from '@wordpress/commands'; +import { useCommand } from '@wordpress/commands'; import { __ } from '@wordpress/i18n'; import { plus } from '@wordpress/icons'; -import { getPath } from '@wordpress/url'; -import { store as coreStore } from '@wordpress/core-data'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { useCallback, useMemo } from '@wordpress/element'; -import { store as noticesStore } from '@wordpress/notices'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import { unlock } from './lock-unlock'; - -const { useHistory } = unlock( routerPrivateApis ); - -function useAddNewPageCommand() { - const isSiteEditor = getPath( window.location.href )?.includes( - 'site-editor.php' - ); - const history = useHistory(); - const isBlockBasedTheme = useSelect( ( select ) => { - return select( coreStore ).getCurrentTheme()?.is_block_theme; - }, [] ); - const { saveEntityRecord } = useDispatch( coreStore ); - const { createErrorNotice } = useDispatch( noticesStore ); - - const createPageEntity = useCallback( - async ( { close } ) => { - try { - const page = await saveEntityRecord( - 'postType', - 'page', - { - status: 'draft', - }, - { - throwOnError: true, - } - ); - if ( page?.id ) { - history.push( { - postId: page.id, - postType: 'page', - canvas: 'edit', - } ); - } - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while creating the item.' ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } finally { - close(); - } - }, - [ createErrorNotice, history, saveEntityRecord ] - ); - - const commands = useMemo( () => { - const addNewPage = - isSiteEditor && isBlockBasedTheme - ? createPageEntity - : () => - ( document.location.href = - 'post-new.php?post_type=page' ); - return [ - { - name: 'core/add-new-page', - label: __( 'Add new page' ), - icon: plus, - callback: addNewPage, - }, - ]; - }, [ createPageEntity, isSiteEditor, isBlockBasedTheme ] ); - - return { - isLoading: false, - commands, - }; -} export function useAdminNavigationCommands() { useCommand( { @@ -97,9 +14,12 @@ export function useAdminNavigationCommands() { document.location.href = 'post-new.php'; }, } ); - - useCommandLoader( { + useCommand( { name: 'core/add-new-page', - hook: useAddNewPageCommand, + label: __( 'Add new page' ), + icon: plus, + callback: () => { + document.location.href = 'post-new.php?post_type=page'; + }, } ); } diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 24dcfd52b7b586..348c8466836c69 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -1,10 +1,8 @@ -## 2.8.0 (2024-09-19) - -### Enhancements +## Unreleased -- Added TypeScript variant of the template ([#64577](https://github.com/WordPress/gutenberg/pull/64577)). +## 2.8.0 (2024-09-19) ## 2.7.0 (2024-09-05) diff --git a/packages/create-block-interactive-template/README.md b/packages/create-block-interactive-template/README.md index b50adb49265245..4417c647495c4c 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 4a13743750f748..3e64ce8f629a3c 100644 --- a/packages/create-block-interactive-template/block-templates/README.md.mustache +++ b/packages/create-block-interactive-template/block-templates/README.md.mustache @@ -3,4 +3,6 @@ > **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 4f84b30dbcdbdd..3a41a2981cd8cf 100644 --- a/packages/create-block-interactive-template/block-templates/render.php.mustache +++ b/packages/create-block-interactive-template/block-templates/render.php.mustache @@ -1,3 +1,4 @@ +{{#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 c8aa9f232136e2..1c73fa1c38ff94 100644 --- a/packages/create-block-interactive-template/block-templates/style.scss.mustache +++ b/packages/create-block-interactive-template/block-templates/style.scss.mustache @@ -9,19 +9,4 @@ 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 3fcf1ba365d265..b4bae3939461dd 100644 --- a/packages/create-block-interactive-template/block-templates/view.js.mustache +++ b/packages/create-block-interactive-template/block-templates/view.js.mustache @@ -1,23 +1,15 @@ -{{#isDefaultVariant}} +{{#isBasicVariant}} /** * WordPress dependencies */ -import { store, getContext } from '@wordpress/interactivity'; +import { store, getContext } from "@wordpress/interactivity"; -const { state } = store( '{{namespace}}', { - state: { - get themeText() { - return state.isDark ? state.darkText : state.lightText; - } - }, +store( '{{namespace}}', { actions: { - toggleOpen() { + toggle: () => { const context = getContext(); context.isOpen = ! context.isOpen; }, - toggleTheme() { - state.isDark = ! state.isDark; - } }, callbacks: { logIsOpen: () => { @@ -27,4 +19,5 @@ const { state } = store( '{{namespace}}', { }, }, } ); -{{/isDefaultVariant}} + +{{/isBasicVariant}} diff --git a/packages/create-block-interactive-template/block-templates/view.ts.mustache b/packages/create-block-interactive-template/block-templates/view.ts.mustache deleted file mode 100644 index 11670442d73704..00000000000000 --- a/packages/create-block-interactive-template/block-templates/view.ts.mustache +++ /dev/null @@ -1,46 +0,0 @@ -{{#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 94f615df2747f2..bb203b7023e281 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,14 +24,7 @@ module.exports = { }, }, variants: { - default: {}, - typescript: { - slug: 'example-interactive-typescript', - title: 'Example Interactive TypeScript', - description: - 'An interactive block with the Interactivity API using TypeScript.', - viewScriptModule: 'file:./view.ts', - }, + basic: {}, }, pluginTemplatesPath: join( __dirname, 'plugin-templates' ), blockTemplatesPath: join( __dirname, 'block-templates' ), diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 95b8fc898555c3..3ce895680a15c5 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -2,10 +2,6 @@ DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.). -DataViews is data agnostic, it can work with data coming from a static (JSON file) or dynamic source (HTTP Request) — it just requires the data to be an array of objects that have an unique identifier. Consumers are responsible to query the data source appropiately based on the DataViews props: - -![DataViews flow](https://developer.wordpress.org/files/2024/09/368600071-20aa078f-7c3d-406d-8dd0-8b764addd22a.png "DataViews flow") - ## Installation Install the module diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index 02e81b2b0913d8..48fdf6906b0774 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -8,7 +8,6 @@ import type { ChangeEvent } from 'react'; */ import { Button, - __experimentalDropdownContentWrapper as DropdownContentWrapper, Dropdown, __experimentalToggleGroupControl as ToggleGroupControl, __experimentalToggleGroupControlOption as ToggleGroupControlOption, @@ -28,7 +27,6 @@ import { __, _x, sprintf } from '@wordpress/i18n'; import { memo, useContext, useMemo } from '@wordpress/element'; import { chevronDown, chevronUp, cog, seen, unseen } from '@wordpress/icons'; import warning from '@wordpress/warning'; -import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies @@ -57,8 +55,6 @@ interface ViewTypeMenuProps { defaultLayouts?: SupportedLayouts; } -const DATAVIEWS_CONFIG_POPOVER_PROPS = { placement: 'bottom-end', offset: 9 }; - function ViewTypeMenu( { defaultLayouts = { list: {}, grid: {}, table: {} }, }: ViewTypeMenuProps ) { @@ -514,7 +510,7 @@ function SettingsSection( { ); } -function DataviewsViewConfigDropdown( { +function DataviewsViewConfigContent( { density, setDensity, }: { @@ -522,52 +518,25 @@ function DataviewsViewConfigDropdown( { setDensity: React.Dispatch< React.SetStateAction< number > >; } ) { const { view } = useContext( DataViewsContext ); - const popoverId = useInstanceId( - _DataViewsViewConfig, - 'dataviews-view-config-dropdown' - ); - return ( - { - return ( - -
-
diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php deleted file mode 100644 index bdaec8d1b67a9d..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.asset.php +++ /dev/null @@ -1,9 +0,0 @@ - array( - '@wordpress/interactivity', - array( - 'id' => '@wordpress/interactivity-router', - 'import' => 'dynamic', - ), - ), -); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js deleted file mode 100644 index 83f016e2eac16a..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-context/view.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * WordPress dependencies - */ -import { store, getContext, getServerContext } from '@wordpress/interactivity'; - -store( 'test/get-server-context', { - actions: { - *navigate( e ) { - e.preventDefault(); - const { actions } = yield import( - '@wordpress/interactivity-router' - ); - yield actions.navigate( e.target.href ); - }, - attemptModification() { - try { - getServerContext().prop = 'updated from client'; - getContext().result = 'unexpectedly modified ❌'; - } catch ( e ) { - getContext().result = 'not modified ✅'; - } - }, - }, - callbacks: { - updateServerContextParent() { - const ctx = getContext(); - const { prop, newProp, nested, inherited } = getServerContext(); - ctx.prop = prop; - ctx.newProp = newProp; - ctx.nested.prop = nested.prop; - ctx.nested.newProp = nested.newProp; - ctx.inherited.prop = inherited.prop; - ctx.inherited.newProp = inherited.newProp; - }, - updateServerContextChild() { - const ctx = getContext(); - const { prop, newProp, nested, inherited } = getServerContext(); - ctx.prop = prop; - ctx.newProp = newProp; - ctx.nested.prop = nested.prop; - ctx.nested.newProp = nested.newProp; - ctx.inherited.prop = inherited.prop; - ctx.inherited.newProp = inherited.newProp; - }, - }, -} ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json deleted file mode 100644 index abf76eb9beddcc..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/block.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, - "name": "test/get-server-state", - "title": "E2E Interactivity tests - getServerState", - "category": "text", - "icon": "heart", - "description": "", - "supports": { - "interactivity": true - }, - "textdomain": "e2e-interactivity", - "viewScriptModule": "file:./view.js", - "render": "file:./render.php" -} diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php deleted file mode 100644 index abc4efd8272d5b..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/render.php +++ /dev/null @@ -1,50 +0,0 @@ - - -
-
-
-
-
- - - - - -
diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php deleted file mode 100644 index bdaec8d1b67a9d..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.asset.php +++ /dev/null @@ -1,9 +0,0 @@ - array( - '@wordpress/interactivity', - array( - 'id' => '@wordpress/interactivity-router', - 'import' => 'dynamic', - ), - ), -); diff --git a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js b/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js deleted file mode 100644 index db2992ec4a5863..00000000000000 --- a/packages/e2e-tests/plugins/interactive-blocks/get-server-state/view.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * WordPress dependencies - */ -import { store, getServerState, getContext } from '@wordpress/interactivity'; - -const { state } = store( 'test/get-server-state', { - actions: { - *navigate( e ) { - e.preventDefault(); - const { actions } = yield import( - '@wordpress/interactivity-router' - ); - yield actions.navigate( e.target.href ); - }, - attemptModification() { - try { - getServerState().prop = 'updated from client'; - getContext().result = 'unexpectedly modified ❌'; - } catch ( e ) { - getContext().result = 'not modified ✅'; - } - }, - }, - callbacks: { - updateState() { - const { prop, newProp, nested } = getServerState(); - state.prop = prop; - state.newProp = newProp; - state.nested.prop = nested.prop; - state.nested.newProp = nested.newProp; - }, - }, -} ); diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js index 69f1925c7b0e44..4993f12153b9e4 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js @@ -36,7 +36,8 @@ function SuggestionListItem( { diff --git a/packages/edit-site/src/components/add-new-template/index.js b/packages/edit-site/src/components/add-new-template/index.js index bb0acbe62cd659..933ea6df1198bb 100644 --- a/packages/edit-site/src/components/add-new-template/index.js +++ b/packages/edit-site/src/components/add-new-template/index.js @@ -107,7 +107,8 @@ function TemplateListItem( { } ) { return ( ); 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 4ca87bf200f178..b76192ddfcb5ca 100644 --- a/packages/edit-site/src/components/global-styles-sidebar/style.scss +++ b/packages/edit-site/src/components/global-styles-sidebar/style.scss @@ -22,7 +22,14 @@ flex-direction: column; min-height: 100%; - &__panel { + &__panel, + &__navigator-provider { + display: flex; + flex-direction: column; + flex: 1; + } + + &__navigator-screen { flex: 1; } } diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index bc6906a769af48..60d7e314d7776a 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -272,6 +272,19 @@ 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 ] ); } diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index 7bd3c41a6a22ad..6e5d8de8142f42 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -82,7 +82,6 @@ padding-right: $grid-unit-10; padding-left: $grid-unit-20; overflow: hidden; - height: $header-height; } .edit-widgets-header__title { 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 af0e9b30ae83b4..fcf7adfa77635c 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 @@ -17,20 +17,21 @@ import { __, _x } from '@wordpress/i18n'; */ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import usePostContentBlocks from '../provider/use-post-content-blocks'; function ContentOnlySettingsMenuItems( { clientId, onClose } ) { - const postContentBlocks = usePostContentBlocks(); const { entity, onNavigateToEntityRecord, canEditTemplates } = useSelect( ( select ) => { const { + getBlockEditingMode, getBlockParentsByBlockName, getSettings, getBlockAttributes, - getBlockParents, } = select( blockEditorStore ); - const { getCurrentTemplateId, getRenderingMode } = - select( editorStore ); + const contentOnly = + getBlockEditingMode( clientId ) === 'contentOnly'; + if ( ! contentOnly ) { + return {}; + } const patternParent = getBlockParentsByBlockName( clientId, 'core/block', @@ -44,20 +45,19 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { 'wp_block', getBlockAttributes( patternParent ).ref ); - } else if ( - getRenderingMode() === 'template-locked' && - ! getBlockParents( clientId ).some( ( parent ) => - postContentBlocks.includes( parent ) - ) - ) { - record = select( coreStore ).getEntityRecord( - 'postType', - 'wp_template', - getCurrentTemplateId() + } else { + const { getCurrentTemplateId } = select( editorStore ); + const templateId = getCurrentTemplateId(); + const { getContentLockingParent } = unlock( + select( blockEditorStore ) ); - } - if ( ! record ) { - return {}; + if ( ! getContentLockingParent( clientId ) && templateId ) { + record = select( coreStore ).getEntityRecord( + 'postType', + 'wp_template', + templateId + ); + } } const _canEditTemplates = select( coreStore ).canUser( 'create', { kind: 'postType', @@ -70,7 +70,7 @@ function ContentOnlySettingsMenuItems( { clientId, onClose } ) { getSettings().onNavigateToEntityRecord, }; }, - [ clientId, postContentBlocks ] + [ clientId ] ); if ( ! entity ) { diff --git a/packages/editor/src/components/document-tools/index.js b/packages/editor/src/components/document-tools/index.js index 6a8c20c8d70551..54121652bbf131 100644 --- a/packages/editor/src/components/document-tools/index.js +++ b/packages/editor/src/components/document-tools/index.js @@ -38,8 +38,10 @@ function DocumentTools( { className, disableBlockTools = false } ) { listViewShortcut, inserterSidebarToggleRef, listViewToggleRef, + hasFixedToolbar, showIconLabels, } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); const { get } = select( preferencesStore ); const { isListViewOpened, @@ -58,6 +60,7 @@ function DocumentTools( { className, disableBlockTools = false } ) { ), inserterSidebarToggleRef: getInserterSidebarToggleRef(), listViewToggleRef: getListViewToggleRef(), + hasFixedToolbar: getSettings().hasFixedToolbar, showIconLabels: get( 'core', 'showIconLabels' ), isDistractionFree: get( 'core', 'distractionFree' ), isVisualMode: getEditorMode() === 'visual', @@ -134,7 +137,7 @@ function DocumentTools( { className, disableBlockTools = false } ) { ) } { ( isWideViewport || ! showIconLabels ) && ( <> - { isLargeViewport && ( + { isLargeViewport && ! hasFixedToolbar && ( { const { get } = select( preferencesStore ); const { getEditorSettings, getPostTypeLabel } = select( editorStore ); const editorSettings = getEditorSettings(); const postTypeLabel = getPostTypeLabel(); - const { isZoomOut: _isZoomOut } = unlock( select( blockEditorStore ) ); return { mode: select( editorStore ).getEditorMode(), @@ -97,7 +94,8 @@ export default function EditorInterface( { showBlockBreadcrumbs: get( 'core', 'showBlockBreadcrumbs' ), // translators: Default label for the Document in the Block Breadcrumb. documentLabel: postTypeLabel || _x( 'Document', 'noun' ), - isZoomOut: _isZoomOut(), + blockEditorMode: + select( blockEditorStore ).__unstableGetEditorMode(), }; }, [] ); const isLargeViewport = useViewportMatch( 'medium' ); @@ -208,7 +206,7 @@ export default function EditorInterface( { isLargeViewport && showBlockBreadcrumbs && isRichEditingEnabled && - ! isZoomOut && + blockEditorMode !== 'zoom-out' && mode === 'visual' && ( ) diff --git a/packages/editor/src/components/plugin-sidebar/index.js b/packages/editor/src/components/plugin-sidebar/index.js index 56a954cadffb69..b9c0177e30fc42 100644 --- a/packages/editor/src/components/plugin-sidebar/index.js +++ b/packages/editor/src/components/plugin-sidebar/index.js @@ -3,6 +3,7 @@ */ import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { ComplementaryArea } from '@wordpress/interface'; /** @@ -76,9 +77,12 @@ import { store as editorStore } from '../../store'; * ``` */ export default function PluginSidebar( { className, ...props } ) { - const { postTitle } = useSelect( ( select ) => { + const { postTitle, shortcut } = useSelect( ( select ) => { return { postTitle: select( editorStore ).getEditedPostAttribute( 'title' ), + shortcut: select( + keyboardShortcutsStore + ).getShortcutRepresentation( 'core/editor/toggle-sidebar' ), }; }, [] ); return ( @@ -87,6 +91,7 @@ export default function PluginSidebar( { className, ...props } ) { className="editor-sidebar" smallScreenTitle={ postTitle || __( '(no title)' ) } scope="core" + toggleShortcut={ shortcut } { ...props } /> ); diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 71e18a4d6a9c82..0460b27b616e81 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { Button } from '@wordpress/components'; -import { Component } from '@wordpress/element'; +import { Component, createRef } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -17,6 +17,7 @@ const noop = () => {}; export class PostPublishButton extends Component { constructor( props ) { super( props ); + this.buttonNode = createRef(); this.createOnClick = this.createOnClick.bind( this ); this.closeEntitiesSavedStates = @@ -27,6 +28,21 @@ export class PostPublishButton extends Component { }; } + componentDidMount() { + if ( this.props.focusOnMount ) { + // This timeout is necessary to make sure the `useEffect` hook of + // `useFocusReturn` gets the correct element (the button that opens the + // PostPublishPanel) otherwise it will get this button. + this.timeoutID = setTimeout( () => { + this.buttonNode.current.focus(); + }, 0 ); + } + } + + componentWillUnmount() { + clearTimeout( this.timeoutID ); + } + createOnClick( callback ) { return ( ...args ) => { const { hasNonPostEntityChanges, setEntitiesSavedStatesCallback } = @@ -166,6 +182,7 @@ export class PostPublishButton extends Component { return ( <>
-
- -
) }
diff --git a/packages/editor/src/components/post-publish-panel/style.scss b/packages/editor/src/components/post-publish-panel/style.scss index 7b075717651781..9892cf5430f9a2 100644 --- a/packages/editor/src/components/post-publish-panel/style.scss +++ b/packages/editor/src/components/post-publish-panel/style.scss @@ -68,12 +68,12 @@ } .editor-post-publish-panel__header-publish-button { - padding-left: $grid-unit-05; + padding-right: $grid-unit-05; justify-content: center; } .editor-post-publish-panel__header-cancel-button { - padding-right: $grid-unit-05; + padding-left: $grid-unit-05; } .editor-post-publish-panel__header-published { diff --git a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap index b074159ac423d4..1dd75fffaa7b6a 100644 --- a/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap +++ b/packages/editor/src/components/post-publish-panel/test/__snapshots__/index.js.snap @@ -433,24 +433,24 @@ exports[`PostPublishPanel should render the pre-publish panel if post status is class="editor-post-publish-panel__header" >
@@ -586,24 +586,24 @@ exports[`PostPublishPanel should render the pre-publish panel if the post is not class="editor-post-publish-panel__header" >
@@ -783,24 +783,24 @@ exports[`PostPublishPanel should render the spinner if the post is being saved 1 class="editor-post-publish-panel__header" >
diff --git a/packages/editor/src/components/preview-dropdown/index.js b/packages/editor/src/components/preview-dropdown/index.js index 38565f4b04abed..ecc5bc610a3027 100644 --- a/packages/editor/src/components/preview-dropdown/index.js +++ b/packages/editor/src/components/preview-dropdown/index.js @@ -26,7 +26,6 @@ import { ActionItem } from '@wordpress/interface'; * Internal dependencies */ import { store as editorStore } from '../../store'; -import { store as blockEditorStore } from '@wordpress/block-editor'; import PostPreviewButton from '../post-preview-button'; export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { @@ -45,12 +44,6 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { }; }, [] ); const { setDeviceType } = useDispatch( editorStore ); - const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - - const handleDevicePreviewChange = ( newDeviceType ) => { - setDeviceType( newDeviceType ); - __unstableSetEditorMode( 'edit' ); - }; const isMobile = useViewportMatch( 'medium', '<' ); if ( isMobile ) { @@ -120,7 +113,7 @@ export default function PreviewDropdown( { forceIsAutosaveable, disabled } ) { { isTemplate && ( diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js index ae4fd1075fc261..9abb0e14079d5e 100644 --- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -3,32 +3,52 @@ */ import { useSelect, useRegistry } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useEffect } from '@wordpress/element'; +import { useEffect, useMemo } from '@wordpress/element'; +import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import usePostContentBlocks from './use-post-content-blocks'; +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +const POST_CONTENT_BLOCK_TYPES = [ + 'core/post-title', + 'core/post-featured-image', + 'core/post-content', +]; /** * Component that when rendered, makes it so that the site editor allows only * page content to be edited. */ export default function DisableNonPageContentBlocks() { - const contentOnlyIds = usePostContentBlocks(); - const templateParts = useSelect( ( select ) => { - const { getBlocksByName } = select( blockEditorStore ); - return getBlocksByName( 'core/template-part' ); - }, [] ); - const disabledIds = useSelect( + const contentOnlyBlockTypes = useMemo( + () => [ + ...applyFilters( + 'editor.postContentBlockTypes', + POST_CONTENT_BLOCK_TYPES + ), + 'core/template-part', + ], + [] + ); + + // Note that there are two separate subscriptions because the result for each + // returns a new array. + const contentOnlyIds = useSelect( ( select ) => { - const { getBlockOrder } = select( blockEditorStore ); - return templateParts.flatMap( ( clientId ) => - getBlockOrder( clientId ) - ); + const { getPostBlocksByName } = unlock( select( editorStore ) ); + return getPostBlocksByName( contentOnlyBlockTypes ); }, - [ templateParts ] + [ contentOnlyBlockTypes ] ); + const disabledIds = useSelect( ( select ) => { + const { getBlocksByName, getBlockOrder } = select( blockEditorStore ); + return getBlocksByName( 'core/template-part' ).flatMap( ( clientId ) => + getBlockOrder( clientId ) + ); + }, [] ); const registry = useRegistry(); @@ -41,9 +61,6 @@ export default function DisableNonPageContentBlocks() { for ( const clientId of contentOnlyIds ) { setBlockEditingMode( clientId, 'contentOnly' ); } - for ( const clientId of templateParts ) { - setBlockEditingMode( clientId, 'contentOnly' ); - } for ( const clientId of disabledIds ) { setBlockEditingMode( clientId, 'disabled' ); } @@ -55,15 +72,12 @@ export default function DisableNonPageContentBlocks() { for ( const clientId of contentOnlyIds ) { unsetBlockEditingMode( clientId ); } - for ( const clientId of templateParts ) { - unsetBlockEditingMode( clientId ); - } for ( const clientId of disabledIds ) { unsetBlockEditingMode( clientId ); } } ); }; - }, [ templateParts, contentOnlyIds, disabledIds, registry ] ); + }, [ contentOnlyIds, disabledIds, registry ] ); return null; } diff --git a/packages/editor/src/components/provider/use-post-content-blocks.js b/packages/editor/src/components/provider/use-post-content-blocks.js deleted file mode 100644 index bdd277157e47e0..00000000000000 --- a/packages/editor/src/components/provider/use-post-content-blocks.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { useMemo } from '@wordpress/element'; -import { applyFilters } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import { store as editorStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const POST_CONTENT_BLOCK_TYPES = [ - 'core/post-title', - 'core/post-featured-image', - 'core/post-content', -]; - -export default function usePostContentBlocks() { - const contentOnlyBlockTypes = useMemo( - () => [ - ...applyFilters( - 'editor.postContentBlockTypes', - POST_CONTENT_BLOCK_TYPES - ), - ], - [] - ); - - // Note that there are two separate subscriptions because the result for each - // returns a new array. - const contentOnlyIds = useSelect( - ( select ) => { - const { getPostBlocksByName } = unlock( select( editorStore ) ); - return getPostBlocksByName( contentOnlyBlockTypes ); - }, - [ contentOnlyBlockTypes ] - ); - - return contentOnlyIds; -} diff --git a/packages/editor/src/components/visual-editor/index.js b/packages/editor/src/components/visual-editor/index.js index 88d2dac8ffd77c..2ff115272d614b 100644 --- a/packages/editor/src/components/visual-editor/index.js +++ b/packages/editor/src/components/visual-editor/index.js @@ -174,19 +174,17 @@ function VisualEditor( { hasRootPaddingAwareAlignments, themeHasDisabledLayoutStyles, themeSupportsLayout, - isZoomedOut, + isZoomOutMode, } = useSelect( ( select ) => { - const { getSettings, isZoomOut: _isZoomOut } = unlock( - select( blockEditorStore ) - ); - + const { getSettings, __unstableGetEditorMode } = + select( blockEditorStore ); const _settings = getSettings(); return { themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, themeSupportsLayout: _settings.supportsLayout, hasRootPaddingAwareAlignments: _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, - isZoomedOut: _isZoomOut(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', }; }, [] ); @@ -338,7 +336,7 @@ function VisualEditor( { ] ); const zoomOutProps = - isZoomedOut && ! isTabletViewport + isZoomOutMode && ! isTabletViewport ? { scale: 'default', frameSize: '48px', @@ -357,7 +355,7 @@ function VisualEditor( { // Disable resizing in mobile viewport. ! isMobileViewport && // Dsiable resizing in zoomed-out mode. - ! isZoomedOut; + ! isZoomOutMode; const shouldIframe = ! disableIframe || [ 'Tablet', 'Mobile' ].includes( deviceType ); diff --git a/packages/editor/src/components/zoom-out-toggle/index.js b/packages/editor/src/components/zoom-out-toggle/index.js index 214b1c51fd6255..e8c7b1e50510ab 100644 --- a/packages/editor/src/components/zoom-out-toggle/index.js +++ b/packages/editor/src/components/zoom-out-toggle/index.js @@ -8,27 +8,16 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { square as zoomOutIcon } from '@wordpress/icons'; -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - const ZoomOutToggle = () => { - const { isZoomOut } = useSelect( ( select ) => ( { - isZoomOut: unlock( select( blockEditorStore ) ).isZoomOut(), + const { isZoomOutMode } = useSelect( ( select ) => ( { + isZoomOutMode: + select( blockEditorStore ).__unstableGetEditorMode() === 'zoom-out', } ) ); - const { resetZoomLevel, setZoomLevel, __unstableSetEditorMode } = unlock( - useDispatch( blockEditorStore ) - ); + const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); const handleZoomOut = () => { - if ( isZoomOut ) { - resetZoomLevel(); - } else { - setZoomLevel( 50 ); - } - __unstableSetEditorMode( isZoomOut ? 'edit' : 'zoom-out' ); + __unstableSetEditorMode( isZoomOutMode ? 'edit' : 'zoom-out' ); }; return ( @@ -36,7 +25,7 @@ const ZoomOutToggle = () => { onClick={ handleZoomOut } icon={ zoomOutIcon } label={ __( 'Toggle Zoom Out' ) } - isPressed={ isZoomOut } + isPressed={ isZoomOutMode } size="compact" /> ); diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 42f311973709dd..6989bcdc0c802c 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -6,7 +6,6 @@ ### 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/directives.tsx b/packages/interactivity/src/directives.tsx index 340880954683da..cde39d830499a2 100644 --- a/packages/interactivity/src/directives.tsx +++ b/packages/interactivity/src/directives.tsx @@ -142,19 +142,14 @@ export default () => { const defaultEntry = context.find( ( { suffix } ) => suffix === 'default' ); - const { client: inheritedClient, server: inheritedServer } = - useContext( inheritedContext ); + const inheritedValue = useContext( inheritedContext ); const ns = defaultEntry!.namespace; - const client = useRef( proxifyState( ns, {} ) ); - const server = useRef( proxifyState( ns, {}, { readOnly: true } ) ); + const currentValue = useRef( proxifyState( ns, {} ) ); // No change should be made if `defaultEntry` does not exist. const contextStack = useMemo( () => { - const result = { - client: { ...inheritedClient }, - server: { ...inheritedServer }, - }; + const result = { ...inheritedValue }; if ( defaultEntry ) { const { namespace, value } = defaultEntry; // Check that the value is a JSON object. Send a console warning if not. @@ -164,22 +159,17 @@ export default () => { ); } deepMerge( - client.current, + currentValue.current, deepClone( value ) as object, false ); - deepMerge( server.current, deepClone( value ) as object ); - result.client[ namespace ] = proxifyContext( - client.current, - inheritedClient[ namespace ] - ); - result.server[ namespace ] = proxifyContext( - server.current, - inheritedServer[ namespace ] + result[ namespace ] = proxifyContext( + currentValue.current, + inheritedValue[ namespace ] ); } return result; - }, [ defaultEntry, inheritedClient, inheritedServer ] ); + }, [ defaultEntry, inheritedValue ] ); return createElement( Provider, { value: contextStack }, children ); }, @@ -573,24 +563,17 @@ export default () => { suffix === 'default' ? 'item' : kebabToCamelCase( suffix ); const itemContext = proxifyContext( proxifyState( namespace, {} ), - inheritedValue.client[ namespace ] + inheritedValue[ namespace ] ); const mergedContext = { - client: { - ...inheritedValue.client, - [ namespace ]: itemContext, - }, - server: { ...inheritedValue.server }, + ...inheritedValue, + [ namespace ]: itemContext, }; // Set the item after proxifying the context. - mergedContext.client[ namespace ][ itemProp ] = item; + mergedContext[ namespace ][ itemProp ] = item; - const scope = { - ...getScope(), - context: mergedContext.client, - serverContext: mergedContext.server, - }; + const scope = { ...getScope(), context: mergedContext }; const key = eachKey ? getEvaluate( { scope } )( eachKey[ 0 ] ) : item; diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index 6b55ec014aa799..215da8afef9b5b 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -93,7 +93,7 @@ interface DirectivesProps { } // Main context. -const context = createContext< any >( { client: {}, server: {} } ); +const context = createContext< any >( {} ); // WordPress Directives. const directiveCallbacks: Record< string, DirectiveCallback > = {}; @@ -190,13 +190,9 @@ const resolve = ( path: string, namespace: string ) => { } let resolvedStore = stores.get( namespace ); if ( typeof resolvedStore === 'undefined' ) { - resolvedStore = store( - namespace, - {}, - { - lock: universalUnlock, - } - ); + resolvedStore = store( namespace, undefined, { + lock: universalUnlock, + } ); } const current = { ...resolvedStore, @@ -257,9 +253,7 @@ const Directives = ( { // element ref, state and props. const scope = useRef< Scope >( {} as Scope ).current; scope.evaluate = useCallback( getEvaluate( { scope } ), [] ); - const { client, server } = useContext( context ); - scope.context = client; - scope.serverContext = server; + scope.context = useContext( context ); /* eslint-disable react-hooks/rules-of-hooks */ scope.ref = previousScope?.ref || useRef( null ); /* eslint-enable react-hooks/rules-of-hooks */ diff --git a/packages/interactivity/src/index.ts b/packages/interactivity/src/index.ts index 9d013e4e744ed5..336c2a97226db7 100644 --- a/packages/interactivity/src/index.ts +++ b/packages/interactivity/src/index.ts @@ -16,8 +16,8 @@ import { getNamespace } from './namespaces'; import { parseServerData, populateServerData } from './store'; import { proxifyState } from './proxies'; -export { store, getConfig, getServerState } from './store'; -export { getContext, getServerContext, getElement } from './scopes'; +export { store, getConfig } from './store'; +export { getContext, getElement } from './scopes'; export { withScope, useWatch, diff --git a/packages/interactivity/src/proxies/state.ts b/packages/interactivity/src/proxies/state.ts index c91d8f6ab90a5b..ec49c4b27c4adb 100644 --- a/packages/interactivity/src/proxies/state.ts +++ b/packages/interactivity/src/proxies/state.ts @@ -46,8 +46,6 @@ const proxyToProps: WeakMap< export const hasPropSignal = ( proxy: object, key: string ) => proxyToProps.has( proxy ) && proxyToProps.get( proxy )!.has( key ); -const readOnlyProxies = new WeakSet(); - /** * Returns the {@link PropSignal | `PropSignal`} instance associated with the * specified prop in the passed proxy. @@ -79,11 +77,8 @@ const getPropSignal = ( if ( get ) { prop.setGetter( get ); } else { - const readOnly = readOnlyProxies.has( proxy ); prop.setValue( - shouldProxy( value ) - ? proxifyState( ns, value, { readOnly } ) - : value + shouldProxy( value ) ? proxifyState( ns, value ) : value ); } } @@ -153,9 +148,6 @@ const stateHandlers: ProxyHandler< object > = { value: unknown, receiver: object ): boolean { - if ( readOnlyProxies.has( receiver ) ) { - return false; - } setNamespace( getNamespaceFromProxy( receiver ) ); try { return Reflect.set( target, key, value, receiver ); @@ -169,10 +161,6 @@ const stateHandlers: ProxyHandler< object > = { key: string, desc: PropertyDescriptor ): boolean { - if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { - return false; - } - const isNew = ! ( key in target ); const result = Reflect.defineProperty( target, key, desc ); @@ -211,10 +199,6 @@ const stateHandlers: ProxyHandler< object > = { }, deleteProperty( target: object, key: string ): boolean { - if ( readOnlyProxies.has( getProxyFromObject( target )! ) ) { - return false; - } - const result = Reflect.deleteProperty( target, key ); if ( result ) { @@ -246,10 +230,8 @@ const stateHandlers: ProxyHandler< object > = { * Returns the proxy associated with the given state object, creating it if it * does not exist. * - * @param namespace The namespace that will be associated to this proxy. - * @param obj The object to proxify. - * @param options Options. - * @param options.readOnly Read-only. + * @param namespace The namespace that will be associated to this proxy. + * @param obj The object to proxify. * * @throws Error if the object cannot be proxified. Use {@link shouldProxy} to * check if a proxy can be created for a specific object. @@ -258,15 +240,8 @@ const stateHandlers: ProxyHandler< object > = { */ export const proxifyState = < T extends object >( namespace: string, - obj: T, - options?: { readOnly?: boolean } -): T => { - const proxy = createProxy( namespace, obj, stateHandlers ) as T; - if ( options?.readOnly ) { - readOnlyProxies.add( proxy ); - } - return proxy; -}; + obj: T +): T => createProxy( namespace, obj, stateHandlers ) as T; /** * Reads the value of the specified property without subscribing to it. diff --git a/packages/interactivity/src/proxies/test/state-proxy.ts b/packages/interactivity/src/proxies/test/state-proxy.ts index 4b0d2b0a708c3a..92500189fc8309 100644 --- a/packages/interactivity/src/proxies/test/state-proxy.ts +++ b/packages/interactivity/src/proxies/test/state-proxy.ts @@ -9,7 +9,7 @@ import { effect } from '@preact/signals'; /** * Internal dependencies */ -import { proxifyState, peek, deepMerge } from '../'; +import { proxifyState, peek } from '../'; import { setScope, resetScope, getContext, getElement } from '../../scopes'; import { setNamespace, resetNamespace } from '../../namespaces'; @@ -1265,202 +1265,5 @@ describe( 'Interactivity API', () => { expect( x ).toBe( undefined ); } ); } ); - - describe( 'read-only', () => { - it( "should not allow modifying a prop's value", () => { - const readOnlyState = proxifyState( - 'test', - { prop: 'value', nested: { prop: 'value' } }, - { readOnly: true } - ); - - expect( () => { - readOnlyState.prop = 'new value'; - } ).toThrow(); - expect( () => { - readOnlyState.nested.prop = 'new value'; - } ).toThrow(); - } ); - - it( 'should not allow modifying a prop descriptor', () => { - const readOnlyState = proxifyState( - 'test', - { prop: 'value', nested: { prop: 'value' } }, - { readOnly: true } - ); - - expect( () => { - Object.defineProperty( readOnlyState, 'prop', { - get: () => 'value from getter', - writable: true, - enumerable: false, - } ); - } ).toThrow(); - expect( () => { - Object.defineProperty( readOnlyState.nested, 'prop', { - get: () => 'value from getter', - writable: true, - enumerable: false, - } ); - } ).toThrow(); - } ); - - it( 'should not allow adding new props', () => { - const readOnlyState = proxifyState< any >( - 'test', - { prop: 'value', nested: { prop: 'value' } }, - { readOnly: true } - ); - - expect( () => { - readOnlyState.newProp = 'value'; - } ).toThrow(); - expect( () => { - readOnlyState.nested.newProp = 'value'; - } ).toThrow(); - } ); - - it( 'should not allow removing props', () => { - const readOnlyState = proxifyState< any >( - 'test', - { prop: 'value', nested: { prop: 'value' } }, - { readOnly: true } - ); - - expect( () => { - delete readOnlyState.prop; - } ).toThrow(); - expect( () => { - delete readOnlyState.nested.prop; - } ).toThrow(); - } ); - - it( 'should not allow adding items to an array', () => { - const readOnlyState = proxifyState( - 'test', - { array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, - { readOnly: true } - ); - - expect( () => readOnlyState.array.push( 4 ) ).toThrow(); - expect( () => readOnlyState.nested.array.push( 4 ) ).toThrow(); - } ); - - it( 'should not allow removing items from an array', () => { - const readOnlyState = proxifyState( - 'test', - { array: [ 1, 2, 3 ], nested: { array: [ 1, 2, 3 ] } }, - { readOnly: true } - ); - - expect( () => readOnlyState.array.pop() ).toThrow(); - expect( () => readOnlyState.nested.array.pop() ).toThrow(); - } ); - - it( 'should allow subscribing to prop changes', () => { - const readOnlyState = proxifyState( - 'test', - { - prop: 'value', - nested: { prop: 'value' }, - }, - { readOnly: true } - ); - - const spy1 = jest.fn( () => readOnlyState.prop ); - const spy2 = jest.fn( () => readOnlyState.nested.prop ); - - effect( spy1 ); - effect( spy2 ); - expect( spy1 ).toHaveBeenCalledTimes( 1 ); - expect( spy2 ).toHaveBeenCalledTimes( 1 ); - expect( spy1 ).toHaveLastReturnedWith( 'value' ); - expect( spy2 ).toHaveLastReturnedWith( 'value' ); - - deepMerge( readOnlyState, { prop: 'new value' } ); - - expect( spy1 ).toHaveBeenCalledTimes( 2 ); - expect( spy2 ).toHaveBeenCalledTimes( 1 ); - expect( spy1 ).toHaveLastReturnedWith( 'new value' ); - expect( spy2 ).toHaveLastReturnedWith( 'value' ); - - deepMerge( readOnlyState, { nested: { prop: 'new value' } } ); - - expect( spy1 ).toHaveBeenCalledTimes( 2 ); - expect( spy2 ).toHaveBeenCalledTimes( 2 ); - expect( spy1 ).toHaveLastReturnedWith( 'new value' ); - expect( spy2 ).toHaveLastReturnedWith( 'new value' ); - } ); - - it( 'should allow subscribing to new props', () => { - const readOnlyState = proxifyState< any >( - 'test', - { - prop: 'value', - nested: { prop: 'value' }, - }, - { readOnly: true } - ); - - const spy1 = jest.fn( () => readOnlyState.newProp ); - const spy2 = jest.fn( () => readOnlyState.nested.newProp ); - - effect( spy1 ); - effect( spy2 ); - expect( spy1 ).toHaveBeenCalledTimes( 1 ); - expect( spy2 ).toHaveBeenCalledTimes( 1 ); - expect( spy1 ).toHaveLastReturnedWith( undefined ); - expect( spy2 ).toHaveLastReturnedWith( undefined ); - - deepMerge( readOnlyState, { newProp: 'value' } ); - - expect( spy1 ).toHaveBeenCalledTimes( 2 ); - expect( spy2 ).toHaveBeenCalledTimes( 1 ); - expect( spy1 ).toHaveLastReturnedWith( 'value' ); - expect( spy2 ).toHaveLastReturnedWith( undefined ); - - deepMerge( readOnlyState, { nested: { newProp: 'value' } } ); - - expect( spy1 ).toHaveBeenCalledTimes( 2 ); - expect( spy2 ).toHaveBeenCalledTimes( 2 ); - expect( spy1 ).toHaveLastReturnedWith( 'value' ); - expect( spy2 ).toHaveLastReturnedWith( 'value' ); - } ); - - it( 'should allow subscribing to array changes', () => { - const readOnlyState = proxifyState< any >( - 'test', - { - array: [ 1, 2, 3 ], - nested: { array: [ 1, 2, 3 ] }, - }, - { readOnly: true } - ); - - const spy1 = jest.fn( () => readOnlyState.array[ 0 ] ); - const spy2 = jest.fn( () => readOnlyState.nested.array[ 0 ] ); - - effect( spy1 ); - effect( spy2 ); - expect( spy1 ).toHaveBeenCalledTimes( 1 ); - expect( spy2 ).toHaveBeenCalledTimes( 1 ); - expect( spy1 ).toHaveLastReturnedWith( 1 ); - expect( spy2 ).toHaveLastReturnedWith( 1 ); - - deepMerge( readOnlyState, { array: [ 4, 5, 6 ] } ); - - expect( spy1 ).toHaveBeenCalledTimes( 2 ); - expect( spy2 ).toHaveBeenCalledTimes( 1 ); - expect( spy1 ).toHaveLastReturnedWith( 4 ); - expect( spy2 ).toHaveLastReturnedWith( 1 ); - - deepMerge( readOnlyState, { nested: { array: [] } } ); - - expect( spy1 ).toHaveBeenCalledTimes( 2 ); - expect( spy2 ).toHaveBeenCalledTimes( 2 ); - expect( spy1 ).toHaveLastReturnedWith( 4 ); - expect( spy2 ).toHaveLastReturnedWith( undefined ); - } ); - } ); } ); } ); diff --git a/packages/interactivity/src/scopes.ts b/packages/interactivity/src/scopes.ts index 722305f6bee112..2e78755ec4bbe6 100644 --- a/packages/interactivity/src/scopes.ts +++ b/packages/interactivity/src/scopes.ts @@ -12,7 +12,6 @@ import type { Evaluate } from './hooks'; export interface Scope { evaluate: Evaluate; context: object; - serverContext: object; ref: RefObject< HTMLElement >; attributes: createElement.JSX.HTMLAttributes; } @@ -97,46 +96,3 @@ export const getElement = () => { attributes: deepImmutable( attributes ), } ); }; - -/** - * Retrieves the part of the inherited context defined and updated from the - * server. - * - * The object returned is read-only, and includes the context defined in PHP - * with `wp_interactivity_data_wp_context()`, including the corresponding - * inherited properties. When `actions.navigate()` is called, this object is - * updated to reflect the changes in the new visited page, without affecting the - * context returned by `getContext()`. Directives can subscribe to those changes - * to update the context if needed. - * - * @example - * ```js - * store('...', { - * callbacks: { - * updateServerContext() { - * const context = getContext(); - * const serverContext = getServerContext(); - * // Override some property with the new value that came from the server. - * context.overridableProp = serverContext.overridableProp; - * }, - * }, - * }); - * ``` - * - * @param namespace Store namespace. By default, the namespace where the calling - * function exists is used. - * @return The server context content. - */ -export const getServerContext = < T extends object >( - namespace?: string -): T => { - const scope = getScope(); - if ( globalThis.SCRIPT_DEBUG ) { - if ( ! scope ) { - throw Error( - 'Cannot call `getServerContext()` when there is no scope. If you are using an async function, please consider using a generator instead. If you are using some sort of async callbacks, like `setTimeout`, please wrap the callback with `withScope(callback)`.' - ); - } - } - return scope.serverContext[ namespace || getNamespace() ]; -}; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index b147e0f61163bf..c74764b902e194 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -12,7 +12,6 @@ export const stores = new Map(); const rawStores = new Map(); const storeLocks = new Map(); const storeConfigs = new Map(); -const serverStates = new Map(); /** * Get the defined config for the store with the passed namespace. @@ -23,39 +22,6 @@ const serverStates = new Map(); export const getConfig = ( namespace?: string ) => storeConfigs.get( namespace || getNamespace() ) || {}; -/** - * Get the part of the state defined and updated from the server. - * - * The object returned is read-only, and includes the state defined in PHP with - * `wp_interactivity_state()`. When using `actions.navigate()`, this object is - * updated to reflect the changes in its properites, without affecting the state - * returned by `store()`. Directives can subscribe to those changes to update - * the state if needed. - * - * @example - * ```js - * const { state } = store('myStore', { - * callbacks: { - * updateServerState() { - * const serverState = getServerState(); - * // Override some property with the new value that came from the server. - * state.overridableProp = serverState.overridableProp; - * }, - * }, - * }); - * ``` - * - * @param namespace Store's namespace from which to retrieve the server state. - * @return The server state for the given namespace. - */ -export const getServerState = ( namespace?: string ) => { - const ns = namespace || getNamespace(); - if ( ! serverStates.has( ns ) ) { - serverStates.set( ns, proxifyState( ns, {}, { readOnly: true } ) ); - } - return serverStates.get( ns ); -}; - interface StoreOptions { /** * Property to block/unblock private store namespaces. @@ -84,42 +50,6 @@ 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.'; @@ -168,34 +98,17 @@ export const universalUnlock = * * @return A reference to the namespace content. */ - -// 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 >( +export function store< S extends object = {} >( namespace: string, - storePart: ConvertPromisesToGenerators< T >, + storePart?: S, options?: StoreOptions -): Prettify< ConvertGeneratorsToPromises< T > >; +): S; -// Overload for when types are divided into multiple parts. export function store< T extends object >( namespace: string, - storePart: ConvertPromisesToGenerators< DeepPartial< T > >, + storePart?: T, options?: StoreOptions -): Prettify< ConvertGeneratorsToPromises< T > >; +): T; export function store( namespace: string, @@ -274,7 +187,6 @@ export const populateServerData = ( data?: { Object.entries( data!.state ).forEach( ( [ namespace, state ] ) => { const st = store< any >( namespace, {}, { lock: universalUnlock } ); deepMerge( st.state, state, false ); - deepMerge( getServerState( namespace ), state ); } ); } if ( isPlainObject( data?.config ) ) { diff --git a/packages/interactivity/src/test/store.ts b/packages/interactivity/src/test/store.ts deleted file mode 100644 index 1092001db03143..00000000000000 --- a/packages/interactivity/src/test/store.ts +++ /dev/null @@ -1,286 +0,0 @@ -/** - * 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 deleted file mode 100644 index 6a90abc2ba2210..00000000000000 --- a/packages/interactivity/tsconfig.test.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$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/packages/interface/src/components/complementary-area-toggle/index.js b/packages/interface/src/components/complementary-area-toggle/index.js index 15f9e48d71083e..b6690b7df5fc5d 100644 --- a/packages/interface/src/components/complementary-area-toggle/index.js +++ b/packages/interface/src/components/complementary-area-toggle/index.js @@ -17,7 +17,6 @@ function ComplementaryAreaToggle( { icon, selectedIcon, name, - shortcut, ...props } ) { const ComponentToUse = as; @@ -27,10 +26,8 @@ function ComplementaryAreaToggle( { identifier, [ identifier, scope ] ); - const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); - return ( ); diff --git a/packages/interface/src/components/complementary-area/index.js b/packages/interface/src/components/complementary-area/index.js index d9fa8e71acb23a..363a6ee9dea76c 100644 --- a/packages/interface/src/components/complementary-area/index.js +++ b/packages/interface/src/components/complementary-area/index.js @@ -275,7 +275,6 @@ function ComplementaryArea( { showTooltip={ ! showIconLabels } variant={ showIconLabels ? 'tertiary' : undefined } size="compact" - shortcut={ toggleShortcut } /> ) } diff --git a/packages/widgets/src/blocks/legacy-widget/edit/index.js b/packages/widgets/src/blocks/legacy-widget/edit/index.js index c5ca43211e58e6..f371786c106d6f 100644 --- a/packages/widgets/src/blocks/legacy-widget/edit/index.js +++ b/packages/widgets/src/blocks/legacy-widget/edit/index.js @@ -11,11 +11,13 @@ import { BlockControls, InspectorControls, BlockIcon, + store as blockEditorStore, } from '@wordpress/block-editor'; import { Flex, FlexBlock, Spinner, Placeholder } from '@wordpress/components'; import { brush as brushIcon } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { useState, useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; import { useEntityRecord } from '@wordpress/core-data'; /** @@ -100,6 +102,11 @@ function NotEmpty( { const { record: widgetType, hasResolved: hasResolvedWidgetType } = useEntityRecord( 'root', 'widgetType', widgetTypeId ); + const isNavigationMode = useSelect( + ( select ) => select( blockEditorStore ).isNavigationMode(), + [] + ); + const setInstance = useCallback( ( nextInstance ) => { setAttributes( { instance: nextInstance } ); }, [] ); @@ -123,7 +130,8 @@ function NotEmpty( { ); } - const mode = idBase && ! isSelected ? 'preview' : 'edit'; + const mode = + idBase && ( isNavigationMode || ! isSelected ) ? 'preview' : 'edit'; return ( <> diff --git a/phpunit/blocks/render-post-template-test.php b/phpunit/blocks/render-post-template-test.php index e929e459654fe7..6241f6f0605164 100644 --- a/phpunit/blocks/render-post-template-test.php +++ b/phpunit/blocks/render-post-template-test.php @@ -122,7 +122,7 @@ public function test_rendering_post_template_with_main_query_loop_already_starte global $wp_query, $wp_the_query; // Query block with post template block. - $content = ''; + $content = ''; $content .= ''; $content .= ''; $content .= ''; diff --git a/storybook/manager-head.html b/storybook/manager-head.html index 7293248ae3e472..ebf2d6891ba0bb 100644 --- a/storybook/manager-head.html +++ b/storybook/manager-head.html @@ -2,7 +2,6 @@ ( function redirectIfStoryMoved() { const PREVIOUSLY_EXPERIMENTAL_COMPONENTS = [ 'alignmentmatrixcontrol', - 'boxcontrol', 'customselectcontrol-v2', 'dimensioncontrol', 'navigation', diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 90a5d2b1da9f10..c556c469698ebd 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -66,7 +66,7 @@ test.describe( 'Block bindings', () => { ); } ); - test( 'should always show the source label in server-only sources', async ( { + test( 'should show the key of the custom field in server sources with key', async ( { editor, } ) => { await editor.insertBlock( { @@ -86,6 +86,30 @@ test.describe( 'Block bindings', () => { const paragraphBlock = editor.canvas.getByRole( 'document', { name: 'Block: Paragraph', } ); + await expect( paragraphBlock ).toHaveText( + 'text_custom_field' + ); + } ); + + test( 'should show the source label in server sources without key', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { + content: 'paragraph default content', + metadata: { + bindings: { + content: { + source: 'core/server-source', + }, + }, + }, + }, + } ); + const paragraphBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Paragraph', + } ); await expect( paragraphBlock ).toHaveText( 'Server Source' ); } ); diff --git a/test/e2e/specs/editor/various/block-moving-mode.spec.js b/test/e2e/specs/editor/various/block-moving-mode.spec.js new file mode 100644 index 00000000000000..5b8ef6bdcd051b --- /dev/null +++ b/test/e2e/specs/editor/various/block-moving-mode.spec.js @@ -0,0 +1,155 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block moving mode', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test( 'can move block', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + } ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + } ); + + // Move the second block in front of the first block. + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Move to' } ).click(); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + ] ); + } ); + + test( 'can move block in the nested block', async ( { editor, page } ) => { + // Create two group blocks with some blocks. + await editor.insertBlock( { name: 'core/group' } ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Enter' ); + await page.getByRole( 'option', { name: 'Paragraph' } ).click(); + await page.keyboard.type( 'First Paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Second Paragraph' ); + + await editor.insertBlock( { name: 'core/group' } ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Enter' ); + await page.getByRole( 'option', { name: 'Paragraph' } ).click(); + await page.keyboard.type( 'Third Paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Fourth Paragraph' ); + + // Move a paragraph block in the first group block into the second group block. + const paragraphBlock = editor.canvas.locator( + 'text="First Paragraph"' + ); + await paragraphBlock.focus(); + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Move to' } ).click(); + await page.keyboard.press( 'ArrowLeft' ); // Select the first group block. + await page.keyboard.press( 'ArrowDown' ); // Select the second group block. + await page.keyboard.press( 'ArrowRight' ); // Enter the second group block. + await page.keyboard.press( 'ArrowDown' ); // Move down in the second group block. + await page.keyboard.press( 'Enter' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'Second Paragraph' }, + }, + ], + }, + { + name: 'core/group', + innerBlocks: [ + { + name: 'core/paragraph', + attributes: { content: 'Third Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + }, + { + name: 'core/paragraph', + attributes: { content: 'Fourth Paragraph' }, + }, + ], + }, + ] ); + } ); + + test( 'can not move inside its own block', async ( { editor, page } ) => { + // Create a paragraph block and a group block. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'First Paragraph' }, + } ); + await editor.insertBlock( { name: 'core/group' } ); + await editor.canvas + .locator( + 'role=button[name="Group: Gather blocks in a container."i]' + ) + .click(); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Enter' ); + await page.getByRole( 'option', { name: 'Paragraph' } ).click(); + await page.keyboard.type( 'Second Paragraph' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Third Paragraph' ); + + // Trying to move the group block into its own. + const groupBlock = editor.canvas.locator( + 'role=document[name="Block: Group"i]' + ); + await groupBlock.focus(); + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Move to' } ).click(); + await page.keyboard.press( 'ArrowRight' ); + await expect( groupBlock ).toHaveClass( /is-selected/ ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js index e1ca121040b974..6a7125d04f7a6a 100644 --- a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js +++ b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js @@ -17,26 +17,108 @@ test.describe( 'Order of block keyboard navigation', () => { await editor.openDocumentSettingsSidebar(); } ); - test( 'permits tabbing through the block toolbar of the paragraph block', async ( { + test( 'permits tabbing through paragraph blocks in the expected order', async ( { editor, KeyboardNavigableBlocks, page, - pageUtils, } ) => { - // Insert three paragraph blocks. - for ( let i = 0; i < 3; i++ ) { + const paragraphBlocks = [ 'Paragraph 0', 'Paragraph 1', 'Paragraph 2' ]; + + // Create 3 paragraphs blocks with some content. + for ( const paragraphBlock of paragraphBlocks ) { await editor.insertBlock( { name: 'core/paragraph' } ); - await page.keyboard.type( `Paragraph ${ i + 1 }` ); + await page.keyboard.type( paragraphBlock ); } - // Select the middle paragraph block. + + // Select the middle block. await page.keyboard.press( 'ArrowUp' ); await editor.showBlockToolbar(); - await pageUtils.pressKeys( 'shift+Tab' ); - await KeyboardNavigableBlocks.navigateThroughBlockToolbar(); + await KeyboardNavigableBlocks.navigateToContentEditorTop(); + await KeyboardNavigableBlocks.tabThroughParagraphBlock( 'Paragraph 1' ); + + // Repeat the same steps to ensure that there is no change introduced in how the focus is handled. + // This prevents the previous regression explained in: https://github.com/WordPress/gutenberg/issues/11773. + await KeyboardNavigableBlocks.navigateToContentEditorTop(); + await KeyboardNavigableBlocks.tabThroughParagraphBlock( 'Paragraph 1' ); + } ); + + test( 'allows tabbing in navigation mode if no block is selected', async ( { + editor, + KeyboardNavigableBlocks, + page, + } ) => { + const paragraphBlocks = [ '0', '1' ]; + + // Create 2 paragraphs blocks with some content. + for ( const paragraphBlock of paragraphBlocks ) { + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( paragraphBlock ); + } + + // Clear the selected block. + const paragraph = editor.canvas + .locator( '[data-type="core/paragraph"]' ) + .getByText( '1' ); + const box = await paragraph.boundingBox(); + await page.mouse.click( box.x - 1, box.y ); + await page.keyboard.press( 'Tab' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add title' ); + + await page.keyboard.press( 'Tab' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( + 'Paragraph Block. Row 1. 0' + ); + + await page.keyboard.press( 'Tab' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( + 'Paragraph Block. Row 2. 1' + ); + + await page.keyboard.press( 'Tab' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); + } ); + + test( 'allows tabbing in navigation mode if no block is selected (reverse)', async ( { + editor, + KeyboardNavigableBlocks, + page, + pageUtils, + } ) => { + const paragraphBlocks = [ '0', '1' ]; + + // Create 2 paragraphs blocks with some content. + for ( const paragraphBlock of paragraphBlocks ) { + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( paragraphBlock ); + } + + // Clear the selected block. + const paragraph = editor.canvas + .locator( '[data-type="core/paragraph"]' ) + .getByText( '1' ); + const box = await paragraph.boundingBox(); + await page.mouse.click( box.x - 1, box.y ); + + // Put focus behind the block list. + await page.evaluate( () => { + document + .querySelector( '.interface-interface-skeleton__sidebar' ) + .focus(); + } ); + + await pageUtils.pressKeys( 'shift+Tab' ); await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Block: Paragraph' + 'Paragraph Block. Row 2. 1' ); + + await pageUtils.pressKeys( 'shift+Tab' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( + 'Paragraph Block. Row 1. 0' + ); + + await pageUtils.pressKeys( 'shift+Tab' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Add title' ); } ); test( 'should navigate correctly with multi selection', async ( { @@ -126,7 +208,31 @@ class KeyboardNavigableBlocks { expect( ariaLabel ).toBe( label ); } - async navigateThroughBlockToolbar() { + async navigateToContentEditorTop() { + // Use 'Ctrl+`' to return to the top of the editor. + await this.pageUtils.pressKeys( 'ctrl+`', { times: 5 } ); + } + + async tabThroughParagraphBlock( paragraphText ) { + await this.tabThroughBlockToolbar(); + + await this.page.keyboard.press( 'Tab' ); + await this.expectLabelToHaveFocus( 'Block: Paragraph' ); + + const activeElement = this.editor.canvas.locator( ':focus' ); + + await expect( activeElement ).toHaveText( paragraphText ); + + await this.page.keyboard.press( 'Tab' ); + await this.expectLabelToHaveFocus( 'Block' ); + + // Need to shift+tab here to end back in the block. If not, we'll be in the next region and it will only require 4 region jumps instead of 5. + await this.pageUtils.pressKeys( 'shift+Tab' ); + await this.expectLabelToHaveFocus( 'Block: Paragraph' ); + } + + async tabThroughBlockToolbar() { + await this.page.keyboard.press( 'Tab' ); await this.expectLabelToHaveFocus( 'Paragraph' ); await this.page.keyboard.press( 'ArrowRight' ); diff --git a/test/e2e/specs/editor/various/publish-panel.spec.js b/test/e2e/specs/editor/various/publish-panel.spec.js index 1fe94ff334f3b2..534fea5289c9e1 100644 --- a/test/e2e/specs/editor/various/publish-panel.spec.js +++ b/test/e2e/specs/editor/various/publish-panel.spec.js @@ -58,7 +58,7 @@ test.describe( 'Post publish panel', () => { ).toBeFocused(); } ); - test( 'should move focus to the cancel button in the panel', async ( { + test( 'should move focus to the publish button in the panel', async ( { editor, page, } ) => { @@ -74,7 +74,7 @@ test.describe( 'Post publish panel', () => { page .getByRole( 'region', { name: 'Editor publish' } ) .locator( ':focus' ) - ).toHaveText( 'Cancel' ); + ).toHaveText( 'Publish' ); } ); test( 'should focus on the post list after publishing', async ( { diff --git a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js index cfaf4e0be9188f..a8e49f7a6b84dd 100644 --- a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js +++ b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js @@ -50,6 +50,32 @@ test.describe( 'Focus toolbar shortcut (alt + F10)', () => { await expect( toolbarUtils.documentToolbarTooltip ).toBeHidden(); } ); + test( 'Focuses correct toolbar in default view options in select mode', async ( { + editor, + page, + toolbarUtils, + } ) => { + // Test: Focus the document toolbar from title + await toolbarUtils.useSelectMode(); + await toolbarUtils.moveToToolbarShortcut(); + await expect( toolbarUtils.documentToolbarButton ).toBeFocused(); + + // Test: Focus the top level toolbar from empty block + await editor.insertBlock( { name: 'core/paragraph' } ); + await toolbarUtils.useSelectMode(); + await toolbarUtils.moveToToolbarShortcut(); + await expect( toolbarUtils.documentToolbarButton ).toBeFocused(); + + // Test: Focus the top level toolbar from paragraph block + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( + 'Focus top level toolbar from paragraph block in select mode.' + ); + await toolbarUtils.useSelectMode(); + await toolbarUtils.moveToToolbarShortcut(); + await expect( toolbarUtils.documentToolbarButton ).toBeFocused(); + } ); + test.describe( 'In Top Toolbar option:', () => { test.beforeEach( async ( { editor } ) => { // Ensure the fixed toolbar option is on diff --git a/test/e2e/specs/editor/various/writing-flow.spec.js b/test/e2e/specs/editor/various/writing-flow.spec.js index 4077d6dcc58200..bd1552ad4cb66a 100644 --- a/test/e2e/specs/editor/various/writing-flow.spec.js +++ b/test/e2e/specs/editor/various/writing-flow.spec.js @@ -106,6 +106,48 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { ] ); } ); + test( 'Should navigate between inner and root blocks in navigation mode', async ( { + page, + writingFlowUtils, + } ) => { + await writingFlowUtils.addDemoContent(); + + // Switch to navigation mode. + await page.keyboard.press( 'Escape' ); + // Arrow up to Columns block. + await page.keyboard.press( 'ArrowUp' ); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/columns' ); + // Arrow right into Column block. + await page.keyboard.press( 'ArrowRight' ); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/column' ); + // Arrow down to reach second Column block. + await page.keyboard.press( 'ArrowDown' ); + // Arrow right again into Paragraph block. + await page.keyboard.press( 'ArrowRight' ); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/paragraph' ); + // Arrow left back to Column block. + await page.keyboard.press( 'ArrowLeft' ); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/column' ); + // Arrow left back to Columns block. + await page.keyboard.press( 'ArrowLeft' ); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/columns' ); + // Arrow up to first paragraph. + await page.keyboard.press( 'ArrowUp' ); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/paragraph' ); + } ); + test( 'should navigate around inline boundaries', async ( { editor, page, @@ -916,6 +958,32 @@ test.describe( 'Writing Flow (@firefox, @webkit)', () => { ` ); } ); + test( 'escape should set select mode and then focus the canvas', async ( { + page, + writingFlowUtils, + } ) => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'Random Paragraph' ); + + // First escape enters navigation mode. + await page.keyboard.press( 'Escape' ); + const navigationButton = page.getByLabel( + 'Paragraph Block. Row 1. Random Paragraph' + ); + await expect( navigationButton ).toBeVisible(); + await expect + .poll( writingFlowUtils.getActiveBlockName ) + .toBe( 'core/paragraph' ); + + // Second escape should send focus to the canvas + await page.keyboard.press( 'Escape' ); + // The navigation button should be hidden. + await expect( navigationButton ).toBeHidden(); + await expect( + page.getByRole( 'region', { name: 'Editor content' } ) + ).toBeFocused(); + } ); + // Checks for regressions of https://github.com/WordPress/gutenberg/issues/40091. test( 'does not deselect the block when selecting text outside the editor canvas', async ( { editor, @@ -1154,11 +1222,11 @@ class WritingFlowUtils { 'role=listbox[name="Blocks"i] >> role=option[name="Paragraph"i]' ); await this.page.keyboard.type( '2nd col' ); // If this text is too long, it may wrap to a new line and cause test failure. That's why we're using "2nd" instead of "Second" here. - await this.editor.showBlockToolbar(); - await this.page.keyboard.press( 'Shift+Tab' ); // Move to toolbar to select parent - await this.page.keyboard.press( 'Enter' ); // Selects the column block. - await this.page.keyboard.press( 'Shift+Tab' ); // Move to toolbar to select parent - await this.page.keyboard.press( 'Enter' ); // Selects the columns block. + + await this.page.keyboard.press( 'Escape' ); // Enter navigation mode. + await this.page.keyboard.press( 'ArrowLeft' ); // Move to the column block. + await this.page.keyboard.press( 'ArrowLeft' ); // Move to the columns block. + await this.page.keyboard.press( 'Enter' ); // Enter edit mode with the columns block selected. await this.page.keyboard.press( 'Enter' ); // Creates a paragraph after the columns block. await this.page.keyboard.type( 'Second paragraph' ); } diff --git a/test/e2e/specs/interactivity/get-sever-context.spec.ts b/test/e2e/specs/interactivity/get-sever-context.spec.ts deleted file mode 100644 index d7bc4075f97604..00000000000000 --- a/test/e2e/specs/interactivity/get-sever-context.spec.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Internal dependencies - */ -import { test, expect } from './fixtures'; - -test.describe( 'getServerContext()', () => { - test.beforeAll( async ( { interactivityUtils: utils } ) => { - const parent = { - prop: 'parent', - nested: { - prop: 'parent', - }, - inherited: { - prop: 'parent', - }, - }; - - const parentModified = { - prop: 'parentModified', - nested: { - prop: 'parentModified', - }, - inherited: { - prop: 'parentModified', - }, - }; - - const parentNewProps = { - prop: 'parent', - newProp: 'parent', - nested: { - prop: 'parent', - newProp: 'parent', - }, - inherited: { - prop: 'parent', - newProp: 'parent', - }, - }; - - const child = { - prop: 'child', - nested: { - prop: 'child', - }, - }; - - const childModified = { - prop: 'childModified', - nested: { - prop: 'childModified', - }, - }; - - const childNewProps = { - prop: 'child', - newProp: 'child', - nested: { - prop: 'child', - newProp: 'child', - }, - }; - - await utils.activatePlugins(); - const link1 = await utils.addPostWithBlock( 'test/get-server-context', { - alias: 'getServerContext() - modified', - attributes: { - parentContext: parentModified, - childContext: childModified, - }, - } ); - const link2 = await utils.addPostWithBlock( 'test/get-server-context', { - alias: 'getServerContext() - new props', - attributes: { - parentContext: parentNewProps, - childContext: childNewProps, - }, - } ); - await utils.addPostWithBlock( 'test/get-server-context', { - alias: 'getServerContext() - main', - attributes: { - links: { modified: link1, newProps: link2 }, - parentContext: parent, - childContext: child, - }, - } ); - } ); - - test.beforeEach( async ( { interactivityUtils: utils, page } ) => { - await page.goto( utils.getLink( 'getServerContext() - main' ) ); - } ); - - test.afterAll( async ( { interactivityUtils: utils } ) => { - await utils.deactivatePlugins(); - await utils.deleteAllPosts(); - } ); - - test( 'should update modified props on navigation', async ( { page } ) => { - const prop = page.getByTestId( 'prop' ); - const nestedProp = page.getByTestId( 'nested.prop' ); - const inheritedProp = page.getByTestId( 'inherited.prop' ); - - await expect( prop ).toHaveText( 'child' ); - await expect( nestedProp ).toHaveText( 'child' ); - await expect( inheritedProp ).toHaveText( 'parent' ); - - await page.getByTestId( 'modified' ).click(); - - await expect( prop ).toHaveText( 'childModified' ); - await expect( nestedProp ).toHaveText( 'childModified' ); - await expect( inheritedProp ).toHaveText( 'parentModified' ); - - await page.goBack(); - - await expect( prop ).toHaveText( 'child' ); - await expect( nestedProp ).toHaveText( 'child' ); - await expect( inheritedProp ).toHaveText( 'parent' ); - } ); - - test( 'should add new props on navigation', async ( { page } ) => { - const newProp = page.getByTestId( 'newProp' ); - const nestedNewProp = page.getByTestId( 'nested.newProp' ); - const inheritedNewProp = page.getByTestId( 'inherited.newProp' ); - - await expect( newProp ).toBeEmpty(); - await expect( nestedNewProp ).toBeEmpty(); - await expect( inheritedNewProp ).toBeEmpty(); - - await page.getByTestId( 'newProps' ).click(); - - await expect( newProp ).toHaveText( 'child' ); - await expect( nestedNewProp ).toHaveText( 'child' ); - await expect( inheritedNewProp ).toHaveText( 'parent' ); - } ); - - test( 'should keep new props on navigation', async ( { page } ) => { - const newProp = page.getByTestId( 'newProp' ); - const nestedNewProp = page.getByTestId( 'nested.newProp' ); - const inheritedNewProp = page.getByTestId( 'inherited.newProp' ); - - await page.getByTestId( 'newProps' ).click(); - - await expect( newProp ).toHaveText( 'child' ); - await expect( nestedNewProp ).toHaveText( 'child' ); - await expect( inheritedNewProp ).toHaveText( 'parent' ); - - await page.goBack(); - - await expect( newProp ).toHaveText( 'child' ); - await expect( nestedNewProp ).toHaveText( 'child' ); - await expect( inheritedNewProp ).toHaveText( 'parent' ); - } ); - - test( 'should prevent any manual modifications', async ( { page } ) => { - const prop = page.getByTestId( 'prop' ); - const button = page.getByTestId( 'tryToModifyServerContext' ); - - await expect( prop ).toHaveText( 'child' ); - await expect( button ).toHaveText( 'modify' ); - - await button.click(); - - await expect( prop ).toHaveText( 'child' ); - await expect( button ).toHaveText( 'not modified ✅' ); - } ); -} ); diff --git a/test/e2e/specs/interactivity/get-sever-state.spec.ts b/test/e2e/specs/interactivity/get-sever-state.spec.ts deleted file mode 100644 index 16406c1d824463..00000000000000 --- a/test/e2e/specs/interactivity/get-sever-state.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Internal dependencies - */ -import { test, expect } from './fixtures'; - -test.describe( 'getServerState()', () => { - test.beforeAll( async ( { interactivityUtils: utils } ) => { - await utils.activatePlugins(); - const link1 = await utils.addPostWithBlock( 'test/get-server-state', { - alias: 'getServerState() - link 1', - attributes: { - state: { - prop: 'link 1', - newProp: 'link 1', - nested: { - prop: 'link 1', - newProp: 'link 1', - }, - }, - }, - } ); - const link2 = await utils.addPostWithBlock( 'test/get-server-state', { - alias: 'getServerState() - link 2', - attributes: { - state: { - prop: 'link 2', - newProp: 'link 2', - nested: { - prop: 'link 2', - newProp: 'link 2', - }, - }, - }, - } ); - await utils.addPostWithBlock( 'test/get-server-state', { - alias: 'getServerState() - main', - attributes: { - title: 'Main', - links: [ link1, link2 ], - state: { - prop: 'main', - nested: { - prop: 'main', - }, - }, - }, - } ); - } ); - - test.beforeEach( async ( { interactivityUtils: utils, page } ) => { - await page.goto( utils.getLink( 'getServerState() - main' ) ); - } ); - - test.afterAll( async ( { interactivityUtils: utils } ) => { - await utils.deactivatePlugins(); - await utils.deleteAllPosts(); - } ); - - test( 'should update existing state props on navigation', async ( { - page, - } ) => { - const prop = page.getByTestId( 'prop' ); - const nestedProp = page.getByTestId( 'nested.prop' ); - - await expect( prop ).toHaveText( 'main' ); - await expect( nestedProp ).toHaveText( 'main' ); - - await page.getByTestId( 'link 1' ).click(); - - await expect( prop ).toHaveText( 'link 1' ); - await expect( nestedProp ).toHaveText( 'link 1' ); - - await page.goBack(); - await expect( prop ).toHaveText( 'main' ); - await expect( nestedProp ).toHaveText( 'main' ); - - await page.getByTestId( 'link 2' ).click(); - - await expect( prop ).toHaveText( 'link 2' ); - await expect( nestedProp ).toHaveText( 'link 2' ); - } ); - - test( 'should add new state props and keep them on navigation', async ( { - page, - } ) => { - const newProp = page.getByTestId( 'newProp' ); - const nestedNewProp = page.getByTestId( 'nested.newProp' ); - - await expect( newProp ).toBeEmpty(); - await expect( nestedNewProp ).toBeEmpty(); - - await page.getByTestId( 'link 1' ).click(); - - await expect( newProp ).toHaveText( 'link 1' ); - await expect( nestedNewProp ).toHaveText( 'link 1' ); - - await page.goBack(); - await expect( newProp ).toHaveText( 'link 1' ); - await expect( nestedNewProp ).toHaveText( 'link 1' ); - - await page.getByTestId( 'link 2' ).click(); - - await expect( newProp ).toHaveText( 'link 2' ); - await expect( nestedNewProp ).toHaveText( 'link 2' ); - } ); - - test( 'should prevent any manual modifications', async ( { page } ) => { - const prop = page.getByTestId( 'prop' ); - const button = page.getByTestId( 'tryToModifyServerState' ); - - await expect( prop ).toHaveText( 'main' ); - await expect( button ).toHaveText( 'modify' ); - - await button.click(); - - await expect( prop ).toHaveText( 'main' ); - await expect( button ).toHaveText( 'not modified ✅' ); - } ); -} ); diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 19318081aa171b..5b049cda252a8b 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -28,12 +28,10 @@ test.describe( 'Site editor command palette', () => { await page.keyboard.type( 'new page' ); await page.getByRole( 'option', { name: 'Add new page' } ).click(); await expect( page ).toHaveURL( - /\/wp-admin\/site-editor.php\?postId=(\d+)&postType=page&canvas=edit/ + '/wp-admin/post-new.php?post_type=page' ); await expect( - editor.canvas - .getByLabel( 'Block: Title' ) - .locator( '[data-rich-text-placeholder="No title"]' ) + editor.canvas.getByRole( 'textbox', { name: 'Add title' } ) ).toBeVisible(); } ); diff --git a/test/e2e/specs/site-editor/navigation.spec.js b/test/e2e/specs/site-editor/navigation.spec.js index 1b92ef2e850e67..4db860b703892c 100644 --- a/test/e2e/specs/site-editor/navigation.spec.js +++ b/test/e2e/specs/site-editor/navigation.spec.js @@ -83,6 +83,19 @@ test.describe( 'Site editor navigation', () => { // The button role should have been removed from the iframe. await expect( editorCanvasButton ).toBeHidden(); + // Test to make sure a Tab keypress works as expected. + // As of this writing, we are in select mode and a tab + // keypress will reveal the header template select mode + // button. This test is not documenting that we _want_ + // that action, but checking that we are within the site + // editor and keypresses work as intened. + await pageUtils.pressKeys( 'Tab' ); + await expect( + page.getByRole( 'button', { + name: 'Template Part Block. Row 1. header', + } ) + ).toBeFocused(); + // Test: We can go back to the main navigation from the editor frame // Move to the document toolbar await pageUtils.pressKeys( 'alt+F10' ); diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js index 38e9d3ee2c58ab..53b4da7be2d61f 100644 --- a/test/e2e/specs/widgets/customizing-widgets.spec.js +++ b/test/e2e/specs/widgets/customizing-widgets.spec.js @@ -465,6 +465,16 @@ test.describe( 'Widgets Customizer', () => { await expect( paragraphBlock ).toBeVisible(); await paragraphBlock.focus(); + + // Expect pressing the Escape key to enter navigation mode, + // but not close the editor. + await page.keyboard.press( 'Escape' ); + await expect( + page.locator( + 'css=.block-editor-block-list__layout.is-navigate-mode' + ) + ).toBeVisible(); + await expect( paragraphBlock ).toBeVisible(); } ); test( 'should move (inner) blocks to another sidebar', async ( { diff --git a/tools/webpack/script-modules.js b/tools/webpack/script-modules.js index 021f11f5f5ed95..18287c96d83c8a 100644 --- a/tools/webpack/script-modules.js +++ b/tools/webpack/script-modules.js @@ -89,11 +89,11 @@ module.exports = { }, output: { devtoolNamespace: 'wp', - filename: '[name].min.js', + filename: './build-module/[name].min.js', library: { type: 'module', }, - path: join( __dirname, '..', '..', 'build-module' ), + path: join( __dirname, '..', '..' ), environment: { module: true }, module: true, chunkFormat: 'module', @@ -102,13 +102,7 @@ module.exports = { resolve: { extensions: [ '.js', '.ts', '.tsx' ], }, - plugins: [ - ...plugins, - new DependencyExtractionWebpackPlugin( { - combineAssets: true, - combinedOutputFile: `./assets.php`, - } ), - ], + plugins: [ ...plugins, new DependencyExtractionWebpackPlugin() ], watchOptions: { ignored: [ '**/node_modules' ], aggregateTimeout: 500, diff --git a/tsconfig.json b/tsconfig.json index 8821ef4404e3b5..3ab54f66019bca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,7 +32,6 @@ { "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" },