diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a54091e4745989..f35a0123f9c987 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -125,7 +125,7 @@ /packages/block-editor/src/components/rich-text @ellatrix @fluiddot @dcalhoun # Project Management -/.github +/.github @desrosj /packages/project-management-automation /packages/report-flaky-tests @kevin940726 diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml index 6a13d4d014fc69..47701d35025d83 100644 --- a/.github/workflows/php-changes-detection.yml +++ b/.github/workflows/php-changes-detection.yml @@ -17,7 +17,7 @@ jobs: - name: Get changed PHP files id: changed-files-php - uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959 # v42.0.2 + uses: tj-actions/changed-files@3f54ebb830831fc121d3263c1857cfbdc310cdb9 # v42.0.4 with: files: | *.{php} diff --git a/bin/api-docs/gen-block-lib-list.js b/bin/api-docs/gen-block-lib-list.js index 90b6b808941896..5595265e686b3f 100644 --- a/bin/api-docs/gen-block-lib-list.js +++ b/bin/api-docs/gen-block-lib-list.js @@ -148,29 +148,52 @@ function getSourceFromFile( filename ) { */ function readBlockJSON( filename ) { const blockjson = require( filename ); - + const { + name, + category, + supports, + attributes, + parent, + ancestor, + __experimental, + } = blockjson; const sourcefile = getSourceFromFile( filename ); - const supportsList = - blockjson.supports !== undefined - ? processObjWithInnerKeys( augmentSupports( blockjson.supports ) ) - : []; - const attributes = getTruthyKeys( blockjson.attributes ); - const parent = blockjson.parent - ? '\n' + `- **Parent:** ${ blockjson.parent.join( ', ' ) }` - : ''; - const experimental = blockjson.__experimental - ? '\n' + `- **Experimental:** ${ blockjson.__experimental }` - : ''; + const blockInfoList = [ `- **Name:** ${ name }` ]; + + if ( __experimental ) { + blockInfoList.push( `- **Experimental:** ${ __experimental }` ); + } + if ( category?.length > 0 ) { + blockInfoList.push( `- **Category:** ${ category }` ); + } + if ( parent?.length > 0 ) { + blockInfoList.push( `- **Parent:** ${ parent.join( ', ' ) }` ); + } + if ( ancestor?.length > 0 ) { + blockInfoList.push( `- **Ancestor:** ${ ancestor.join( ', ' ) }` ); + } + if ( supports ) { + blockInfoList.push( + `- **Supports:** ${ processObjWithInnerKeys( + augmentSupports( supports ) + ) + .sort() + .join( ', ' ) }` + ); + } + const truthyAttributes = getTruthyKeys( attributes ); + if ( truthyAttributes.length ) { + blockInfoList.push( + `- **Attributes:** ${ truthyAttributes.sort().join( ', ' ) }` + ); + } return ` ## ${ blockjson.title } ${ blockjson.description } ([Source](${ sourcefile })) -- **Name:** ${ blockjson.name }${ experimental } -- **Category:** ${ blockjson.category }${ parent } -- **Supports:** ${ supportsList.sort().join( ', ' ) } -- **Attributes:** ${ attributes.sort().join( ', ' ) } +${ blockInfoList.join( '\n' ) } `; } diff --git a/bin/cherry-pick.mjs b/bin/cherry-pick.mjs index dc71eed751cfdc..f0ca37d10f30cb 100644 --- a/bin/cherry-pick.mjs +++ b/bin/cherry-pick.mjs @@ -7,6 +7,7 @@ import readline from 'readline'; import { spawnSync } from 'node:child_process'; const LABEL = process.argv[ 2 ] || 'Backport to WP Beta/RC'; +const BACKPORT_COMPLETED_LABEL = 'Backported to WP Core'; const BRANCH = getCurrentBranch(); const GITHUB_CLI_AVAILABLE = spawnSync( 'gh', [ 'auth', 'status' ] ) ?.stdout?.toString() @@ -334,6 +335,11 @@ function reportSummaryNextSteps( successes, failures ) { nextSteps.push( 'Push this branch' ); nextSteps.push( 'Go to each of the cherry-picked Pull Requests' ); nextSteps.push( `Remove the ${ LABEL } label` ); + + if ( LABEL === 'Backport to WP Beta/RC' ) { + nextSteps.push( `Add the "${ BACKPORT_COMPLETED_LABEL }" label` ); + } + nextSteps.push( 'Request a backport to wordpress-develop if required' ); nextSteps.push( 'Comment, say that PR just got cherry-picked' ); } @@ -363,6 +369,17 @@ function GHcommentAndRemoveLabel( pr ) { try { cli( 'gh', [ 'pr', 'comment', number, '--body', comment ] ); cli( 'gh', [ 'pr', 'edit', number, '--remove-label', LABEL ] ); + + if ( LABEL === 'Backport to WP Beta/RC' ) { + cli( 'gh', [ + 'pr', + 'edit', + number, + '--add-label', + BACKPORT_COMPLETED_LABEL, + ] ); + } + console.log( `✅ ${ number }: ${ comment }` ); } catch ( e ) { console.log( `❌ ${ number }. ${ comment } ` ); diff --git a/changelog.txt b/changelog.txt index 384cabe0591199..2d400eb8d3a13a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,265 @@ == Changelog == += 17.8.0-rc.3 = + +## Changelog + +### Features + +- Patterns: add bulk export patterns action. ([58897](https://github.com/WordPress/gutenberg/pull/58897)) +- Template editor/inspector: show and select related patterns. ([55091](https://github.com/WordPress/gutenberg/pull/55091)) + +#### Layout +- Add toggle for grid types and stabilise Grid block variation. ([59051](https://github.com/WordPress/gutenberg/pull/59051) and [59116](https://github.com/WordPress/gutenberg/pull/59116)) +- Add support for column and row span in grid children. ([58539](https://github.com/WordPress/gutenberg/pull/58539)) + + +### Enhancements + +- Patterns Page: Make category action button compact. ([59203](https://github.com/WordPress/gutenberg/pull/59203)) +- Block Editor: Use hooks instead of HoC in 'SkipToSelectedBlock'. ([59202](https://github.com/WordPress/gutenberg/pull/59202)) +- Font Library: Adds the ability to use generic() in font family names. ([59103](https://github.com/WordPress/gutenberg/pull/59103) and [59037](https://github.com/WordPress/gutenberg/pull/59037)) +- REST API Global Styles Revisions Controller: Return single revision only when it matches the parent id. ([59049](https://github.com/WordPress/gutenberg/pull/59049)) +- CSS & Styling: Tweak link focus outline styles in HTML anchor and custom CSS. ([59048](https://github.com/WordPress/gutenberg/pull/59048)) +- Data Views: Make 'All pages' view label consistent with template and patterns. ([59009](https://github.com/WordPress/gutenberg/pull/59009)) +- Script Modules API: Script Modules add deregister option. ([58830](https://github.com/WordPress/gutenberg/pull/58830)) +- Block Hooks: Add help text to Plugins panel. ([59371](https://github.com/WordPress/gutenberg/pull/59371)) + +#### Custom Fields +- Block Bindings: Lock editing of blocks by default. ([58787](https://github.com/WordPress/gutenberg/pull/58787)) +- Style engine: Rename at_rule to rules_groups and update test/docs. ([58922](https://github.com/WordPress/gutenberg/pull/58922)) + +#### Block Library +- Gallery: Set the 'defaultBlock' setting for inner blocks. ([59168](https://github.com/WordPress/gutenberg/pull/59168)) +- Remove the navigation edit button because it leads to a useless screen. ([59211](https://github.com/WordPress/gutenberg/pull/59211)) +- Set the 'defaultBlock' setting for Columns & List blocks. ([59196](https://github.com/WordPress/gutenberg/pull/59196)) +- Update: Increase footnotes meta priority and separate footnotes meta registration. ([58882](https://github.com/WordPress/gutenberg/pull/58882)) + +#### Site Editor +- Editor: Hide template part and post content blocks in some site editor contexts. ([58928](https://github.com/WordPress/gutenberg/pull/58928)) +- Tweak save hub button. ([58917](https://github.com/WordPress/gutenberg/pull/58917) and [59200](https://github.com/WordPress/gutenberg/pull/59200)) + +#### Components +- CustomSelect: Adapt component for legacy props. ([57902](https://github.com/WordPress/gutenberg/pull/57902)) +- Use `Element.scrollIntoView()` instead of `dom-scroll-into-view`. ([59085](https://github.com/WordPress/gutenberg/pull/59085)) + +#### Global Styles +- Global style changes: Refactor output for a more flexible UI and grouping. ([59055](https://github.com/WordPress/gutenberg/pull/59055)) +- Style theme variations: Add property extraction and merge utils. ([58803](https://github.com/WordPress/gutenberg/pull/58803)) + + +### Bug Fixes + +- Distraction Free Mode: fix ui toggling bugs. ([59061](https://github.com/WordPress/gutenberg/pull/59061)) +- Layout: Refactor responsive logic for grid column spans. ([59057](https://github.com/WordPress/gutenberg/pull/59057)) +- Interactivity API: Only add proxies to plain objects inside the store. ([59039](https://github.com/WordPress/gutenberg/pull/59039)) + +#### Block Hooks +- Fix in Navigation block. ([59021](https://github.com/WordPress/gutenberg/pull/59021)) +- Take controlled blocks into account for toggle state. ([59367](https://github.com/WordPress/gutenberg/pull/59367)) + +#### List View +- Editor: Do not open list view by default on mobile. ([59016](https://github.com/WordPress/gutenberg/pull/59016)) +- Create Block: Add missing `viewScriptModule` field. ([59140](https://github.com/WordPress/gutenberg/pull/59140)) +- Ignore the 'twentytwentyfour' test theme dir created by wp-env. ([59072](https://github.com/WordPress/gutenberg/pull/59072)) +- useEntityBlockEditor: Update 'content' type check. ([59058](https://github.com/WordPress/gutenberg/pull/59058)) + +#### Block Library +- Author, Author Bio, Author Name: Add a fallback for Author Archive Template. ([55451](https://github.com/WordPress/gutenberg/pull/55451)) +- Fix Spacer orientation when inside a block with default flex layout. ([58921](https://github.com/WordPress/gutenberg/pull/58921)) +- Fix WP 6.4/6.3 compat for navigation link variations. ([59126](https://github.com/WordPress/gutenberg/pull/59126)) +- Interactivity API: Fix server side rendering for Search block. ([59029](https://github.com/WordPress/gutenberg/pull/59029)) +- Navigation: Avoid using embedded record from fallback API. ([59076](https://github.com/WordPress/gutenberg/pull/59076)) +- Pagination Numbers: Add `data-wp-key` to pagination numbers if enhanced pagination is enabled. ([58189](https://github.com/WordPress/gutenberg/pull/58189)) +- Revert "Navigation: Refactor mobile overlay breakpoints to JS (#57520)". ([59149](https://github.com/WordPress/gutenberg/pull/59149)) +- Spacer block: Fix `null` label in tooltip when horizontal layout. ([58909](https://github.com/WordPress/gutenberg/pull/58909)) + +#### Data Views +- DataViews: Add loading/no results message in grid view. ([59002](https://github.com/WordPress/gutenberg/pull/59002)) +- DataViews: Correctly display featured image that don't have image sizes. ([59111](https://github.com/WordPress/gutenberg/pull/59111)) +- DataViews: Fix pages list back path. ([59201](https://github.com/WordPress/gutenberg/pull/59201)) +- DataViews: Fix patterns, templates and template parts pagination `z-index`. ([58965](https://github.com/WordPress/gutenberg/pull/58965)) +- DataViews: Fix storybook. ([58842](https://github.com/WordPress/gutenberg/pull/58842)) +- DataViews: Remove second `reset filter` button in filter dialog. ([58960](https://github.com/WordPress/gutenberg/pull/58960)) +- Revert footer in pages list with DataViews. ([59151](https://github.com/WordPress/gutenberg/pull/59151)) + +#### Block Editor +- After Enter transform, skip other onEnter actions like splitting. ([59064](https://github.com/WordPress/gutenberg/pull/59064)) +- Close link preview if collapsed selection when creating link. ([58896](https://github.com/WordPress/gutenberg/pull/58896)) +- Editor: Limit spotlight mode to the editor. ([58817](https://github.com/WordPress/gutenberg/pull/58817)) +- Fix incorrect useAnchor positioning when switching from virtual to rich text elements. ([58900](https://github.com/WordPress/gutenberg/pull/58900)) +- Inserter: Don't select the closest block with 'disabled' editing mode. ([58971](https://github.com/WordPress/gutenberg/pull/58971)) +- Inserter: Fix title condition for media tab previews. ([58993](https://github.com/WordPress/gutenberg/pull/58993)) + +#### Site Editor +- Fix navigation on mobile web. ([59014](https://github.com/WordPress/gutenberg/pull/59014)) +- Fix: Don't render the Transform Into panel if there are no patterns. ([59217](https://github.com/WordPress/gutenberg/pull/59217)) +- Fix: Logical error in filterPatterns on template-panel/hooks.js. ([59218](https://github.com/WordPress/gutenberg/pull/59218)) +- Make command palette string transatables. ([59133](https://github.com/WordPress/gutenberg/pull/59133)) +- Remove left margin on Status help text. ([58775](https://github.com/WordPress/gutenberg/pull/58775)) + +#### Patterns +- Allow editing of image block alt and title attributes in content only mode. ([58998](https://github.com/WordPress/gutenberg/pull/58998)) +- Avoid showing block removal warning when deleting a pattern instance that has overrides. ([59044](https://github.com/WordPress/gutenberg/pull/59044)) +- Block editor: Pass patterns selector as setting. ([58661](https://github.com/WordPress/gutenberg/pull/58661)) +- Fix pattern categories on import. ([58926](https://github.com/WordPress/gutenberg/pull/58926)) +- Site editor: Fix start patterns store selector. ([58813](https://github.com/WordPress/gutenberg/pull/58813)) + +#### Global Styles +- Fix console error in block preview. ([59112](https://github.com/WordPress/gutenberg/pull/59112)) +- Revert "Use all the settings origins for a block that consumes paths with merge #55219" ([58951](https://github.com/WordPress/gutenberg/pull/58951) and [59101](https://github.com/WordPress/gutenberg/pull/59101)) +- Shadows: Don't assume that core provides default shadows. ([58973](https://github.com/WordPress/gutenberg/pull/58973)) + +#### Font Library +- Fixes installed font families not rendering in the editor or frontend. ([59019](https://github.com/WordPress/gutenberg/pull/59019)) +- Font Libary: Add missing translation functions. ([58104](https://github.com/WordPress/gutenberg/pull/58104)) +- Show error message when no fonts found to install. ([58914](https://github.com/WordPress/gutenberg/pull/58914)) +- Create post types on init hook. ([59333](https://github.com/WordPress/gutenberg/pull/59333)) + +#### Synced Patterns +- Fix missing source for binding attributes. ([59194](https://github.com/WordPress/gutenberg/pull/59194)) +- Fix resetting individual blocks to empty optional values for Pattern Overrides. ([59170](https://github.com/WordPress/gutenberg/pull/59170)) +- Fix upload button on overridden empty image block in patterns. ([59169](https://github.com/WordPress/gutenberg/pull/59169)) + +#### Design Tools +- Background image support: Fix issue with background position keyboard entry. ([59050](https://github.com/WordPress/gutenberg/pull/59050)) +- Cover block: Clear the min height field when aspect ratio is set. ([59191](https://github.com/WordPress/gutenberg/pull/59191)) +- Elements: Fix block instance element styles for links applying to buttons. ([59114](https://github.com/WordPress/gutenberg/pull/59114)) +- Cover Block: Restore overflow: Clip rule to allow border radius again. ([59388](https://github.com/WordPress/gutenberg/pull/59388)) + +#### Components +- Modal: Add `box-sizing` reset style. ([58905](https://github.com/WordPress/gutenberg/pull/58905)) +- ToolbarButton: Fix text centering for short labels. ([59117](https://github.com/WordPress/gutenberg/pull/59117)) +- Upgrade Floating UI packages, fix nested iframe positioning bug. ([58932](https://github.com/WordPress/gutenberg/pull/58932)) + +#### Post Editor +- Editor: Fix 'useHideBlocksFromInserter' hook filename. ([59150](https://github.com/WordPress/gutenberg/pull/59150)) +- Fix layout for non viewable post types. ([58962](https://github.com/WordPress/gutenberg/pull/58962)) + +#### Rich Text +- Fix link paste for internal paste. ([59063](https://github.com/WordPress/gutenberg/pull/59063)) +- Revert "Rich text: Pad multiple spaces through en/em replacement". ([58792](https://github.com/WordPress/gutenberg/pull/58792)) + +#### Custom Fields +- Block Bindings: Add block context needed for bindings in PHP. ([58554](https://github.com/WordPress/gutenberg/pull/58554)) +- Block Bindings: Fix disable bindings editing when source is undefined. ([58961](https://github.com/WordPress/gutenberg/pull/58961)) + + +### Accessibility + +- Enter editing mode via Enter or Spacebar. ([58795](https://github.com/WordPress/gutenberg/pull/58795)) +- Block Bindings > Image Block:Mark connected controls as 'readonly'. ([59059](https://github.com/WordPress/gutenberg/pull/59059)) +- Details Block: Try double enter to escape inner blocks. ([58903](https://github.com/WordPress/gutenberg/pull/58903)) +- Font Library: Replace infinite scroll by pagination. ([58794](https://github.com/WordPress/gutenberg/pull/58794)) +- Global Styles: Remove menubar role and improve complementary area header semantics. ([58740](https://github.com/WordPress/gutenberg/pull/58740)) + +#### Block Editor +- Block Mover: Unify visual separator when show button label is on. ([59158](https://github.com/WordPress/gutenberg/pull/59158)) +- Make the custom CSS validation error message accessible. ([56690](https://github.com/WordPress/gutenberg/pull/56690)) +- Restore default border and focus style on image URL input field. ([58505](https://github.com/WordPress/gutenberg/pull/58505)) + +### Performance + +- Pattern Block: Batch replacing actions. ([59075](https://github.com/WordPress/gutenberg/pull/59075)) +- Block Editor: Move StopEditingAsBlocksOnOutsideSelect to Root. ([58412](https://github.com/WordPress/gutenberg/pull/58412)) + + +### Documentation + +- Add contributing guidlines around Component versioning. ([58789](https://github.com/WordPress/gutenberg/pull/58789)) +- Clarify the performance reference commit and how to pick it. ([58927](https://github.com/WordPress/gutenberg/pull/58927)) +- DataViews: Update documentation. ([58847](https://github.com/WordPress/gutenberg/pull/58847)) +- Docs: Clarify the status of the wp-block-styles theme support, and its intent. ([58915](https://github.com/WordPress/gutenberg/pull/58915)) +- Fix move interactivity schema to supports property instead of selectors property. ([59166](https://github.com/WordPress/gutenberg/pull/59166)) +- Storybook: Show badges in sidebar. ([58518](https://github.com/WordPress/gutenberg/pull/58518)) +- Theme docs: Update appearance-tools documentation to reflect opt-in for backgroundSize and aspectRatio. ([59165](https://github.com/WordPress/gutenberg/pull/59165)) +- Update richtext.md. ([59089](https://github.com/WordPress/gutenberg/pull/59089)) + +#### Interactivity API +- Interactivity API: Fix WP version, update new store documentation. ([59107](https://github.com/WordPress/gutenberg/pull/59107)) +- Interactivity API: Update documentation guide with new `wp-interactivity` directive implementation. ([59018](https://github.com/WordPress/gutenberg/pull/59018)) +- Add interactivity property to block supports reference documentation. ([59152](https://github.com/WordPress/gutenberg/pull/59152)) + +#### Schemas +- Block JSON schema: Add `viewScriptModule` field. ([59060](https://github.com/WordPress/gutenberg/pull/59060)) +- Block JSON schema: Update `shadow` definition. ([58910](https://github.com/WordPress/gutenberg/pull/58910)) +- JSON schema: Update schema for background support. ([59127](https://github.com/WordPress/gutenberg/pull/59127)) + +### Code Quality + +- Create Block: Remove deprecated viewModule field. ([59198](https://github.com/WordPress/gutenberg/pull/59198)) +- Editor: Remove the 'all' rendering mode. ([58935](https://github.com/WordPress/gutenberg/pull/58935)) +- Editor: Unify the editor commands between post and site editors. ([59005](https://github.com/WordPress/gutenberg/pull/59005)) +- Relocate 'ErrorBoundary' component unit test folders. ([59031](https://github.com/WordPress/gutenberg/pull/59031)) +- Remove obsolete wp-env configuration from package.json (#58877). ([58899](https://github.com/WordPress/gutenberg/pull/58899)) +- Design Tools > Elements: Make editor selector match theme.json and frontend. ([59167](https://github.com/WordPress/gutenberg/pull/59167)) +- Global Styles: Update sprintf calls using `_n`. ([59160](https://github.com/WordPress/gutenberg/pull/59160)) +- Block API: Revert "Block Hooks: Set ignoredHookedBlocks metada attr upon insertion". ([58969](https://github.com/WordPress/gutenberg/pull/58969)) +- Editor > Rich Text: Remove inline toolbar preference. ([58945](https://github.com/WordPress/gutenberg/pull/58945)) +- Style Variations: Remove preferred style variations legacy support. ([58930](https://github.com/WordPress/gutenberg/pull/58930)) +- REST API > Template Revisions: Move from experimental to compat/6.4. ([58920](https://github.com/WordPress/gutenberg/pull/58920)) + +#### Block Editor +- Block-editor: Auto-register block commands. ([59079](https://github.com/WordPress/gutenberg/pull/59079)) +- BlockSettingsMenu: Combine 'block-editor' store selectors. ([59153](https://github.com/WordPress/gutenberg/pull/59153)) +- Clean up link control CSS. ([58934](https://github.com/WordPress/gutenberg/pull/58934)) +- HeadingLevelDropdown: Remove unnecessary isPressed prop. ([56636](https://github.com/WordPress/gutenberg/pull/56636)) +- Move 'ParentSelectorMenuItem' into a separate file. ([59146](https://github.com/WordPress/gutenberg/pull/59146)) +- Remove 'BlockSettingsMenu' styles. ([59147](https://github.com/WordPress/gutenberg/pull/59147)) + +#### Components +- Add Higher Order Function to ignore Input Method Editor (IME) keydowns. ([59081](https://github.com/WordPress/gutenberg/pull/59081)) +- Add lint rules for theme color CSS var usage. ([59022](https://github.com/WordPress/gutenberg/pull/59022)) +- ColorPicker: Style without accessing InputControl internals. ([59069](https://github.com/WordPress/gutenberg/pull/59069)) +- CustomSelectControl (v1 & v2): Fix errors in unit test setup. ([59038](https://github.com/WordPress/gutenberg/pull/59038)) +- CustomSelectControl: Hard deprecate constrained width. ([58974](https://github.com/WordPress/gutenberg/pull/58974)) + +#### Post Editor +- DocumentBar: Fix browser warning error. ([59193](https://github.com/WordPress/gutenberg/pull/59193)) +- DocumentBar: Simplify component, use framer for animation. ([58656](https://github.com/WordPress/gutenberg/pull/58656)) +- Editor: Remove unused selector value from 'PostTitle'. ([59204](https://github.com/WordPress/gutenberg/pull/59204)) +- Editor: Unify Mode Switcher component between post and site editor. ([59100](https://github.com/WordPress/gutenberg/pull/59100)) + +#### Interactivity API +- Refactor to use string instead of an object on `wp-data-interactive`. ([59034](https://github.com/WordPress/gutenberg/pull/59034)) +- Remove `data-wp-interactive` object for core/router. ([59030](https://github.com/WordPress/gutenberg/pull/59030)) +- Use `data_wp_context` helper in core blocks and remove `data-wp-interactive` object. ([58943](https://github.com/WordPress/gutenberg/pull/58943)) + +#### Site Editor +- Add stylelint rule to prevent theme CSS vars outside of wp-components. ([59020](https://github.com/WordPress/gutenberg/pull/59020)) +- Don't memoize the canvas container title. ([59000](https://github.com/WordPress/gutenberg/pull/59000)) +- Remove old patterns list code and styles. ([58966](https://github.com/WordPress/gutenberg/pull/58966)) + + +### Tools + +- Remove reference to CODE_OF_CONDUCT.md in documentation. ([59206](https://github.com/WordPress/gutenberg/pull/59206)) +- Remove repository specific Code of Conduct. ([59027](https://github.com/WordPress/gutenberg/pull/59027)) +- env: Fix mariadb version to LTS. ([59237](https://github.com/WordPress/gutenberg/pull/59237)) + +#### Testing +- Components: Add sleep() before all Tab() to fix flaky tests. ([59012](https://github.com/WordPress/gutenberg/pull/59012)) +- Components: Try fixing some flaky `Composite` and `Tabs` tests. ([58968](https://github.com/WordPress/gutenberg/pull/58968)) +- Migrate `change-detection` to Playwright. ([58767](https://github.com/WordPress/gutenberg/pull/58767)) +- Tabs: Fix flaky unit tests. ([58629](https://github.com/WordPress/gutenberg/pull/58629)) +- Update test environment default theme versions to latest. ([58955](https://github.com/WordPress/gutenberg/pull/58955)) +- Performance tests: Make site editor performance test backwards compatible. ([59266](https://github.com/WordPress/gutenberg/pull/59266)) +- Performance tests: Update selectors in site editor pattern loading tests. ([59259](https://github.com/WordPress/gutenberg/pull/59259)) + +#### Build Tooling +- Add test:e2e:playwright:debug command to debug Playwright tests. ([58808](https://github.com/WordPress/gutenberg/pull/58808)) +- Updating Storybook to v7.6.15 (latest). ([59074](https://github.com/WordPress/gutenberg/pull/59074)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @ajlende @alexstine @andrewhayward @andrewserong @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @creativecoder @DAreRodz @derekblank @desrosj @draganescu @ellatrix @fabiankaegy @gaambo @glendaviesnz @jameskoster @janboddez @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanfra @kevin940726 @Mamaduka @MarieComet @matiasbenedetto @mirka @noisysocks @ntsekouras @oandregal @ockham @pbking @ramonjd @SantosGuillamot @scruffian @shreyash3087 @t-hamano @talldan @tellthemachines @tyxla @youknowriad + + = 17.8.0-rc.2 = ## Changelog diff --git a/docs/contributors/code/react-native/README.md b/docs/contributors/code/react-native/README.md index 10df7268ddddb6..dd6afc2e8bd561 100644 --- a/docs/contributors/code/react-native/README.md +++ b/docs/contributors/code/react-native/README.md @@ -1,6 +1,6 @@ # React Native mobile editor -The Gutenberg repository includes the source for the [React Native](https://facebook.github.io/react-native/) based editor for mobile. +The Gutenberg repository includes the source for the [React Native](https://reactnative.dev/) based editor for mobile. ## Mind the mobile diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 38ad3e2e11bd13..9376badd212514 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -61,7 +61,6 @@ Prompt visitors to take action with a group of button-style links. ([Source](htt - **Name:** core/buttons - **Category:** design - **Supports:** align (full, wide), anchor, interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), spacing (blockGap, margin), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** ## Calendar @@ -116,6 +115,7 @@ This block is deprecated. Please use the Avatar block instead. ([Source](https:/ - **Name:** core/comment-author-avatar - **Experimental:** fse - **Category:** theme +- **Ancestor:** core/comment-template - **Supports:** color (background, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), ~~html~~, ~~inserter~~ - **Attributes:** height, width @@ -125,6 +125,7 @@ Displays the name of the author of the comment. ([Source](https://github.com/Wor - **Name:** core/comment-author-name - **Category:** theme +- **Ancestor:** core/comment-template - **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** isLink, linkTarget, textAlign @@ -134,6 +135,7 @@ Displays the contents of a comment. ([Source](https://github.com/WordPress/guten - **Name:** core/comment-content - **Category:** theme +- **Ancestor:** core/comment-template - **Supports:** color (background, gradients, link, text), spacing (padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign @@ -143,6 +145,7 @@ Displays the date on which the comment was posted. ([Source](https://github.com/ - **Name:** core/comment-date - **Category:** theme +- **Ancestor:** core/comment-template - **Supports:** color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** format, isLink @@ -152,6 +155,7 @@ Displays a link to edit the comment in the WordPress Dashboard. This link is onl - **Name:** core/comment-edit-link - **Category:** theme +- **Ancestor:** core/comment-template - **Supports:** color (background, gradients, link, ~~text~~), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** linkTarget, textAlign @@ -161,6 +165,7 @@ Displays a link to reply to a comment. ([Source](https://github.com/WordPress/gu - **Name:** core/comment-reply-link - **Category:** theme +- **Ancestor:** core/comment-template - **Supports:** color (background, gradients, link, ~~text~~), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign @@ -172,7 +177,6 @@ Contains the block elements used to display a comment, like the title, date, aut - **Category:** design - **Parent:** core/comments - **Supports:** align, interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** ## Comments @@ -211,7 +215,6 @@ Displays a list of page numbers for comments pagination. ([Source](https://githu - **Category:** theme - **Parent:** core/comments-pagination - **Supports:** color (background, gradients, ~~text~~), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** ## Comments Previous Page @@ -229,6 +232,7 @@ Displays a title with the number of comments. ([Source](https://github.com/WordP - **Name:** core/comments-title - **Category:** theme +- **Ancestor:** core/comments - **Supports:** align, color (background, gradients, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~anchor~~, ~~html~~ - **Attributes:** level, showCommentsCount, showPostTitle, textAlign @@ -275,7 +279,6 @@ Display footnotes added to the page. ([Source](https://github.com/WordPress/gute - **Name:** core/footnotes - **Category:** text - **Supports:** color (background, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~, ~~inserter~~, ~~multiple~~, ~~reusable~~ -- **Attributes:** ## Form @@ -294,6 +297,7 @@ The basic building block for forms. ([Source](https://github.com/WordPress/guten - **Name:** core/form-input - **Experimental:** true - **Category:** common +- **Ancestor:** core/form - **Supports:** anchor, spacing (margin), ~~reusable~~ - **Attributes:** inlineLabel, label, name, placeholder, required, type, value, visibilityPermissions @@ -304,7 +308,7 @@ Provide a notification message after the form has been submitted. ([Source](http - **Name:** core/form-submission-notification - **Experimental:** true - **Category:** common -- **Supports:** +- **Ancestor:** core/form - **Attributes:** type ## Form Submit Button @@ -314,8 +318,7 @@ A submission button for forms. ([Source](https://github.com/WordPress/gutenberg/ - **Name:** core/form-submit-button - **Experimental:** true - **Category:** common -- **Supports:** -- **Attributes:** +- **Ancestor:** core/form ## Classic @@ -491,7 +494,6 @@ Separate your content into a multi-page experience. ([Source](https://github.com - **Category:** design - **Parent:** core/post-content - **Supports:** interactivity (clientNavigation), ~~className~~, ~~customClassName~~, ~~html~~ -- **Attributes:** ## Page List @@ -603,7 +605,6 @@ Displays the contents of a post or page. ([Source](https://github.com/WordPress/ - **Name:** core/post-content - **Category:** theme - **Supports:** align (full, wide), color (background, gradients, link, text), dimensions (minHeight), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~ -- **Attributes:** ## Date @@ -649,7 +650,6 @@ Contains the block elements used to render a post, like the title, date, feature - **Category:** theme - **Parent:** core/query - **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), layout, spacing (blockGap), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** ## Post Terms @@ -714,7 +714,6 @@ Contains the block elements used to render content when no query results are fou - **Category:** theme - **Parent:** core/query - **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ -- **Attributes:** ## Pagination @@ -722,6 +721,7 @@ Displays a paginated navigation to next/previous set of posts, when applicable. - **Name:** core/query-pagination - **Category:** theme +- **Ancestor:** core/query - **Supports:** align, color (background, gradients, link, text), interactivity (clientNavigation), layout (default, ~~allowInheriting~~, ~~allowSwitching~~), typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ - **Attributes:** paginationArrow, showLabel diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 9e9c1ef140c76a..486fcddfe04ac6 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -515,7 +515,7 @@ _Parameters_ _Returns_ -- `?WPDirectInsertBlock`: The block type to be directly inserted. +- `WPDirectInsertBlock|undefined`: The block type to be directly inserted. _Type Definition_ diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 189fe6d2f01a06..0b800757b4ecd0 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -72,7 +72,7 @@ Settings related to shadows. | Property | Type | Default | Props | | --- | --- | --- |--- | -| defaultPresets | boolean | true | | +| defaultPresets | boolean | false | | | presets | array | | name, shadow, slug | --- diff --git a/gutenberg.php b/gutenberg.php index b7580894f0fb4e..1226a3e24917d5 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.3 * Requires PHP: 7.0 - * Version: 17.8.0-rc.2 + * Version: 17.8.0-rc.4 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 82e5503a6b225d..0d03a89bd1d3c6 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -670,6 +670,7 @@ public static function get_element_class_name( $element ) { array( 'spacing', 'margin' ), array( 'spacing', 'padding' ), array( 'typography', 'lineHeight' ), + array( 'shadow', 'defaultPresets' ), ); /** diff --git a/lib/compat/wordpress-6.5/fonts/fonts.php b/lib/compat/wordpress-6.5/fonts/fonts.php index 8307d5217ad426..3911f61ceaec4b 100644 --- a/lib/compat/wordpress-6.5/fonts/fonts.php +++ b/lib/compat/wordpress-6.5/fonts/fonts.php @@ -86,18 +86,21 @@ function gutenberg_create_initial_post_types() { /** * Initializes REST routes. - * - * @since 6.5 */ function gutenberg_create_initial_rest_routes() { - $font_collections_controller = new WP_REST_Font_Collections_Controller(); - $font_collections_controller->register_routes(); + global $wp_version; + + // Runs only if the Font Library is not available in core ( i.e. in core < 6.5-alpha ). + if ( version_compare( $wp_version, '6.5-alpha', '<' ) ) { + $font_collections_controller = new WP_REST_Font_Collections_Controller(); + $font_collections_controller->register_routes(); + } } +add_action( 'rest_api_init', 'gutenberg_create_initial_rest_routes' ); + /** * Initializes REST routes and post types. - * - * @since 6.5 */ function gutenberg_init_font_library() { global $wp_version; @@ -105,11 +108,10 @@ function gutenberg_init_font_library() { // Runs only if the Font Library is not available in core ( i.e. in core < 6.5-alpha ). if ( version_compare( $wp_version, '6.5-alpha', '<' ) ) { gutenberg_create_initial_post_types(); - gutenberg_create_initial_rest_routes(); } } -add_action( 'rest_api_init', 'gutenberg_init_font_library' ); +add_action( 'init', 'gutenberg_init_font_library' ); if ( ! function_exists( 'wp_register_font_collection' ) ) { diff --git a/lib/theme.json b/lib/theme.json index c2ed7fdca39ed5..5f261e2c120043 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -191,7 +191,7 @@ "text": true }, "shadow": { - "defaultPresets": true, + "defaultPresets": false, "presets": [ { "name": "Natural", diff --git a/package-lock.json b/package-lock.json index 8403868d439a16..9c03b2e0e1fafa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.8.0-rc.2", + "version": "17.8.0-rc.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.8.0-rc.2", + "version": "17.8.0-rc.4", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 4cf398db191eff..f93f37999cc6bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.8.0-rc.2", + "version": "17.8.0-rc.4", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index b280cc9c51f6b8..e8480912a87f03 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -6,6 +6,7 @@ import { createContext, useContext } from '@wordpress/element'; export const mayDisplayControlsKey = Symbol( 'mayDisplayControls' ); export const mayDisplayParentControlsKey = Symbol( 'mayDisplayParentControls' ); export const blockEditingModeKey = Symbol( 'blockEditingMode' ); +export const blockBindingsKey = Symbol( 'blockBindings' ); export const DEFAULT_BLOCK_EDIT_CONTEXT = { name: '', diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 457cd919f89381..4e94a8a427510d 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -14,6 +14,7 @@ import { mayDisplayControlsKey, mayDisplayParentControlsKey, blockEditingModeKey, + blockBindingsKey, } from './context'; /** @@ -41,7 +42,8 @@ export default function BlockEdit( { attributes = {}, __unstableLayoutClassNames, } = props; - const { layout = null } = attributes; + const { layout = null, metadata = {} } = attributes; + const { bindings } = metadata; const layoutSupport = hasBlockSupport( name, 'layout', false ) || hasBlockSupport( name, '__experimentalLayout', false ); @@ -62,6 +64,7 @@ export default function BlockEdit( { [ mayDisplayControlsKey ]: mayDisplayControls, [ mayDisplayParentControlsKey ]: mayDisplayParentControls, [ blockEditingModeKey ]: blockEditingMode, + [ blockBindingsKey ]: bindings, } ), [ name, @@ -73,6 +76,7 @@ export default function BlockEdit( { mayDisplayControls, mayDisplayParentControls, blockEditingMode, + bindings, ] ) } > diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index beeda0d9b647b7..136cdef91286e2 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -28,7 +28,7 @@ import PositionControls from '../inspector-controls-tabs/position-controls-panel import useBlockInspectorAnimationSettings from './useBlockInspectorAnimationSettings'; import BlockInfo from '../block-info-slot-fill'; import BlockQuickNavigation from '../block-quick-navigation'; -import { getBorderPanelLabel } from '../../hooks/border'; +import { useBorderPanelLabel } from '../../hooks/border'; function BlockInspectorLockedBlocks( { topLevelLockedBlock } ) { const contentClientIds = useSelect( @@ -114,6 +114,10 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { selectedBlockClientId ); + const borderPanelLabel = useBorderPanelLabel( { + blockName: selectedBlockName, + } ); + if ( count > 1 ) { return (
@@ -138,9 +142,7 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { /> @@ -249,7 +251,7 @@ const BlockInspectorSingleBlock = ( { clientId, blockName } ) => { [ blockName ] ); const blockInformation = useBlockDisplayInformation( clientId ); - const borderPanelLabel = getBorderPanelLabel( { blockName } ); + const borderPanelLabel = useBorderPanelLabel( { blockName } ); return (
diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index f12e63c6d7663e..e5e142e0fa332c 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -453,7 +453,11 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b } } -.block-editor-iframe__body { +.block-editor-iframe__html { transition: all 0.3s; transform-origin: top center; } + +.block-editor-iframe__html[style*="scale"] { + background-color: $gray-300; +} diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 0d9b61314c4ed1..140532de15b768 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -35,6 +35,7 @@ import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; import NavigableToolbar from '../navigable-toolbar'; import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; +import Shuffle from './shuffle'; /** * Renders the block toolbar. @@ -185,6 +186,7 @@ export function PrivateBlockToolbar( {
) } + { shouldShowVisualToolbar && isMultiToolbar && ( ) } diff --git a/packages/block-editor/src/components/block-toolbar/shuffle.js b/packages/block-editor/src/components/block-toolbar/shuffle.js new file mode 100644 index 00000000000000..d84fdacdd3a055 --- /dev/null +++ b/packages/block-editor/src/components/block-toolbar/shuffle.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +import { shuffle } from '@wordpress/icons'; +import { ToolbarButton, ToolbarGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useMemo } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +const EMPTY_ARRAY = []; + +export default function Shuffle( { clientId } ) { + const { categories, patterns } = useSelect( + ( select ) => { + const { + getBlockAttributes, + getBlockRootClientId, + __experimentalGetAllowedPatterns, + } = select( blockEditorStore ); + const attributes = getBlockAttributes( clientId ); + const _categories = attributes?.metadata?.categories || EMPTY_ARRAY; + const rootBlock = getBlockRootClientId( clientId ); + const _patterns = __experimentalGetAllowedPatterns( rootBlock ); + return { + categories: _categories, + patterns: _patterns, + }; + }, + [ clientId ] + ); + const { replaceBlocks } = useDispatch( blockEditorStore ); + const sameCategoryPatternsWithSingleWrapper = useMemo( () => { + if ( + ! categories || + categories.length === 0 || + ! patterns || + patterns.length === 0 + ) { + return EMPTY_ARRAY; + } + return patterns.filter( ( pattern ) => { + return ( + // Check if the pattern has only one top level block, + // otherwise we may shuffle to pattern that will not allow to continue shuffling. + pattern.blocks.length === 1 && + pattern.categories.some( ( category ) => { + return categories.includes( category ); + } ) + ); + } ); + }, [ categories, patterns ] ); + if ( sameCategoryPatternsWithSingleWrapper.length === 0 ) { + return null; + } + return ( + + { + const randomPattern = + sameCategoryPatternsWithSingleWrapper[ + Math.floor( + // eslint-disable-next-line no-restricted-syntax + Math.random() * + sameCategoryPatternsWithSingleWrapper.length + ) + ]; + randomPattern.blocks[ 0 ].attributes = { + ...randomPattern.blocks[ 0 ].attributes, + metadata: { + ...randomPattern.blocks[ 0 ].attributes.metadata, + categories, + }, + }; + replaceBlocks( clientId, randomPattern.blocks ); + } } + /> + + ); +} diff --git a/packages/block-editor/src/components/global-styles/border-panel.js b/packages/block-editor/src/components/global-styles/border-panel.js index f8144f1545aebe..04bb5f9d15b1ad 100644 --- a/packages/block-editor/src/components/global-styles/border-panel.js +++ b/packages/block-editor/src/components/global-styles/border-panel.js @@ -21,23 +21,24 @@ import { useColorsPerOrigin } from './hooks'; import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { overrideOrigins } from '../../store/get-block-settings'; import { setImmutably } from '../../utils/object'; -import { getBorderPanelLabel } from '../../hooks/border'; -import { ShadowPopover } from './shadow-panel-components'; +import { useBorderPanelLabel } from '../../hooks/border'; +import { ShadowPopover, useShadowPresets } from './shadow-panel-components'; -function useHasShadowControl( settings ) { - return !! settings?.shadow; +export function useHasBorderPanel( settings ) { + const controls = Object.values( useHasBorderPanelControls( settings ) ); + return controls.some( Boolean ); } -export function useHasBorderPanel( settings ) { - const controls = [ - useHasBorderColorControl( settings ), - useHasBorderRadiusControl( settings ), - useHasBorderStyleControl( settings ), - useHasBorderWidthControl( settings ), - useHasShadowControl( settings ), - ]; +export function useHasBorderPanelControls( settings ) { + const controls = { + hasBorderColor: useHasBorderColorControl( settings ), + hasBorderRadius: useHasBorderRadiusControl( settings ), + hasBorderStyle: useHasBorderStyleControl( settings ), + hasBorderWidth: useHasBorderWidthControl( settings ), + hasShadow: useHasShadowControl( settings ), + }; - return controls.some( Boolean ); + return controls; } function useHasBorderColorControl( settings ) { @@ -56,6 +57,11 @@ function useHasBorderWidthControl( settings ) { return settings?.border?.width; } +function useHasShadowControl( settings ) { + const shadows = useShadowPresets( settings ); + return !! settings?.shadow && shadows.length > 0; +} + function BorderToolsPanel( { resetAllFilter, onChange, @@ -215,14 +221,16 @@ export default function BorderPanel( { const showBorderByDefault = defaultControls?.color || defaultControls?.width; - const label = getBorderPanelLabel( { + const hasBorderControl = + showBorderColor || + showBorderStyle || + showBorderWidth || + showBorderRadius; + + const label = useBorderPanelLabel( { blockName: name, hasShadowControl, - hasBorderControl: - showBorderColor || - showBorderStyle || - showBorderWidth || - showBorderRadius, + hasBorderControl, } ); return ( @@ -280,9 +288,12 @@ export default function BorderPanel( { isShownByDefault={ defaultControls.shadow } panelId={ panelId } > - - { __( 'Shadow' ) } - + { hasBorderControl ? ( + + { __( 'Shadow' ) } + + ) : null } + @@ -43,6 +45,14 @@ export function ShadowPopoverContainer( { shadow, onShadowChange, settings } ) { activeShadow={ shadow } onSelect={ onShadowChange } /> +
+ +
); @@ -64,6 +74,7 @@ export function ShadowPresets( { presets, activeShadow, onSelect } ) { key={ slug } label={ name } isActive={ shadow === activeShadow } + type={ slug === 'unset' ? 'unset' : 'preset' } onSelect={ () => onSelect( shadow === activeShadow ? undefined : shadow ) } @@ -74,7 +85,7 @@ export function ShadowPresets( { presets, activeShadow, onSelect } ) { ); } -export function ShadowIndicator( { label, isActive, onSelect, shadow } ) { +export function ShadowIndicator( { type, label, isActive, onSelect, shadow } ) { const { CompositeItemV2: CompositeItem } = unlock( componentsPrivateApis ); return ( { + if ( ! settings?.shadow ) { + return EMPTY_ARRAY; + } + + const defaultPresetsEnabled = settings?.shadow?.defaultPresets; + const { default: defaultShadows, theme: themeShadows } = + settings?.shadow?.presets ?? {}; + const unsetShadow = { + name: __( 'Unset' ), + slug: 'unset', + shadow: 'none', + }; + + const shadowPresets = [ + ...( ( defaultPresetsEnabled && defaultShadows ) || EMPTY_ARRAY ), + ...( themeShadows || EMPTY_ARRAY ), + ]; + if ( shadowPresets.length ) { + shadowPresets.unshift( unsetShadow ); + } + + return shadowPresets; + }, [ settings ] ); +} diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index b99759511413a6..d357d2e65ab72a 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -2,10 +2,24 @@ fill: currentColor; } +// @todo: Ideally, popover, swatch size, and gap values should be CSS variables +// to apply precise grid layouts. +// https://github.com/WordPress/gutenberg/blob/954ecae571abbddc113d3a4bd8e1a72230180554/packages/block-editor/src/components/duotone-control/style.scss#L3-L9 .block-editor-global-styles__shadow-popover-container { width: 230px; } +.block-editor-global-styles__shadow__list { + display: flex; + gap: 12px; + flex-wrap: wrap; + padding-bottom: $grid-unit-10; +} + +.block-editor-global-styles__clear-shadow { + text-align: right; +} + .block-editor-global-styles-filters-panel__dropdown, .block-editor-global-styles__shadow-dropdown { display: block; @@ -28,8 +42,6 @@ border-radius: $radius-block-ui; cursor: pointer; padding: 0; - margin-right: $grid-unit-15; - margin-bottom: $grid-unit-15; height: $button-size-small + 2 * $border-width; width: $button-size-small + 2 * $border-width; @@ -46,6 +58,10 @@ &:hover { transform: scale(1.2); } + + &.unset { + background: linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); + } } .block-editor-global-styles-advanced-panel__custom-css-input textarea { diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 673562fde34f58..3d720f718bc1ae 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -30,7 +30,7 @@ import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useWritingFlow } from '../writing-flow'; import { getCompatibilityStyles } from './get-compatibility-styles'; import { store as blockEditorStore } from '../../store'; - +import calculateScale from '../../utils/calculate-scale'; function bubbleEvent( event, Constructor, frame ) { const init = {}; @@ -104,27 +104,53 @@ function Iframe( { contentRef, children, tabIndex = 0, - scale = 1, - frameSize = 0, - expand = false, + shouldZoom = false, readonly, forwardedRef: ref, + title = __( 'Editor canvas' ), ...props } ) { - const { resolvedAssets, isPreviewMode } = useSelect( ( select ) => { - const settings = select( blockEditorStore ).getSettings(); - return { - resolvedAssets: settings.__unstableResolvedAssets, - isPreviewMode: settings.__unstableIsPreviewMode, - }; - }, [] ); + const { resolvedAssets, isPreviewMode, isZoomOutMode } = useSelect( + ( select ) => { + const { getSettings, __unstableGetEditorMode } = + select( blockEditorStore ); + const settings = getSettings(); + return { + resolvedAssets: settings.__unstableResolvedAssets, + isPreviewMode: settings.__unstableIsPreviewMode, + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + }; + }, + [] + ); const { styles = '', scripts = '' } = resolvedAssets; const [ iframeDocument, setIframeDocument ] = useState(); const [ bodyClasses, setBodyClasses ] = useState( [] ); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); - const [ contentResizeListener, { height: contentHeight } ] = - useResizeObserver(); + const [ + contentResizeListener, + { height: contentHeight, width: contentWidth }, + ] = useResizeObserver(); + + // When zoom-out mode is enabled, the iframe is scaled down to fit the + // content within the viewport. + // At 1000px wide, the iframe is scaled to 45%. + // At 400px wide, the iframe is scaled to 90%. + const scale = + isZoomOutMode && shouldZoom + ? calculateScale( + { + maxWidth: 1000, + minWidth: 400, + maxScale: 0.45, + minScale: 0.9, + }, + contentWidth + ) + : 1; + const frameSize = isZoomOutMode ? 100 : 0; + const setRef = useRefEffect( ( node ) => { node._load = () => { setIframeDocument( node.contentDocument ); @@ -139,6 +165,8 @@ function Iframe( { const { documentElement } = contentDocument; iFrameDocument = contentDocument; + documentElement.classList.add( 'block-editor-iframe__html' ); + clearerRef( documentElement ); // Ideally ALL classes that are added through get_body_class should @@ -218,7 +246,19 @@ function Iframe( { - + ${ styles } ${ scripts } @@ -241,28 +281,35 @@ function Iframe( { // top or bottom margin is 0.55 / 2 ((1 - scale) / 2). const marginFromScaling = ( contentHeight * ( 1 - scale ) ) / 2; + useEffect( () => { + if ( iframeDocument && scale !== 1 ) { + iframeDocument.documentElement.style.transform = `scale( ${ scale } )`; + iframeDocument.documentElement.style.marginTop = `${ frameSize }px`; + iframeDocument.documentElement.style.marginBottom = `${ + -marginFromScaling * 2 + frameSize + }px`; + return () => { + iframeDocument.documentElement.style.transform = ''; + iframeDocument.documentElement.style.marginTop = ''; + iframeDocument.documentElement.style.marginBottom = ''; + }; + } + }, [ scale, frameSize, marginFromScaling, iframeDocument ] ); + + // Make sure to not render the before and after focusable div elements in view + // mode. They're only needed to capture focus in edit mode. + const shouldRenderFocusCaptureElements = tabIndex >= 0 && ! isPreviewMode; + return ( <> - { tabIndex >= 0 && before } + { shouldRenderFocusCaptureElements && before } { /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ } - { tabIndex >= 0 && after } + { shouldRenderFocusCaptureElements && after } ); } diff --git a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js index 80acf0e639f9e5..25a3dc9dadfa96 100644 --- a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js +++ b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js @@ -37,11 +37,11 @@ function useShallowMemo( value ) { * in inner blocks. * @param {string[]} prioritizedInserterBlocks Block names and/or block variations to be prioritized in the inserter, in the format {blockName}/{variationName}. * @param {?WPDirectInsertBlock} defaultBlock The default block to insert: [ blockName, { blockAttributes } ]. - * @param {?Function|boolean} directInsert If a default block should be inserted directly by the appender. + * @param {?boolean} directInsert If a default block should be inserted directly by the appender. * * @param {?WPDirectInsertBlock} __experimentalDefaultBlock A deprecated prop for the default block to insert: [ blockName, { blockAttributes } ]. Use `defaultBlock` instead. * - * @param {?Function|boolean} __experimentalDirectInsert A deprecated prop for whether a default block should be inserted directly by the appender. Use `directInsert` instead. + * @param {?boolean} __experimentalDirectInsert A deprecated prop for whether a default block should be inserted directly by the appender. Use `directInsert` instead. * * @param {string} [templateLock] The template lock specified for the inner * blocks component. (e.g. "all") @@ -138,6 +138,16 @@ export default function useNestedSettingsUpdate( newSettings.directInsert = directInsert; } + if ( + newSettings.directInsert !== undefined && + typeof newSettings.directInsert !== 'boolean' + ) { + deprecated( 'Using `Function` as a `directInsert` argument', { + alternative: '`boolean` values', + since: '6.5', + } ); + } + // Batch updates to block list settings to avoid triggering cascading renders // for each container block included in a tree and optimize initial render. // To avoid triggering updateBlockListSettings for each container block diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 6a38e52cbffba1..f0256d0d35d8ff 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -66,10 +66,13 @@ function InserterMenu( insertionIndex: __experimentalInsertionIndex, shouldFocusBlock, } ); - const { showPatterns } = useSelect( + const { isZoomOutMode, showPatterns } = useSelect( ( select ) => { - const { hasAllowedPatterns } = unlock( select( blockEditorStore ) ); + const { hasAllowedPatterns, __unstableGetEditorMode } = unlock( + select( blockEditorStore ) + ); return { + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', showPatterns: hasAllowedPatterns( destinationRootClientId ), }; }, @@ -77,7 +80,8 @@ function InserterMenu( ); const mediaCategories = useMediaCategories( destinationRootClientId ); - const showMedia = mediaCategories.length > 0; + const showMedia = mediaCategories.length > 0 && ! isZoomOutMode; + const showBlocks = ! isZoomOutMode; const onInsert = useCallback( ( blocks, meta, shouldForceFocusBlock ) => { @@ -249,19 +253,21 @@ function InserterMenu( __experimentalInsertionIndex } showBlockDirectory + showBlocks={ showBlocks } shouldFocusBlock={ shouldFocusBlock } /> ) } { showAsTabs && ( ) } - { ! delayedFilterValue && ! showAsTabs && ( + { ! delayedFilterValue && ! showAsTabs && showBlocks && (
{ blocksTab }
diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js index edd99609ea916c..d213bdd1af227a 100644 --- a/packages/block-editor/src/components/inserter/search-results.js +++ b/packages/block-editor/src/components/inserter/search-results.js @@ -50,6 +50,7 @@ function InserterSearchResults( { shouldFocusBlock = true, prioritizePatterns, selectBlockOnInsert, + showBlocks = true, } ) { const debouncedSpeak = useDebounce( speak, 500 ); @@ -167,7 +168,7 @@ function InserterSearchResults( { const hasItems = filteredBlockTypes.length > 0 || filteredBlockPatterns.length > 0; - const blocksUI = !! filteredBlockTypes.length && ( + const blocksUI = showBlocks && !! filteredBlockTypes.length && ( { __( 'Blocks' ) } } > diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 35c18e1d9acce5..685355a81a2971 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -767,3 +767,13 @@ $block-inserter-tabs-height: 44px; } } } + +.is-zoom-out { + .block-editor-inserter__menu { + display: flex; + } + + .block-editor-inserter__patterns-category-dialog { + position: static; + } +} diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index 4795c3ce4fdc24..ee920df4ca214c 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -29,13 +29,14 @@ const mediaTab = { }; function InserterTabs( { + showBlocks = true, showPatterns = false, showMedia = false, onSelect, tabsContents, } ) { const tabs = [ - blocksTab, + showBlocks && blocksTab, showPatterns && patternsTab, showMedia && mediaTab, ].filter( Boolean ); diff --git a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js index eab4d47e0c3723..8386b545c72500 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/styles-tab.js @@ -9,10 +9,10 @@ import { __ } from '@wordpress/i18n'; */ import BlockStyles from '../block-styles'; import InspectorControls from '../inspector-controls'; -import { getBorderPanelLabel } from '../../hooks/border'; +import { useBorderPanelLabel } from '../../hooks/border'; const StylesTab = ( { blockName, clientId, hasBlockStyles } ) => { - const borderPanelLabel = getBorderPanelLabel( { blockName } ); + const borderPanelLabel = useBorderPanelLabel( { blockName } ); return ( <> diff --git a/packages/block-editor/src/components/resizable-box-popover/index.js b/packages/block-editor/src/components/resizable-box-popover/index.js index 8a49c1631287a1..61f599663a4f5d 100644 --- a/packages/block-editor/src/components/resizable-box-popover/index.js +++ b/packages/block-editor/src/components/resizable-box-popover/index.js @@ -16,7 +16,7 @@ export default function ResizableBoxPopover( { return ( diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 5f94206c78752f..458f5a96609b65 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -26,6 +26,7 @@ import { getBlockType, store as blocksStore } from '@wordpress/blocks'; */ import { useBlockEditorAutocompleteProps } from '../autocomplete'; import { useBlockEditContext } from '../block-edit'; +import { blockBindingsKey } from '../block-edit/context'; import FormatToolbarContainer from './format-toolbar-container'; import { store as blockEditorStore } from '../../store'; import { useUndoAutomaticChange } from './use-undo-automatic-change'; @@ -117,11 +118,9 @@ export function RichTextWrapper( props = removeNativeProps( props ); const anchorRef = useRef(); - const { - clientId, - isSelected: isBlockSelected, - name: blockName, - } = useBlockEditContext(); + const context = useBlockEditContext(); + const { clientId, isSelected: isBlockSelected, name: blockName } = context; + const blockBindings = context[ blockBindingsKey ]; const selector = ( select ) => { // Avoid subscribing to the block editor store if the block is not // selected. @@ -129,12 +128,10 @@ export function RichTextWrapper( return { isSelected: false }; } - const { getSelectionStart, getSelectionEnd, getBlockAttributes } = + const { getSelectionStart, getSelectionEnd } = select( blockEditorStore ); const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd(); - const blockBindings = - getBlockAttributes( clientId )?.metadata?.bindings; let isSelected; @@ -147,48 +144,57 @@ export function RichTextWrapper( isSelected = selectionStart.clientId === clientId; } - // Disable Rich Text editing if block bindings specify that. - let disableBoundBlocks = false; - if ( blockBindings && blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) { - const blockTypeAttributes = getBlockType( blockName ).attributes; - const { getBlockBindingsSource } = unlock( select( blocksStore ) ); - for ( const [ attribute, args ] of Object.entries( - blockBindings - ) ) { - if ( - blockTypeAttributes?.[ attribute ]?.source !== 'rich-text' - ) { - break; - } - - // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. - const blockBindingsSource = getBlockBindingsSource( - args.source - ); - if ( - ! blockBindingsSource || - blockBindingsSource.lockAttributesEditing - ) { - disableBoundBlocks = true; - break; - } - } - } - return { selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, - disableBoundBlocks, }; }; - const { selectionStart, selectionEnd, isSelected, disableBoundBlocks } = - useSelect( selector, [ - clientId, - identifier, - originalIsSelected, - isBlockSelected, - ] ); + const { selectionStart, selectionEnd, isSelected } = useSelect( selector, [ + clientId, + identifier, + originalIsSelected, + isBlockSelected, + ] ); + + const disableBoundBlocks = useSelect( + ( select ) => { + // Disable Rich Text editing if block bindings specify that. + let _disableBoundBlocks = false; + if ( blockBindings && blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) { + const blockTypeAttributes = + getBlockType( blockName ).attributes; + const { getBlockBindingsSource } = unlock( + select( blocksStore ) + ); + for ( const [ attribute, args ] of Object.entries( + blockBindings + ) ) { + if ( + blockTypeAttributes?.[ attribute ]?.source !== + 'rich-text' + ) { + break; + } + + // If the source is not defined, or if its value of `lockAttributesEditing` is `true`, disable it. + const blockBindingsSource = getBlockBindingsSource( + args.source + ); + if ( + ! blockBindingsSource || + blockBindingsSource.lockAttributesEditing + ) { + _disableBoundBlocks = true; + break; + } + } + } + + return _disableBoundBlocks; + }, + [ blockBindings, blockName ] + ); const shouldDisableEditing = disableEditing || disableBoundBlocks; diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js index 4daf70e7fa3c74..6b40a82d72d4b2 100644 --- a/packages/block-editor/src/components/rich-text/use-enter.js +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -21,6 +21,10 @@ export function useEnter( props ) { propsRef.current = props; return useRefEffect( ( element ) => { function onKeyDown( event ) { + if ( event.target.contentEditable !== 'true' ) { + return; + } + if ( event.defaultPrevented ) { return; } diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 1e0b8e894d2067..eb84352ab62f09 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -35,12 +35,12 @@ function BlockHooksControlPure( { name, clientId } ) { const { blockIndex, rootClientId, innerBlocksLength } = useSelect( ( select ) => { - const { getBlock, getBlockIndex, getBlockRootClientId } = + const { getBlocks, getBlockIndex, getBlockRootClientId } = select( blockEditorStore ); return { blockIndex: getBlockIndex( clientId ), - innerBlocksLength: getBlock( clientId )?.innerBlocks?.length, + innerBlocksLength: getBlocks( clientId )?.length, rootClientId: getBlockRootClientId( clientId ), }; }, @@ -49,7 +49,7 @@ function BlockHooksControlPure( { name, clientId } ) { const hookedBlockClientIds = useSelect( ( select ) => { - const { getBlock, getGlobalBlockCount } = + const { getBlocks, getGlobalBlockCount } = select( blockEditorStore ); const _hookedBlockClientIds = hookedBlocksForCurrentBlock.reduce( @@ -69,7 +69,7 @@ function BlockHooksControlPure( { name, clientId } ) { // Any of the current block's siblings (with the right block type) qualifies // as a hooked block (inserted `before` or `after` the current one), as the block // might've been automatically inserted and then moved around a bit by the user. - candidates = getBlock( rootClientId )?.innerBlocks; + candidates = getBlocks( rootClientId ); break; case 'first_child': @@ -77,7 +77,7 @@ function BlockHooksControlPure( { name, clientId } ) { // Any of the current block's child blocks (with the right block type) qualifies // as a hooked first or last child block, as the block might've been automatically // inserted and then moved around a bit by the user. - candidates = getBlock( clientId ).innerBlocks; + candidates = getBlocks( clientId ); break; } @@ -161,6 +161,11 @@ function BlockHooksControlPure( { name, clientId } ) { title={ __( 'Plugins' ) } initialOpen={ true } > +

+ { __( + 'Manage the inclusion of blocks added automatically by plugins.' + ) } +

{ Object.keys( groupedHookedBlocks ).map( ( vendor ) => { return ( diff --git a/packages/block-editor/src/hooks/block-hooks.scss b/packages/block-editor/src/hooks/block-hooks.scss index dc05d6e6947bb0..4d871233de482b 100644 --- a/packages/block-editor/src/hooks/block-hooks.scss +++ b/packages/block-editor/src/hooks/block-hooks.scss @@ -13,4 +13,10 @@ .components-toggle-control .components-h-stack .components-h-stack { flex-direction: row; } + + .block-editor-hooks__block-hooks-helptext { + color: $gray-700; + font-size: $helptext-font-size; + margin-bottom: $grid-unit-20; + } } diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index c743b457fc05cc..a8ed9bccaf7df9 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -18,9 +18,14 @@ import { useSelect } from '@wordpress/data'; import { getColorClassName } from '../components/colors'; import InspectorControls from '../components/inspector-controls'; import useMultipleOriginColorsAndGradients from '../components/colors-gradients/use-multiple-origin-colors-and-gradients'; -import { cleanEmptyObject, shouldSkipSerialization } from './utils'; +import { + cleanEmptyObject, + shouldSkipSerialization, + useBlockSettings, +} from './utils'; import { useHasBorderPanel, + useHasBorderPanelControls, BorderPanel as StylesBorderPanel, } from '../components/global-styles'; import { store as blockEditorStore } from '../store'; @@ -220,14 +225,21 @@ export function hasShadowSupport( blockName ) { return hasBlockSupport( blockName, SHADOW_SUPPORT_KEY ); } -export function getBorderPanelLabel( { +export function useBorderPanelLabel( { blockName, hasBorderControl, hasShadowControl, } = {} ) { + const settings = useBlockSettings( blockName ); + const controls = useHasBorderPanelControls( settings ); + if ( ! hasBorderControl && ! hasShadowControl && blockName ) { - hasBorderControl = hasBorderSupport( blockName ); - hasShadowControl = hasShadowSupport( blockName ); + hasBorderControl = + controls?.hasBorderColor || + controls?.hasBorderStyle || + controls?.hasBorderWidth || + controls?.hasBorderRadius; + hasShadowControl = controls?.hasShadow; } if ( hasBorderControl && hasShadowControl ) { diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 6f03ddac2c6504..f78a39230e89bb 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -19,8 +19,7 @@ import { DimensionsPanel as StylesDimensionsPanel, useHasDimensionsPanel, } from '../components/global-styles'; -import { MarginVisualizer } from './margin'; -import { PaddingVisualizer } from './padding'; +import { MarginVisualizer, PaddingVisualizer } from './spacing-visualizer'; import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; import { cleanEmptyObject, shouldSkipSerialization } from './utils'; diff --git a/packages/block-editor/src/hooks/margin.js b/packages/block-editor/src/hooks/margin.js deleted file mode 100644 index 7be1179d295100..00000000000000 --- a/packages/block-editor/src/hooks/margin.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useRef, useEffect } from '@wordpress/element'; -import isShallowEqual from '@wordpress/is-shallow-equal'; - -/** - * Internal dependencies - */ -import BlockPopoverCover from '../components/block-popover/cover'; -import { __unstableUseBlockElement as useBlockElement } from '../components/block-list/use-block-props/use-block-refs'; - -function getComputedCSS( element, property ) { - return element.ownerDocument.defaultView - .getComputedStyle( element ) - .getPropertyValue( property ); -} - -export function MarginVisualizer( { clientId, attributes, forceShow } ) { - const blockElement = useBlockElement( clientId ); - const [ style, setStyle ] = useState(); - - const margin = attributes?.style?.spacing?.margin; - - useEffect( () => { - if ( - ! blockElement || - null === blockElement.ownerDocument.defaultView - ) { - return; - } - - const top = getComputedCSS( blockElement, 'margin-top' ); - const right = getComputedCSS( blockElement, 'margin-right' ); - const bottom = getComputedCSS( blockElement, 'margin-bottom' ); - const left = getComputedCSS( blockElement, 'margin-left' ); - - setStyle( { - borderTopWidth: top, - borderRightWidth: right, - borderBottomWidth: bottom, - borderLeftWidth: left, - top: top ? `-${ top }` : 0, - right: right ? `-${ right }` : 0, - bottom: bottom ? `-${ bottom }` : 0, - left: left ? `-${ left }` : 0, - } ); - }, [ blockElement, margin ] ); - - const [ isActive, setIsActive ] = useState( false ); - const valueRef = useRef( margin ); - const timeoutRef = useRef(); - - const clearTimer = () => { - if ( timeoutRef.current ) { - window.clearTimeout( timeoutRef.current ); - } - }; - - useEffect( () => { - if ( ! isShallowEqual( margin, valueRef.current ) && ! forceShow ) { - setIsActive( true ); - valueRef.current = margin; - - timeoutRef.current = setTimeout( () => { - setIsActive( false ); - }, 400 ); - } - - return () => { - setIsActive( false ); - clearTimer(); - }; - }, [ margin, forceShow ] ); - - if ( ! isActive && ! forceShow ) { - return null; - } - - return ( - -
- - ); -} diff --git a/packages/block-editor/src/hooks/padding.js b/packages/block-editor/src/hooks/padding.js deleted file mode 100644 index 50eed7ac05d5e9..00000000000000 --- a/packages/block-editor/src/hooks/padding.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useRef, useEffect } from '@wordpress/element'; -import isShallowEqual from '@wordpress/is-shallow-equal'; - -/** - * Internal dependencies - */ -import BlockPopoverCover from '../components/block-popover/cover'; -import { __unstableUseBlockElement as useBlockElement } from '../components/block-list/use-block-props/use-block-refs'; - -function getComputedCSS( element, property ) { - return element.ownerDocument.defaultView - .getComputedStyle( element ) - .getPropertyValue( property ); -} - -export function PaddingVisualizer( { clientId, value, forceShow } ) { - const blockElement = useBlockElement( clientId ); - const [ style, setStyle ] = useState(); - - const padding = value?.spacing?.padding; - - useEffect( () => { - if ( - ! blockElement || - null === blockElement.ownerDocument.defaultView - ) { - return; - } - - setStyle( { - borderTopWidth: getComputedCSS( blockElement, 'padding-top' ), - borderRightWidth: getComputedCSS( blockElement, 'padding-right' ), - borderBottomWidth: getComputedCSS( blockElement, 'padding-bottom' ), - borderLeftWidth: getComputedCSS( blockElement, 'padding-left' ), - } ); - }, [ blockElement, padding ] ); - - const [ isActive, setIsActive ] = useState( false ); - const valueRef = useRef( padding ); - const timeoutRef = useRef(); - - const clearTimer = () => { - if ( timeoutRef.current ) { - window.clearTimeout( timeoutRef.current ); - } - }; - - useEffect( () => { - if ( ! isShallowEqual( padding, valueRef.current ) && ! forceShow ) { - setIsActive( true ); - valueRef.current = padding; - - timeoutRef.current = setTimeout( () => { - setIsActive( false ); - }, 400 ); - } - - return () => { - setIsActive( false ); - clearTimer(); - }; - }, [ padding, forceShow ] ); - - if ( ! isActive && ! forceShow ) { - return null; - } - - return ( - -
- - ); -} diff --git a/packages/block-editor/src/hooks/spacing-visualizer.js b/packages/block-editor/src/hooks/spacing-visualizer.js new file mode 100644 index 00000000000000..7189b0f480d2cc --- /dev/null +++ b/packages/block-editor/src/hooks/spacing-visualizer.js @@ -0,0 +1,126 @@ +/** + * WordPress dependencies + */ +import { + useState, + useRef, + useLayoutEffect, + useEffect, + useReducer, +} from '@wordpress/element'; +import isShallowEqual from '@wordpress/is-shallow-equal'; + +/** + * Internal dependencies + */ +import BlockPopoverCover from '../components/block-popover/cover'; +import { __unstableUseBlockElement as useBlockElement } from '../components/block-list/use-block-props/use-block-refs'; + +function SpacingVisualizer( { clientId, value, computeStyle, forceShow } ) { + const blockElement = useBlockElement( clientId ); + const [ style, updateStyle ] = useReducer( () => + computeStyle( blockElement ) + ); + + useLayoutEffect( () => { + if ( ! blockElement ) { + return; + } + // It's not sufficient to read the computed spacing value when value.spacing changes as + // useEffect may run before the browser recomputes CSS. We therefore combine + // useLayoutEffect and two rAF calls to ensure that we read the spacing after the current + // paint but before the next paint. + // See https://github.com/WordPress/gutenberg/pull/59227. + window.requestAnimationFrame( () => + window.requestAnimationFrame( updateStyle ) + ); + }, [ blockElement, value ] ); + + const previousValue = useRef( value ); + const [ isActive, setIsActive ] = useState( false ); + + useEffect( () => { + if ( isShallowEqual( value, previousValue.current ) || forceShow ) { + return; + } + + setIsActive( true ); + previousValue.current = value; + + const timeout = setTimeout( () => { + setIsActive( false ); + }, 400 ); + + return () => { + setIsActive( false ); + clearTimeout( timeout ); + }; + }, [ value, forceShow ] ); + + if ( ! isActive && ! forceShow ) { + return null; + } + + return ( + +
+ + ); +} + +function getComputedCSS( element, property ) { + return element.ownerDocument.defaultView + .getComputedStyle( element ) + .getPropertyValue( property ); +} + +export function MarginVisualizer( { clientId, value, forceShow } ) { + return ( + { + const top = getComputedCSS( blockElement, 'margin-top' ); + const right = getComputedCSS( blockElement, 'margin-right' ); + const bottom = getComputedCSS( blockElement, 'margin-bottom' ); + const left = getComputedCSS( blockElement, 'margin-left' ); + return { + borderTopWidth: top, + borderRightWidth: right, + borderBottomWidth: bottom, + borderLeftWidth: left, + top: top ? `-${ top }` : 0, + right: right ? `-${ right }` : 0, + bottom: bottom ? `-${ bottom }` : 0, + left: left ? `-${ left }` : 0, + }; + } } + forceShow={ forceShow } + /> + ); +} + +export function PaddingVisualizer( { clientId, value, forceShow } ) { + return ( + ( { + borderTopWidth: getComputedCSS( blockElement, 'padding-top' ), + borderRightWidth: getComputedCSS( + blockElement, + 'padding-right' + ), + borderBottomWidth: getComputedCSS( + blockElement, + 'padding-bottom' + ), + borderLeftWidth: getComputedCSS( blockElement, 'padding-left' ), + } ) } + forceShow={ forceShow } + /> + ); +} diff --git a/packages/block-editor/src/hooks/padding.scss b/packages/block-editor/src/hooks/spacing.scss similarity index 84% rename from packages/block-editor/src/hooks/padding.scss rename to packages/block-editor/src/hooks/spacing.scss index dbb99919881893..52894d8ff2ba62 100644 --- a/packages/block-editor/src/hooks/padding.scss +++ b/packages/block-editor/src/hooks/spacing.scss @@ -1,4 +1,4 @@ -.block-editor__padding-visualizer { +.block-editor__spacing-visualizer { position: absolute; top: 0; bottom: 0; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 018c9ff9115185..e9281727804f1c 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -924,10 +924,8 @@ export const __unstableExpandSelection = export const mergeBlocks = ( firstBlockClientId, secondBlockClientId ) => ( { registry, select, dispatch } ) => { - const blocks = [ firstBlockClientId, secondBlockClientId ]; - dispatch( { type: 'MERGE_BLOCKS', blocks } ); - - const [ clientIdA, clientIdB ] = blocks; + const clientIdA = firstBlockClientId; + const clientIdB = secondBlockClientId; const blockA = select.getBlock( clientIdA ); const blockAType = getBlockType( blockA.name ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 87d22e3e4727d7..8f2e5e4e5ccc80 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2231,56 +2231,41 @@ export const __experimentalGetAllowedBlocks = createSelector( * @param {Object} state Editor state. * @param {?string} rootClientId Optional root client ID of block list. * - * @return {?WPDirectInsertBlock} The block type to be directly inserted. + * @return {WPDirectInsertBlock|undefined} The block type to be directly inserted. * * @typedef {Object} WPDirectInsertBlock * @property {string} name The type of block. * @property {?Object} attributes Attributes to pass to the newly created block. * @property {?Array} attributesToCopy Attributes to be copied from adjecent blocks when inserted. */ -export const getDirectInsertBlock = createSelector( - ( state, rootClientId = null ) => { - if ( ! rootClientId ) { - return; - } - const defaultBlock = - state.blockListSettings[ rootClientId ]?.defaultBlock; - const directInsert = - state.blockListSettings[ rootClientId ]?.directInsert; - if ( ! defaultBlock || ! directInsert ) { - return; - } - if ( typeof directInsert === 'function' ) { - return directInsert( getBlock( state, rootClientId ) ) - ? defaultBlock - : null; - } - return defaultBlock; - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.tree.get( rootClientId ), - ] -); +export function getDirectInsertBlock( state, rootClientId = null ) { + if ( ! rootClientId ) { + return; + } + const { defaultBlock, directInsert } = + state.blockListSettings[ rootClientId ] ?? {}; + if ( ! defaultBlock || ! directInsert ) { + return; + } -export const __experimentalGetDirectInsertBlock = createSelector( - ( state, rootClientId = null ) => { - deprecated( - 'wp.data.select( "core/block-editor" ).__experimentalGetDirectInsertBlock', - { - alternative: - 'wp.data.select( "core/block-editor" ).getDirectInsertBlock', - since: '6.3', - version: '6.4', - } - ); - return getDirectInsertBlock( state, rootClientId ); - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.tree.get( rootClientId ), - ] -); + return defaultBlock; +} + +export function __experimentalGetDirectInsertBlock( + state, + rootClientId = null +) { + deprecated( + 'wp.data.select( "core/block-editor" ).__experimentalGetDirectInsertBlock', + { + alternative: + 'wp.data.select( "core/block-editor" ).getDirectInsertBlock', + since: '6.3', + version: '6.4', + } + ); + return getDirectInsertBlock( state, rootClientId ); +} export const __experimentalGetParsedPattern = createRegistrySelector( ( select ) => @@ -2293,11 +2278,21 @@ export const __experimentalGetParsedPattern = createRegistrySelector( if ( ! pattern ) { return null; } + const blocks = parse( pattern.content, { + __unstableSkipMigrationLogs: true, + } ); + if ( blocks.length === 1 ) { + blocks[ 0 ].attributes = { + ...blocks[ 0 ].attributes, + metadata: { + ...( blocks[ 0 ].attributes.metadata || {} ), + categories: pattern.categories, + }, + }; + } return { ...pattern, - blocks: parse( pattern.content, { - __unstableSkipMigrationLogs: true, - } ), + blocks, }; }, getAllPatternsDependants( select ) ) ); diff --git a/packages/block-editor/src/store/test/actions.js b/packages/block-editor/src/store/test/actions.js index 932e97d95e2f2e..f960363cdb0ddb 100644 --- a/packages/block-editor/src/store/test/actions.js +++ b/packages/block-editor/src/store/test/actions.js @@ -832,10 +832,6 @@ describe( 'actions', () => { blockB.clientId )( { select, dispatch } ); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'MERGE_BLOCKS', - blocks: [ blockA.clientId, blockB.clientId ], - } ); expect( dispatch.selectBlock ).toHaveBeenCalledWith( 'chicken' ); } ); diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index d7aa3ebcc12d08..1cbc49f58551e5 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -53,8 +53,8 @@ @import "./hooks/color.scss"; @import "./hooks/dimensions.scss"; @import "./hooks/layout.scss"; -@import "./hooks/padding.scss"; @import "./hooks/position.scss"; +@import "./hooks/spacing.scss"; @import "./hooks/typography.scss"; @import "./components/block-toolbar/style.scss"; diff --git a/packages/block-editor/src/utils/calculate-scale.js b/packages/block-editor/src/utils/calculate-scale.js new file mode 100644 index 00000000000000..f07ba24ea341a1 --- /dev/null +++ b/packages/block-editor/src/utils/calculate-scale.js @@ -0,0 +1,20 @@ +const clamp = ( lowerlimit, width, upperlimit ) => { + if ( width < lowerlimit ) return lowerlimit; + if ( width > upperlimit ) return upperlimit; + return width; +}; + +export default function calculateScale( scaleConfig, width ) { + const scaleSlope = + ( scaleConfig.maxScale - scaleConfig.minScale ) / + ( scaleConfig.maxWidth - scaleConfig.minWidth ); + + const scaleIntercept = + scaleConfig.minScale - scaleSlope * scaleConfig.minWidth; + + return clamp( + scaleConfig.maxScale, + scaleSlope * width + scaleIntercept, + scaleConfig.minScale + ); +} diff --git a/packages/block-library/src/cover/edit/block-controls.js b/packages/block-library/src/cover/edit/block-controls.js index 59aaaaffe77d75..c4137ad2a8409a 100644 --- a/packages/block-library/src/cover/edit/block-controls.js +++ b/packages/block-library/src/cover/edit/block-controls.js @@ -8,6 +8,7 @@ import { MediaReplaceFlow, __experimentalBlockAlignmentMatrixControl as BlockAlignmentMatrixControl, __experimentalBlockFullHeightAligmentControl as FullHeightAlignmentControl, + privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; @@ -15,6 +16,9 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { ALLOWED_MEDIA_TYPES } from '../shared'; +import { unlock } from '../../lock-unlock'; + +const { cleanEmptyObject } = unlock( blockEditorPrivateApis ); export default function CoverBlockControls( { attributes, @@ -30,7 +34,10 @@ export default function CoverBlockControls( { const [ prevMinHeightValue, setPrevMinHeightValue ] = useState( minHeight ); const [ prevMinHeightUnit, setPrevMinHeightUnit ] = useState( minHeightUnit ); - const isMinFullHeight = minHeightUnit === 'vh' && minHeight === 100; + const isMinFullHeight = + minHeightUnit === 'vh' && + minHeight === 100 && + ! attributes?.style?.dimensions?.aspectRatio; const toggleMinFullHeight = () => { if ( isMinFullHeight ) { // If there aren't previous values, take the default ones. @@ -51,10 +58,17 @@ export default function CoverBlockControls( { setPrevMinHeightValue( minHeight ); setPrevMinHeightUnit( minHeightUnit ); - // Set full height. + // Set full height, and clear any aspect ratio value. return setAttributes( { minHeight: 100, minHeightUnit: 'vh', + style: cleanEmptyObject( { + ...attributes?.style, + dimensions: { + ...attributes?.style?.dimensions, + aspectRatio: undefined, // Reset aspect ratio when minHeight is set. + }, + } ), } ); }; diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index 3b4eac41a0d3b4..837e3834e2e1ba 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -8,9 +8,10 @@ align-items: center; padding: 1em; // Prevent the `wp-block-cover__background` span from overflowing the container when border-radius is applied. + // `overflow: hidden` is provided as a fallback for browsers that don't support `overflow: clip`. + overflow: hidden; // Use clip instead of overflow: hidden so that sticky position works on child elements. - // Use overflow-x instead of overflow so that aspect-ratio allows content to expand the area of the cover block. - overflow-x: clip; + overflow: clip; // This block has customizable padding, border-box makes that more predictable. box-sizing: border-box; // Keep the flex layout direction to the physical direction (LTR) in RTL languages. diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index d21680a2784931..1be2d577b7141b 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -97,6 +97,20 @@ const PatternEdit = ( { attributes, clientId } ) => { injectThemeAttributeInBlockTemplateContent( block ) ) ); + // If the pattern has a single block and categories, we should add the + // categories of the pattern to the block's metadata. + if ( + clonedBlocks.length === 1 && + selectedPattern.categories?.length > 0 + ) { + clonedBlocks[ 0 ].attributes = { + ...clonedBlocks[ 0 ].attributes, + metadata: { + ...clonedBlocks[ 0 ].attributes.metadata, + categories: selectedPattern.categories, + }, + }; + } const rootEditingMode = getBlockEditingMode( rootClientId ); registry.batch( () => { // Temporarily set the root block to default mode to allow replacing the pattern. diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index 0c146400cd74fb..fe4146406ddf60 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -331,7 +331,7 @@ const SiteLogo = ( { <> { setAttributes( { shouldSyncIcon: value } ); setIcon( value ? logoId : undefined ); diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index e37db3feb63d3b..b974e61e109605 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -212,7 +212,7 @@ class VideoEdit extends Component { render() { const { setAttributes, attributes, isSelected, wasBlockJustInserted } = this.props; - const { id, src, guid } = attributes; + const { id, src } = attributes; const { videoContainerHeight } = this.state; const toolbarEditButton = ( @@ -236,10 +236,7 @@ class VideoEdit extends Component { > ); - // NOTE: `guid` is not part of the block's attribute definition. This case - // handled here is a temporary fix until a we find a better approach. - const isSourcePresent = src || ( guid && id ); - if ( ! isSourcePresent ) { + if ( ! src ) { return ( { initialHtml: `
- - `, - } ); - const addVideoButton = screen.queryByText( 'Add video' ); - expect( addVideoButton ).toBeNull(); - } ); - - it( `should not render empty state when 'guid' and 'id' attributes are present`, async () => { - await initializeEditor( { - initialHtml: ` - -
-https://videopress.com/ -
`, } ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d4dfaac153f010..769d201e587d07 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,11 @@ ### Bug Fix - `Tooltip`: Explicitly set system font to avoid CSS bleed ([#59307](https://github.com/WordPress/gutenberg/pull/59307)). +- `Button`: Fix focus outline in disabled primary variant ([#59391](https://github.com/WordPress/gutenberg/pull/59391)). + +### Internal + +- `SnackbarList`, `Snackbar`: add unit tests ([#59157](https://github.com/WordPress/gutenberg/pull/59157)). ## 27.0.0 (2024-02-21) diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 590974f7705967..25095d33958976 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -84,9 +84,7 @@ outline: none; &:focus:enabled { - box-shadow: - 0 0 0 $border-width $components-color-background, - 0 0 0 3px $components-color-accent; + box-shadow: inset 0 0 0 1px $components-color-background, 0 0 0 var(--wp-admin-border-width-focus) $components-color-accent; } } diff --git a/packages/components/src/radio-group/stories/index.story.tsx b/packages/components/src/radio-group/stories/index.story.tsx index cfa0a253fb7a0b..d9cf34c93b2ed1 100644 --- a/packages/components/src/radio-group/stories/index.story.tsx +++ b/packages/components/src/radio-group/stories/index.story.tsx @@ -16,6 +16,7 @@ import { useState } from '@wordpress/element'; const meta: Meta< typeof RadioGroup > = { title: 'Components (Deprecated)/RadioGroup', + id: 'components-radiogroup', component: RadioGroup, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 subcomponents: { Radio }, diff --git a/packages/components/src/snackbar/index.tsx b/packages/components/src/snackbar/index.tsx index 48eb8ed4873801..c7a5a046a5e924 100644 --- a/packages/components/src/snackbar/index.tsx +++ b/packages/components/src/snackbar/index.tsx @@ -138,9 +138,12 @@ function UnforwardedSnackbar( className={ classes } onClick={ ! explicitDismiss ? dismissMe : undefined } tabIndex={ 0 } - role={ ! explicitDismiss ? 'button' : '' } + role={ ! explicitDismiss ? 'button' : undefined } onKeyPress={ ! explicitDismiss ? dismissMe : undefined } - aria-label={ ! explicitDismiss ? __( 'Dismiss this notice' ) : '' } + aria-label={ + ! explicitDismiss ? __( 'Dismiss this notice' ) : undefined + } + data-testid="snackbar" >
{ icon && ( diff --git a/packages/components/src/snackbar/list.tsx b/packages/components/src/snackbar/list.tsx index e423cf6c3a7f7e..13772f1f396f20 100644 --- a/packages/components/src/snackbar/list.tsx +++ b/packages/components/src/snackbar/list.tsx @@ -73,7 +73,12 @@ export function SnackbarList( { ( notice: SnackbarListProps[ 'notices' ][ number ] ) => () => onRemove?.( notice.id ); return ( -
+
{ children } { notices.map( ( notice ) => { diff --git a/packages/components/src/snackbar/stories/list.story.tsx b/packages/components/src/snackbar/stories/list.story.tsx index 5a759ddc661bfa..8548ffba1e1f09 100644 --- a/packages/components/src/snackbar/stories/list.story.tsx +++ b/packages/components/src/snackbar/stories/list.story.tsx @@ -70,7 +70,6 @@ Default.args = { }, ], content: 'Post published.', - isDismissible: true, explicitDismiss: false, }, { @@ -83,7 +82,6 @@ Default.args = { }, ], content: 'Post updated.', - isDismissible: true, explicitDismiss: false, }, { @@ -91,7 +89,6 @@ Default.args = { spokenMessage: 'All content copied.', actions: [], content: 'All content copied.', - isDismissible: true, explicitDismiss: false, }, ], diff --git a/packages/components/src/snackbar/test/index.tsx b/packages/components/src/snackbar/test/index.tsx new file mode 100644 index 00000000000000..6be4e58c928123 --- /dev/null +++ b/packages/components/src/snackbar/test/index.tsx @@ -0,0 +1,267 @@ +/** + * External dependencies + */ +import { render, screen, within } from '@testing-library/react'; +import { click } from '@ariakit/test'; + +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { SVG, Path } from '@wordpress/primitives'; + +/** + * Internal dependencies + */ +import Snackbar from '../index'; + +jest.mock( '@wordpress/a11y', () => ( { speak: jest.fn() } ) ); +const mockedSpeak = jest.mocked( speak ); + +describe( 'Snackbar', () => { + const testId = 'snackbar'; + + beforeEach( () => { + mockedSpeak.mockReset(); + } ); + + it( 'should render correctly', () => { + render( Message ); + + const snackbar = screen.getByTestId( testId ); + + expect( snackbar ).toBeVisible(); + expect( snackbar ).toHaveTextContent( 'Message' ); + } ); + + it( 'should render with an additional className', () => { + render( Message ); + + expect( screen.getByTestId( testId ) ).toHaveClass( 'gutenberg' ); + } ); + + it( 'should render with an icon', () => { + const testIcon = ( + + + + ); + + render( Message ); + + const snackbar = screen.getByTestId( testId ); + const icon = within( snackbar ).getByTestId( 'icon' ); + + expect( icon ).toBeVisible(); + } ); + + it( 'should be dismissible by clicking the snackbar', async () => { + const onRemove = jest.fn(); + const onDismiss = jest.fn(); + + render( + + Message + + ); + + const snackbar = screen.getByTestId( testId ); + + expect( snackbar ).toHaveAttribute( 'role', 'button' ); + expect( snackbar ).toHaveAttribute( + 'aria-label', + 'Dismiss this notice' + ); + + await click( snackbar ); + + expect( onRemove ).toHaveBeenCalledTimes( 1 ); + expect( onDismiss ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should not be dismissible by clicking the snackbar when the `explicitDismiss` prop is set to `true`', async () => { + const onRemove = jest.fn(); + const onDismiss = jest.fn(); + + render( + + Message + + ); + + const snackbar = screen.getByTestId( testId ); + + expect( snackbar ).not.toHaveAttribute( 'role', 'button' ); + expect( snackbar ).not.toHaveAttribute( + 'aria-label', + 'Dismiss this notice' + ); + expect( snackbar ).toHaveClass( + 'components-snackbar-explicit-dismiss' + ); + + await click( snackbar ); + + expect( onRemove ).not.toHaveBeenCalled(); + expect( onDismiss ).not.toHaveBeenCalled(); + } ); + + it( 'should be dismissible by clicking the close button when the `explicitDismiss` prop is set to `true`', async () => { + const onRemove = jest.fn(); + const onDismiss = jest.fn(); + + render( + + Message + + ); + + const snackbar = screen.getByTestId( testId ); + const closeButton = within( snackbar ).getByRole( 'button', { + name: 'Dismiss this notice', + } ); + + await click( closeButton ); + + expect( onRemove ).toHaveBeenCalledTimes( 1 ); + expect( onDismiss ).toHaveBeenCalledTimes( 1 ); + } ); + + describe( 'actions', () => { + it( 'should render only the first action with a warning when multiple actions are passed', () => { + render( + + Message + + ); + + expect( console ).toHaveWarnedWith( + 'Snackbar can only have one action. Use Notice if your message requires many actions.' + ); + + const snackbar = screen.getByTestId( testId ); + const action = within( snackbar ).getByRole( 'link' ); + + expect( action ).toBeVisible(); + expect( action ).toHaveTextContent( 'One' ); + } ); + + it( 'should be rendered as a link when the `url` prop is set', () => { + render( + + Post updated. + + ); + + const snackbar = screen.getByTestId( testId ); + const link = within( snackbar ).getByRole( 'link', { + name: 'View post', + } ); + + expect( link ).toHaveAttribute( 'href', 'https://example.com' ); + } ); + + it( 'should be rendered as a button and call `onClick` when the `onClick` prop is set', async () => { + const onClick = jest.fn(); + + render( + + Post updated. + + ); + + const snackbar = screen.getByTestId( testId ); + const button = within( snackbar ).getByRole( 'button', { + name: 'View post', + } ); + + await click( button ); + + expect( onClick ).toHaveBeenCalledTimes( 1 ); + } ); + + it( 'should be rendered as a link when the `url` prop and the `onClick` are set', () => { + render( + {}, + }, + ] } + > + Post updated. + + ); + + const snackbar = screen.getByTestId( testId ); + const link = within( snackbar ).getByRole( 'link', { + name: 'View post', + } ); + expect( link ).toBeVisible(); + } ); + } ); + + describe( 'useSpokenMessage', () => { + it( 'should speak the given message', () => { + render( FYI ); + + expect( speak ).toHaveBeenCalledWith( 'FYI', 'polite' ); + } ); + + it( 'should speak the given message by explicit politeness', () => { + render( Uh oh! ); + + expect( speak ).toHaveBeenCalledWith( 'Uh oh!', 'assertive' ); + } ); + + it( 'should coerce a message to a string', () => { + // This test assumes that `@wordpress/a11y` is capable of handling + // markup strings appropriately. + render( + + With emphasis this time. + + ); + + expect( speak ).toHaveBeenCalledWith( + 'With emphasis this time.', + 'polite' + ); + } ); + + it( 'should not re-speak an effectively equivalent element message', () => { + const { rerender } = render( + + With emphasis this time. + + ); + rerender( + + With emphasis this time. + + ); + + expect( speak ).toHaveBeenCalledTimes( 1 ); + } ); + } ); +} ); diff --git a/packages/components/src/snackbar/test/list.tsx b/packages/components/src/snackbar/test/list.tsx new file mode 100644 index 00000000000000..1b3749d42c61ef --- /dev/null +++ b/packages/components/src/snackbar/test/list.tsx @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { click } from '@ariakit/test'; + +/** + * Internal dependencies + */ +import SnackbarList from '../list'; + +window.scrollTo = jest.fn(); + +describe( 'SnackbarList', () => { + afterEach( () => { + jest.resetAllMocks(); + } ); + + it( 'should get focus after a snackbar is dismissed', async () => { + render( + {} } + /> + ); + + await click( + screen.getAllByRole( 'button', { + name: 'Dismiss this notice', + } )[ 0 ] + ); + + expect( screen.getByTestId( 'snackbar-list' ) ).toHaveFocus(); + } ); +} ); diff --git a/packages/components/src/snackbar/types.ts b/packages/components/src/snackbar/types.ts index 539c4c3ebdf65e..13ed98ade8aa7a 100644 --- a/packages/components/src/snackbar/types.ts +++ b/packages/components/src/snackbar/types.ts @@ -6,7 +6,11 @@ import type { MutableRefObject, ReactNode } from 'react'; /** * Internal dependencies */ -import type { NoticeProps, NoticeChildren } from '../notice/types'; +import type { + NoticeProps, + NoticeChildren, + NoticeAction, +} from '../notice/types'; type SnackbarOnlyProps = { /** @@ -28,8 +32,32 @@ type SnackbarOnlyProps = { listRef?: MutableRefObject< HTMLDivElement | null >; }; -export type SnackbarProps = Omit< NoticeProps, '__unstableHTML' > & - SnackbarOnlyProps; +export type SnackbarProps = Pick< + NoticeProps, + | 'className' + | 'children' + | 'spokenMessage' + | 'onRemove' + | 'politeness' + | 'onDismiss' +> & + SnackbarOnlyProps & { + /** + * An array of action objects. Each member object should contain: + * + * - `label`: `string` containing the text of the button/link + * - `url`: `string` OR `onClick`: `( event: SyntheticEvent ) => void` to specify + * what the action does. + * + * The default appearance of an action button is inferred based on whether + * `url` or `onClick` are provided, rendering the button as a link if + * appropriate. If both props are provided, `url` takes precedence, and the + * action button will render as an anchor tag. + * + * @default [] + */ + actions?: Pick< NoticeAction, 'label' | 'url' | 'onClick' >[]; + }; export type SnackbarListProps = { notices: Array< diff --git a/packages/create-block-interactive-template/README.md b/packages/create-block-interactive-template/README.md index 9ae25fba2a1417..4417c647495c4c 100644 --- a/packages/create-block-interactive-template/README.md +++ b/packages/create-block-interactive-template/README.md @@ -10,7 +10,7 @@ This block template can be used by running the following command: npx @wordpress/create-block --template @wordpress/create-block-interactive-template ``` -It requires Gutenberg 17.5 or higher. +It requires at least WordPress 6.5 or Gutenberg 17.7. ## Contributing to this package diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index 7fdc9331a2474b..979c3127b9ed52 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -64,12 +64,16 @@ const mapValues = ( obj, callback ) => ] ) ); -// Convert Map objects to plain objects -const mapToObject = ( key, state ) => { +// Convert non serializable types to plain objects +const devToolsReplacer = ( key, state ) => { if ( state instanceof Map ) { return Object.fromEntries( state ); } + if ( state instanceof window.HTMLElement ) { + return null; + } + return state; }; @@ -421,7 +425,7 @@ function instantiateReduxStore( key, options, registry, thunkArgs ) { name: key, instanceId: key, serialize: { - replacer: mapToObject, + replacer: devToolsReplacer, }, } ) ); diff --git a/packages/e2e-test-utils-playwright/src/request-utils/index.ts b/packages/e2e-test-utils-playwright/src/request-utils/index.ts index 5036f3d0e8a97c..f6818945e16936 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/index.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/index.ts @@ -93,7 +93,7 @@ class RequestUtils { }, } ); - const requestUtils = new RequestUtils( requestContext, { + const requestUtils = new this( requestContext, { user, storageState, storageStatePath, diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php index 0b8e6e1012d1a4..d1a7aa9211f105 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php @@ -15,6 +15,13 @@ array( 'clientNavigationDisabled' => true ) ); } + +if ( isset( $attributes['data'] ) ) { + wp_interactivity_state( + 'router', + array( 'data' => $attributes['data'] ) + ); +} ?>
NaN + NaN NaN - $link ) { - $i = $key += 1; - echo <<link $i - link $i with hash + +
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js index 1e137969936a09..b2d4ad0dc1ddeb 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/view.js @@ -6,14 +6,23 @@ import { store } from '@wordpress/interactivity'; const { state } = store( 'router', { state: { status: 'idle', - navigations: 0, + navigations: { + pending: 0, + count: 0, + }, timeout: 10000, + data: { + get getterProp() { + return `value from getter (${ state.data.prop1 })`; + } + } }, actions: { *navigate( e ) { e.preventDefault(); - state.navigations += 1; + state.navigations.count += 1; + state.navigations.pending += 1; state.status = 'busy'; const force = e.target.dataset.forceNavigation === 'true'; @@ -24,9 +33,9 @@ const { state } = store( 'router', { ); yield actions.navigate( e.target.href, { force, timeout } ); - state.navigations -= 1; + state.navigations.pending -= 1; - if ( state.navigations === 0 ) { + if ( state.navigations.pending === 0 ) { state.status = 'idle'; } }, diff --git a/packages/edit-post/src/components/editor-initialization/index.js b/packages/edit-post/src/components/editor-initialization/index.js index 0ba9cc863f473f..bf61d569fc81c8 100644 --- a/packages/edit-post/src/components/editor-initialization/index.js +++ b/packages/edit-post/src/components/editor-initialization/index.js @@ -10,11 +10,10 @@ import { * Data component used for initializing the editor and re-initializes * when postId changes or on unmount. * - * @param {number} postId The id of the post. * @return {null} This is a data component so does not render any ui. */ -export default function EditorInitialization( { postId } ) { - useBlockSelectionListener( postId ); - useUpdatePostLinkListener( postId ); +export default function EditorInitialization() { + useBlockSelectionListener(); + useUpdatePostLinkListener(); return null; } diff --git a/packages/edit-post/src/components/editor-initialization/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/listener-hooks.js index 73872b4d7110e4..a5534135f9cfb8 100644 --- a/packages/edit-post/src/components/editor-initialization/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/listener-hooks.js @@ -19,24 +19,19 @@ import { /** * This listener hook monitors for block selection and triggers the appropriate * sidebar state. - * - * @param {number} postId The current post id. */ -export const useBlockSelectionListener = ( postId ) => { +export const useBlockSelectionListener = () => { const { hasBlockSelection, isEditorSidebarOpened, isDistractionFree } = - useSelect( - ( select ) => { - const { get } = select( preferencesStore ); - return { - hasBlockSelection: - !! select( blockEditorStore ).getBlockSelectionStart(), - isEditorSidebarOpened: - select( STORE_NAME ).isEditorSidebarOpened(), - isDistractionFree: get( 'core', 'distractionFree' ), - }; - }, - [ postId ] - ); + useSelect( ( select ) => { + const { get } = select( preferencesStore ); + return { + hasBlockSelection: + !! select( blockEditorStore ).getBlockSelectionStart(), + isEditorSidebarOpened: + select( STORE_NAME ).isEditorSidebarOpened(), + isDistractionFree: get( 'core', 'distractionFree' ), + }; + }, [] ); const { openGeneralSidebar } = useDispatch( STORE_NAME ); @@ -49,21 +44,24 @@ export const useBlockSelectionListener = ( postId ) => { } else { openGeneralSidebar( 'edit-post/document' ); } - }, [ hasBlockSelection, isEditorSidebarOpened ] ); + }, [ + hasBlockSelection, + isDistractionFree, + isEditorSidebarOpened, + openGeneralSidebar, + ] ); }; /** * This listener hook monitors any change in permalink and updates the view * post link in the admin bar. - * - * @param {number} postId */ -export const useUpdatePostLinkListener = ( postId ) => { +export const useUpdatePostLinkListener = () => { const { newPermalink } = useSelect( ( select ) => ( { newPermalink: select( editorStore ).getCurrentPost().link, } ), - [ postId ] + [] ); const nodeToUpdate = useRef(); @@ -71,7 +69,7 @@ export const useUpdatePostLinkListener = ( postId ) => { nodeToUpdate.current = document.querySelector( VIEW_AS_PREVIEW_LINK_SELECTOR ) || document.querySelector( VIEW_AS_LINK_SELECTOR ); - }, [ postId ] ); + }, [] ); useEffect( () => { if ( ! newPermalink || ! nodeToUpdate.current ) { diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js index 5087d303fafe1e..b9fd01cd5a2242 100644 --- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js @@ -88,14 +88,14 @@ describe( 'listener hook tests', () => { } ); describe( 'useBlockSelectionListener', () => { const registry = createRegistry( mockStores ); - const TestComponent = ( { postId } ) => { - useBlockSelectionListener( postId ); + const TestComponent = () => { + useBlockSelectionListener(); return null; }; const TestedOutput = () => { return ( - + ); }; @@ -177,14 +177,14 @@ describe( 'listener hook tests', () => { describe( 'useUpdatePostLinkListener', () => { const registry = createRegistry( mockStores ); - const TestComponent = ( { postId } ) => { - useUpdatePostLinkListener( postId ); + const TestComponent = () => { + useUpdatePostLinkListener(); return null; }; - const TestedOutput = ( { postId = 10 } ) => { + const TestedOutput = () => { return ( - + ); }; @@ -222,7 +222,7 @@ describe( 'listener hook tests', () => { } ); const { rerender } = render( ); - rerender( ); + rerender( ); expect( mockSelector ).toHaveBeenCalledTimes( 1 ); act( () => { diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index f179ee6156e63e..fd44d3dae4ca4a 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -96,7 +96,7 @@ function Editor( { > - + diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index b9fe954e79ad55..fd0e730945ece1 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -538,7 +538,7 @@ export const isEditingTemplate = createRegistrySelector( ( select ) => () => { since: '6.5', alternative: `select( 'core/editor' ).getRenderingMode`, } ); - return select( editorStore ).getCurrentPostType() !== 'post-only'; + return select( editorStore ).getCurrentPostType() === 'wp_template'; } ); /** diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 01bc4cdfa2ddfc..a2b146d8dc95db 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -26,10 +26,9 @@ import { const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); function EditorCanvas( { enableResizing, settings, children, ...props } ) { - const { hasBlocks, isFocusMode, templateType, canvasMode, isZoomOutMode } = - useSelect( ( select ) => { - const { getBlockCount, __unstableGetEditorMode } = - select( blockEditorStore ); + const { hasBlocks, isFocusMode, templateType, canvasMode } = useSelect( + ( select ) => { + const { getBlockCount } = select( blockEditorStore ); const { getEditedPostType, getCanvasMode } = unlock( select( editSiteStore ) ); @@ -38,11 +37,12 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { return { templateType: _templateType, isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', canvasMode: getCanvasMode(), hasBlocks: !! getBlockCount(), }; - }, [] ); + }, + [] + ); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const [ isFocused, setIsFocused ] = useState( false ); @@ -52,8 +52,11 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { } }, [ canvasMode ] ); - const viewModeProps = { - 'aria-label': __( 'Editor Canvas' ), + // In view mode, make the canvas iframe be perceived and behave as a button + // to switch to edit mode, with a meaningful label and no title attribute. + const viewModeIframeProps = { + 'aria-label': __( 'Edit' ), + title: null, role: 'button', tabIndex: 0, onFocus: () => setIsFocused( true ), @@ -107,9 +110,7 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { renderAppender={ showBlockAppender } styles={ styles } iframeProps={ { - expand: isZoomOutMode, - scale: isZoomOutMode ? 0.45 : undefined, - frameSize: isZoomOutMode ? 100 : undefined, + shouldZoom: true, className: classnames( 'edit-site-visual-editor__editor-canvas', { @@ -117,7 +118,7 @@ function EditorCanvas( { enableResizing, settings, children, ...props } ) { } ), ...props, - ...( canvasMode === 'view' ? viewModeProps : {} ), + ...( canvasMode === 'view' ? viewModeIframeProps : {} ), } } > { children } diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index 3d042f612f29ed..fa7fd7e13df3e4 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -22,7 +22,6 @@ position: relative; height: 100%; display: block; - overflow: hidden; background-color: $gray-300; // Centralize the editor horizontally (flex-direction is column). align-items: center; @@ -62,8 +61,6 @@ .components-resizable-box__container { margin: 0 auto; - // Removing this will cancel the bottom margins in the iframe. - overflow: auto; } &.is-view-mode { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 35276b0ad8b2b4..8da33192859f14 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -162,16 +162,6 @@ function FontLibraryProvider( { children } ) { // Demo const [ loadedFontUrls ] = useState( new Set() ); - // Theme data - const { site, currentTheme } = useSelect( ( select ) => { - return { - site: select( coreStore ).getSite(), - currentTheme: select( coreStore ).getCurrentTheme(), - }; - } ); - const themeUrl = - site?.url + '/wp-content/themes/' + currentTheme?.stylesheet; - const getAvailableFontsOutline = ( availableFontFamilies ) => { const outline = availableFontFamilies.reduce( ( acc, font ) => { const availableFontFaces = @@ -416,7 +406,7 @@ function FontLibraryProvider( { children } ) { // If the font doesn't have a src, don't load it. if ( ! fontFace.src ) return; // Get the src of the font. - const src = getDisplaySrcFromFontFace( fontFace.src, themeUrl ); + const src = getDisplaySrcFromFontFace( fontFace.src ); // If the font is already loaded, don't load it again. if ( ! src || loadedFontUrls.has( src ) ) return; // Load the font in the browser. diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 583c19c7aeb967..f22a2f15693f85 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -100,7 +100,10 @@ function InstalledFonts() { ) } - + + { notice && ( diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 011f09b12a841f..1458b47cd010a3 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -121,7 +121,13 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) { } } -export function getDisplaySrcFromFontFace( input, urlPrefix ) { +/** + * Retrieves the display source from a font face src. + * + * @param {string|string[]} input - The font face src. + * @return {string|undefined} The display source or undefined if the input is invalid. + */ +export function getDisplaySrcFromFontFace( input ) { if ( ! input ) { return; } @@ -132,9 +138,9 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { } else { src = input; } - // If it is a theme font, we need to make the url absolute - if ( src.startsWith( 'file:.' ) && urlPrefix ) { - src = src.replace( 'file:.', urlPrefix ); + // It's expected theme fonts will already be loaded in the browser. + if ( src.startsWith( 'file:.' ) ) { + return; } if ( ! isUrlEncoded( src ) ) { src = encodeURI( src ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js index 9c6235443a0992..3cbdc0283f1a9e 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getDisplaySrcFromFontFace.spec.js @@ -21,33 +21,22 @@ describe( 'getDisplaySrcFromFontFace', () => { ); } ); - it( 'makes URL absolute when it starts with file:. and urlPrefix is given', () => { - const input = 'file:./font1'; - const urlPrefix = 'http://example.com'; - expect( getDisplaySrcFromFontFace( input, urlPrefix ) ).toBe( - 'http://example.com/font1' - ); - } ); - - it( 'does not modify URL if it does not start with file:.', () => { - const input = [ 'http://some-other-place.com/font1' ]; - const urlPrefix = 'http://example.com'; - expect( getDisplaySrcFromFontFace( input, urlPrefix ) ).toBe( - 'http://some-other-place.com/font1' - ); + it( 'return undefined when the url starts with file:', () => { + const input = 'file:./theme/assets/font1.ttf'; + expect( getDisplaySrcFromFontFace( input ) ).toBe( undefined ); } ); it( 'encodes the URL if it is not encoded', () => { - const input = 'file:./assets/font one with spaces.ttf'; + const input = 'https://example.org/font one with spaces.ttf'; expect( getDisplaySrcFromFontFace( input ) ).toBe( - 'file:./assets/font%20one%20with%20spaces.ttf' + 'https://example.org/font%20one%20with%20spaces.ttf' ); } ); it( 'does not encode the URL if it is already encoded', () => { - const input = 'file:./font%20one'; + const input = 'https://example.org/fonts/font%20one.ttf'; expect( getDisplaySrcFromFontFace( input ) ).toBe( - 'file:./font%20one' + 'https://example.org/fonts/font%20one.ttf' ); } ); } ); diff --git a/packages/edit-site/src/components/global-styles/screen-typography.js b/packages/edit-site/src/components/global-styles/screen-typography.js index f76dc6fb381004..40e2ab08320b75 100644 --- a/packages/edit-site/src/components/global-styles/screen-typography.js +++ b/packages/edit-site/src/components/global-styles/screen-typography.js @@ -9,7 +9,7 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import TypographyElements from './typogrphy-elements'; +import TypographyElements from './typography-elements'; import FontFamilies from './font-families'; import ScreenHeader from './header'; diff --git a/packages/edit-site/src/components/global-styles/typogrphy-elements.js b/packages/edit-site/src/components/global-styles/typography-elements.js similarity index 100% rename from packages/edit-site/src/components/global-styles/typogrphy-elements.js rename to packages/edit-site/src/components/global-styles/typography-elements.js diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index fcb0a74b0b3b88..d15be016173b03 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -70,6 +70,7 @@ export default function Layout() { const { isDistractionFree, + isZoomOutMode, hasFixedToolbar, hasBlockSelected, canvasMode, @@ -96,6 +97,9 @@ export default function Layout() { 'core', 'distractionFree' ), + isZoomOutMode: + select( blockEditorStore ).__unstableGetEditorMode() === + 'zoom-out', hasBlockSelected: select( blockEditorStore ).getBlockSelectionStart(), }; @@ -172,6 +176,7 @@ export default function Layout() { 'is-full-canvas': canvasMode === 'edit', 'has-fixed-toolbar': hasFixedToolbar, 'is-block-toolbar-visible': hasBlockSelected, + 'is-zoom-out': isZoomOutMode, } ) } > diff --git a/packages/edit-site/src/components/page-patterns/index.js b/packages/edit-site/src/components/page-patterns/index.js index 9cf02dbd3e2ab4..ed6d60734c9cc8 100644 --- a/packages/edit-site/src/components/page-patterns/index.js +++ b/packages/edit-site/src/components/page-patterns/index.js @@ -210,6 +210,26 @@ function Title( { item, categoryId } ) { } return ( + + { item.type === PATTERN_TYPES.theme ? ( + item.title + ) : ( + + ) } + { itemIcon && ! isNonUserPattern && ( ) } - - { item.type === PATTERN_TYPES.theme ? ( - item.title - ) : ( - - ) } - ); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/edit-button.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/edit-button.js deleted file mode 100644 index 962062a96da743..00000000000000 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/edit-button.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { pencil } from '@wordpress/icons'; -/** - * Internal dependencies - */ -import SidebarButton from '../sidebar-button'; -import { useLink } from '../routes/link'; -import { NAVIGATION_POST_TYPE } from '../../utils/constants'; - -export default function EditButton( { postId } ) { - const linkInfo = useLink( { - postId, - postType: NAVIGATION_POST_TYPE, - canvas: 'edit', - } ); - return ( - - ); -} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js index 960e0363f2e588..e6348531516f66 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/single-navigation-menu.js @@ -10,7 +10,6 @@ import { SidebarNavigationScreenWrapper } from '../sidebar-navigation-screen-nav import ScreenNavigationMoreMenu from './more-menu'; import NavigationMenuEditor from './navigation-menu-editor'; import buildNavigationLabel from '../sidebar-navigation-screen-navigation-menus/build-navigation-label'; -import EditButton from './edit-button'; export default function SingleNavigationMenu( { navigationMenu, @@ -30,7 +29,6 @@ export default function SingleNavigationMenu( { onSave={ handleSave } onDuplicate={ handleDuplicate } /> - } title={ buildNavigationLabel( diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js index 2188fade413dbb..7c120133c674ee 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates-browse/content.js @@ -31,7 +31,7 @@ function TemplateDataviewItem( { template, isActive } ) { export default function DataviewsTemplatesSidebarContent( { activeView, postType, - config, + title, } ) { const { records } = useEntityRecords( 'postType', postType, { per_page: -1, @@ -54,7 +54,7 @@ export default function DataviewsTemplatesSidebarContent( { } /> diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index 7cf049282618b3..aea839840dda99 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -74,7 +74,7 @@ function SidebarScreens() { } backPath="/page" /> diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index c86efc10eafb3c..8b6ba0093b4dc3 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -98,7 +98,7 @@ body.js.site-editor-php { } .interface-interface-skeleton__content { - background-color: $gray-900; + background-color: $gray-300; } } diff --git a/packages/editor/src/components/commands/index.js b/packages/editor/src/components/commands/index.js index eb97e8d10b93a8..f1de29af690d01 100644 --- a/packages/editor/src/components/commands/index.js +++ b/packages/editor/src/components/commands/index.js @@ -25,9 +25,12 @@ function useEditorCommandLoader() { isFocusMode, isPreviewMode, isViewable, + isCodeEditingEnabled, + isRichEditingEnabled, } = useSelect( ( select ) => { const { get } = select( preferencesStore ); - const { isListViewOpened, getCurrentPostType } = select( editorStore ); + const { isListViewOpened, getCurrentPostType, getEditorSettings } = + select( editorStore ); const { getSettings } = select( blockEditorStore ); const { getPostType } = select( coreStore ); @@ -40,6 +43,8 @@ function useEditorCommandLoader() { isTopToolbar: get( 'core', 'fixedToolbar' ), isPreviewMode: getSettings().__unstableIsPreviewMode, isViewable: getPostType( getCurrentPostType() )?.viewable ?? false, + isCodeEditingEnabled: getEditorSettings().codeEditingEnabled, + isRichEditingEnabled: getEditorSettings().richEditingEnabled, }; }, [] ); const { toggle } = useDispatch( preferencesStore ); @@ -51,6 +56,7 @@ function useEditorCommandLoader() { toggleDistractionFree, } = useDispatch( editorStore ); const { getCurrentPostId } = useSelect( editorStore ); + const allowSwitchEditorMode = isCodeEditingEnabled && isRichEditingEnabled; if ( isPreviewMode ) { return { commands: [], isLoading: false }; @@ -141,18 +147,20 @@ function useEditorCommandLoader() { }, } ); - commands.push( { - name: 'core/toggle-code-editor', - label: - editorMode === 'visual' - ? __( 'Open code editor' ) - : __( 'Exit code editor' ), - icon: code, - callback: ( { close } ) => { - switchEditorMode( editorMode === 'visual' ? 'text' : 'visual' ); - close(); - }, - } ); + if ( allowSwitchEditorMode ) { + commands.push( { + name: 'core/toggle-code-editor', + label: + editorMode === 'visual' + ? __( 'Open code editor' ) + : __( 'Exit code editor' ), + icon: code, + callback: ( { close } ) => { + switchEditorMode( editorMode === 'visual' ? 'text' : 'visual' ); + close(); + }, + } ); + } commands.push( { name: 'core/toggle-breadcrumbs', diff --git a/packages/editor/src/components/provider/constants.js b/packages/editor/src/components/provider/constants.js deleted file mode 100644 index a81b2fd37563af..00000000000000 --- a/packages/editor/src/components/provider/constants.js +++ /dev/null @@ -1,5 +0,0 @@ -export const PAGE_CONTENT_BLOCK_TYPES = [ - 'core/post-title', - 'core/post-featured-image', - 'core/post-content', -]; 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 48a8119350d78f..fd4722ebe40f4d 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 @@ -2,39 +2,46 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { - useBlockEditingMode, - store as blockEditorStore, -} from '@wordpress/block-editor'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; -/** - * Internal dependencies - */ -import { PAGE_CONTENT_BLOCK_TYPES } from './constants'; +const PAGE_CONTENT_BLOCKS = [ + 'core/post-title', + 'core/post-featured-image', + 'core/post-content', +]; + +function useDisableNonPageContentBlocks() { + const contentIds = useSelect( ( select ) => { + const { getBlocksByName, getBlockParents, getBlockName } = + select( blockEditorStore ); + return getBlocksByName( PAGE_CONTENT_BLOCKS ).filter( ( clientId ) => + getBlockParents( clientId ).every( ( parentClientId ) => { + const parentBlockName = getBlockName( parentClientId ); + return ( + parentBlockName !== 'core/query' && + ! PAGE_CONTENT_BLOCKS.includes( parentBlockName ) + ); + } ) + ); + }, [] ); -function DisableBlock( { clientId } ) { - const isDescendentOfQueryLoop = useSelect( - ( select ) => { - const { getBlockParentsByBlockName } = select( blockEditorStore ); - return ( - getBlockParentsByBlockName( clientId, 'core/query' ).length !== - 0 - ); - }, - [ clientId ] - ); - const mode = isDescendentOfQueryLoop ? undefined : 'contentOnly'; const { setBlockEditingMode, unsetBlockEditingMode } = useDispatch( blockEditorStore ); + useEffect( () => { - if ( mode ) { - setBlockEditingMode( clientId, mode ); - return () => { - unsetBlockEditingMode( clientId ); - }; + setBlockEditingMode( '', 'disabled' ); // Disable editing at the root level. + + for ( const contentId of contentIds ) { + setBlockEditingMode( contentId, 'contentOnly' ); // Re-enable each content block. } - }, [ clientId, mode, setBlockEditingMode, unsetBlockEditingMode ] ); + return () => { + unsetBlockEditingMode( '' ); + for ( const contentId of contentIds ) { + unsetBlockEditingMode( contentId ); + } + }; + }, [ contentIds, setBlockEditingMode, unsetBlockEditingMode ] ); } /** @@ -42,14 +49,5 @@ function DisableBlock( { clientId } ) { * page content to be edited. */ export default function DisableNonPageContentBlocks() { - useBlockEditingMode( 'disabled' ); - const clientIds = useSelect( ( select ) => { - return select( blockEditorStore ).getBlocksByName( - PAGE_CONTENT_BLOCK_TYPES - ); - }, [] ); - - return clientIds.map( ( clientId ) => { - return ; - } ); + useDisableNonPageContentBlocks(); } diff --git a/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js new file mode 100644 index 00000000000000..d76530828a7999 --- /dev/null +++ b/packages/editor/src/components/provider/test/disable-non-page-content-blocks.js @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; + +/** + * WordPress dependencies + */ +import { createRegistry, RegistryProvider } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import DisableNonPageContentBlocks from '../disable-non-page-content-blocks'; + +describe( 'DisableNonPageContentBlocks', () => { + it( 'disables page content blocks', () => { + const testBlocks = { + 0: 'core/template-part', + /**/ '00': 'core/site-title', + /**/ '01': 'core/navigation', + 1: 'core/group', + /**/ 10: 'core/post-title', + /**/ 11: 'core/post-featured-image', + /**/ 12: 'core/post-content', + /**/ /**/ 120: 'core/paragraph', + /**/ /**/ 121: 'core/post-featured-image', + 2: 'core/query', + /**/ 20: 'core/post-title', + /**/ 21: 'core/post-featured-image', + /**/ 22: 'core/post-content', + 3: 'core/template-part', + /**/ 30: 'core/paragraph', + }; + + const setBlockEditingMode = jest.fn( () => ( { + type: 'SET_BLOCK_EDITING_MODE', + } ) ); + const unsetBlockEditingMode = jest.fn( () => ( { + type: 'UNSET_BLOCK_EDITING_MODE', + } ) ); + + const registry = createRegistry( { + 'core/block-editor': { + reducer: () => {}, + selectors: { + getBlocksByName( state, blockNames ) { + return Object.keys( testBlocks ).filter( ( clientId ) => + blockNames.includes( testBlocks[ clientId ] ) + ); + }, + getBlockParents( state, clientId ) { + return clientId.slice( 0, -1 ).split( '' ); + }, + getBlockName( state, clientId ) { + return testBlocks[ clientId ]; + }, + }, + actions: { + setBlockEditingMode, + unsetBlockEditingMode, + }, + }, + } ); + + const { unmount } = render( + + + + ); + + expect( setBlockEditingMode.mock.calls ).toEqual( [ + [ '', 'disabled' ], // root + [ '10', 'contentOnly' ], // post-title + [ '11', 'contentOnly' ], // post-featured-image + [ '12', 'contentOnly' ], // post-content + // NOT the post-featured-image nested within post-content + // NOT any of the content blocks within query + ] ); + + unmount(); + + expect( unsetBlockEditingMode.mock.calls ).toEqual( [ + [ '' ], // root + [ '10' ], // post-title + [ '11' ], // post-featured-image + [ '12' ], // post-content + ] ); + } ); +} ); diff --git a/packages/element/README.md b/packages/element/README.md index 5636fdda56a525..9ebf2a632d5019 100755 --- a/packages/element/README.md +++ b/packages/element/README.md @@ -247,7 +247,7 @@ This is the same concept as the React Native implementation. _Related_ -- Here is an example of how to use the select method: +- Here is an example of how to use the select method: _Usage_ diff --git a/packages/element/src/platform.js b/packages/element/src/platform.js index c646b6c86d51a2..841cd06e4cabb5 100644 --- a/packages/element/src/platform.js +++ b/packages/element/src/platform.js @@ -17,7 +17,7 @@ const Platform = { * * This is the same concept as the React Native implementation. * - * @see https://facebook.github.io/react-native/docs/platform-specific-code#platform-module + * @see https://reactnative.dev/docs/platform-specific-code#platform-module * * Here is an example of how to use the select method: * @example diff --git a/packages/html-entities/README.md b/packages/html-entities/README.md index 211215c50a451b..1133c7df156194 100644 --- a/packages/html-entities/README.md +++ b/packages/html-entities/README.md @@ -23,6 +23,8 @@ Decodes the HTML entities from a given string. _Usage_ ```js +import { decodeEntities } from '@wordpress/html-entities'; + const result = decodeEntities( 'á' ); console.log( result ); // result will be "á" ``` diff --git a/packages/html-entities/src/index.js b/packages/html-entities/src/index.js index 503d6f69daf82e..1df4ec41484bdd 100644 --- a/packages/html-entities/src/index.js +++ b/packages/html-entities/src/index.js @@ -8,6 +8,8 @@ let _decodeTextArea; * * @example * ```js + * import { decodeEntities } from '@wordpress/html-entities'; + * * const result = decodeEntities( 'á' ); * console.log( result ); // result will be "á" * ``` diff --git a/packages/interactivity-router/CHANGELOG.md b/packages/interactivity-router/CHANGELOG.md index 72a9dd459a688c..799425e4cd9d51 100644 --- a/packages/interactivity-router/CHANGELOG.md +++ b/packages/interactivity-router/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Fix navigate() issues related to initial state merges. ([#57134](https://github.com/WordPress/gutenberg/pull/57134)) + ## 1.2.0 (2024-02-21) ## 1.1.0 (2024-02-09) diff --git a/packages/interactivity-router/src/index.js b/packages/interactivity-router/src/index.js index 724a2660df41dc..03d399338167ce 100644 --- a/packages/interactivity-router/src/index.js +++ b/packages/interactivity-router/src/index.js @@ -3,10 +3,18 @@ */ import { store, privateApis, getConfig } from '@wordpress/interactivity'; -const { directivePrefix, getRegionRootFragment, initialVdom, toVdom, render } = - privateApis( - 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.' - ); +const { + directivePrefix, + getRegionRootFragment, + initialVdom, + toVdom, + render, + parseInitialData, + populateInitialData, + batch, +} = privateApis( + 'I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.' +); // The cache of visited and prefetched pages. const pages = new Map(); @@ -45,20 +53,24 @@ const regionsToVdom = ( dom, { vdom } = {} ) => { : toVdom( region ); } ); const title = dom.querySelector( 'title' )?.innerText; - return { regions, title }; + const initialData = parseInitialData( dom ); + return { regions, title, initialData }; }; // Render all interactive regions contained in the given page. const renderRegions = ( page ) => { - const attrName = `data-${ directivePrefix }-router-region`; - document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { - const id = region.getAttribute( attrName ); - const fragment = getRegionRootFragment( region ); - render( page.regions[ id ], fragment ); + batch( () => { + populateInitialData( page.initialData ); + const attrName = `data-${ directivePrefix }-router-region`; + document.querySelectorAll( `[${ attrName }]` ).forEach( ( region ) => { + const id = region.getAttribute( attrName ); + const fragment = getRegionRootFragment( region ); + render( page.regions[ id ], fragment ); + } ); + if ( page.title ) { + document.title = page.title; + } } ); - if ( page.title ) { - document.title = page.title; - } }; /** @@ -176,7 +188,11 @@ export const { state, actions } = store( 'core/router', { // out, and let the newer execution to update the HTML. if ( navigatingTo !== href ) return; - if ( page ) { + if ( + page && + ! page.initialData?.config?.[ 'core/router' ] + ?.clientNavigationDisabled + ) { renderRegions( page ); window.history[ options.replace ? 'replaceState' : 'pushState' diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 8e48ead8429d3b..1e81760b8d05c1 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fixes + +- Prevent passing state proxies as receivers to deepSignal proxy handlers. ([#57134](https://github.com/WordPress/gutenberg/pull/57134)) + ## 5.1.0 (2024-02-21) ### Bug Fixes diff --git a/packages/interactivity/README.md b/packages/interactivity/README.md index 6c1e98d0115564..a6b2a7fb6f3dc0 100644 --- a/packages/interactivity/README.md +++ b/packages/interactivity/README.md @@ -17,7 +17,7 @@ These Core blocks are already powered by the API: > **Note** > This step is only required if you are using this API outside of WordPress. > -> Within WordPress, the package is already bundled in Core, so all you need to do to ensure it is loaded, by adding `wp-interactivity` to the dependency array of the module script. +> Within WordPress, the package is already bundled in Core, so all you need to do to ensure it is loaded, by adding `@wordpress/interactivity` to the dependency array of the script module. > >This happens automatically when you use the dependency extraction Webpack plugin that is used in tools like wp-scripts. diff --git a/packages/interactivity/docs/api-reference.md b/packages/interactivity/docs/api-reference.md index b755b124646679..319a3974430140 100644 --- a/packages/interactivity/docs/api-reference.md +++ b/packages/interactivity/docs/api-reference.md @@ -1,52 +1,23 @@ # API Reference -> **Note** -> Interactivity API is only available for WordPress 6.5 and above. +
+Interactivity API is only available for WordPress 6.5 and above. +
-To add interactivity to blocks using the Interactivity API, developers can use: +To add interactions to blocks using the Interactivity API, developers can use: -- **Directives** - added to the markup to add specific behavior to the DOM elements of the block. -- **Store** - that contains the logic and data (state, actions, or side effects, among others) needed for the behavior. +- **Directives:** Added to the markup to add specific behavior to the DOM elements of the block +- **Store:** Contains the logic and data (state, actions, side effects, etc.) needed for the behavior DOM elements are connected to data stored in the state and context through directives. If data in the state or context change directives will react to those changes, updating the DOM accordingly (see [diagram](https://excalidraw.com/#json=T4meh6lltJh6TCX51NTIu,DmIhxYSGFTL_ywZFbsmuSw)). ![State & Directives](https://make.wordpress.org/core/files/2024/02/interactivity-state-directives.png) -## Table of Contents - -- [The directives](#the-directives) - - [List of Directives](#list-of-directives) - - [`wp-interactive`](#wp-interactive) ![](https://img.shields.io/badge/DECLARATIVE-afd2e3.svg) - - [`wp-context`](#wp-context) ![](https://img.shields.io/badge/STATE-afd2e3.svg) - - [`wp-bind`](#wp-bind) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) - - [`wp-class`](#wp-class) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) - - [`wp-style`](#wp-style) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) - - [`wp-text`](#wp-text) ![](https://img.shields.io/badge/CONTENT-afd2e3.svg) - - [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) - - [`wp-on-window`](#wp-on-window) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) - - [`wp-on-document`](#wp-on-document) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) - - [`wp-watch`](#wp-watch) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - - [`wp-init`](#wp-init) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - - [`wp-run`](#wp-run) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - - [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) - - [`wp-each`](#wp-each) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) - - [Values of directives are references to store properties](#values-of-directives-are-references-to-store-properties) -- [The store](#the-store) - - [Elements of the store](#elements-of-the-store) - - [State](#state) - - [Actions](#actions) - - [Side Effects](#side-effects) - - [Setting the store](#setting-the-store) - - [On the client side](#on-the-client-side) - - [On the server side](#on-the-server-side) - -## The directives +## What are directives? Directives are custom attributes that are added to the markup of your block to add behavior to its DOM elements. This can be done in the `render.php` file (for dynamic blocks) or the `save.js` file (for static blocks). -Interactivity API directives use the `data-` prefix. - -_Example of directives used in the HTML markup_ +Interactivity API directives use the `data-` prefix. Here's an example of directives used in HTML markup. ```html
I'm also interactive, and I can also use directives!

-```html +
**Note** > The use of `data-wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `data-wp-interactive` has not been added for the sake of simplicity. Also, the `data-wp-interactive` directive will be injected automatically in the future. -#### `wp-context` +### `wp-context` It provides a **local** state available to a specific HTML node and its children. The `wp-context` directive accepts a stringified JSON as a value. -_Example of `wp-context` directive_ - ```php //render.php
@@ -135,9 +104,7 @@ store( "myPlugin", { }, } ); ``` - -
Different contexts can be defined at different levels, and deeper levels will merge their own context with any parent one: @@ -156,13 +123,9 @@ Different contexts can be defined at different levels, and deeper levels will me
``` -#### `wp-bind` - -It allows setting HTML attributes on elements based on a boolean or string value. - -> This directive follows the syntax `data-wp-bind--attribute`. +### `wp-bind` -_Example of `wp-bind` directive_ +This directive allows setting HTML attributes on elements based on a boolean or string value. It follows the syntax `data-wp-bind--attribute`. ```html
  • @@ -194,34 +157,28 @@ store( "myPlugin", { }, } ); ``` - -
    The `wp-bind` directive is executed: -- When the element is created. -- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). +- When the element is created +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference) When `wp-bind` directive references a callback to get its final value: - The `wp-bind` directive will be executed each time there's a change on any of the properties of the `state` or `context` used inside this callback. - The returned value in the callback function is used to change the value of the associated attribute. -The `wp-bind` will do different things over the DOM element is applied, depending on its value: - - - If the value is `true`, the attribute is added: `
    `. - - If the value is `false`, the attribute is removed: `
    `. - - If the value is a string, the attribute is added with its value assigned: `
    `. +The `wp-bind` will do different things when the DOM element is applied, depending on its value: -#### `wp-class` + - If the value is `true`, the attribute is added: `
    ` + - If the value is `false`, the attribute is removed: `
    ` + - If the value is a string, the attribute is added with its value assigned: `
    ` -It adds or removes a class to an HTML element, depending on a boolean value. +### `wp-class` -> This directive follows the syntax `data-wp-class--classname`. - -_Example of `wp-class` directive_ +This directive adds or removes a class to an HTML element, depending on a boolean value. It follows the syntax `data-wp-class--classname`. ```html
    @@ -255,26 +212,20 @@ store( "myPlugin", { } } ); ``` - -
    The `wp-class` directive is executed: -- When the element is created. -- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). +- When the element is created +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference) When `wp-class` directive references a callback to get its final boolean value, the callback receives the class name: `className`. The boolean value received by the directive is used to toggle (add when `true` or remove when `false`) the associated class name from the `class` attribute. -#### `wp-style` +### `wp-style` -It adds or removes inline style to an HTML element, depending on its value. - -> This directive follows the syntax `data-wp-style--css-property`. - -_Example of `wp-style` directive_ +This directive adds or removes inline style to an HTML element, depending on its value. It follows the syntax `data-wp-style--css-property`. ```html
    @@ -297,23 +248,21 @@ store( "myPlugin", { }, } ); ``` - -
    The `wp-style` directive is executed: -- When the element is created. -- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). +- When the element is created +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference) When `wp-style` directive references a callback to get its final value, the callback receives the class style property: `css-property`. -The value received by the directive is used to add or remove the style attribute with the associated CSS property: : +The value received by the directive is used to add or remove the style attribute with the associated CSS property: -- If the value is `false`, the style attribute is removed: `
    `. -- If the value is a string, the attribute is added with its value assigned: `
    `. +- If the value is `false`, the style attribute is removed: `
    ` +- If the value is a string, the attribute is added with its value assigned: `
    ` -#### `wp-text` +### `wp-text` It sets the inner text of an HTML element. @@ -339,24 +288,18 @@ store( "myPlugin", { }, } ); ``` - -
    The `wp-text` directive is executed: -- When the element is created. -- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). +- When the element is created +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference) The returned value is used to change the inner content of the element: `
    value
    `. -#### `wp-on` +### `wp-on` -It runs code on dispatched DOM events like `click` or `keyup`. - -> The syntax of this directive is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`). - -_Example of `wp-on` directive_ +This directive runs code on dispatched DOM events like `click` or `keyup`. The syntax is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`). ```php