diff --git a/changelog.txt b/changelog.txt index 75cfddbf5612e8..fbb5fe66bfdcea 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,23 +1,6 @@ == Changelog == -= 17.8.0-rc.4 = - -## Changelog - -### Tools - -#### Testing -Fix failing Dropdown Menu e2e tests. ([59356](https://github.com/WordPress/gutenberg/pull/59356)) - - -## Contributors - -The following contributors merged PRs in this release: - -@Mamaduka - - -= 17.8.0-rc.3 = += 17.8.0 = ## Changelog @@ -70,10 +53,7 @@ The following contributors merged PRs in this release: - 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)) +- Cover Block: Restore overflow: Clip rule to allow border radius again. ([59388](https://github.com/WordPress/gutenberg/pull/59388)) #### List View - Editor: Do not open list view by default on mobile. ([59016](https://github.com/WordPress/gutenberg/pull/59016)) @@ -100,6 +80,10 @@ The following contributors merged PRs in this release: - 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 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)) + #### 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)) @@ -129,9 +113,9 @@ The following contributors merged PRs in this release: #### 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)) +- Font Library: 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)) +- Font Library: 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)) @@ -142,7 +126,6 @@ The following contributors merged PRs in this release: - 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)) @@ -262,6 +245,7 @@ The following contributors merged PRs in this release: - 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)) +- Fix failing Dropdown Menu e2e tests. ([59356](https://github.com/WordPress/gutenberg/pull/59356)) #### Build Tooling - Add test:e2e:playwright:debug command to debug Playwright tests. ([58808](https://github.com/WordPress/gutenberg/pull/58808)) @@ -277,510 +261,6 @@ 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 - -### 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)) - -#### 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)) - -#### 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)) - -#### 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)) - -#### 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 @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.1 = - -## 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)) - -#### 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)) - -#### 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)) - -#### 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)) - -#### 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)) - -#### 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 @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.7.0 = diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index a385c1ced9e43c..b287d574e56b45 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 16.8-17.7 | 6.5 | | 16.2-16.7 | 6.4.3 | | 16.2-16.7 | 6.4.2 | | 16.2-16.7 | 6.4.1 | 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 0b800757b4ecd0..2c66b227acf04a 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -33,11 +33,12 @@ Setting that enables the following UI tools: - background: backgroundImage, backgroundSize - border: color, radius, style, width -- color: link +- color: link, heading, button, caption - dimensions: aspectRatio, minHeight - position: sticky - spacing: blockGap, margin, padding - typography: lineHeight +- shadow: defaultPresets --- @@ -203,6 +204,19 @@ Generate custom CSS custom properties of the form `--wp--custom--{key}--{nested- ## Styles +### background + +Background styles + +| Property | Type | Props | +| --- | --- |--- | +| backgroundImage | string, object | | +| backgroundPosition | string, object | | +| backgroundRepeat | string, object | | +| backgroundSize | string, object | | + +--- + ### border Border styles. diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index 4b5f5614d64c9f..64a9dbaf7f2774 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -30,6 +30,28 @@ function gutenberg_register_background_support( $block_type ) { } } +/** + * Given a theme.json or block background styles, returns the background styles for a block. + * + * @since 6.6.0 + * + * @param array $background_styles Background style properties. + * @return array Style engine array of CSS string and style declarations. + */ +function gutenberg_get_background_support_styles( $background_styles = array() ) { + $background_image_source = isset( $background_styles['backgroundImage']['source'] ) ? $background_styles['backgroundImage']['source'] : null; + $background_styles['backgroundSize'] = ! empty( $background_styles['backgroundSize'] ) ? $background_styles['backgroundSize'] : 'cover'; + + if ( 'file' === $background_image_source && ! empty( $background_styles['backgroundImage']['url'] ) ) { + // If the background size is set to `contain` and no position is set, set the position to `center`. + if ( 'contain' === $background_styles['backgroundSize'] && ! isset( $background_styles['backgroundPosition'] ) ) { + $background_styles['backgroundPosition'] = 'center'; + } + } + + return gutenberg_style_engine_get_styles( array( 'background' => $background_styles ) ); +} + /** * Renders the background styles to the block wrapper. * This block support uses the `render_block` hook to ensure that @@ -46,38 +68,13 @@ function gutenberg_render_background_support( $block_content, $block ) { if ( ! $has_background_image_support || - wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ) + wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ) || + ! isset( $block_attributes['style']['background'] ) ) { return $block_content; } - $background_image_source = $block_attributes['style']['background']['backgroundImage']['source'] ?? null; - $background_image_url = $block_attributes['style']['background']['backgroundImage']['url'] ?? null; - $background_size = $block_attributes['style']['background']['backgroundSize'] ?? 'cover'; - $background_position = $block_attributes['style']['background']['backgroundPosition'] ?? null; - $background_repeat = $block_attributes['style']['background']['backgroundRepeat'] ?? null; - - $background_block_styles = array(); - - if ( - 'file' === $background_image_source && - $background_image_url - ) { - // Set file based background URL. - // TODO: In a follow-up, similar logic could be added to inject a featured image url. - $background_block_styles['backgroundImage']['url'] = $background_image_url; - // Only output the background size and repeat when an image url is set. - $background_block_styles['backgroundSize'] = $background_size; - $background_block_styles['backgroundRepeat'] = $background_repeat; - $background_block_styles['backgroundPosition'] = $background_position; - - // If the background size is set to `contain` and no position is set, set the position to `center`. - if ( 'contain' === $background_size && ! isset( $background_position ) ) { - $background_block_styles['backgroundPosition'] = 'center'; - } - } - - $styles = gutenberg_style_engine_get_styles( array( 'background' => $background_block_styles ) ); + $styles = gutenberg_get_background_support_styles( $block_attributes['style']['background'] ); if ( ! empty( $styles['css'] ) ) { // Inject background styles to the first element, presuming it's the wrapper, if it exists. diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 48cb206fd7894e..30b5d8d02084a8 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -594,7 +594,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { 'declarations' => $child_layout_declarations, ); - /** + /* * If columnSpan is set, and the parent grid is responsive, i.e. if it has a minimumColumnWidth set, * the columnSpan should be removed on small grids. If there's a minimumColumnWidth, the grid is responsive. * But if the minimumColumnWidth value wasn't changed, it won't be set. In that case, if columnCount doesn't @@ -606,7 +606,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $parent_column_value = floatval( $parent_column_width ); $parent_column_unit = explode( $parent_column_value, $parent_column_width ); - /** + /* * If there is no unit, the width has somehow been mangled so we reset both unit and value * to defaults. * Additionally, the unit should be one of px, rem or em, so that also needs to be checked. @@ -622,7 +622,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { } } - /** + /* * A default gap value is used for this computation because custom gap values may not be * viable to use in the computation of the container query value. */ @@ -639,7 +639,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { ); } - /** + /* * Add to the style engine store to enqueue and render layout styles. * Return styles here just to check if any exist. */ @@ -800,7 +800,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { } } - /** + /* * Attempts to refer to the inner-block wrapping element by its class attribute. * * When examining a block's inner content, if a block has inner blocks, then @@ -890,13 +890,13 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { return $processor->get_updated_html(); } -/** +/* * Add a `render_block_data` filter to fetch the parent block layout data. */ add_filter( 'render_block_data', function ( $parsed_block, $source_block, $parent_block ) { - /** + /* * Check if the parent block exists and if it has a layout attribute. * If it does, add the parent layout to the parsed block. */ @@ -945,7 +945,7 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { return $block_content; } - /** + /* * This filter runs after the layout classnames have been added to the block, so they * have to be removed from the outer wrapper and then added to the inner. */ @@ -961,7 +961,7 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { } } } else { - /** + /* * The class_list method was only added in 6.4 so this needs a temporary fallback. * This fallback should be removed when the minimum supported version is 6.4. */ diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 10c29a78d6ff6a..f98adfac67a017 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -203,6 +203,7 @@ class WP_Theme_JSON_Gutenberg { * removed the `--wp--style--block-gap` property. * @since 6.2.0 Added `outline-*`, and `min-height` properties. * @since 6.3.0 Added `writing-mode` property. + * @since 6.6.0 Added `background-[image|position|repeat|size]` properties. * * @var array */ @@ -210,6 +211,10 @@ class WP_Theme_JSON_Gutenberg { 'aspect-ratio' => array( 'dimensions', 'aspectRatio' ), 'background' => array( 'color', 'gradient' ), 'background-color' => array( 'color', 'background' ), + 'background-image' => array( 'background', 'backgroundImage' ), + 'background-position' => array( 'background', 'backgroundPosition' ), + 'background-repeat' => array( 'background', 'backgroundRepeat' ), + 'background-size' => array( 'background', 'backgroundSize' ), 'border-radius' => array( 'border', 'radius' ), 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), @@ -461,10 +466,17 @@ class WP_Theme_JSON_Gutenberg { * added new property `shadow`, * updated `blockGap` to be allowed at any level. * @since 6.2.0 Added `outline`, and `minHeight` properties. + * @since 6.6.0 Added `background` sub properties to top-level only. * * @var array */ const VALID_STYLES = array( + 'background' => array( + 'backgroundImage' => 'top', + 'backgroundPosition' => 'top', + 'backgroundRepeat' => 'top', + 'backgroundSize' => 'top', + ), 'border' => array( 'color' => null, 'radius' => null, @@ -1334,7 +1346,6 @@ public function get_block_custom_css_nodes() { return $block_nodes; } - /** * Returns the global styles custom CSS for a single block. * @@ -2120,6 +2131,12 @@ protected static function compute_style_properties( $styles, $settings = array() } } + // Processes background styles. + if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) { + $background_styles = gutenberg_get_background_support_styles( $styles['background'] ); + $value = $background_styles['declarations'][ $css_property ] ?? $value; + } + // Skip if empty and not "0" or value represents array of longhand values. $has_missing_value = empty( $value ) && ! is_numeric( $value ); if ( $has_missing_value || is_array( $value ) ) { diff --git a/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php index 76c3d49ca8085f..e5f9891f04c471 100644 --- a/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php +++ b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php @@ -15,11 +15,30 @@ * @return mixed The value computed for the source. */ function gutenberg_block_bindings_pattern_overrides_callback( $source_attrs, $block_instance, $attribute_name ) { - if ( empty( $block_instance->attributes['metadata']['id'] ) ) { + if ( ! isset( $block_instance->context['pattern/overrides'] ) ) { return null; } - $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, 'values', $attribute_name ), null ); + + $override_content = $block_instance->context['pattern/overrides']; + + // Back compat. Pattern overrides previously used a metadata `id` instead of `name`. + // We check first for the name, and if it exists, use that value. + if ( isset( $block_instance->attributes['metadata']['name'] ) ) { + $metadata_name = $block_instance->attributes['metadata']['name']; + if ( array_key_exists( $metadata_name, $override_content ) ) { + return _wp_array_get( $override_content, array( $metadata_name, $attribute_name ), null ); + } + } + + // Next check for the `id`. + if ( isset( $block_instance->attributes['metadata']['id'] ) ) { + $metadata_id = $block_instance->attributes['metadata']['id']; + if ( array_key_exists( $metadata_id, $override_content ) ) { + return _wp_array_get( $override_content, array( $metadata_id, $attribute_name ), null ); + } + } + + return null; } /** diff --git a/lib/compat/wordpress-6.5/compat.php b/lib/compat/wordpress-6.5/compat.php index 78447927125894..39edaef83e5cc8 100644 --- a/lib/compat/wordpress-6.5/compat.php +++ b/lib/compat/wordpress-6.5/compat.php @@ -36,3 +36,18 @@ function array_is_list( $arr ) { return true; } } + +/** + * Sets a global JS variable used to flag whether to direct the Site Logo block's admin urls + * to the Customizer. This allows Gutenberg running on versions of WordPress < 6.5.0 to + * support the previous location for the Site Icon settings. This function should not be + * backported to core, and should be removed when the required WP core version for Gutenberg + * is >= 6.5.0. + */ +function gutenberg_add_use_customizer_site_logo_url_flag() { + if ( ! is_wp_version_compatible( '6.5' ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalUseCustomizerSiteLogoUrl = true', 'before' ); + } +} + +add_action( 'admin_init', 'gutenberg_add_use_customizer_site_logo_url_flag' ); diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php index 8a4040e3397e0c..1d65e0f63aab9f 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php @@ -858,7 +858,21 @@ protected function sanitize_src( $value ) { */ protected function handle_font_file_upload( $file ) { add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); - add_filter( 'upload_dir', 'wp_get_font_dir' ); + + /* + * Set the upload directory to the fonts directory. + * + * wp_get_font_dir() contains the 'font_dir' hook, whose callbacks are + * likely to call wp_get_upload_dir(). + * + * To avoid an infinite loop, don't hook wp_get_font_dir() to 'upload_dir'. + * Instead, just pass its return value to the 'upload_dir' callback. + */ + $font_dir = wp_get_font_dir(); + $set_upload_dir = function () use ( $font_dir ) { + return $font_dir; + }; + add_filter( 'upload_dir', $set_upload_dir ); $overrides = array( 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ), @@ -875,8 +889,7 @@ protected function handle_font_file_upload( $file ) { ); $uploaded_file = wp_handle_upload( $file, $overrides ); - - remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_dir', $set_upload_dir ); remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); return $uploaded_file; diff --git a/lib/compat/wordpress-6.5/fonts/fonts.php b/lib/compat/wordpress-6.5/fonts/fonts.php index 3911f61ceaec4b..55dc2d3429af92 100644 --- a/lib/compat/wordpress-6.5/fonts/fonts.php +++ b/lib/compat/wordpress-6.5/fonts/fonts.php @@ -201,16 +201,6 @@ function gutenberg_register_font_collections() { * * @since 6.5.0 * - * @param array $defaults { - * Array of information about the upload directory. - * - * @type string $path Base directory and subdirectory or full path to the fonts upload directory. - * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. - * @type string $subdir Subdirectory - * @type string $basedir Path without subdir. - * @type string $baseurl URL path without subdir. - * @type string|false $error False or error message. - * } * @return array $defaults { * Array of information about the upload directory. * @@ -222,19 +212,20 @@ function gutenberg_register_font_collections() { * @type string|false $error False or error message. * } */ - function wp_get_font_dir( $defaults = array() ) { + function wp_get_font_dir() { $site_path = ''; if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { $site_path = '/sites/' . get_current_blog_id(); } - // Sets the defaults. - $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; - $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; - $defaults['subdir'] = ''; - $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; - $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; - $defaults['error'] = false; + $defaults = array( + 'path' => path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path, + 'url' => untrailingslashit( content_url( 'fonts' ) ) . $site_path, + 'subdir' => '', + 'basedir' => path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path, + 'baseurl' => untrailingslashit( content_url( 'fonts' ) ) . $site_path, + 'error' => false, + ); /** * Filters the fonts directory data. diff --git a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php index 395aece766cb65..adbfb0e1800a03 100644 --- a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php +++ b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php @@ -151,7 +151,7 @@ function wp_interactivity_config( string $store_namespace, array $config = array } } -if ( ! function_exists( 'data_wp_context' ) ) { +if ( ! function_exists( 'wp_interactivity_data_wp_context' ) ) { /** * Generates a `data-wp-context` directive attribute by encoding a context * array. @@ -162,7 +162,7 @@ function wp_interactivity_config( string $store_namespace, array $config = array * * Example: * - *
true, 'count' => 0 ) ); ?>> + *
true, 'count' => 0 ) ); ?>> * * @since 6.5.0 * @@ -171,7 +171,7 @@ function wp_interactivity_config( string $store_namespace, array $config = array * @return string A complete `data-wp-context` directive with a JSON encoded value representing the context array and * the store namespace if specified. */ - function data_wp_context( array $context, string $store_namespace = '' ): string { + function wp_interactivity_data_wp_context( array $context, string $store_namespace = '' ): string { return 'data-wp-context=\'' . ( $store_namespace ? $store_namespace . '::' : '' ) . ( empty( $context ) ? '{}' : wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ) . diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index 3aeaf745fe49f1..9419ae4a716b98 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -224,7 +224,7 @@ function gutenberg_add_global_styles_block_custom_css() { $stylesheet_handle = 'global-styles'; - /** + /* * When `wp_should_load_separate_core_block_assets()` is true, follow a similar * logic to the one in `gutenberg_add_global_styles_for_blocks` to add the custom * css only when the block is rendered. diff --git a/lib/script-loader.php b/lib/script-loader.php index 1c0e5f5a941fe6..01008a0da8967e 100644 --- a/lib/script-loader.php +++ b/lib/script-loader.php @@ -54,18 +54,18 @@ function gutenberg_enqueue_global_styles() { // Add each block as an inline css. gutenberg_add_global_styles_for_blocks(); - /** + /* * Add the custom CSS for the global styles. * Before that, dequeue the Customizer's custom CSS * and add it before the global styles custom CSS. + * Don't enqueue Customizer's custom CSS separately. */ - // Don't enqueue Customizer's custom CSS separately. remove_action( 'wp_head', 'wp_custom_css_cb', 101 ); $custom_css = wp_get_custom_css(); if ( ! wp_should_load_separate_core_block_assets() ) { - /** + /* * If loading all block assets together, add both * the base and block custom CSS at once. Else load * the base custom CSS only, and the block custom CSS diff --git a/package-lock.json b/package-lock.json index 9c03b2e0e1fafa..8c6b476dc0e53a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.8.0-rc.4", + "version": "17.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.8.0-rc.4", + "version": "17.8.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55967,8 +55967,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url", - "nanoid": "^3.3.4" + "@wordpress/url": "file:../url" }, "engines": { "node": ">=16.0.0" @@ -56142,7 +56141,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.112.0", + "version": "1.114.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56155,7 +56154,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.112.0", + "version": "1.114.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56166,7 +56165,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.112.0", + "version": "1.114.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -70828,8 +70827,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url", - "nanoid": "^3.3.4" + "@wordpress/url": "file:../url" } }, "@wordpress/plugins": { diff --git a/package.json b/package.json index f93f37999cc6bd..ce777b3c1b96e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.8.0-rc.4", + "version": "17.8.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/base-styles/_default-custom-properties.scss b/packages/base-styles/_default-custom-properties.scss index 52dfeb3899d772..5760753c48ce85 100644 --- a/packages/base-styles/_default-custom-properties.scss +++ b/packages/base-styles/_default-custom-properties.scss @@ -1,4 +1,3 @@ - // It is important to include these styles in all built stylesheets. // This allows to CSS variables post CSS plugin to generate fallbacks. // It also provides default CSS variables for npm package consumers. @@ -6,4 +5,5 @@ @include admin-scheme(#007cba); --wp-block-synced-color: #7a00df; --wp-block-synced-color--rgb: #{hex-to-rgb(#7a00df)}; + --wp-bound-block-color: #9747ff; } diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index 109926ebf411a9..a1de82c2081cc5 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -367,6 +367,14 @@ } } +@mixin link-reset { + &:focus { + color: var(--wp-admin-theme-color--rgb); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color, #007cba); + border-radius: $radius-block-ui; + } +} + // The editor input reset with increased specificity to avoid theme styles bleeding in. @mixin editor-input-reset() { font-family: $editor-html-font !important; diff --git a/packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js b/packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js new file mode 100644 index 00000000000000..4b2d3df725a66b --- /dev/null +++ b/packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { ToolbarItem, ToolbarGroup, Icon } from '@wordpress/components'; +import { connection } from '@wordpress/icons'; +import { _x } from '@wordpress/i18n'; + +export default function BlockBindingsToolbarIndicator() { + return ( + + + + + + ); +} diff --git a/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss new file mode 100644 index 00000000000000..4aeabdf8acf6e8 --- /dev/null +++ b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss @@ -0,0 +1,14 @@ +.block-editor-block-bindings-toolbar-indicator { + display: inline-flex; + align-items: center; + height: 48px; + padding: 6px; + + svg g { + stroke: var(--wp-bound-block-color); + fill: transparent; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; + } +} diff --git a/packages/block-editor/src/components/block-list/block-outline.native.js b/packages/block-editor/src/components/block-list/block-outline.native.js index 76f0f8cb947941..92146816eb1529 100644 --- a/packages/block-editor/src/components/block-list/block-outline.native.js +++ b/packages/block-editor/src/components/block-list/block-outline.native.js @@ -14,16 +14,11 @@ import { usePreferredColorSchemeStyle } from '@wordpress/compose'; import styles from './block.scss'; const TEXT_BLOCKS_WITH_OUTLINE = [ 'core/missing', 'core/freeform' ]; +const DESIGN_BLOCKS_WITHOUT_OUTLINE = [ 'core/button', 'core/spacer' ]; +const MEDIA_BLOCKS_WITH_OUTLINE = [ 'core/audio', 'core/file' ]; -function BlockOutline( { - blockCategory, - hasInnerBlocks, - isRootList, - isSelected, - name, -} ) { +function BlockOutline( { blockCategory, hasInnerBlocks, isSelected, name } ) { const textBlockWithOutline = TEXT_BLOCKS_WITH_OUTLINE.includes( name ); - const socialBlockWithOutline = name.includes( 'core/social-link' ); const hasBlockTextCategory = blockCategory === 'text' && ! textBlockWithOutline; @@ -44,19 +39,39 @@ function BlockOutline( { hasBlockTextCategory && styles.solidBorderTextContent, ]; - const shoudlShowOutline = - isSelected && - ( ( hasBlockTextCategory && hasInnerBlocks ) || - ( ! hasBlockTextCategory && hasInnerBlocks ) || - ( ! hasBlockTextCategory && isRootList ) || - socialBlockWithOutline || - textBlockWithOutline ); - - return ( - shoudlShowOutline && ( - - ) - ); + if ( ! isSelected ) { + return null; + } + + let shouldShowOutline = true; + if ( hasBlockTextCategory && ! hasInnerBlocks ) { + shouldShowOutline = false; + } else if ( + blockCategory === 'media' && + ! hasInnerBlocks && + ! MEDIA_BLOCKS_WITH_OUTLINE.includes( name ) + ) { + shouldShowOutline = false; + } else if ( blockCategory === 'media' && name === 'core/cover' ) { + shouldShowOutline = false; + } else if ( + blockCategory === 'design' && + DESIGN_BLOCKS_WITHOUT_OUTLINE.includes( name ) + ) { + shouldShowOutline = false; + } + + if ( shouldShowOutline ) { + return ( + + ); + } + + return null; } export default BlockOutline; diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index 818fc6e1a9900f..a1878aa3f150dc 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -93,7 +93,6 @@ function BlockWrapper( { draggingEnabled, hasInnerBlocks, isDescendentBlockSelected, - isRootList, isSelected, isTouchable, marginHorizontal, @@ -137,7 +136,6 @@ function BlockWrapper( { @@ -361,7 +359,6 @@ function BlockListBlock( { hasInnerBlocks={ hasInnerBlocks } isDescendentBlockSelected={ isDescendentBlockSelected } isFocused={ isFocused } - isRootList={ ! rootClientId } isSelected={ isSelected } isStackedHorizontally={ isStackedHorizontally } isTouchable={ isTouchable } diff --git a/packages/block-editor/src/components/block-list/test/block-outline.native.js b/packages/block-editor/src/components/block-list/test/block-outline.native.js new file mode 100644 index 00000000000000..6e549542ad6672 --- /dev/null +++ b/packages/block-editor/src/components/block-list/test/block-outline.native.js @@ -0,0 +1,255 @@ +/** + * External dependencies + */ +import { render, screen } from 'test/helpers'; + +/** + * Internal dependencies + */ +import BlockOutline from '../block-outline'; + +describe( 'BlockOutline', () => { + describe( 'containing an unselected block', () => { + it( 'should not render an outline', async () => { + render( + + ); + + expect( screen.queryByTestId( 'block-outline' ) ).toBeNull(); + } ); + } ); + + describe( 'containing a block with inner blocks', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + + describe( 'containing a design category block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + + describe( 'containing a text category block', () => { + it( 'should not render an outline', async () => { + render( + + ); + + expect( screen.queryByTestId( 'block-outline' ) ).toBeNull(); + } ); + + describe( 'with inner blocks', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + } ); + + describe( 'containing a widget category block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + + describe( 'containing a spacer block', () => { + it( 'should not render an outline', async () => { + render( + + ); + + expect( screen.queryByTestId( 'block-outline' ) ).toBeNull(); + } ); + } ); + + describe( 'containing a button block', () => { + it( 'should not render an outline', async () => { + render( + + ); + + expect( screen.queryByTestId( 'block-outline' ) ).toBeNull(); + } ); + } ); + + describe( 'containing a social link block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + + describe( 'when platform specific', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + } ); + + describe( 'containing a media block', () => { + it( 'should not render an outline', async () => { + render( + + ); + + expect( screen.queryByTestId( 'block-outline' ) ).toBeNull(); + } ); + + describe( 'with inner blocks', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + + describe( 'when cover block', () => { + it( 'should not render an outline', async () => { + render( + + ); + + expect( + screen.queryByTestId( 'block-outline' ) + ).toBeNull(); + } ); + } ); + } ); + + describe( 'when a file block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + + describe( 'when an audio block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + } ); + + describe( 'containing a freeform block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); + + describe( 'containing a missing block', () => { + it( 'should render an outline', async () => { + render( + + ); + + expect( screen.getByTestId( 'block-outline' ) ).toBeVisible(); + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 08b43fa46257e4..c929c1014dc030 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -19,13 +19,17 @@ import useMovingAnimation from '../../use-moving-animation'; import { PrivateBlockContext } from '../private-block-context'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; -import { useBlockEditContext } from '../../block-edit/context'; +import { + blockBindingsKey, + useBlockEditContext, +} from '../../block-edit/context'; import { useFocusHandler } from './use-focus-handler'; import { useEventHandlers } from './use-selected-block-event-handlers'; import { useNavModeExit } from './use-nav-mode-exit'; import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; +import { canBindBlock } from '../../../hooks/use-bindings-attributes'; /** * This hook is used to lightly mark an element as a block element. The element @@ -123,6 +127,12 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { ] ); const blockEditContext = useBlockEditContext(); + const hasBlockBindings = !! blockEditContext[ blockBindingsKey ]; + const bindingsStyle = + hasBlockBindings && canBindBlock( name ) + ? { '--wp-admin-theme-color': 'var(--wp-bound-block-color)' } + : {}; + // Ensures it warns only inside the `edit` implementation for the block. if ( blockApiVersion < 2 && clientId === blockEditContext.clientId ) { warning( @@ -168,7 +178,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { wrapperProps.className, defaultClassName ), - style: { ...wrapperProps.style, ...props.style }, + style: { ...wrapperProps.style, ...props.style, ...bindingsStyle }, }; } diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js b/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js index bf4fc55879448a..01cc462e507ecf 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js @@ -22,7 +22,7 @@ import { store as blockEditorStore } from '../../../store'; export function useEventHandlers( { clientId, isSelected } ) { const { getBlockRootClientId, getBlockIndex } = useSelect( blockEditorStore ); - const { insertDefaultBlock, removeBlock } = useDispatch( blockEditorStore ); + const { insertAfterBlock, removeBlock } = useDispatch( blockEditorStore ); return useRefEffect( ( node ) => { @@ -57,11 +57,7 @@ export function useEventHandlers( { clientId, isSelected } ) { event.preventDefault(); if ( keyCode === ENTER ) { - insertDefaultBlock( - {}, - getBlockRootClientId( clientId ), - getBlockIndex( clientId ) + 1 - ); + insertAfterBlock( clientId ); } else { removeBlock( clientId ); } @@ -90,7 +86,7 @@ export function useEventHandlers( { clientId, isSelected } ) { isSelected, getBlockRootClientId, getBlockIndex, - insertDefaultBlock, + insertAfterBlock, removeBlock, ] ); diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 140532de15b768..e566096d54f269 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -34,9 +34,10 @@ import { useShowHoveredOrFocusedGestures } from './utils'; import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; import NavigableToolbar from '../navigable-toolbar'; -import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; import Shuffle from './shuffle'; - +import BlockBindingsIndicator from '../block-bindings-toolbar-indicator'; +import { useHasBlockToolbar } from './use-has-block-toolbar'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; /** * Renders the block toolbar. * @@ -61,8 +62,10 @@ export function PrivateBlockToolbar( { blockClientIds, isDefaultEditingMode, blockType, + blockName, shouldShowVisualToolbar, showParentSelector, + isUsingBindings, } = useSelect( ( select ) => { const { getBlockName, @@ -72,6 +75,7 @@ export function PrivateBlockToolbar( { isBlockValid, getBlockRootClientId, getBlockEditingMode, + getBlockAttributes, } = select( blockEditorStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; @@ -82,20 +86,21 @@ export function PrivateBlockToolbar( { const parentBlockType = getBlockType( parentBlockName ); const _isDefaultEditingMode = getBlockEditingMode( selectedBlockClientId ) === 'default'; + const _blockName = getBlockName( selectedBlockClientId ); const isValid = selectedBlockClientIds.every( ( id ) => isBlockValid( id ) ); const isVisual = selectedBlockClientIds.every( ( id ) => getBlockMode( id ) === 'visual' ); + const _isUsingBindings = !! getBlockAttributes( selectedBlockClientId ) + ?.metadata?.bindings; return { blockClientId: selectedBlockClientId, blockClientIds: selectedBlockClientIds, isDefaultEditingMode: _isDefaultEditingMode, - blockType: - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ), - + blockName: _blockName, + blockType: selectedBlockClientId && getBlockType( _blockName ), shouldShowVisualToolbar: isValid && isVisual, rootClientId: blockRootClientId, showParentSelector: @@ -108,6 +113,7 @@ export function PrivateBlockToolbar( { ) && selectedBlockClientIds.length === 1 && _isDefaultEditingMode, + isUsingBindings: _isUsingBindings, }; }, [] ); @@ -122,15 +128,8 @@ export function PrivateBlockToolbar( { const isLargeViewport = ! useViewportMatch( 'medium', '<' ); - const isToolbarEnabled = - blockType && - hasBlockSupport( blockType, '__experimentalToolbar', true ); - const hasAnyBlockControls = useHasAnyBlockControls(); - - if ( - ! isToolbarEnabled || - ( ! isDefaultEditingMode && ! hasAnyBlockControls ) - ) { + const hasBlockToolbar = useHasBlockToolbar(); + if ( ! hasBlockToolbar ) { return null; } @@ -166,6 +165,9 @@ export function PrivateBlockToolbar( { { ! isMultiToolbar && isLargeViewport && isDefaultEditingMode && } + { isUsingBindings && canBindBlock( blockName ) && ( + + ) } { ( shouldShowVisualToolbar || isMultiToolbar ) && isDefaultEditingMode && (
{ + const { + getBlockEditingMode, + getBlockName, + getSelectedBlockClientIds, + } = select( blockEditorStore ); + + const selectedBlockClientIds = getSelectedBlockClientIds(); + const selectedBlockClientId = selectedBlockClientIds[ 0 ]; + const isDefaultEditingMode = + getBlockEditingMode( selectedBlockClientId ) === 'default'; + const blockType = + selectedBlockClientId && + getBlockType( getBlockName( selectedBlockClientId ) ); + const isToolbarEnabled = + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ); + + if ( + ! isToolbarEnabled || + ( ! isDefaultEditingMode && ! hasAnyBlockControls ) + ) { + return false; + } + + return true; + }, + [ hasAnyBlockControls ] + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index e00058eb905329..3959257ecf4e86 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -5,7 +5,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -20,14 +19,13 @@ import BlockToolbarBreadcrumb from './block-toolbar-breadcrumb'; import { store as blockEditorStore } from '../../store'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; +import { useShowBlockTools } from './use-show-block-tools'; function selector( select ) { const { getSelectedBlockClientId, getFirstMultiSelectedBlockClientId, - getBlock, getSettings, - hasMultiSelection, __unstableGetEditorMode, isTyping, } = select( blockEditorStore ); @@ -35,36 +33,13 @@ function selector( select ) { const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); - const { name = '', attributes = {} } = getBlock( clientId ) || {}; const editorMode = __unstableGetEditorMode(); - const hasSelectedBlock = clientId && name; - const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( { - name, - attributes, - } ); - const _showEmptyBlockSideInserter = - clientId && - ! isTyping() && - editorMode === 'edit' && - isUnmodifiedDefaultBlock( { name, attributes } ); - const maybeShowBreadcrumb = - hasSelectedBlock && - ! hasMultiSelection() && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ); return { clientId, hasFixedToolbar: getSettings().hasFixedToolbar, isTyping: isTyping(), isZoomOutMode: editorMode === 'zoom-out', - showEmptyBlockSideInserter: _showEmptyBlockSideInserter, - showBreadcrumb: ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, - showBlockToolbar: - ! getSettings().hasFixedToolbar && - ! _showEmptyBlockSideInserter && - hasSelectedBlock && - ! isEmptyDefaultBlock && - ! maybeShowBreadcrumb, }; } @@ -82,18 +57,20 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const { - clientId, - hasFixedToolbar, - isTyping, - isZoomOutMode, - showEmptyBlockSideInserter, - showBreadcrumb, - showBlockToolbar, - } = useSelect( selector, [] ); + const { clientId, hasFixedToolbar, isTyping, isZoomOutMode } = useSelect( + selector, + [] + ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = useSelect( blockEditorStore ); + + const { + showEmptyBlockSideInserter, + showBreadcrumb, + showBlockToolbarPopover, + } = useShowBlockTools(); + const { duplicateBlocks, removeBlocks, @@ -186,7 +163,7 @@ export default function BlockTools( { /> ) } - { showBlockToolbar && ( + { showBlockToolbarPopover && ( { + const { + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + getBlock, + getSettings, + hasMultiSelection, + __unstableGetEditorMode, + isTyping, + } = select( blockEditorStore ); + + const clientId = + getSelectedBlockClientId() || + getFirstMultiSelectedBlockClientId(); + + const { name = '', attributes = {} } = getBlock( clientId ) || {}; + const editorMode = __unstableGetEditorMode(); + const hasSelectedBlock = clientId && name; + const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( { + name, + attributes, + } ); + const _showEmptyBlockSideInserter = + clientId && + ! isTyping() && + editorMode === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ); + const maybeShowBreadcrumb = + hasSelectedBlock && + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ); + + return { + showEmptyBlockSideInserter: _showEmptyBlockSideInserter, + showBreadcrumb: + ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, + showBlockToolbarPopover: + hasBlockToolbar && + ! getSettings().hasFixedToolbar && + ! _showEmptyBlockSideInserter && + hasSelectedBlock && + ! isEmptyDefaultBlock && + ! maybeShowBreadcrumb, + showFixedToolbar: + editorMode !== 'zoom-out' && + hasBlockToolbar && + getSettings().hasFixedToolbar, + }; + }, + [ hasBlockToolbar ] + ); +} diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 4e8ea6dc0ff95b..f5cb968924459a 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -600,6 +600,7 @@ const STYLE_KEYS = [ 'filter', 'outline', 'shadow', + 'background', ]; function pickStyleKeys( treeToPickFrom ) { diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 6b9de943ea0bf2..1214ec4ec7c08a 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -14,7 +14,12 @@ import { Tooltip, } from '@wordpress/components'; import { forwardRef } from '@wordpress/element'; -import { Icon, lockSmall as lock, pinSmall } from '@wordpress/icons'; +import { + Icon, + connection, + lockSmall as lock, + pinSmall, +} from '@wordpress/icons'; import { SPACE, ENTER, BACKSPACE, DELETE } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; @@ -32,11 +37,12 @@ import { useBlockLock } from '../block-lock'; import { store as blockEditorStore } from '../../store'; import useListViewImages from './use-list-view-images'; import { useListViewContext } from './context'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; function ListViewBlockSelectButton( { className, - block: { clientId }, + block: { clientId, name: blockName }, onClick, onContextMenu, onMouseDown, @@ -66,6 +72,7 @@ function ListViewBlockSelectButton( getBlockRootClientId, getBlockOrder, getBlocksByClientId, + getBlockAttributes, canRemoveBlocks, } = useSelect( blockEditorStore ); const { duplicateBlocks, multiSelect, removeBlocks } = @@ -75,6 +82,8 @@ function ListViewBlockSelectButton( const images = useListViewImages( { clientId, isExpanded } ); const { rootClientId } = useListViewContext(); + const isConnected = getBlockAttributes( clientId )?.metadata?.bindings; + const positionLabel = blockInformation?.positionLabel ? sprintf( // translators: 1: Position of selected block, e.g. "Sticky" or "Fixed". @@ -278,6 +287,11 @@ function ListViewBlockSelectButton( ) } + { isConnected && canBindBlock( blockName ) && ( + + + + ) } { positionLabel && isSticky && ( diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 11cf1fafa0e14b..1245bfbabcb7a7 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -557,3 +557,11 @@ $block-navigation-max-indent: 8; .list-view-appender__description { display: none; } + +.block-editor-list-view-block-select-button__bindings svg g { + stroke: var(--wp-bound-block-color); + fill: transparent; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} diff --git a/packages/block-editor/src/components/list-view/use-clipboard-handler.js b/packages/block-editor/src/components/list-view/use-clipboard-handler.js index cd25c71e9bf7c4..dd3ac65ac79d24 100644 --- a/packages/block-editor/src/components/list-view/use-clipboard-handler.js +++ b/packages/block-editor/src/components/list-view/use-clipboard-handler.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; /** @@ -15,6 +15,7 @@ import { getPasteBlocks, setClipboardBlocks } from '../writing-flow/utils'; // This hook borrows from useClipboardHandler in ../writing-flow/use-clipboard-handler.js // and adds behaviour for the list view, while skipping partial selection. export default function useClipboardHandler( { selectBlock } ) { + const registry = useRegistry(); const { getBlockOrder, getBlockRootClientId, @@ -106,7 +107,7 @@ export default function useClipboardHandler( { selectBlock } ) { notifyCopy( event.type, selectedBlockClientIds ); const blocks = getBlocksByClientId( selectedBlockClientIds ); - setClipboardBlocks( event, blocks ); + setClipboardBlocks( event, blocks, registry ); } if ( event.type === 'cut' ) { diff --git a/packages/block-editor/src/components/responsive-block-control/README.md b/packages/block-editor/src/components/responsive-block-control/README.md index ec6e1f4beb018d..1d99caa024a419 100644 --- a/packages/block-editor/src/components/responsive-block-control/README.md +++ b/packages/block-editor/src/components/responsive-block-control/README.md @@ -2,7 +2,7 @@ `ResponsiveBlockControl` provides a standardised interface for the creation of Block controls that require **different settings per viewport** (ie: "responsive" settings). -For example, imagine your Block provides a control which affords the ability to change a "padding" value used in the Block display. Consider that whilst this setting may work well on "large" screens, the same value may not work well on smaller screens (it may be too large for example). As a result, you now need to provide a padding control _per viewport/screensize_. +For example, imagine your Block provides a control which affords the ability to change a "padding" value used in the Block display. Consider that whilst this setting may work well on "large" screens, the same value may not work well on smaller screens (it may be too large for example). As a result, you now need to provide a padding control _per viewport/screen size_. `ResponsiveBlockControl` provides a standardised component for the creation of such interfaces within Gutenberg. @@ -25,7 +25,7 @@ import { useState } from 'react'; import { registerBlockType } from '@wordpress/blocks'; import { InspectorControls, - ResponsiveBlockControl, + __experimentalResponsiveBlockControl as ResponsiveBlockControl, } from '@wordpress/block-editor'; import { DimensionControl, @@ -194,7 +194,7 @@ const renderResponsiveControls = ( viewports ) => { ### `toggleLabel` - **Type:** `String` -- **Default:** `Use the same %s on all screensizes.` (where "%s" is the `property` prop - see above ) +- **Default:** `Use the same %s on all screen sizes.` (where "%s" is the `property` prop - see above ) - **Required:** `false` Optional label used for the toggle control which switches the interface between showing responsive controls or not. @@ -213,7 +213,7 @@ Optional label used for the toggle control which switches the interface between - **Required:** `false` -Optional object describing the attributes of the default value. By default this is `All` which indicates the control will affect "all viewports/screensizes". +Optional object describing the attributes of the default value. By default this is `All` which indicates the control will affect "all viewports/screen sizes". ### `viewports` diff --git a/packages/block-editor/src/components/responsive-block-control/index.js b/packages/block-editor/src/components/responsive-block-control/index.js index 6105d0134a518d..575018fd7319aa 100644 --- a/packages/block-editor/src/components/responsive-block-control/index.js +++ b/packages/block-editor/src/components/responsive-block-control/index.js @@ -52,7 +52,7 @@ function ResponsiveBlockControl( props ) { toggleLabel || sprintf( /* translators: %s: Property value for the control (eg: margin, padding, etc.). */ - __( 'Use the same %s on all screensizes.' ), + __( 'Use the same %s on all screen sizes.' ), property ); diff --git a/packages/block-editor/src/components/responsive-block-control/test/index.js b/packages/block-editor/src/components/responsive-block-control/test/index.js index 4fdc760f4945f3..ad169515717d6e 100644 --- a/packages/block-editor/src/components/responsive-block-control/test/index.js +++ b/packages/block-editor/src/components/responsive-block-control/test/index.js @@ -81,7 +81,7 @@ describe( 'Basic rendering', () => { } ); const toggleState = screen.getByRole( 'checkbox', { - name: 'Use the same padding on all screensizes.', + name: 'Use the same padding on all screen sizes.', checked: true, } ); @@ -268,7 +268,7 @@ describe( 'Default and Responsive modes', () => { // Select elements based on what the user can see. const toggleInput = screen.getByRole( 'checkbox', { - name: 'Use the same padding on all screensizes.', + name: 'Use the same padding on all screen sizes.', checked: true, } ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 458f5a96609b65..7236e74b2f6d68 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -47,7 +47,7 @@ import { getAllowedFormats } from './utils'; import { Content } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; -import { BLOCK_BINDINGS_ALLOWED_BLOCKS } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -161,7 +161,7 @@ export function RichTextWrapper( ( select ) => { // Disable Rich Text editing if block bindings specify that. let _disableBoundBlocks = false; - if ( blockBindings && blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) { + if ( blockBindings && canBindBlock( blockName ) ) { const blockTypeAttributes = getBlockType( blockName ).attributes; const { getBlockBindingsSource } = unlock( diff --git a/packages/block-editor/src/components/url-popover/index.js b/packages/block-editor/src/components/url-popover/index.js index b5bbe8f50958bb..d060a464cc306f 100644 --- a/packages/block-editor/src/components/url-popover/index.js +++ b/packages/block-editor/src/components/url-popover/index.js @@ -92,12 +92,12 @@ const URLPopover = forwardRef( /> ) }
- { showSettings && ( -
- { renderSettings() } -
- ) }
+ { showSettings && ( +
+ { renderSettings() } +
+ ) } { additionalControls && ! showSettings && (
{ additionalControls } diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js index 8528655c1dcc9e..43e887888dbd13 100644 --- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js +++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js @@ -5,7 +5,7 @@ import { documentHasSelection, documentHasUncollapsedSelection, } from '@wordpress/dom'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; /** @@ -16,6 +16,7 @@ import { useNotifyCopy } from '../../utils/use-notify-copy'; import { getPasteBlocks, setClipboardBlocks } from './utils'; export default function useClipboardHandler() { + const registry = useRegistry(); const { getBlocksByClientId, getSelectedBlockClientIds, @@ -104,7 +105,7 @@ export default function useClipboardHandler() { blocks = [ head, ...inBetweenBlocks, tail ]; } - setClipboardBlocks( event, blocks ); + setClipboardBlocks( event, blocks, registry ); } } diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js index ef1827077ccbf1..2a2010854ed205 100644 --- a/packages/block-editor/src/components/writing-flow/utils.js +++ b/packages/block-editor/src/components/writing-flow/utils.js @@ -8,36 +8,51 @@ import { pasteHandler, findTransform, getBlockTransforms, + store as blocksStore, } from '@wordpress/blocks'; /** * Internal dependencies */ import { getPasteEventData } from '../../utils/pasting'; +import { store as blockEditorStore } from '../../store'; + +export const requiresWrapperOnCopy = Symbol( 'requiresWrapperOnCopy' ); /** * Sets the clipboard data for the provided blocks, with both HTML and plain * text representations. * - * @param {ClipboardEvent} event Clipboard event. - * @param {WPBlock[]} blocks Blocks to set as clipboard data. + * @param {ClipboardEvent} event Clipboard event. + * @param {WPBlock[]} blocks Blocks to set as clipboard data. + * @param {Object} registry The registry to select from. */ -export function setClipboardBlocks( event, blocks ) { +export function setClipboardBlocks( event, blocks, registry ) { let _blocks = blocks; - const wrapperBlockName = event.clipboardData.getData( - '__unstableWrapperBlockName' - ); - if ( wrapperBlockName ) { - _blocks = createBlock( - wrapperBlockName, - JSON.parse( - event.clipboardData.getData( - '__unstableWrapperBlockAttributes' - ) - ), - _blocks - ); + const [ firstBlock ] = blocks; + + if ( firstBlock ) { + const firstBlockType = registry + .select( blocksStore ) + .getBlockType( firstBlock.name ); + + if ( firstBlockType[ requiresWrapperOnCopy ] ) { + const { getBlockRootClientId, getBlockName, getBlockAttributes } = + registry.select( blockEditorStore ); + const wrapperBlockClientId = getBlockRootClientId( + firstBlock.clientId + ); + const wrapperBlockName = getBlockName( wrapperBlockClientId ); + + if ( wrapperBlockName ) { + _blocks = createBlock( + wrapperBlockName, + getBlockAttributes( wrapperBlockClientId ), + _blocks + ); + } + } } const serialized = serialize( _blocks ); diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index eb84352ab62f09..93bf87f42124b1 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -19,18 +19,28 @@ import { store as blockEditorStore } from '../store'; const EMPTY_OBJECT = {}; -function BlockHooksControlPure( { name, clientId } ) { +function BlockHooksControlPure( { + name, + clientId, + metadata: { ignoredHookedBlocks = [] } = {}, +} ) { const blockTypes = useSelect( ( select ) => select( blocksStore ).getBlockTypes(), [] ); + // A hooked block added via a filter will not be exposed through a block + // type's `blockHooks` property; however, if the containing layout has been + // modified, it will be present in the anchor block's `ignoredHookedBlocks` + // metadata. const hookedBlocksForCurrentBlock = useMemo( () => blockTypes?.filter( - ( { blockHooks } ) => blockHooks && name in blockHooks + ( { name: blockName, blockHooks } ) => + ( blockHooks && name in blockHooks ) || + ignoredHookedBlocks.includes( blockName ) ), - [ blockTypes, name ] + [ blockTypes, name, ignoredHookedBlocks ] ); const { blockIndex, rootClientId, innerBlocksLength } = useSelect( @@ -79,6 +89,16 @@ function BlockHooksControlPure( { name, clientId } ) { // inserted and then moved around a bit by the user. candidates = getBlocks( clientId ); break; + + case undefined: + // If we haven't found a blockHooks field with a relative position for the hooked + // block, it means that it was added by a filter. In this case, we look for the block + // both among the current block's siblings and its children. + candidates = [ + ...getBlocks( rootClientId ), + ...getBlocks( clientId ), + ]; + break; } const hookedBlock = candidates?.find( @@ -151,6 +171,18 @@ function BlockHooksControlPure( { name, clientId } ) { false ); break; + + case undefined: + // If we do not know the relative position, it is because the block was + // added via a filter. In this case, we default to inserting it after the + // current block. + insertBlock( + block, + blockIndex + 1, + rootClientId, // Insert as a child of the current block's parent + false + ); + break; } }; @@ -219,6 +251,7 @@ function BlockHooksControlPure( { name, clientId } ) { export default { edit: BlockHooksControlPure, + attributeKeys: [ 'metadata' ], hasSupport() { return true; }, diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 0e5b6614f07cbf..5cd8cb46b3b7e7 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -4,12 +4,13 @@ import { getBlockType, store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; +import { useLayoutEffect, useCallback, useState } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; +import { RichTextData } from '@wordpress/rich-text'; + /** * Internal dependencies */ -import { store as blockEditorStore } from '../store'; -import { useBlockEditContext } from '../components/block-edit/context'; import { unlock } from '../lock-unlock'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ @@ -22,87 +23,238 @@ import { unlock } from '../lock-unlock'; * @return {WPHigherOrderComponent} Higher-order component. */ -export const BLOCK_BINDINGS_ALLOWED_BLOCKS = { +const BLOCK_BINDINGS_ALLOWED_BLOCKS = { 'core/paragraph': [ 'content' ], 'core/heading': [ 'content' ], 'core/image': [ 'url', 'title', 'alt' ], 'core/button': [ 'url', 'text', 'linkTarget' ], }; -const createEditFunctionWithBindingsAttribute = () => - createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { clientId, name: blockName } = useBlockEditContext(); - const blockBindingsSources = unlock( - useSelect( blocksStore ) - ).getAllBlockBindingsSources(); - const { getBlockAttributes } = useSelect( blockEditorStore ); - - const updatedAttributes = getBlockAttributes( clientId ); - if ( updatedAttributes?.metadata?.bindings ) { - Object.entries( updatedAttributes.metadata.bindings ).forEach( - ( [ attributeName, settings ] ) => { - const source = blockBindingsSources[ settings.source ]; - - if ( source && source.useSource ) { - // Second argument (`updateMetaValue`) will be used to update the value in the future. - const { - placeholder, - useValue: [ metaValue = null ] = [], - } = source.useSource( props, settings.args ); - - if ( placeholder && ! metaValue ) { - // If the attribute is `src` or `href`, a placeholder can't be used because it is not a valid url. - // Adding this workaround until attributes and metadata fields types are improved and include `url`. - const htmlAttribute = - getBlockType( blockName ).attributes[ - attributeName - ].attribute; - if ( - htmlAttribute === 'src' || - htmlAttribute === 'href' - ) { - updatedAttributes[ attributeName ] = null; - } else { - updatedAttributes[ attributeName ] = - placeholder; - } - } - - if ( metaValue ) { - updatedAttributes[ attributeName ] = metaValue; - } - } - } - ); +/** + * Based on the given block name, + * check if it is possible to bind the block. + * + * @param {string} blockName - The block name. + * @return {boolean} Whether it is possible to bind the block to sources. + */ +export function canBindBlock( blockName ) { + return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; +} + +/** + * Based on the given block name and attribute name, + * check if it is possible to bind the block attribute. + * + * @param {string} blockName - The block name. + * @param {string} attributeName - The attribute name. + * @return {boolean} Whether it is possible to bind the block attribute. + */ +export function canBindAttribute( blockName, attributeName ) { + return ( + canBindBlock( blockName ) && + BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) + ); +} + +/** + * This component is responsible for detecting and + * propagating data changes from the source to the block. + * + * @param {Object} props - The component props. + * @param {string} props.attrName - The attribute name. + * @param {Object} props.blockProps - The block props with bound attribute. + * @param {Object} props.source - Source handler. + * @param {Object} props.args - The arguments to pass to the source. + * @param {Function} props.onPropValueChange - The function to call when the attribute value changes. + * @return {null} Data-handling component. Render nothing. + */ +const BindingConnector = ( { + args, + attrName, + blockProps, + source, + onPropValueChange, +} ) => { + const { placeholder, value: propValue } = source.useSource( + blockProps, + args + ); + + const { name: blockName } = blockProps; + const attrValue = blockProps.attributes[ attrName ]; + + const updateBoundAttibute = useCallback( + ( newAttrValue, prevAttrValue ) => { + /* + * If the attribute is a RichTextData instance, + * (core/paragraph, core/heading, core/button, etc.) + * compare its HTML representation with the new value. + * + * To do: it looks like a workaround. + * Consider improving the attribute and metadata fields types. + */ + if ( prevAttrValue instanceof RichTextData ) { + // Bail early if the Rich Text value is the same. + if ( prevAttrValue.toHTMLString() === newAttrValue ) { + return; + } + + /* + * To preserve the value type, + * convert the new value to a RichTextData instance. + */ + newAttrValue = RichTextData.fromHTMLString( newAttrValue ); + } + + if ( prevAttrValue === newAttrValue ) { + return; } - return ( + onPropValueChange( { [ attrName ]: newAttrValue } ); + }, + [ attrName, onPropValueChange ] + ); + + useLayoutEffect( () => { + if ( typeof propValue !== 'undefined' ) { + updateBoundAttibute( propValue, attrValue ); + } else if ( placeholder ) { + /* + * Placeholder fallback. + * If the attribute is `src` or `href`, + * a placeholder can't be used because it is not a valid url. + * Adding this workaround until + * attributes and metadata fields types are improved and include `url`. + */ + const htmlAttribute = + getBlockType( blockName ).attributes[ attrName ].attribute; + + if ( htmlAttribute === 'src' || htmlAttribute === 'href' ) { + updateBoundAttibute( null ); + return; + } + + updateBoundAttibute( placeholder ); + } + }, [ + updateBoundAttibute, + propValue, + attrValue, + placeholder, + blockName, + attrName, + ] ); + + return null; +}; + +/** + * BlockBindingBridge acts like a component wrapper + * that connects the bound attributes of a block + * to the source handlers. + * For this, it creates a BindingConnector for each bound attribute. + * + * @param {Object} props - The component props. + * @param {Object} props.blockProps - The BlockEdit props object. + * @param {Object} props.bindings - The block bindings settings. + * @param {Function} props.onPropValueChange - The function to call when the attribute value changes. + * @return {null} Data-handling component. Render nothing. + */ +function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) { + const blockBindingsSources = unlock( + useSelect( blocksStore ) + ).getAllBlockBindingsSources(); + + return ( + <> + { Object.entries( bindings ).map( + ( [ attrName, boundAttribute ] ) => { + // Bail early if the block doesn't have a valid source handler. + const source = + blockBindingsSources[ boundAttribute.source ]; + if ( ! source?.useSource ) { + return null; + } + + return ( + + ); + } + ) } + + ); +} + +const withBlockBindingSupport = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + /* + * Collect and update the bound attributes + * in a separate state. + */ + const [ boundAttributes, setBoundAttributes ] = useState( {} ); + const updateBoundAttributes = useCallback( + ( newAttributes ) => + setBoundAttributes( ( prev ) => ( { + ...prev, + ...newAttributes, + } ) ), + [] + ); + + /* + * Create binding object filtering + * only the attributes that can be bound. + */ + const bindings = Object.fromEntries( + Object.entries( props.attributes.metadata?.bindings || {} ).filter( + ( [ attrName ] ) => canBindAttribute( props.name, attrName ) + ) + ); + + return ( + <> + { Object.keys( bindings ).length > 0 && ( + + ) } + - ); - }, - 'useBoundAttributes' - ); + + ); + }, + 'withBlockBindingSupport' +); /** * Filters a registered block's settings to enhance a block's `edit` component * to upgrade bound attributes. * - * @param {WPBlockSettings} settings Registered block settings. - * + * @param {WPBlockSettings} settings - Registered block settings. + * @param {string} name - Block name. * @return {WPBlockSettings} Filtered block settings. */ -function shimAttributeSource( settings ) { - if ( ! ( settings.name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { +function shimAttributeSource( settings, name ) { + if ( ! canBindBlock( name ) ) { return settings; } - settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit ); - return settings; + return { + ...settings, + edit: withBlockBindingSupport( settings.edit ), + }; } addFilter( diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index ec6843ead24895..92da4b87196325 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -10,7 +10,7 @@ import { ComposedPrivateInserter as PrivateInserter } from './components/inserte import { default as PrivateQuickInserter } from './components/inserter/quick-inserter'; import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; -import { useCanBlockToolbarBeFocused } from './utils/use-can-block-toolbar-be-focused'; +import { useShowBlockTools } from './components/block-tools/use-show-block-tools'; import { cleanEmptyObject, useStyleOverride } from './hooks/utils'; import BlockQuickNavigation from './components/block-quick-navigation'; import { LayoutStyle } from './components/block-list/layout'; @@ -27,6 +27,7 @@ import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; import { selectBlockPatternsKey } from './store/private-keys'; +import { requiresWrapperOnCopy } from './components/writing-flow/utils'; import { PrivateRichText } from './components/rich-text/'; /** @@ -44,7 +45,7 @@ lock( privateApis, { PrivateListView, ResizableBoxPopover, BlockInfo, - useCanBlockToolbarBeFocused, + useShowBlockTools, cleanEmptyObject, useStyleOverride, BlockQuickNavigation, @@ -59,5 +60,6 @@ lock( privateApis, { usesContextKey, useFlashEditableBlocks, selectBlockPatternsKey, + requiresWrapperOnCopy, PrivateRichText, } ); diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 1cbc49f58551e5..015cffde42a239 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -1,5 +1,6 @@ @import "./autocompleters/style.scss"; @import "./components/block-alignment-control/style.scss"; +@import "./components/block-bindings-toolbar-indicator/style.scss"; @import "./components/block-canvas/style.scss"; @import "./components/block-icon/style.scss"; @import "./components/block-inspector/style.scss"; diff --git a/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js b/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js deleted file mode 100644 index f118c88dc2b1d4..00000000000000 --- a/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../store'; -import { unlock } from '../lock-unlock'; - -/** - * Returns true if the block toolbar should be able to receive focus. - * - * @return {boolean} Whether the block toolbar should be able to receive focus - */ -export function useCanBlockToolbarBeFocused() { - return useSelect( ( select ) => { - const { - __unstableGetEditorMode, - getBlock, - getSettings, - getSelectedBlockClientId, - getFirstMultiSelectedBlockClientId, - } = unlock( select( blockEditorStore ) ); - - const selectedBlockId = - getFirstMultiSelectedBlockClientId() || getSelectedBlockClientId(); - const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( - getBlock( selectedBlockId ) || {} - ); - - // Fixed Toolbar can be focused when: - // - a block is selected - // - fixed toolbar is on - // Block Toolbar Popover can be focused when: - // - a block is selected - // - we are in edit mode - // - it is not an empty default block - return ( - !! selectedBlockId && - ( getSettings().hasFixedToolbar || - ( __unstableGetEditorMode() === 'edit' && - ! isEmptyDefaultBlock ) ) - ); - }, [] ); -} diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index b50de773a42ce7..e98f845cb56a73 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { getBlobByURL, isBlobURL } from '@wordpress/blob'; +import { isBlobURL } from '@wordpress/blob'; import { Disabled, PanelBody, @@ -21,11 +21,9 @@ import { MediaPlaceholder, MediaReplaceFlow, useBlockProps, - store as blockEditorStore, } from '@wordpress/block-editor'; -import { useEffect } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { audio as icon } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; @@ -33,6 +31,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import { createUpgradedEmbedBlock } from '../embed/util'; +import { useUploadMediaFromBlobURL } from '../utils/hooks'; import { Caption } from '../utils/caption'; const ALLOWED_MEDIA_TYPES = [ 'audio' ]; @@ -47,22 +46,13 @@ function AudioEdit( { } ) { const { id, autoplay, loop, preload, src } = attributes; const isTemporaryAudio = ! id && isBlobURL( src ); - const { getSettings } = useSelect( blockEditorStore ); - useEffect( () => { - if ( ! id && isBlobURL( src ) ) { - const file = getBlobByURL( src ); - - if ( file ) { - getSettings().mediaUpload( { - filesList: [ file ], - onFileChange: ( [ media ] ) => onSelectAudio( media ), - onError: ( e ) => onUploadError( e ), - allowedTypes: ALLOWED_MEDIA_TYPES, - } ); - } - } - }, [] ); + useUploadMediaFromBlobURL( { + url: src, + allowedTypes: ALLOWED_MEDIA_TYPES, + onChange: onSelectAudio, + onError: onUploadError, + } ); function toggleAttribute( attribute ) { return ( newValue ) => { diff --git a/packages/block-library/src/block/deprecated.js b/packages/block-library/src/block/deprecated.js index 7bc243bbf4ce98..f820867fff6271 100644 --- a/packages/block-library/src/block/deprecated.js +++ b/packages/block-library/src/block/deprecated.js @@ -1,4 +1,75 @@ -// v1: Migrate and rename the `overrides` attribute to the `content` attribute. +const isObject = ( obj ) => + typeof obj === 'object' && ! Array.isArray( obj ) && obj !== null; + +// v2: Migrate to a more condensed version of the 'content' attribute attribute. +const v2 = { + attributes: { + ref: { + type: 'number', + }, + content: { + type: 'object', + }, + }, + supports: { + customClassName: false, + html: false, + inserter: false, + renaming: false, + }, + // Force this deprecation to run whenever there's a values sub-property that's an object. + // + // This could fail in the future if a block ever has binding to a `values` attribute. + // Some extra protection is added to ensure `values` is an object, but this only reduces + // the likelihood, it doesn't solve it completely. + isEligible( { content } ) { + return ( + !! content && + Object.keys( content ).every( + ( contentKey ) => + content[ contentKey ].values && + isObject( content[ contentKey ].values ) + ) + ); + }, + /* + * Old attribute format: + * content: { + * "V98q_x": { + * // The attribute values are now stored as a 'values' sub-property. + * values: { content: 'My content value' }, + * // ... additional metadata, like the block name can be stored here. + * } + * } + * + * New attribute format: + * content: { + * "V98q_x": { + * content: 'My content value', + * } + * } + */ + migrate( attributes ) { + const { content, ...retainedAttributes } = attributes; + + if ( content && Object.keys( content ).length ) { + const updatedContent = { ...content }; + + for ( const contentKey in content ) { + updatedContent[ contentKey ] = content[ contentKey ].values; + } + + return { + ...retainedAttributes, + content: updatedContent, + }; + } + + return attributes; + }, +}; + +// v1: Rename the `overrides` attribute to the `content` attribute. const v1 = { attributes: { ref: { @@ -23,16 +94,12 @@ const v1 = { * overrides: { * // An key is an id that represents a block. * // The values are the attribute values of the block. - * "V98q_x": { content: 'dwefwefwefwe' } + * "V98q_x": { content: 'My content value' } * } * * New attribute format: * content: { - * "V98q_x": { - * // The attribute values are now stored as a 'values' sub-property. - * values: { content: 'dwefwefwefwe' }, - * // ... additional metadata, like the block name can be stored here. - * } + * "V98q_x": { content: 'My content value' } * } * */ @@ -42,9 +109,7 @@ const v1 = { const content = {}; Object.keys( overrides ).forEach( ( id ) => { - content[ id ] = { - values: overrides[ id ], - }; + content[ id ] = overrides[ id ]; } ); return { @@ -54,4 +119,4 @@ const v1 = { }, }; -export default [ v1 ]; +export default [ v2, v1 ]; diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 5efe245c935fc8..ddacc47dbd0391 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -42,6 +42,25 @@ const { PARTIAL_SYNCING_SUPPORTED_BLOCKS } = unlock( patternsPrivateApis ); const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; +function getLegacyIdMap( blocks, content, nameCount = {} ) { + let idToClientIdMap = {}; + for ( const block of blocks ) { + if ( block?.innerBlocks?.length ) { + idToClientIdMap = { + ...idToClientIdMap, + ...getLegacyIdMap( block.innerBlocks, content, nameCount ), + }; + } + + const id = block.attributes.metadata?.id; + const clientId = block.clientId; + if ( id && content?.[ id ] ) { + idToClientIdMap[ clientId ] = id; + } + } + return idToClientIdMap; +} + const useInferredLayout = ( blocks, parentLayout ) => { const initialInferredAlignmentRef = useRef(); @@ -101,25 +120,31 @@ function getOverridableAttributes( block ) { function applyInitialContentValuesToInnerBlocks( blocks, content = {}, - defaultValues + defaultValues, + legacyIdMap ) { return blocks.map( ( block ) => { const innerBlocks = applyInitialContentValuesToInnerBlocks( block.innerBlocks, content, - defaultValues + defaultValues, + legacyIdMap ); - const blockId = block.attributes.metadata?.id; - if ( ! hasOverridableAttributes( block ) || ! blockId ) + const metadataName = + legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name; + + if ( ! metadataName || ! hasOverridableAttributes( block ) ) { return { ...block, innerBlocks }; + } + const attributes = getOverridableAttributes( block ); const newAttributes = { ...block.attributes }; for ( const attributeKey of attributes ) { - defaultValues[ blockId ] ??= {}; - defaultValues[ blockId ][ attributeKey ] = + defaultValues[ metadataName ] ??= {}; + defaultValues[ metadataName ][ attributeKey ] = block.attributes[ attributeKey ]; - const contentValues = content[ blockId ]?.values; + const contentValues = content[ metadataName ]; if ( contentValues?.[ attributeKey ] !== undefined ) { newAttributes[ attributeKey ] = contentValues[ attributeKey ]; } @@ -142,29 +167,40 @@ function isAttributeEqual( attribute1, attribute2 ) { return attribute1 === attribute2; } -function getContentValuesFromInnerBlocks( blocks, defaultValues ) { +function getContentValuesFromInnerBlocks( blocks, defaultValues, legacyIdMap ) { /** @type {Record}>} */ const content = {}; for ( const block of blocks ) { if ( block.name === patternBlockName ) continue; - Object.assign( - content, - getContentValuesFromInnerBlocks( block.innerBlocks, defaultValues ) - ); - const blockId = block.attributes.metadata?.id; - if ( ! hasOverridableAttributes( block ) || ! blockId ) continue; + if ( block.innerBlocks.length ) { + Object.assign( + content, + getContentValuesFromInnerBlocks( + block.innerBlocks, + defaultValues, + legacyIdMap + ) + ); + } + const metadataName = + legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name; + if ( ! metadataName || ! hasOverridableAttributes( block ) ) { + continue; + } + const attributes = getOverridableAttributes( block ); + for ( const attributeKey of attributes ) { if ( ! isAttributeEqual( block.attributes[ attributeKey ], - defaultValues[ blockId ][ attributeKey ] + defaultValues?.[ metadataName ]?.[ attributeKey ] ) ) { - content[ blockId ] ??= { values: {}, blockName: block.name }; + content[ metadataName ] ??= {}; // TODO: We need a way to represent `undefined` in the serialized overrides. // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 - content[ blockId ].values[ attributeKey ] = + content[ metadataName ][ attributeKey ] = block.attributes[ attributeKey ] === undefined ? // TODO: We use an empty string to represent undefined for now until // we support a richer format for overrides and the block binding API. @@ -278,8 +314,15 @@ export default function ReusableBlockEdit( { [ editedRecord.blocks, editedRecord.content ] ); + const legacyIdMap = useRef( {} ); + // Apply the initial overrides from the pattern block to the inner blocks. useEffect( () => { + // Build a map of clientIds to the old nano id system to provide back compat. + legacyIdMap.current = getLegacyIdMap( + initialBlocks, + initialContent.current + ); defaultContent.current = {}; const originalEditingMode = getBlockEditingMode( patternClientId ); // Replace the contents of the blocks with the overrides. @@ -291,7 +334,8 @@ export default function ReusableBlockEdit( { applyInitialContentValuesToInnerBlocks( initialBlocks, initialContent.current, - defaultContent.current + defaultContent.current, + legacyIdMap.current ) ); } ); @@ -343,7 +387,8 @@ export default function ReusableBlockEdit( { setAttributes( { content: getContentValuesFromInnerBlocks( blocks, - defaultContent.current + defaultContent.current, + legacyIdMap.current ), } ); } ); diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 4aabe98ffa4650..49b5786eacc79f 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -48,26 +48,35 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->run_shortcode( $reusable_block->post_content ); $content = $wp_embed->autoembed( $content ); - // Back compat, the content attribute was previously named overrides and - // had a slightly different format. For blocks that have not been migrated, - // also convert the format here so that the provided `pattern/overrides` - // context is correct. - if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) { - $migrated_content = array(); - foreach ( $attributes['overrides'] as $id => $values ) { - $migrated_content[ $id ] = array( - 'values' => $values, - ); + // Back compat. + // For blocks that have not been migrated in the editor, add some back compat + // so that front-end rendering continues to work. + + // This matches the `v2` deprecation. Removes the inner `values` property + // from every item. + if ( isset( $attributes['content'] ) ) { + foreach ( $attributes['content'] as &$content_data ) { + if ( isset( $content_data['values'] ) ) { + $is_assoc_array = is_array( $content_data['values'] ) && ! wp_is_numeric_array( $content_data['values'] ); + + if ( $is_assoc_array ) { + $content_data = $content_data['values']; + } + } } - $attributes['content'] = $migrated_content; } - $has_pattern_overrides = isset( $attributes['content'] ); + + // This matches the `v1` deprecation. Rename `overrides` to `content`. + if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) { + $attributes['content'] = $attributes['overrides']; + } /** * We set the `pattern/overrides` context through the `render_block_context` * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ + $has_pattern_overrides = isset( $attributes['content'] ); if ( $has_pattern_overrides ) { $filter_block_context = static function ( $context ) use ( $attributes ) { $context['pattern/overrides'] = $attributes['content']; diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index 528a488039acfd..0ce8e107a0074b 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import { isBlobURL } from '@wordpress/blob'; import { __unstableGetAnimateClassName as getAnimateClassName, ResizableBox, @@ -36,6 +36,7 @@ import { store as noticesStore } from '@wordpress/notices'; import FileBlockInspector from './inspector'; import { browserSupportsPdfs } from './utils'; import removeAnchorTag from '../utils/remove-anchor-tag'; +import { useUploadMediaFromBlobURL } from '../utils/hooks'; export const MIN_PREVIEW_HEIGHT = 200; export const MAX_PREVIEW_HEIGHT = 2000; @@ -72,7 +73,6 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { displayPreview, previewHeight, } = attributes; - const { getSettings } = useSelect( blockEditorStore ); const { media } = useSelect( ( select ) => ( { media: @@ -86,20 +86,13 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { const { createErrorNotice } = useDispatch( noticesStore ); const { toggleSelection } = useDispatch( blockEditorStore ); - useEffect( () => { - // Upload a file drag-and-dropped into the editor. - if ( isBlobURL( href ) ) { - const file = getBlobByURL( href ); - - getSettings().mediaUpload( { - filesList: [ file ], - onFileChange: ( [ newMedia ] ) => onSelectFile( newMedia ), - onError: onUploadError, - } ); - - revokeBlobURL( href ); - } + useUploadMediaFromBlobURL( { + url: href, + onChange: onSelectFile, + onError: onUploadError, + } ); + useEffect( () => { if ( RichText.isEmpty( downloadButtonText ) ) { setAttributes( { downloadButtonText: _x( 'Download', 'button label' ), diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index e1e221bc9575a0..489343e1dfef77 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -20,7 +20,7 @@ import { useBlockEditingMode, } from '@wordpress/block-editor'; import { useEffect, useRef, useState } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { image as icon, plugins as pluginsIcon } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; @@ -336,7 +336,7 @@ export function ImageEdit( { } ); // Much of this description is duplicated from MediaPlaceholder. - const { lockUrlControls = false } = useSelect( + const { lockUrlControls = false, lockUrlControlsMessage } = useSelect( ( select ) => { if ( ! isSingleSelected ) { return {}; @@ -351,6 +351,13 @@ export function ImageEdit( { !! metadata?.bindings?.url && ( ! blockBindingsSource || blockBindingsSource?.lockAttributesEditing ), + lockUrlControlsMessage: blockBindingsSource?.label + ? sprintf( + /* translators: %s: Label of the bindings source. */ + __( 'Connected to %s' ), + blockBindingsSource.label + ) + : __( 'Connected to dynamic data' ), }; }, [ isSingleSelected ] @@ -387,7 +394,7 @@ export function ImageEdit( { - { __( 'Connected to a custom field' ) } + { lockUrlControlsMessage } ) : ( content diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index ea0f82a2e1986b..e55f6bfa03d73e 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -410,7 +410,9 @@ export default function Image( { lockUrlControls = false, lockHrefControls = false, lockAltControls = false, + lockAltControlsMessage, lockTitleControls = false, + lockTitleControlsMessage, lockCaption = false, } = useSelect( ( select ) => { @@ -454,10 +456,24 @@ export default function Image( { !! altBinding && ( ! altBindingSource || altBindingSource?.lockAttributesEditing ), + lockAltControlsMessage: altBindingSource?.label + ? sprintf( + /* translators: %s: Label of the bindings source. */ + __( 'Connected to %s' ), + altBindingSource.label + ) + : __( 'Connected to dynamic data' ), lockTitleControls: !! titleBinding && ( ! titleBindingSource || titleBindingSource?.lockAttributesEditing ), + lockTitleControlsMessage: titleBindingSource?.label + ? sprintf( + /* translators: %s: Label of the bindings source. */ + __( 'Connected to %s' ), + titleBindingSource.label + ) + : __( 'Connected to dynamic data' ), }; }, [ clientId, isSingleSelected, metadata?.bindings ] @@ -557,11 +573,7 @@ export default function Image( { disabled={ lockAltControls } help={ lockAltControls ? ( - <> - { __( - 'Connected to a custom field' - ) } - + <>{ lockAltControlsMessage } ) : ( <> @@ -607,11 +619,7 @@ export default function Image( { disabled={ lockTitleControls } help={ lockTitleControls ? ( - <> - { __( - 'Connected to a custom field' - ) } - + <>{ lockTitleControlsMessage } ) : ( <> { __( @@ -652,11 +660,7 @@ export default function Image( { readOnly={ lockAltControls } help={ lockAltControls ? ( - <> - { __( - 'Connected to a custom field' - ) } - + <>{ lockAltControlsMessage } ) : ( <> @@ -694,7 +698,7 @@ export default function Image( { readOnly={ lockTitleControls } help={ lockTitleControls ? ( - <>{ __( 'Connected to a custom field' ) } + <>{ lockTitleControlsMessage } ) : ( <> { __( diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 46cbd3a94831d5..467154f76992e1 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -29,7 +29,6 @@ import { useOutdentListItem, useSplit, useMerge, - useCopy, } from './hooks'; import { convertToListItems } from './utils'; @@ -79,7 +78,7 @@ export default function ListItemEdit( { mergeBlocks, } ) { const { placeholder, content } = attributes; - const blockProps = useBlockProps( { ref: useCopy( clientId ) } ); + const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( blockProps, { renderAppender: false, __unstableDisableDropZone: true, diff --git a/packages/block-library/src/list-item/hooks/index.js b/packages/block-library/src/list-item/hooks/index.js index 3bbc3167abed32..1687adbe740d0a 100644 --- a/packages/block-library/src/list-item/hooks/index.js +++ b/packages/block-library/src/list-item/hooks/index.js @@ -4,4 +4,3 @@ export { default as useEnter } from './use-enter'; export { default as useSpace } from './use-space'; export { default as useSplit } from './use-split'; export { default as useMerge } from './use-merge'; -export { default as useCopy } from './use-copy'; diff --git a/packages/block-library/src/list-item/hooks/use-copy.js b/packages/block-library/src/list-item/hooks/use-copy.js deleted file mode 100644 index 7a76019ad11a4b..00000000000000 --- a/packages/block-library/src/list-item/hooks/use-copy.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * WordPress dependencies - */ -import { useRefEffect } from '@wordpress/compose'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; - -export default function useCopy( clientId ) { - const { getBlockRootClientId, getBlockName, getBlockAttributes } = - useSelect( blockEditorStore ); - - return useRefEffect( ( node ) => { - function onCopy( event ) { - // The event propagates through all nested lists, so don't override - // when copying nested list items. - if ( event.clipboardData.getData( '__unstableWrapperBlockName' ) ) { - return; - } - - const rootClientId = getBlockRootClientId( clientId ); - event.clipboardData.setData( - '__unstableWrapperBlockName', - getBlockName( rootClientId ) - ); - event.clipboardData.setData( - '__unstableWrapperBlockAttributes', - JSON.stringify( getBlockAttributes( rootClientId ) ) - ); - } - - node.addEventListener( 'copy', onCopy ); - node.addEventListener( 'cut', onCopy ); - return () => { - node.removeEventListener( 'copy', onCopy ); - node.removeEventListener( 'cut', onCopy ); - }; - }, [] ); -} diff --git a/packages/block-library/src/list-item/hooks/use-indent-list-item.js b/packages/block-library/src/list-item/hooks/use-indent-list-item.js index 6eb5d9d73ba658..2cd62d201d61f1 100644 --- a/packages/block-library/src/list-item/hooks/use-indent-list-item.js +++ b/packages/block-library/src/list-item/hooks/use-indent-list-item.js @@ -61,5 +61,7 @@ export default function useIndentListItem( clientId ) { clonedBlocks[ clonedBlocks.length - 1 ].clientId ); } + + return true; }, [ clientId ] ); } diff --git a/packages/block-library/src/list-item/hooks/use-outdent-list-item.js b/packages/block-library/src/list-item/hooks/use-outdent-list-item.js index 85c433fbeffada..a17890eada6c51 100644 --- a/packages/block-library/src/list-item/hooks/use-outdent-list-item.js +++ b/packages/block-library/src/list-item/hooks/use-outdent-list-item.js @@ -93,5 +93,7 @@ export default function useOutdentListItem() { removeBlock( parentListId, shouldSelectParent ); } } ); + + return true; }, [] ); } diff --git a/packages/block-library/src/list-item/hooks/use-space.js b/packages/block-library/src/list-item/hooks/use-space.js index deb6313e4b1b0e..ee4d8bdbf8786e 100644 --- a/packages/block-library/src/list-item/hooks/use-space.js +++ b/packages/block-library/src/list-item/hooks/use-space.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useRefEffect } from '@wordpress/compose'; -import { SPACE } from '@wordpress/keycodes'; +import { SPACE, TAB } from '@wordpress/keycodes'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; @@ -10,11 +10,13 @@ import { useSelect } from '@wordpress/data'; * Internal dependencies */ import useIndentListItem from './use-indent-list-item'; +import useOutdentListItem from './use-outdent-list-item'; export default function useSpace( clientId ) { const { getSelectionStart, getSelectionEnd, getBlockIndex } = useSelect( blockEditorStore ); const indentListItem = useIndentListItem( clientId ); + const outdentListItem = useOutdentListItem(); return useRefEffect( ( element ) => { @@ -23,9 +25,8 @@ export default function useSpace( clientId ) { if ( event.defaultPrevented || - keyCode !== SPACE || + ( keyCode !== SPACE && keyCode !== TAB ) || // Only override when no modifiers are pressed. - shiftKey || altKey || metaKey || ctrlKey @@ -33,18 +34,24 @@ export default function useSpace( clientId ) { return; } - if ( getBlockIndex( clientId ) === 0 ) { - return; - } - const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd(); if ( selectionStart.offset === 0 && selectionEnd.offset === 0 ) { - event.preventDefault(); - indentListItem(); + if ( shiftKey ) { + // Note that backspace behaviour in defined in onMerge. + if ( keyCode === TAB ) { + if ( outdentListItem() ) { + event.preventDefault(); + } + } + } else if ( getBlockIndex( clientId ) !== 0 ) { + if ( indentListItem() ) { + event.preventDefault(); + } + } } } diff --git a/packages/block-library/src/list-item/index.js b/packages/block-library/src/list-item/index.js index 00adc1c2c40266..07c5bb7fda9015 100644 --- a/packages/block-library/src/list-item/index.js +++ b/packages/block-library/src/list-item/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { listItem as icon } from '@wordpress/icons'; +import { privateApis } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,6 +12,7 @@ import metadata from './block.json'; import edit from './edit'; import save from './save'; import transforms from './transforms'; +import { unlock } from '../lock-unlock'; const { name } = metadata; @@ -27,6 +29,7 @@ export const settings = { }; }, transforms, + [ unlock( privateApis ).requiresWrapperOnCopy ]: true, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 3a3a654aee6126..1d73d09bbd1fbb 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -552,7 +552,7 @@ private static function get_nav_element_directives( $is_interactive ) { return ''; } // When adding to this array be mindful of security concerns. - $nav_element_context = data_wp_context( + $nav_element_context = wp_interactivity_data_wp_context( array( 'overlayOpenedBy' => array( 'click' => false, diff --git a/packages/block-library/src/query/utils.js b/packages/block-library/src/query/utils.js index 2ce4acd4a782cf..213d9bc96cd64b 100644 --- a/packages/block-library/src/query/utils.js +++ b/packages/block-library/src/query/utils.js @@ -131,16 +131,18 @@ export const useTaxonomies = ( postType ) => { const taxonomies = useSelect( ( select ) => { const { getTaxonomies } = select( coreStore ); - const filteredTaxonomies = getTaxonomies( { + return getTaxonomies( { type: postType, per_page: -1, - context: 'view', } ); - return filteredTaxonomies; }, [ postType ] ); - return taxonomies; + return useMemo( () => { + return taxonomies?.filter( + ( { visibility } ) => !! visibility?.publicly_queryable + ); + }, [ taxonomies ] ); }; /** diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index c368c2ab03dbf8..ca8c70edfa907d 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -179,7 +179,7 @@ function render_block_core_search( $attributes ) { if ( $is_expandable_searchfield ) { $aria_label_expanded = __( 'Submit Search' ); $aria_label_collapsed = __( 'Expand search field' ); - $form_context = data_wp_context( + $form_context = wp_interactivity_data_wp_context( array( 'isSearchInputVisible' => $open_by_default, 'inputId' => $input_id, diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index fe4146406ddf60..079811f0aae958 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -268,6 +268,14 @@ const SiteLogo = ( { ); + // Support the previous location for the Site Icon settings. To be removed + // when the required WP core version for Gutenberg is >= 6.5.0. + const shouldUseNewUrl = ! window?.__experimentalUseCustomizerSiteLogoUrl; + + const siteIconSettingsUrl = shouldUseNewUrl + ? siteUrl + '/wp-admin/options-general.php' + : siteUrl + '/wp-admin/customize.php?autofocus[section]=title_tagline'; + const syncSiteIconHelpText = createInterpolateElement( __( 'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. To use a custom icon that is different from your site logo, use the Site Icon settings.' @@ -276,10 +284,7 @@ const SiteLogo = ( { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content diff --git a/packages/block-library/src/utils/hooks.js b/packages/block-library/src/utils/hooks.js index 4b64c529299467..a89031b5f99ce9 100644 --- a/packages/block-library/src/utils/hooks.js +++ b/packages/block-library/src/utils/hooks.js @@ -2,6 +2,9 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; +import { useLayoutEffect, useEffect, useRef } from '@wordpress/element'; +import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; /** @@ -19,6 +22,53 @@ export function useCanEditEntity( kind, name, recordId ) { ); } -export default { - useCanEditEntity, -}; +/** + * Handles uploading a media file from a blob URL on mount. + * + * @param {Object} args Upload media arguments. + * @param {string} args.url Blob URL. + * @param {?Array} args.allowedTypes Array of allowed media types. + * @param {Function} args.onChange Function called when the media is uploaded. + * @param {Function} args.onError Function called when an error happens. + */ +export function useUploadMediaFromBlobURL( args = {} ) { + const latestArgs = useRef( args ); + const { getSettings } = useSelect( blockEditorStore ); + + useLayoutEffect( () => { + latestArgs.current = args; + } ); + + useEffect( () => { + if ( + ! latestArgs.current.url || + ! isBlobURL( latestArgs.current.url ) + ) { + return; + } + + const file = getBlobByURL( latestArgs.current.url ); + if ( ! file ) { + return; + } + + const { url, allowedTypes, onChange, onError } = latestArgs.current; + const { mediaUpload } = getSettings(); + + mediaUpload( { + filesList: [ file ], + allowedTypes, + onFileChange: ( [ media ] ) => { + if ( isBlobURL( media?.url ) ) { + return; + } + + revokeBlobURL( url ); + onChange( media ); + }, + onError: ( message ) => { + onError( message ); + }, + } ); + }, [ getSettings ] ); +} diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index 60c841b18a9318..65a8e952d4b9b0 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { getBlobByURL, isBlobURL } from '@wordpress/blob'; +import { isBlobURL } from '@wordpress/blob'; import { BaseControl, Button, @@ -24,12 +24,11 @@ import { MediaUploadCheck, MediaReplaceFlow, useBlockProps, - store as blockEditorStore, } from '@wordpress/block-editor'; import { useRef, useEffect } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { video as icon } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; @@ -37,6 +36,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import { createUpgradedEmbedBlock } from '../embed/util'; +import { useUploadMediaFromBlobURL } from '../utils/hooks'; import VideoCommonSettings from './edit-common-settings'; import TracksEditor from './tracks-editor'; import Tracks from './tracks'; @@ -75,21 +75,13 @@ function VideoEdit( { const posterImageButton = useRef(); const { id, controls, poster, src, tracks } = attributes; const isTemporaryVideo = ! id && isBlobURL( src ); - const { getSettings } = useSelect( blockEditorStore ); - useEffect( () => { - if ( ! id && isBlobURL( src ) ) { - const file = getBlobByURL( src ); - if ( file ) { - getSettings().mediaUpload( { - filesList: [ file ], - onFileChange: ( [ media ] ) => onSelectVideo( media ), - onError: onUploadError, - allowedTypes: ALLOWED_MEDIA_TYPES, - } ); - } - } - }, [] ); + useUploadMediaFromBlobURL( { + url: src, + allowedTypes: ALLOWED_MEDIA_TYPES, + onChange: onSelectVideo, + onError: onUploadError, + } ); useEffect( () => { // Placeholder may be rendered. diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 7062c98404a2a6..62933d69d764f4 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -36,6 +36,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { requiresOptOut: true, useEngine: true, }, + backgroundImage: { + value: [ 'background', 'backgroundImage' ], + support: [ 'background', 'backgroundImage' ], + useEngine: true, + }, backgroundRepeat: { value: [ 'background', 'backgroundRepeat' ], support: [ 'background', 'backgroundRepeat' ], diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 769d201e587d07..6cdde409cbd176 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,11 +5,18 @@ ### Bug Fix - `Tooltip`: Explicitly set system font to avoid CSS bleed ([#59307](https://github.com/WordPress/gutenberg/pull/59307)). +- `HStack`, `VStack`: Stop passing invalid props to underlying element ([#59416](https://github.com/WordPress/gutenberg/pull/59416)). - `Button`: Fix focus outline in disabled primary variant ([#59391](https://github.com/WordPress/gutenberg/pull/59391)). +- `Button`: Place `children` before the icon when `iconPosition` is `right` ([#59489](https://github.com/WordPress/gutenberg/pull/59489)). ### Internal - `SnackbarList`, `Snackbar`: add unit tests ([#59157](https://github.com/WordPress/gutenberg/pull/59157)). +- Remove unused `useLatestRef()` hook ([#59471](https://github.com/WordPress/gutenberg/pull/59471)). + +### Experimental + +- `CustomSelectControlV2`: Remove legacy adapter layer ([#59420](https://github.com/WordPress/gutenberg/pull/59420)). ## 27.0.0 (2024-02-21) diff --git a/packages/components/src/button/index.tsx b/packages/components/src/button/index.tsx index 966075dd6e2b9a..a16f190e44704b 100644 --- a/packages/components/src/button/index.tsx +++ b/packages/components/src/button/index.tsx @@ -223,10 +223,10 @@ export function UnforwardedButton( ) } { text && <>{ text } } + { children } { icon && iconPosition === 'right' && ( ) } - { children } ); diff --git a/packages/components/src/custom-select-control-v2/default-component/index.tsx b/packages/components/src/custom-select-control-v2/default-component/index.tsx index 746861ed03b5a5..e5650202a41605 100644 --- a/packages/components/src/custom-select-control-v2/default-component/index.tsx +++ b/packages/components/src/custom-select-control-v2/default-component/index.tsx @@ -8,8 +8,11 @@ import * as Ariakit from '@ariakit/react'; */ import _CustomSelect from '../custom-select'; import type { CustomSelectProps } from '../types'; +import type { WordPressComponentProps } from '../../context'; -function CustomSelect( props: CustomSelectProps ) { +function CustomSelect( + props: WordPressComponentProps< CustomSelectProps, 'button', false > +) { const { defaultValue, onChange, value, ...restProps } = props; // Forward props + store from v2 implementation const store = Ariakit.useSelectStore( { diff --git a/packages/components/src/custom-select-control-v2/index.tsx b/packages/components/src/custom-select-control-v2/index.tsx index 58ca626be91619..f05191ad8fc0b1 100644 --- a/packages/components/src/custom-select-control-v2/index.tsx +++ b/packages/components/src/custom-select-control-v2/index.tsx @@ -1,5 +1,5 @@ /** * Internal dependencies */ -export { default as CustomSelect } from './legacy-adapter'; +export { default as CustomSelect } from './default-component'; export { default as CustomSelectItem } from './custom-select-item'; diff --git a/packages/components/src/custom-select-control-v2/legacy-adapter.tsx b/packages/components/src/custom-select-control-v2/legacy-adapter.tsx deleted file mode 100644 index ab7fc74d977929..00000000000000 --- a/packages/components/src/custom-select-control-v2/legacy-adapter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Internal dependencies - */ -import _LegacyCustomSelect from './legacy-component'; -import _NewCustomSelect from './default-component'; -import type { CustomSelectProps, LegacyCustomSelectProps } from './types'; -import type { WordPressComponentProps } from '../context'; - -function isLegacy( props: any ): props is LegacyCustomSelectProps { - return typeof props.options !== 'undefined'; -} - -function CustomSelect( - props: - | LegacyCustomSelectProps - | WordPressComponentProps< CustomSelectProps, 'button', false > -) { - if ( isLegacy( props ) ) { - return <_LegacyCustomSelect { ...props } />; - } - - return <_NewCustomSelect { ...props } />; -} - -export default CustomSelect; diff --git a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx new file mode 100644 index 00000000000000..dbb1ac1d784022 --- /dev/null +++ b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx @@ -0,0 +1,457 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { click, press, sleep, type, waitFor } from '@ariakit/test'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import UncontrolledCustomSelect from '..'; + +const customClass = 'amber-skies'; + +const legacyProps = { + label: 'label!', + options: [ + { + key: 'flower1', + name: 'violets', + }, + { + key: 'flower2', + name: 'crimson clover', + className: customClass, + }, + { + key: 'flower3', + name: 'poppy', + }, + { + key: 'color1', + name: 'amber', + className: customClass, + }, + { + key: 'color2', + name: 'aquamarine', + style: { + backgroundColor: 'rgb(127, 255, 212)', + rotate: '13deg', + }, + }, + ], +}; + +const LegacyControlledCustomSelect = ( { + options, + onChange, + ...restProps +}: React.ComponentProps< typeof UncontrolledCustomSelect > ) => { + const [ value, setValue ] = useState( options[ 0 ] ); + return ( + { + onChange?.( args ); + setValue( args.selectedItem ); + } } + value={ options.find( + ( option: any ) => option.key === value.key + ) } + /> + ); +}; + +describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', LegacyControlledCustomSelect ], +] )( 'CustomSelectControl (%s)', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; + + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( + legacyProps.options[ 0 ].name + ); + } ); + + it( 'Should apply class only to options that have a className defined', async () => { + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ a className added + const itemsWithClass = legacyProps.options.filter( + ( option ) => option.className !== undefined + ); + + // assert against filtered array + itemsWithClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveClass( + customClass + ) + ); + + // return an array of items _without_ a className added + const itemsWithoutClass = legacyProps.options.filter( + ( option ) => option.className === undefined + ); + + // assert against filtered array + itemsWithoutClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass( + customClass + ) + ); + } ); + + it( 'Should apply styles only to options that have styles defined', async () => { + const customStyles = + 'background-color: rgb(127, 255, 212); rotate: 13deg;'; + + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ styles added + const styledItems = legacyProps.options.filter( + ( option ) => option.style !== undefined + ); + + // assert against filtered array + styledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( + customStyles + ) + ); + + // return an array of items _without_ styles added + const unstyledItems = legacyProps.options.filter( + ( option ) => option.style === undefined + ); + + // assert against filtered array + unstyledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveStyle( + customStyles + ) + ); + } ); + + it( 'does not show selected hint by default', async () => { + render( + + ); + await waitFor( () => + expect( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ).not.toHaveTextContent( 'Hint' ) + ); + } ); + + it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { + render( + + ); + + await waitFor( () => + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveTextContent( /hint/i ) + ); + } ); + + it( 'shows selected hint in list of options when added', async () => { + render( + + ); + + await click( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ); + + expect( screen.getByRole( 'option', { name: /hint/i } ) ).toBeVisible(); + } ); + + it( 'Should return object onChange', async () => { + const mockOnChange = jest.fn(); + + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: { key: 'violets', name: 'violets' }, + type: '', + } ) + ); + + await click( + screen.getByRole( 'option', { + name: 'aquamarine', + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: expect.objectContaining( { + name: 'aquamarine', + } ), + type: '', + } ) + ); + } ); + + it( 'Should return selectedItem object when specified onChange', async () => { + const mockOnChange = jest.fn( + ( { selectedItem } ) => selectedItem.key + ); + + render( ); + + await sleep(); + await press.Tab(); + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveFocus(); + + await type( 'p' ); + await press.Enter(); + + expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await press.ArrowDown(); + await press.Enter(); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + } ); + + it( 'Should be able to type characters to select matching options', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await type( 'a' ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); + } ); + + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await type( 'aq' ); + + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); + + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + } ); + + it( 'Should have correct aria-selected value for selections', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + // get all items except for first option + const unselectedItems = legacyProps.options.filter( + ( { name } ) => name !== legacyProps.options[ 0 ].name + ); + + // assert that all other items have aria-selected="false" + unselectedItems.map( ( { name } ) => + expect( + screen.getByRole( 'option', { name, selected: false } ) + ).toBeVisible() + ); + + // assert that first item has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: true, + } ) + ).toBeVisible(); + + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + + // click combobox to mount listbox with options again + await click( currentSelectedItem ); + + // check that first item is has aria-selected="false" after new selection + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: false, + } ) + ).toBeVisible(); + + // check that new selected item now has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'poppy', + selected: true, + } ) + ).toBeVisible(); + } ); + } ); +} ); diff --git a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx index 9faa285cd72abb..f97b2376e9debd 100644 --- a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx +++ b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx @@ -11,12 +11,11 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import _LegacyCustomSelect from '../legacy-component'; -import { CustomSelect } from '..'; +import CustomSelect from '../legacy-component'; -const meta: Meta< typeof _LegacyCustomSelect > = { +const meta: Meta< typeof CustomSelect > = { title: 'Components (Experimental)/CustomSelectControl v2/Legacy', - component: _LegacyCustomSelect, + component: CustomSelect, argTypes: { onChange: { control: { type: null } }, value: { control: { type: null } }, @@ -43,11 +42,11 @@ const meta: Meta< typeof _LegacyCustomSelect > = { }; export default meta; -const Template: StoryFn< typeof _LegacyCustomSelect > = ( props ) => { +const Template: StoryFn< typeof CustomSelect > = ( props ) => { const [ fontSize, setFontSize ] = useState( props.options[ 0 ] ); const onChange: React.ComponentProps< - typeof _LegacyCustomSelect + typeof CustomSelect >[ 'onChange' ] = ( changeObject ) => { setFontSize( changeObject.selectedItem ); props.onChange?.( changeObject ); diff --git a/packages/components/src/custom-select-control-v2/test/index.tsx b/packages/components/src/custom-select-control-v2/test/index.tsx index dc8df0813b169b..fc8552b7a612a7 100644 --- a/packages/components/src/custom-select-control-v2/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen } from '@testing-library/react'; -import { click, press, sleep, type, waitFor } from '@ariakit/test'; +import { click, press, sleep, type } from '@ariakit/test'; /** * WordPress dependencies @@ -13,100 +13,115 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import { CustomSelect as UncontrolledCustomSelect, CustomSelectItem } from '..'; -import type { CustomSelectProps, LegacyCustomSelectProps } from '../types'; +import type { CustomSelectProps } from '../types'; + +const items = [ + { + key: 'flower1', + value: 'violets', + }, + { + key: 'flower2', + value: 'crimson clover', + }, + { + key: 'flower3', + value: 'poppy', + }, + { + key: 'color1', + value: 'amber', + }, + { + key: 'color2', + value: 'aquamarine', + }, +]; -const customClass = 'amber-skies'; - -const legacyProps = { +const defaultProps = { label: 'label!', - options: [ - { - key: 'flower1', - name: 'violets', - }, - { - key: 'flower2', - name: 'crimson clover', - className: customClass, - }, - { - key: 'flower3', - name: 'poppy', - }, - { - key: 'color1', - name: 'amber', - className: customClass, - }, - { - key: 'color2', - name: 'aquamarine', - style: { - backgroundColor: 'rgb(127, 255, 212)', - rotate: '13deg', - }, - }, - ], + children: items.map( ( { value, key } ) => ( + + ) ), }; -const LegacyControlledCustomSelect = ( { - options, - onChange, - ...restProps -}: LegacyCustomSelectProps ) => { - const [ value, setValue ] = useState( options[ 0 ] ); +const ControlledCustomSelect = ( props: CustomSelectProps ) => { + const [ value, setValue ] = useState< string | string[] >(); return ( { - onChange?.( args ); - setValue( args.selectedItem ); + { ...props } + onChange={ ( nextValue: string | string[] ) => { + setValue( nextValue ); + props.onChange?.( nextValue ); } } - value={ options.find( - ( option: any ) => option.key === value.key - ) } + value={ value } /> ); }; -describe( 'With Legacy Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', LegacyControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; +describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', ControlledCustomSelect ], +] )( 'CustomSelectControlV2 (%s)', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; - it( 'Should replace the initial selection when a new item is selected', async () => { - render( ); + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); - await click( currentSelectedItem ); + await click( currentSelectedItem ); - await click( - screen.getByRole( 'option', { - name: 'crimson clover', - } ) - ); + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); - await click( currentSelectedItem ); + await click( currentSelectedItem ); - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { - render( ); + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( items[ 0 ].value ); + } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { + render( ); const currentSelectedItem = screen.getByRole( 'combobox', { expanded: false, @@ -114,417 +129,67 @@ describe( 'With Legacy Props', () => { await sleep(); await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + await press.Enter(); expect( screen.getByRole( 'listbox', { name: 'label!', } ) - ).toBeVisible(); - - await press.Escape(); - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - } ) - ).not.toBeInTheDocument(); - - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); - } ); - - it( 'Should apply class only to options that have a className defined', async () => { - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - // return an array of items _with_ a className added - const itemsWithClass = legacyProps.options.filter( - ( option ) => option.className !== undefined - ); - - // assert against filtered array - itemsWithClass.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveClass( - customClass - ) - ); - - // return an array of items _without_ a className added - const itemsWithoutClass = legacyProps.options.filter( - ( option ) => option.className === undefined - ); - - // assert against filtered array - itemsWithoutClass.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveClass( customClass ) - ); - } ); - - it( 'Should apply styles only to options that have styles defined', async () => { - const customStyles = - 'background-color: rgb(127, 255, 212); rotate: 13deg;'; - - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - // return an array of items _with_ styles added - const styledItems = legacyProps.options.filter( - ( option ) => option.style !== undefined - ); - - // assert against filtered array - styledItems.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( - customStyles - ) - ); - - // return an array of items _without_ styles added - const unstyledItems = legacyProps.options.filter( - ( option ) => option.style === undefined - ); - - // assert against filtered array - unstyledItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveStyle( customStyles ) - ); - } ); - - it( 'does not show selected hint by default', async () => { - render( - - ); - await waitFor( () => - expect( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ).not.toHaveTextContent( 'Hint' ) - ); - } ); - - it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { - render( - - ); - - await waitFor( () => - expect( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ).toHaveTextContent( /hint/i ) - ); - } ); - - it( 'shows selected hint in list of options when added', async () => { - render( - - ); - - await click( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ); - - expect( - screen.getByRole( 'option', { name: /hint/i } ) - ).toBeVisible(); - } ); - - it( 'Should return object onChange', async () => { - const mockOnChange = jest.fn(); - - render( - - ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - expect( mockOnChange ).toHaveBeenNthCalledWith( - 1, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: { key: 'violets', name: 'violets' }, - type: '', - } ) - ); + ).toHaveFocus(); - await click( - screen.getByRole( 'option', { - name: 'aquamarine', - } ) - ); + await press.ArrowDown(); + await press.Enter(); - expect( mockOnChange ).toHaveBeenNthCalledWith( - 2, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: expect.objectContaining( { - name: 'aquamarine', - } ), - type: '', - } ) - ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); } ); - it( 'Should return selectedItem object when specified onChange', async () => { - const mockOnChange = jest.fn( - ( { selectedItem } ) => selectedItem.key - ); + it( 'Should be able to type characters to select matching options', async () => { + render( ); - render( - - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); await sleep(); await press.Tab(); + await press.Enter(); expect( - screen.getByRole( 'combobox', { - expanded: false, + screen.getByRole( 'listbox', { + name: 'label!', } ) ).toHaveFocus(); - await type( 'p' ); + await type( 'a' ); await press.Enter(); - - expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); } ); - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); - - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); - } ); - - it( 'Should be able to type characters to select matching options', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); - - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await type( 'aq' ); - - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should have correct aria-selected value for selections', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // get all items except for first option - const unselectedItems = legacyProps.options.filter( - ( { name } ) => name !== legacyProps.options[ 0 ].name - ); - - // assert that all other items have aria-selected="false" - unselectedItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name, selected: false } ) - ).toBeVisible() - ); - - // assert that first item has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: true, - } ) - ).toBeVisible(); - - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); - // click combobox to mount listbox with options again - await click( currentSelectedItem ); + await type( 'aq' ); - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: false, - } ) - ).toBeVisible(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); - } ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); } ); - } ); -} ); - -describe( 'static typing', () => { - <> - { /* @ts-expect-error - when `options` prop is passed, `onChange` should have legacy signature */ } - undefined } - /> - undefined } - /> - undefined } - > - foobar - - { /* @ts-expect-error - when `children` are passed, `onChange` should have new default signature */ } - undefined } - > - foobar - - ; -} ); - -const defaultProps = { - label: 'label!', - children: legacyProps.options.map( ( { name, key } ) => ( - - ) ), -}; - -const ControlledCustomSelect = ( props: CustomSelectProps ) => { - const [ value, setValue ] = useState< string | string[] >(); - return ( - { - setValue( nextValue ); - props.onChange?.( nextValue ); - } } - value={ value } - /> - ); -}; -describe( 'With Default Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', ControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; - - it( 'Should replace the initial selection when a new item is selected', async () => { + it( 'Should have correct aria-selected value for selections', async () => { render( ); const currentSelectedItem = screen.getByRole( 'combobox', { @@ -533,320 +198,134 @@ describe( 'With Default Props', () => { await click( currentSelectedItem ); - await click( + // assert that first item has aria-selected="true" + expect( screen.getByRole( 'option', { - name: 'crimson clover', + name: 'violets', + selected: true, } ) - ); + ).toBeVisible(); - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + // click combobox to mount listbox with options again await click( currentSelectedItem ); - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); - - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); - } ); - - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); + // check that first item is has aria-selected="false" after new selection expect( - screen.getByRole( 'listbox', { - name: 'label!', + screen.getByRole( 'option', { + name: 'violets', + selected: false, } ) ).toBeVisible(); - await press.Escape(); + // check that new selected item now has aria-selected="true" expect( - screen.queryByRole( 'listbox', { - name: 'label!', + screen.getByRole( 'option', { + name: 'poppy', + selected: true, } ) - ).not.toBeInTheDocument(); - - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); + ).toBeVisible(); } ); + } ); - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); - - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); - } ); - - it( 'Should be able to type characters to select matching options', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); - - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); + describe( 'Multiple selection', () => { + it( 'Should be able to select multiple items when provided an array', async () => { + const onChangeMock = jest.fn(); - await type( 'aq' ); + // initial selection as defaultValue + const defaultValues = [ + 'incandescent glow', + 'ultraviolet morning light', + ]; - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); + render( + + { [ + 'aurora borealis green', + 'flamingo pink sunrise', + 'incandescent glow', + 'rose blush', + 'ultraviolet morning light', + ].map( ( item ) => ( + + { item } + + ) ) } + + ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should have correct aria-selected value for selections', async () => { - render( ); + // ensure more than one item is selected due to defaultValues + expect( currentSelectedItem ).toHaveTextContent( + `${ defaultValues.length } items selected` + ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + await click( currentSelectedItem ); - await click( currentSelectedItem ); + expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( + 'aria-multiselectable' + ); - // assert that first item has aria-selected="true" + // ensure defaultValues are selected in list of items + defaultValues.forEach( ( value ) => expect( screen.getByRole( 'option', { - name: 'violets', + name: value, selected: true, } ) - ).toBeVisible(); - - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); - - // click combobox to mount listbox with options again - await click( currentSelectedItem ); + ).toBeVisible() + ); - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: 'violets', - selected: false, - } ) - ).toBeVisible(); + // name of next selection + const nextSelectionName = 'rose blush'; - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); + // element for next selection + const nextSelection = screen.getByRole( 'option', { + name: nextSelectionName, } ); - } ); - - describe( 'Multiple selection', () => { - it( 'Should be able to select multiple items when provided an array', async () => { - const onChangeMock = jest.fn(); - - // initial selection as defaultValue - const defaultValues = [ - 'incandescent glow', - 'ultraviolet morning light', - ]; - - render( - - { [ - 'aurora borealis green', - 'flamingo pink sunrise', - 'incandescent glow', - 'rose blush', - 'ultraviolet morning light', - ].map( ( item ) => ( - - { item } - - ) ) } - - ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - // ensure more than one item is selected due to defaultValues - expect( currentSelectedItem ).toHaveTextContent( - `${ defaultValues.length } items selected` - ); - - await click( currentSelectedItem ); - - expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( - 'aria-multiselectable' - ); - - // ensure defaultValues are selected in list of items - defaultValues.forEach( ( value ) => - expect( - screen.getByRole( 'option', { - name: value, - selected: true, - } ) - ).toBeVisible() - ); - - // name of next selection - const nextSelectionName = 'rose blush'; - - // element for next selection - const nextSelection = screen.getByRole( 'option', { - name: nextSelectionName, - } ); - // click next selection to add another item to current selection - await click( nextSelection ); + // click next selection to add another item to current selection + await click( nextSelection ); - // updated array containing defaultValues + the item just selected - const updatedSelection = - defaultValues.concat( nextSelectionName ); + // updated array containing defaultValues + the item just selected + const updatedSelection = defaultValues.concat( nextSelectionName ); - expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); + expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); - expect( nextSelection ).toHaveAttribute( 'aria-selected' ); + expect( nextSelection ).toHaveAttribute( 'aria-selected' ); - // expect increased array length for current selection - expect( currentSelectedItem ).toHaveTextContent( - `${ updatedSelection.length } items selected` - ); - } ); - - it( 'Should be able to deselect items when provided an array', async () => { - // initial selection as defaultValue - const defaultValues = [ - 'aurora borealis green', - 'incandescent glow', - 'key lime green', - 'rose blush', - 'ultraviolet morning light', - ]; - - render( - - { defaultValues.map( ( item ) => ( - - { item } - - ) ) } - - ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // Array containing items to deselect - const nextSelection = [ - 'aurora borealis green', - 'rose blush', - 'incandescent glow', - ]; - - // Deselect some items by clicking them to ensure that changes - // are reflected correctly - await Promise.all( - nextSelection.map( async ( value ) => { - await click( - screen.getByRole( 'option', { name: value } ) - ); - expect( - screen.getByRole( 'option', { - name: value, - selected: false, - } ) - ).toBeVisible(); - } ) - ); - - // expect different array length from defaultValues due to deselecting items - expect( currentSelectedItem ).toHaveTextContent( - `${ - defaultValues.length - nextSelection.length - } items selected` - ); - } ); + // expect increased array length for current selection + expect( currentSelectedItem ).toHaveTextContent( + `${ updatedSelection.length } items selected` + ); } ); - it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { - const renderValue = ( value: string | string[] ) => { - return {; - }; + it( 'Should be able to deselect items when provided an array', async () => { + // initial selection as defaultValue + const defaultValues = [ + 'aurora borealis green', + 'incandescent glow', + 'key lime green', + 'rose blush', + 'ultraviolet morning light', + ]; render( - - - { renderValue( 'april-29' ) } - - - { renderValue( 'july-9' ) } - + + { defaultValues.map( ( item ) => ( + + { item } + + ) ) } ); @@ -854,26 +333,77 @@ describe( 'With Default Props', () => { expanded: false, } ); - expect( currentSelectedItem ).toBeVisible(); + await click( currentSelectedItem ); - // expect that the initial selection renders an image - expect( currentSelectedItem ).toContainElement( - screen.getByRole( 'img', { name: 'april-29' } ) + // Array containing items to deselect + const nextSelection = [ + 'aurora borealis green', + 'rose blush', + 'incandescent glow', + ]; + + // Deselect some items by clicking them to ensure that changes + // are reflected correctly + await Promise.all( + nextSelection.map( async ( value ) => { + await click( + screen.getByRole( 'option', { name: value } ) + ); + expect( + screen.getByRole( 'option', { + name: value, + selected: false, + } ) + ).toBeVisible(); + } ) ); - expect( - screen.queryByRole( 'img', { name: 'july-9' } ) - ).not.toBeInTheDocument(); - - await click( currentSelectedItem ); + // expect different array length from defaultValues due to deselecting items + expect( currentSelectedItem ).toHaveTextContent( + `${ + defaultValues.length - nextSelection.length + } items selected` + ); + } ); + } ); - // expect that the other image is only visible after opening popover with options - expect( - screen.getByRole( 'img', { name: 'july-9' } ) - ).toBeVisible(); - expect( - screen.getByRole( 'option', { name: 'july-9' } ) - ).toBeVisible(); + it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { + const renderValue = ( value: string | string[] ) => { + return {; + }; + + render( + + + { renderValue( 'april-29' ) } + + + { renderValue( 'july-9' ) } + + + ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); + + expect( currentSelectedItem ).toBeVisible(); + + // expect that the initial selection renders an image + expect( currentSelectedItem ).toContainElement( + screen.getByRole( 'img', { name: 'april-29' } ) + ); + + expect( + screen.queryByRole( 'img', { name: 'july-9' } ) + ).not.toBeInTheDocument(); + + await click( currentSelectedItem ); + + // expect that the other image is only visible after opening popover with options + expect( screen.getByRole( 'img', { name: 'july-9' } ) ).toBeVisible(); + expect( + screen.getByRole( 'option', { name: 'july-9' } ) + ).toBeVisible(); } ); } ); diff --git a/packages/components/src/custom-select-control-v2/types.ts b/packages/components/src/custom-select-control-v2/types.ts index b32f3ee2113080..51b0cb6b38d6a7 100644 --- a/packages/components/src/custom-select-control-v2/types.ts +++ b/packages/components/src/custom-select-control-v2/types.ts @@ -92,7 +92,6 @@ type LegacyOnChangeObject = { }; export type LegacyCustomSelectProps = { - children?: never; /** * Optional classname for the component. */ diff --git a/packages/components/src/flex/flex/hook.ts b/packages/components/src/flex/flex/hook.ts index df53667915999c..f5648c187c7628 100644 --- a/packages/components/src/flex/flex/hook.ts +++ b/packages/components/src/flex/flex/hook.ts @@ -22,7 +22,7 @@ import type { FlexProps } from '../types'; function useDeprecatedProps( props: WordPressComponentProps< FlexProps, 'div' > -): WordPressComponentProps< FlexProps, 'div' > { +): Omit< typeof props, 'isReversed' > { const { isReversed, ...otherProps } = props; if ( typeof isReversed !== 'undefined' ) { diff --git a/packages/components/src/h-stack/hook.tsx b/packages/components/src/h-stack/hook.tsx index b7b8cf92954a1d..bb473a881b9dea 100644 --- a/packages/components/src/h-stack/hook.tsx +++ b/packages/components/src/h-stack/hook.tsx @@ -47,7 +47,8 @@ export function useHStack( props: WordPressComponentProps< Props, 'div' > ) { gap: spacing, }; - const flexProps = useFlex( propsForFlex ); + // Omit `isColumn` because it's not used in HStack. + const { isColumn, ...flexProps } = useFlex( propsForFlex ); return flexProps; } diff --git a/packages/components/src/h-stack/test/index.tsx b/packages/components/src/h-stack/test/index.tsx index ea4f9c94fbe7f8..75352f3dfd22f0 100644 --- a/packages/components/src/h-stack/test/index.tsx +++ b/packages/components/src/h-stack/test/index.tsx @@ -39,4 +39,14 @@ describe( 'props', () => { ); expect( container ).toMatchSnapshot(); } ); + + test( 'should not pass through invalid props to the `as` component', () => { + const AsComponent = ( props: JSX.IntrinsicElements[ 'div' ] ) => { + return
; + }; + + render( foobar ); + + expect( console ).not.toHaveErrored(); + } ); } ); diff --git a/packages/components/src/mobile/image/index.native.js b/packages/components/src/mobile/image/index.native.js index aec8f94d1b5088..c3e010c40c3b8a 100644 --- a/packages/components/src/mobile/image/index.native.js +++ b/packages/components/src/mobile/image/index.native.js @@ -289,13 +289,9 @@ const ImageComponent = ( { key={ url } style={ imageContainerStyles } > - { isSelected && - highlightSelected && - ! ( - isUploadInProgress || - isUploadFailed || - isUploadPaused - ) && } + { isSelected && highlightSelected && ( + + ) } { ! imageData ? ( diff --git a/packages/components/src/utils/hooks/index.js b/packages/components/src/utils/hooks/index.js index beacf31b7562cd..896f691cdc5427 100644 --- a/packages/components/src/utils/hooks/index.js +++ b/packages/components/src/utils/hooks/index.js @@ -2,4 +2,3 @@ export { default as useControlledState } from './use-controlled-state'; export { default as useUpdateEffect } from './use-update-effect'; export { useControlledValue } from './use-controlled-value'; export { useCx } from './use-cx'; -export { useLatestRef } from './use-latest-ref'; diff --git a/packages/components/src/utils/hooks/test/use-latest-ref.js b/packages/components/src/utils/hooks/test/use-latest-ref.js deleted file mode 100644 index 0c424a62390332..00000000000000 --- a/packages/components/src/utils/hooks/test/use-latest-ref.js +++ /dev/null @@ -1,119 +0,0 @@ -/** - * External dependencies - */ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { useLatestRef } from '..'; - -function debounce( callback, timeout = 0 ) { - let timeoutId = 0; - return ( ...args ) => { - window.clearTimeout( timeoutId ); - timeoutId = window.setTimeout( () => callback( ...args ), timeout ); - }; -} - -function useDebounce( callback, timeout = 0 ) { - const callbackRef = useLatestRef( callback ); - return debounce( ( ...args ) => callbackRef.current( ...args ), timeout ); -} - -function Example() { - const [ count, setCount ] = useState( 0 ); - const increment = () => setCount( count + 1 ); - const incrementDebounced = debounce( increment, 50 ); - const incrementDebouncedWithLatestRef = useDebounce( increment, 50 ); - - return ( - <> -
Count: { count }
- - -
- - - ); -} - -function getCount() { - return screen.getByText( /Count:/ ).innerHTML; -} - -function incrementCount() { - fireEvent.click( screen.getByText( 'Increment immediately' ) ); -} - -function incrementCountDebounced() { - fireEvent.click( screen.getByText( 'Increment debounced' ) ); -} - -function incrementCountDebouncedRef() { - fireEvent.click( - screen.getByText( 'Increment debounced with latest ref' ) - ); -} - -describe( 'useLatestRef', () => { - describe( 'Example', () => { - // Prove the example works as expected. - it( 'should start at 0', () => { - render( ); - - expect( getCount() ).toEqual( 'Count: 0' ); - } ); - - it( 'should increment immediately', () => { - render( ); - - incrementCount(); - - expect( getCount() ).toEqual( 'Count: 1' ); - } ); - - it( 'should increment after debouncing', async () => { - render( ); - - incrementCountDebounced(); - - expect( getCount() ).toEqual( 'Count: 0' ); - await waitFor( () => expect( getCount() ).toEqual( 'Count: 1' ) ); - } ); - - it( 'should increment after debouncing with latest ref', async () => { - render( ); - - incrementCountDebouncedRef(); - - expect( getCount() ).toEqual( 'Count: 0' ); - await waitFor( () => expect( getCount() ).toEqual( 'Count: 1' ) ); - } ); - } ); - - it( 'should increment to one', async () => { - render( ); - - incrementCountDebounced(); - incrementCount(); - - await waitFor( () => expect( getCount() ).toEqual( 'Count: 1' ) ); - } ); - - it( 'should increment to two', async () => { - render( ); - - incrementCountDebouncedRef(); - incrementCount(); - - await waitFor( () => expect( getCount() ).toEqual( 'Count: 2' ) ); - } ); -} ); diff --git a/packages/components/src/utils/hooks/use-latest-ref.ts b/packages/components/src/utils/hooks/use-latest-ref.ts deleted file mode 100644 index 5db9591b9e073c..00000000000000 --- a/packages/components/src/utils/hooks/use-latest-ref.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * External dependencies - */ -import type { RefObject } from 'react'; - -/** - * WordPress dependencies - */ -import { useRef } from '@wordpress/element'; -import { useIsomorphicLayoutEffect } from '@wordpress/compose'; - -/** - * Creates a reference for a prop. This is useful for preserving dependency - * memoization for hooks like useCallback. - * - * @see https://codesandbox.io/s/uselatestref-mlj3i?file=/src/App.tsx - * - * @param value The value to reference - * @return The prop reference. - */ -export function useLatestRef< T >( value: T ): RefObject< T > { - const ref = useRef( value ); - - useIsomorphicLayoutEffect( () => { - ref.current = value; - } ); - - return ref; -} diff --git a/packages/components/src/v-stack/test/index.tsx b/packages/components/src/v-stack/test/index.tsx index cf35691d0858f6..e8ea1ab0ee396f 100644 --- a/packages/components/src/v-stack/test/index.tsx +++ b/packages/components/src/v-stack/test/index.tsx @@ -39,4 +39,14 @@ describe( 'props', () => { ); expect( container ).toMatchSnapshot(); } ); + + test( 'should not pass through invalid props to the `as` component', () => { + const AsComponent = ( props: JSX.IntrinsicElements[ 'div' ] ) => { + return
; + }; + + render( foobar ); + + expect( console ).not.toHaveErrored(); + } ); } ); diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 7e45bec25856f7..7c8216b095cb43 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -51,6 +51,9 @@ .dataviews-view-table-wrapper { overflow-x: auto; + flex-grow: 1; + display: flex; + flex-direction: column; } .dataviews-view-table { @@ -219,7 +222,6 @@ color: $gray-900; text-overflow: ellipsis; white-space: nowrap; - overflow: hidden; display: block; width: 100%; @@ -235,6 +237,7 @@ &:hover { color: $gray-900; } + @include link-reset(); } button.components-button.is-link { @@ -473,6 +476,10 @@ .dataviews-no-results, .dataviews-loading { padding: 0 $grid-unit-40; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; } .dataviews-view-table-selection-checkbox label { diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 3b46a7424dc1b1..61c37955222aa4 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -11,6 +11,7 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, Tooltip, + Spinner, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useAsyncList } from '@wordpress/compose'; @@ -190,7 +191,7 @@ export default function ViewGrid( { 'dataviews-no-results': ! isLoading, } ) } > -

{ isLoading ? __( 'Loading…' ) : __( 'No results' ) }

+

{ isLoading ? : __( 'No results' ) }

) } diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index 8490c315e88541..ca6de677b99fd0 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -11,6 +11,7 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, Button, + Spinner, } from '@wordpress/components'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { info } from '@wordpress/icons'; @@ -60,7 +61,7 @@ export default function ViewList( { } ) } > { ! hasData && ( -

{ isLoading ? __( 'Loading…' ) : __( 'No results' ) }

+

{ isLoading ? : __( 'No results' ) }

) }
); diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 9737aa3d462835..c77323d3c796d3 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -14,6 +14,7 @@ import { Icon, privateApis as componentsPrivateApis, CheckboxControl, + Spinner, } from '@wordpress/components'; import { forwardRef, @@ -464,7 +465,7 @@ function ViewTable( { id={ tableNoticeId } > { ! hasData && ( -

{ isLoading ? __( 'Loading…' ) : __( 'No results' ) }

+

{ isLoading ? : __( 'No results' ) }

) }
diff --git a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts index 3b187625fd47cf..11bc11c43f603c 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts @@ -55,18 +55,26 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { const canvasDoc = // @ts-ignore document.activeElement?.contentDocument ?? document; - const clipboardDataTransfer = new DataTransfer(); + const event = new ClipboardEvent( _type, { + bubbles: true, + cancelable: true, + clipboardData: new DataTransfer(), + } ); + + if ( ! event.clipboardData ) { + throw new Error( 'ClipboardEvent.clipboardData is null' ); + } if ( _type === 'paste' ) { - clipboardDataTransfer.setData( + event.clipboardData.setData( 'text/plain', _clipboardData[ 'text/plain' ] ); - clipboardDataTransfer.setData( + event.clipboardData.setData( 'text/html', _clipboardData[ 'text/html' ] ); - clipboardDataTransfer.setData( + event.clipboardData.setData( 'rich-text', _clipboardData[ 'rich-text' ] ); @@ -85,22 +93,16 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { ) .join( '' ); } - clipboardDataTransfer.setData( 'text/plain', plainText ); - clipboardDataTransfer.setData( 'text/html', html ); + event.clipboardData.setData( 'text/plain', plainText ); + event.clipboardData.setData( 'text/html', html ); } - canvasDoc.activeElement?.dispatchEvent( - new ClipboardEvent( _type, { - bubbles: true, - cancelable: true, - clipboardData: clipboardDataTransfer, - } ) - ); + canvasDoc.activeElement.dispatchEvent( event ); return { - 'text/plain': clipboardDataTransfer.getData( 'text/plain' ), - 'text/html': clipboardDataTransfer.getData( 'text/html' ), - 'rich-text': clipboardDataTransfer.getData( 'rich-text' ), + 'text/plain': event.clipboardData.getData( 'text/plain' ), + 'text/html': event.clipboardData.getData( 'text/html' ), + 'rich-text': event.clipboardData.getData( 'rich-text' ), }; }, [ type, clipboardDataHolder ] as const diff --git a/packages/e2e-test-utils-playwright/src/request-utils/posts.ts b/packages/e2e-test-utils-playwright/src/request-utils/posts.ts index 5e32c0c877555c..3b1231619df6e9 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/posts.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/posts.ts @@ -64,7 +64,7 @@ export async function createPost( const post = await this.rest< Post >( { method: 'POST', path: `/wp/v2/posts`, - params: { ...payload }, + data: { ...payload }, } ); return post; diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 62a6a462d8dd18..187a03b763a13f 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { BlockToolbar, + privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; import { @@ -40,6 +41,7 @@ import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; +const { useShowBlockTools } = unlock( blockEditorPrivateApis ); const { DocumentTools, PostViewLink, PreviewDropdown } = unlock( editorPrivateApis ); @@ -63,7 +65,6 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { isTextEditor, blockSelectionStart, hasActiveMetaboxes, - hasFixedToolbar, isPublishSidebarOpened, showIconLabels, hasHistory, @@ -81,11 +82,13 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { .onNavigateToPreviousEntityRecord, isPublishSidebarOpened: select( editPostStore ).isPublishSidebarOpened(), - hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), showIconLabels: getPreference( 'core', 'showIconLabels' ), }; }, [] ); + const { showFixedToolbar } = useShowBlockTools(); + const showTopToolbar = isLargeViewport && showFixedToolbar; + const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = useState( true ); const hasBlockSelection = !! blockSelectionStart; @@ -116,7 +119,7 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { className="edit-post-header__toolbar" > - { hasFixedToolbar && isLargeViewport && ( + { showTopToolbar && ( <>
- { hasBlockSelection && ( -