From 1129dcf586535923708055adc67c924685bcad5c Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Wed, 27 Dec 2023 15:25:50 +0100 Subject: [PATCH 01/17] Modules API: Fix Interactivity not working on Classic Themes. (#57396) * Move modules scripts to footer on classic themes * Move only required modules * Move also the module preloads * Make preload only for block themes at head * Revert "Make preload only for block themes at head" This reverts commit 3edb69fc6f2ef4ea38b5ce4e4fa97425a0de2b1d. --- .../modules/class-gutenberg-modules.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/experimental/modules/class-gutenberg-modules.php b/lib/experimental/modules/class-gutenberg-modules.php index bbe51f1376a7db..0a2da03545776e 100644 --- a/lib/experimental/modules/class-gutenberg-modules.php +++ b/lib/experimental/modules/class-gutenberg-modules.php @@ -262,14 +262,15 @@ function gutenberg_dequeue_module( $module_identifier ) { Gutenberg_Modules::dequeue( $module_identifier ); } -// Prints the import map in the head tag. -add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map' ) ); +$modules_position = wp_is_block_theme() ? 'wp_head' : 'wp_footer'; +// Prints the import map in the head tag in block themes. Otherwise in the footer. +add_action( $modules_position, array( 'Gutenberg_Modules', 'print_import_map' ) ); -// Prints the enqueued modules in the head tag. -add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); +// Prints the enqueued modules in the head tag in block themes. Otherwise in the footer. +add_action( $modules_position, array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); -// Prints the preloaded modules in the head tag. -add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_module_preloads' ) ); +// Prints the preloaded modules in the head tag in block themes. Otherwise in the footer. +add_action( $modules_position, array( 'Gutenberg_Modules', 'print_module_preloads' ) ); // Prints the script that loads the import map polyfill in the footer. add_action( 'wp_footer', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); From e5b995a35c82faf035bc285ba8af58c0e45f171a Mon Sep 17 00:00:00 2001 From: Tim Broddin Date: Wed, 27 Dec 2023 17:15:59 +0100 Subject: [PATCH 02/17] Replace block variation buttons with ToggleGroupControl (#45654) * Replace block variation buttons with ToggleGroupControl * Make the control 40px high * Only show ToggleGroupControl when 5 or less options * Use instead of extra className * Consolidate label and aria-label * Switch to `__next40pxDefaultSize` prop * Remove invalid `showTooltip` prop * Add `__nextHasNoMarginBottom` prop * Simplify onChange * Remove unnecessary Flex * Remove unnecessary fieldset --------- Co-authored-by: Lena Morita --- .../block-variation-transforms/index.js | 52 +++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js index 4c90009ea00006..f005515a63f17b 100644 --- a/packages/block-editor/src/components/block-variation-transforms/index.js +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -8,6 +8,8 @@ import { DropdownMenu, MenuGroup, MenuItemsChoice, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, VisuallyHidden, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -95,6 +97,43 @@ function VariationsDropdown( { ); } +function VariationsToggleGroupControl( { + className, + onSelectVariation, + selectedValue, + variations, +} ) { + return ( +
+ + { variations.map( ( variation ) => ( + + ) ) } + +
+ ); +} + function __experimentalBlockVariationTransforms( { blockClientId } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); const { activeBlockVariation, variations } = useSelect( @@ -138,12 +177,19 @@ function __experimentalBlockVariationTransforms( { blockClientId } ) { } ); }; - const baseClass = 'block-editor-block-variation-transforms'; - // Skip rendering if there are no variations if ( ! variations?.length ) return null; - const Component = hasUniqueIcons ? VariationsButtons : VariationsDropdown; + const baseClass = 'block-editor-block-variation-transforms'; + + // Show buttons if there are more than 5 variations because the ToggleGroupControl does not wrap + const showButtons = variations.length > 5; + + const ButtonComponent = showButtons + ? VariationsButtons + : VariationsToggleGroupControl; + + const Component = hasUniqueIcons ? ButtonComponent : VariationsDropdown; return ( Date: Wed, 27 Dec 2023 16:39:28 +0000 Subject: [PATCH 03/17] Bump plugin version to 17.4.0-rc.1 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 69d5a6732a7e37..406dae417b3aa3 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.3.0 + * Version: 17.4.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index c2d1cb70f7e459..960ae127ea85a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.3.0", + "version": "17.4.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.3.0", + "version": "17.4.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 4587306d20a3b0..44a7a4a2d5bef3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.3.0", + "version": "17.4.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -392,4 +392,4 @@ ], "welcome-build-command": "npm run dev" } -} \ No newline at end of file +} From b46686e61b7d8abdcc535d27c0f4692001203199 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 27 Dec 2023 16:54:40 +0000 Subject: [PATCH 04/17] Update Changelog for 17.4.0-rc.1 --- changelog.txt | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) diff --git a/changelog.txt b/changelog.txt index 58b1b833d2c71e..2ca3d6277edb8b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,303 @@ == Changelog == += 17.4.0-rc.1 = + + +## Changelog + +### Enhancements + +#### Components +- Refactor experimental dropdown menu usages to latest version. ([55625](https://github.com/WordPress/gutenberg/pull/55625)) +- Replace `TabPanel` with `Tabs` in the Block Inspector. ([56995](https://github.com/WordPress/gutenberg/pull/56995)) +- Replace `TabPanel` with `Tabs` in the editor Global Styles color palette. ([57126](https://github.com/WordPress/gutenberg/pull/57126)) +- ToggleGroupControl: Update large button size to 32px. ([57338](https://github.com/WordPress/gutenberg/pull/57338)) +- Update ariakit version. ([57325](https://github.com/WordPress/gutenberg/pull/57325)) +- `CustomSelect`: Add `WordPressComponentsProps`. ([56998](https://github.com/WordPress/gutenberg/pull/56998)) +- `Modal`: Improve application of body class names. ([55430](https://github.com/WordPress/gutenberg/pull/55430)) +- - Components: Replace `TabPanel` with `Tabs` in the Block Inserter. ([56918](https://github.com/WordPress/gutenberg/pull/56918)) + +#### Site Editor +- Add Page Attributes panel. ([57151](https://github.com/WordPress/gutenberg/pull/57151)) +- Add Post Taxonomies panel. ([57049](https://github.com/WordPress/gutenberg/pull/57049)) +- Add View Link. ([57153](https://github.com/WordPress/gutenberg/pull/57153)) +- Add the Discussion panel. ([57150](https://github.com/WordPress/gutenberg/pull/57150)) +- Add the featured image panel. ([57053](https://github.com/WordPress/gutenberg/pull/57053)) +- Editor: Unify revision panel between post and site editors. ([57010](https://github.com/WordPress/gutenberg/pull/57010)) +- Social Link block: Obfuscate email address. ([57384](https://github.com/WordPress/gutenberg/pull/57384)) + +#### Design Tools +- Background image: Add backgroundSize and repeat features. ([57005](https://github.com/WordPress/gutenberg/pull/57005)) +- Make sure theme color palette presets are output when appearance tools are enabled. ([57190](https://github.com/WordPress/gutenberg/pull/57190)) +- Move tools panel to the left of the inspector. ([55785](https://github.com/WordPress/gutenberg/pull/55785)) + +#### Global Styles +- Global styles revisions: Add pagination. ([56799](https://github.com/WordPress/gutenberg/pull/56799)) +- Global styles revisions: Integrate style book. ([56800](https://github.com/WordPress/gutenberg/pull/56800)) + +### Bug Fixes + +#### Components +- DropdownMenu V2: Add fallback styles for when subgrid is not supported. ([57327](https://github.com/WordPress/gutenberg/pull/57327)) +- DropdownMenuV2: Do not collapse suffix width. ([57238](https://github.com/WordPress/gutenberg/pull/57238)) +- Fix DayButton dot position and expand Button area. ([55502](https://github.com/WordPress/gutenberg/pull/55502)) +- Fix logic of `has-text` class addition in Button. ([56949](https://github.com/WordPress/gutenberg/pull/56949)) +- Palette Edit: Don't discards colors with default name and slug. ([54332](https://github.com/WordPress/gutenberg/pull/54332)) +- PaletteEdit: Consider digits when generating kebab-cased slug. ([56713](https://github.com/WordPress/gutenberg/pull/56713)) +- RadioControl: Fully encapsulate styles. ([57347](https://github.com/WordPress/gutenberg/pull/57347)) +- Tabs: Make sure individual `Tab`s are linked to the correct `TabPanel`s. ([57033](https://github.com/WordPress/gutenberg/pull/57033)) +- Text selection: Show CSS hack to Safari only. ([57300](https://github.com/WordPress/gutenberg/pull/57300)) +- Truncate: Improve handling of non-string children. ([57261](https://github.com/WordPress/gutenberg/pull/57261)) +- `FormTokenField`: Handle `disabled` prop on internal `Button`. ([57187](https://github.com/WordPress/gutenberg/pull/57187)) +- tab panel: Don't render hidden content by default. ([57046](https://github.com/WordPress/gutenberg/pull/57046)) + +#### Block Editor +- Exclude disabled buttons when setting initialIndex of NavigableToolbar. ([57280](https://github.com/WordPress/gutenberg/pull/57280)) +- Fix BlockSwitcher checks for showing a Dropdown menu or not. ([57047](https://github.com/WordPress/gutenberg/pull/57047)) +- Fix block lock toolbar item stealing focus when mounted with StrictMode. ([57185](https://github.com/WordPress/gutenberg/pull/57185)) +- Fix content lock UI regression. ([56974](https://github.com/WordPress/gutenberg/pull/56974)) +- Fix vertical overflow when inserter is open in post and site editor. ([57127](https://github.com/WordPress/gutenberg/pull/57127)) +- Hide drop indicator where block isn't allowed to drop. ([56843](https://github.com/WordPress/gutenberg/pull/56843)) +- InserterListItem: Use item.isDisabled to detect disabled item. ([57161](https://github.com/WordPress/gutenberg/pull/57161)) +- More settings tip: Add explicit font size. ([55835](https://github.com/WordPress/gutenberg/pull/55835)) +- Writing flow: Absorb partial multi selection dispatching. ([47525](https://github.com/WordPress/gutenberg/pull/47525)) +- Fix the block 'edit' property validation. ([57193](https://github.com/WordPress/gutenberg/pull/57193)) + +#### Block Library +- Audio: Hide some controls when multi-editing blocks. ([57376](https://github.com/WordPress/gutenberg/pull/57376)) +- Disable resizing when viewport is small and wide-aligned. ([57041](https://github.com/WordPress/gutenberg/pull/57041)) +- Gallery: Hide some controls when multi-editing blocks. ([57378](https://github.com/WordPress/gutenberg/pull/57378)) +- Image Block: Get lightbox trigger button ref via data-wp-init. ([57089](https://github.com/WordPress/gutenberg/pull/57089)) +- Image: Hide caption control when multi-editing images. ([57357](https://github.com/WordPress/gutenberg/pull/57357)) +- Video: Hide some controls when multi-editing blocks. ([57375](https://github.com/WordPress/gutenberg/pull/57375)) +- Image Block: Fix deprecation when width/height attribute is number. ([57063](https://github.com/WordPress/gutenberg/pull/57063)) + +#### Patterns +- Fix: Viewport width not accounted for in the Site Editor > Patterns view. ([55803](https://github.com/WordPress/gutenberg/pull/55803)) +- Override all the labels of the pattern categories taxonomy. ([57094](https://github.com/WordPress/gutenberg/pull/57094)) +- Pattern Category: Change show_tagcloud to false. ([57212](https://github.com/WordPress/gutenberg/pull/57212)) +- Patterns (unsynced): Prevent infinite loops due to recursive patterns. ([56511](https://github.com/WordPress/gutenberg/pull/56511)) + +#### Site Editor +- Fix image upload bug. ([57040](https://github.com/WordPress/gutenberg/pull/57040)) +- Reduce clearance around the Frame in the site editor. ([57023](https://github.com/WordPress/gutenberg/pull/57023)) +- Swap Template: Show the right templates for the right post type. ([57149](https://github.com/WordPress/gutenberg/pull/57149)) +- Save Button: Fix the translation of the Activate button. ([57147](https://github.com/WordPress/gutenberg/pull/57147)) +- SlotFill: Allow contextual SlotFillProviders. ([56779](https://github.com/WordPress/gutenberg/pull/56779)) + +#### Post Editor +- Editor: Use visibility selector for PostTemplatePanel. ([57224](https://github.com/WordPress/gutenberg/pull/57224)) +- Fix: Code editor title width in classic theme. ([56922](https://github.com/WordPress/gutenberg/pull/56922)) + + +#### Design Tools +- Allow default duotone styles in classic themes. ([57191](https://github.com/WordPress/gutenberg/pull/57191)) +- StylesPreview: Fix endless loop of ratio calculations when on the threshold of a scrollbar. ([57090](https://github.com/WordPress/gutenberg/pull/57090)) + +#### Interactivity API +- Fix namespaces in nested interactive regions. ([57029](https://github.com/WordPress/gutenberg/pull/57029)) +- Fix Interactivity not working on Classic Themes. ([57396](https://github.com/WordPress/gutenberg/pull/57396)) +#### Collaborative Editing +- Collab editing: Ensure block attributes are serialisable. ([57025](https://github.com/WordPress/gutenberg/pull/57025)) + +### Accessibility + +- Font size picker: Fix Reset button focus loss. ([57196](https://github.com/WordPress/gutenberg/pull/57196)) +- a11y: Apply focus style to revision items. ([57039](https://github.com/WordPress/gutenberg/pull/57039)) +- [a11y] Fix: Use spans instead of headings on dataviews table view page title. ([56956](https://github.com/WordPress/gutenberg/pull/56956)) + + +### Performance + +#### Block Editor +- Block: Combine store subscriptions. ([56994](https://github.com/WordPress/gutenberg/pull/56994)) +- Combine selectors in the 'BackgroundImagePanelItem' component. ([57159](https://github.com/WordPress/gutenberg/pull/57159)) +- InnerBlocks: overlay: Remove viewport size condition. ([57135](https://github.com/WordPress/gutenberg/pull/57135)) +- Prevent layout re-rendering when changing selected block. ([57136](https://github.com/WordPress/gutenberg/pull/57136)) +- Prevent re-rendering the editor header when the selected block changes. ([57140](https://github.com/WordPress/gutenberg/pull/57140)) +- Rewrite moving animation for better load performance. ([57133](https://github.com/WordPress/gutenberg/pull/57133)) +- Rich text: Avoid block editor subscription if not selected. ([57226](https://github.com/WordPress/gutenberg/pull/57226)) +- Try removing extra memoization for individual style panels. ([57160](https://github.com/WordPress/gutenberg/pull/57160)) +- hooks: Manage save props in one place. ([57043](https://github.com/WordPress/gutenberg/pull/57043)) + +#### Block Library +- Gallery: Combine useSelect calls. ([57240](https://github.com/WordPress/gutenberg/pull/57240)) +- Image: Reduce 'block-editor' store subscriptions. ([57358](https://github.com/WordPress/gutenberg/pull/57358)) +- List: Avoid useSelect in block render. ([57077](https://github.com/WordPress/gutenberg/pull/57077)) +- Blocks: Simplify/optimise isUnmodifiedBlock. ([56919](https://github.com/WordPress/gutenberg/pull/56919)) +- InnerBlocks: Combine store subscriptions. ([57032](https://github.com/WordPress/gutenberg/pull/57032)) + + +#### Components +- Navigator: Use CSS animations instead of framer-motion. ([56909](https://github.com/WordPress/gutenberg/pull/56909)) +- useSelect: Only invalidate on subscribe if store changed. ([57108](https://github.com/WordPress/gutenberg/pull/57108)) + +### Experiments + +#### Data Views +- Add hover style to table rows. ([57058](https://github.com/WordPress/gutenberg/pull/57058)) +- Add: See revisions template action. ([57175](https://github.com/WordPress/gutenberg/pull/57175)) +- Code Quality: Update: Reuse view revisions action on templates and pages. ([57208](https://github.com/WordPress/gutenberg/pull/57208)) +- DataViews: Add list layout to templates. ([57014](https://github.com/WordPress/gutenberg/pull/57014)) +- DataViews: Align filter implementations. ([57059](https://github.com/WordPress/gutenberg/pull/57059)) +- DataViews: Centralize control of filter visibility in the `Filters` component. ([57056](https://github.com/WordPress/gutenberg/pull/57056)) +- DataViews: Close actions menu upon switching layouts. ([57015](https://github.com/WordPress/gutenberg/pull/57015)) +- DataViews: Display column header when the field is only filterable. ([57051](https://github.com/WordPress/gutenberg/pull/57051)) +- DataViews: Fix bug on operators count for table layout. ([57048](https://github.com/WordPress/gutenberg/pull/57048)) +- DataViews: Improve naming for easy identification in React devtools. ([57385](https://github.com/WordPress/gutenberg/pull/57385)) +- DataViews: Improve preview. ([57116](https://github.com/WordPress/gutenberg/pull/57116)) +- DataViews: Make `deferredRendering` prop optional. ([57334](https://github.com/WordPress/gutenberg/pull/57334)) +- DataViews: Make `getItemId` optional. ([57308](https://github.com/WordPress/gutenberg/pull/57308)) +- DataViews: Make filters footprint more condensed. ([56983](https://github.com/WordPress/gutenberg/pull/56983)) +- DataViews: Mark the new Templates pages as stable. ([57109](https://github.com/WordPress/gutenberg/pull/57109)) +- DataViews: Memoize `onSelectionChange` callback. ([57390](https://github.com/WordPress/gutenberg/pull/57390)) +- DataViews: Remove `paginationInfo` prop from ViewComponent. ([57306](https://github.com/WordPress/gutenberg/pull/57306)) +- DataViews: Remove reference to edit site class. ([57075](https://github.com/WordPress/gutenberg/pull/57075)) +- DataViews: Rename `operatorsFromField` to `sanitizeOperators`. ([57050](https://github.com/WordPress/gutenberg/pull/57050)) +- DataViews: Update `onChangeView` memoization. ([57393](https://github.com/WordPress/gutenberg/pull/57393)) +- DataViews: Update documentation. ([57305](https://github.com/WordPress/gutenberg/pull/57305)) +- DataViews: Use SelectControl for selecting a page. ([57215](https://github.com/WordPress/gutenberg/pull/57215)) +- Dataviews: Simplify pagination. ([57071](https://github.com/WordPress/gutenberg/pull/57071)) +- Fix: Empty dropdown(s) on the grid view. ([57316](https://github.com/WordPress/gutenberg/pull/57316)) +- Fix: Template list title font styles. ([57027](https://github.com/WordPress/gutenberg/pull/57027)) +- Sort order: Use unicode characters instead of svg icons. ([56833](https://github.com/WordPress/gutenberg/pull/56833)) +- Update: Make secondary actions trigger always visible. ([57174](https://github.com/WordPress/gutenberg/pull/57174)) +- Use default variants for Filter and View buttons. ([57057](https://github.com/WordPress/gutenberg/pull/57057)) + +#### Patterns +- Fix broken undo history stack for Pattern Overrides. ([57088](https://github.com/WordPress/gutenberg/pull/57088)) +- Fix unsaved pattern not reflecting on pattern overrides. ([57148](https://github.com/WordPress/gutenberg/pull/57148)) +- [Pattern Overrides] Use a single checkbox to turn on pattern overrides for all allowed attributes. ([57009](https://github.com/WordPress/gutenberg/pull/57009)) + +#### List View +- Allow right-click to open block settings dropdown, add editor setting. ([50273](https://github.com/WordPress/gutenberg/pull/50273)) + +### Documentation + +- Add new section on markup representation of a block. ([57230](https://github.com/WordPress/gutenberg/pull/57230)) +- Added additional explanations to attributes and supports sections. ([57120](https://github.com/WordPress/gutenberg/pull/57120)) +- Added documentation for the text-decoration-control component. ([57184](https://github.com/WordPress/gutenberg/pull/57184)) +- CardHeader used instead of CardFooter. ([45585](https://github.com/WordPress/gutenberg/pull/45585)) +- Components: Update TypeScript-related tips in Contributing Guide. ([57267](https://github.com/WordPress/gutenberg/pull/57267)) +- Doc: Block Wrapper - enclose html tag, use correct keyword `supports` not `support`. ([56906](https://github.com/WordPress/gutenberg/pull/56906)) +- Docs/getting started readme. ([57223](https://github.com/WordPress/gutenberg/pull/57223)) +- Docs: Add new "Build your first block" tutorial to the Getting Started section of the BEH. ([56931](https://github.com/WordPress/gutenberg/pull/56931)) +- Docs: Fundamentals block development/block in the editor -- add page to manifest (toc). ([57179](https://github.com/WordPress/gutenberg/pull/57179)) +- Docs: Fundamentals of Block Development - block in the editor. ([56488](https://github.com/WordPress/gutenberg/pull/56488)) +- Enforce heading sentence case throughout the BEH. ([57143](https://github.com/WordPress/gutenberg/pull/57143)) +- Fix grammar and typos in the Block Development Environment doc. ([57123](https://github.com/WordPress/gutenberg/pull/57123)) +- Fix incorrect links in ToggleGroupControl docs. ([57236](https://github.com/WordPress/gutenberg/pull/57236)) +- Fix: Typo on footnotes docs. ([57348](https://github.com/WordPress/gutenberg/pull/57348)) +- Fixes heading hierarchy in block filters. ([57239](https://github.com/WordPress/gutenberg/pull/57239)) +- Minor heading to improve readability and reference. ([57102](https://github.com/WordPress/gutenberg/pull/57102)) +- Platform Docs: Fix missing link. ([57145](https://github.com/WordPress/gutenberg/pull/57145)) +- Remove Radix UI-related documentation. ([57336](https://github.com/WordPress/gutenberg/pull/57336)) +- Remove links to excalidraw diagrams from images. ([56980](https://github.com/WordPress/gutenberg/pull/56980)) +- Remove the "How to use JavaScript in the Block Editor" documentation. ([57166](https://github.com/WordPress/gutenberg/pull/57166)) +- Remove unnecessary TOCs from documentation. ([57087](https://github.com/WordPress/gutenberg/pull/57087)) +- Restore descriptions for deprecated button props. ([37690](https://github.com/WordPress/gutenberg/pull/37690)) +- Small heading fix. ([57098](https://github.com/WordPress/gutenberg/pull/57098)) +- Snackbar: Remove `__unstableHTML` prop from TS. ([57218](https://github.com/WordPress/gutenberg/pull/57218)) +- Update block toolbar and settings sidebar image. ([57203](https://github.com/WordPress/gutenberg/pull/57203)) +- fix: Updated the link for "Building a custom block editor". ([57319](https://github.com/WordPress/gutenberg/pull/57319)) + + +### Code Quality + +- Add missing block/theme json $schema property. ([57201](https://github.com/WordPress/gutenberg/pull/57201)) +- Add missing period in block descriptions. ([57131](https://github.com/WordPress/gutenberg/pull/57131)) +- Fix code style in Gutenberg_HTML_Tag_Processor_6_5. ([57030](https://github.com/WordPress/gutenberg/pull/57030)) +- Font size picker: Use Button API for keeping focus on reset. ([57221](https://github.com/WordPress/gutenberg/pull/57221)) +- Mobile: Fix getPxFromCssUnit circular dependency. ([57045](https://github.com/WordPress/gutenberg/pull/57045)) + +#### Block Editor +- Editor: Move the Excerpt panel to the editor package. ([57096](https://github.com/WordPress/gutenberg/pull/57096)) +- Refactor useCanContextualToolbarShow for simplicity and clarity. ([56914](https://github.com/WordPress/gutenberg/pull/56914)) +- Remove unnecessary isDisabled option on useShortcut for BlockToolbarPopover. ([56907](https://github.com/WordPress/gutenberg/pull/56907)) +- Replace block variation buttons with ToggleGroupControl. ([45654](https://github.com/WordPress/gutenberg/pull/45654)) +- Replace isSmall prop #53560. ([53599](https://github.com/WordPress/gutenberg/pull/53599)) +- Several little refactors. ([57107](https://github.com/WordPress/gutenberg/pull/57107)) +- useBlockTypesState: Divide useSelect call into two. ([57163](https://github.com/WordPress/gutenberg/pull/57163)) +- useInputRules: Remove unneeded check for inputRule. ([57164](https://github.com/WordPress/gutenberg/pull/57164)) + +#### Components +- Adding unit tests for `useCompositeState` to `Composite` component. ([56645](https://github.com/WordPress/gutenberg/pull/56645)) +- Delete experimental radix-ui based DropdownMenu component. ([55626](https://github.com/WordPress/gutenberg/pull/55626)) +- GradientPicker: Use index while iterating over gradient entries to avoid React "duplicated key" warning. ([57361](https://github.com/WordPress/gutenberg/pull/57361)) +- TabPanel: Refactor unit tests to @ariakit/test. ([57302](https://github.com/WordPress/gutenberg/pull/57302)) +- Tabs: Update styling to more closely match previous implementation. ([57275](https://github.com/WordPress/gutenberg/pull/57275)) +- ToggleGroupControl: Fix snapshots. ([57367](https://github.com/WordPress/gutenberg/pull/57367)) +- Move kebabCase() function from block-editor package and mark it as private API. ([56758](https://github.com/WordPress/gutenberg/pull/56758)) + +#### Block Library +- Gallery Block: Remove unused attributes property in shortcode transformation. ([57356](https://github.com/WordPress/gutenberg/pull/57356)) +- Remove cruft in Button block editor styles. ([30950](https://github.com/WordPress/gutenberg/pull/30950)) +- Tag Cloud: Replace 'withSelect' HoC with 'useSelect'. ([57194](https://github.com/WordPress/gutenberg/pull/57194)) + +#### Global Styles +- Global styles revisions: Refactor sticky pagination footer to use CSS. ([57294](https://github.com/WordPress/gutenberg/pull/57294)) +- Relocate revisions from fill to sidebar component. ([57034](https://github.com/WordPress/gutenberg/pull/57034)) +- Simplify the conditions in GlobalStylesEditorCanvasContainerLink. ([57144](https://github.com/WordPress/gutenberg/pull/57144)) + +#### List View +- Editor: Move and unify the inserter and list view states. ([57158](https://github.com/WordPress/gutenberg/pull/57158)) +- Editor: Unify the list view shortcut registration and definition. ([57200](https://github.com/WordPress/gutenberg/pull/57200)) +- ListView: Replace prop drilldown by a stable ref in store. ([57198](https://github.com/WordPress/gutenberg/pull/57198)) + +#### Typography +- Font Library: Consolidate existing API rest endpoints. ([57282](https://github.com/WordPress/gutenberg/pull/57282)) +- Quality: Replace wpKebabCase function with kebabCase function from components package. ([57038](https://github.com/WordPress/gutenberg/pull/57038)) + +#### Site Editor +- Editor: Move the panel visibility state from the edit-post to the editor package. ([57012](https://github.com/WordPress/gutenberg/pull/57012)) +- Remove `isResizing` variable from layout component as it is always false. ([57119](https://github.com/WordPress/gutenberg/pull/57119)) +- Allow disabling and enabling comments. ([57205](https://github.com/WordPress/gutenberg/pull/57205)) + +#### Navigation Menus +- Add basic test coverage for Navigation Menu editing mode. ([56871](https://github.com/WordPress/gutenberg/pull/56871)) +- Fix flaky Navigation focus mode test. ([57016](https://github.com/WordPress/gutenberg/pull/57016)) + +#### Plugin +- Implement proposal to remove the WordPress-Docs ruleset. ([56982](https://github.com/WordPress/gutenberg/pull/56982)) +- i18n: Make menu and title labels in Gutenberg plugin translatable. ([43500](https://github.com/WordPress/gutenberg/pull/43500)) + +#### Modules API +- Refactor, tests, and final dependencies array structure. ([57231](https://github.com/WordPress/gutenberg/pull/57231)) + +### Tools + +- Dependency Extraction Webpack Plugin: Fix test issues. ([57332](https://github.com/WordPress/gutenberg/pull/57332)) + +#### Testing +- Add an end-to-end test to check image upload working in site editor. ([57086](https://github.com/WordPress/gutenberg/pull/57086)) +- Components: Add unit test `__experimentalExpandOnFocus` unit tests for `FormTokenField`. ([57122](https://github.com/WordPress/gutenberg/pull/57122)) +- Global styles revisions e2e: Tidy up selectors to open revisions panel. ([57146](https://github.com/WordPress/gutenberg/pull/57146)) +- Migrate 'Comments Form' end-to-end tests to Playwright. ([57337](https://github.com/WordPress/gutenberg/pull/57337)) +- Migrate Site Editor 'settings sidebar' end-to-end tests to Playwright. ([57392](https://github.com/WordPress/gutenberg/pull/57392)) +- Migrate remaining 'block switcher' end-to-end tests to Playwright. ([57381](https://github.com/WordPress/gutenberg/pull/57381)) +- e2e: Try to fix flaky font-library test. ([57092](https://github.com/WordPress/gutenberg/pull/57092)) + +#### Build Tooling +- DependencyExtractionWebpackPlugin: Drop webpack4 and node<18. ([57303](https://github.com/WordPress/gutenberg/pull/57303)) +- Disable webpack perf hints when compiling packages. ([57155](https://github.com/WordPress/gutenberg/pull/57155)) +- Update to Node.js 20.x. ([56331](https://github.com/WordPress/gutenberg/pull/56331)) + +## First time contributors + +The following PRs were merged by first time contributors: + +- @dotslashbit: fix: Updated the link for "Building a custom block editor". ([57319](https://github.com/WordPress/gutenberg/pull/57319)) +- @koen12344: Restore descriptions for deprecated button props. ([37690](https://github.com/WordPress/gutenberg/pull/37690)) +- @kurudrive: Fix DayButton dot position and expand Button area. ([55502](https://github.com/WordPress/gutenberg/pull/55502)) + + +## Contributors + +The following contributors merged PRs in this release: + +@andrewhayward @andrewserong @anton-vlasenko @arthur791004 @atachibana @bacoords @BenjaminZekavica @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @colorful-tones @DAreRodz @dcalhoun @ddryo @derekblank @desrosj @dotslashbit @ellatrix @fluiddot @geriux @getdave @glendaviesnz @gvgvgvijayan @jameskoster @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @koen12344 @kurudrive @luisherranz @Mamaduka @matiasbenedetto @mcsf @mhimon @mirka @ndiego @ntsekouras @oandregal @ockham @ramonjd @richtabor @SiobhyB @sirreal @stokesman @t-hamano @tellthemachines @TimBroddin @youknowriad @yuliyan + + = 17.2.4 = ## Changelog From 53f7f2079344a1845e2a4c8f0342b1c4e961b8a1 Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Wed, 27 Dec 2023 14:41:15 -0600 Subject: [PATCH 05/17] Break out the Curating the Editor Experience doc into its own How-to Guides section (#57289) * Break out into individual docs. * Update the new pages. * Add the manifest and toc. * Remove broken link. * Add links. * Clarify intro. --- .../curating-the-editor-experience.md | 459 ------------------ .../curating-the-editor-experience/README.md | 25 + .../block-locking.md | 69 +++ .../disable-editor-functionality.md | 89 ++++ .../filters-and-hooks.md | 109 +++++ .../patterns.md | 91 ++++ .../theme-json.md | 210 ++++++++ docs/manifest.json | 32 +- docs/toc.json | 20 +- 9 files changed, 643 insertions(+), 461 deletions(-) delete mode 100644 docs/how-to-guides/curating-the-editor-experience.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/README.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/block-locking.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/patterns.md create mode 100644 docs/how-to-guides/curating-the-editor-experience/theme-json.md diff --git a/docs/how-to-guides/curating-the-editor-experience.md b/docs/how-to-guides/curating-the-editor-experience.md deleted file mode 100644 index 1d14918df7eb5b..00000000000000 --- a/docs/how-to-guides/curating-the-editor-experience.md +++ /dev/null @@ -1,459 +0,0 @@ -# Curating the Editor Experience - -The purpose of this guide is to offer various ways one can lock down and curate the experience of using WordPress, especially with the introduction of more design tools and full site editing functionality. - -For information around adding functionality to a theme, rather than curating and locking, please review this guide on [Converting a classic theme to a block theme](https://developer.wordpress.org/themes/block-themes/converting-a-classic-theme-to-a-block-theme/). - -## Locking APIs - -**Lock the ability to move or remove specific blocks** - -Users have the ability to lock and unlock blocks via the editor. The locking UI has options for preventing blocks from being moved within the content canvas or removed: - -![Image of locking interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20interface.png?raw=true) - -Keep in mind that you can apply locking options to blocks nested inside of a containing block by turning on the "Apply to all blocks inside" option. However, you cannot mass lock blocks otherwise. - -**Lock the ability to edit certain blocks** - -Alongside the ability to lock moving or removing blocks, the [Navigation Block](https://github.com/WordPress/gutenberg/pull/44739) and [Reusable block](https://github.com/WordPress/gutenberg/pull/39950) have an additional capability: lock the ability to edit the contents of the block. This locks the ability to make changes to any blocks inside of either block type. - -**Apply block locking to patterns or templates** - -When building patterns or templates, theme authors can use these same UI tools to set the default locked state of blocks. For example, a theme author could lock various pieces of a header. Keep in mind that by default, users with editing access can unlock these blocks. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways and here’s more context on [creating a template with locked blocks](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/). You can build these patterns in the editor itself, including adding locking options, before following the [documentation to register them](/docs/reference-guides/block-api/block-patterns.md). - -**Apply content only editing in patterns or templates** - -This functionality was introduced in WordPress 6.1. In contrast to block locking, which disables the ability to move or remove blocks, content only editing is both designed for use at the pattern or template level and hides all design tools, while still allowing for the ability to edit the content of the blocks. This provides a great way to simplify the interface for users and preserve a design. When this option is added, the following changes occur: - -- Non-content child blocks (containers, spacers, columns, etc) are hidden from list view, un-clickable on the canvas, and entirely un-editable. -- The Inspector will display a list of all child 'content' blocks. Clicking a block in this list reveals its settings panel. -- The main List View only shows content blocks, all at the same level regardless of actual nesting. -- Children blocks within the overall content locked container are automatically move / remove locked. -- Additional child blocks cannot be inserted, further preserving the design and layout. -- There is a link in the block toolbar to ‘Modify’ that a user can toggle on/off to have access to the broader design tools. Currently, it's not possibly to programmatically remove this option. - -This option can be applied to Columns, Cover, and Group blocks as well as third-party blocks that have the templateLock attribute in its block.json. To adopt this functionality, you need to use `"templateLock":"contentOnly"`. [Here's an example of a pattern](https://gist.github.com/annezazu/d62acd2514cea558be6cea97fe28ff3c) with this functionality in place. For more information, please [review the relevant documentation](/docs/reference-guides/block-api/block-templates.md#locking). - -Note: There is no UI in place to manage content locking and it must be managed at the code level. - -**Change permissions to control locking ability** - -Agencies and plugin authors can offer an even more curated experience by limiting which users have [permission to lock and unlock blocks](https://make.wordpress.org/core/2022/05/05/block-locking-settings-in-wordpress-6-0/). By default, anyone who is an administrator will have access to lock and unlock blocks. - -Developers can add a filter to the [block_editor_settings_all](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) hook to configure permissions around locking blocks. The hook passes two parameters to the callback function: - -- `$settings` - An array of configurable settings for the editor. - -- `$context` - An instance of WP_Block_Editor_Context, an object that contains information about the current editor. - -Specifically, developers can alter the `$settings['canLockBlocks']` value by setting it to `true` or `false`, typically by running through one or more conditional checks. - -The following example disables block locking permissions for all users when editing a page: - -```php -add_filter( 'block_editor_settings_all', function( $settings, $context ) { - if ( $context->post && 'page' === $context->post->post_type ) { - $settings['canLockBlocks'] = false; - } - - return $settings; -}, 10, 2 ); -``` - -Another common use case may be to only allow users who can edit the visual design of the site (theme editing) to lock or unlock blocks. The best option would be to test against the `edit_theme_options` capability, as shown in the following code snippet: - -```php -add_filter( 'block_editor_settings_all', function( $settings ) { - $settings['canLockBlocks'] = current_user_can( 'edit_theme_options' ); - - return $settings; -} ); -``` - -Developers may use any type of conditional check to determine who can lock/unlock blocks. This is merely a small sampling of what is possible via the filter hook. - - -## Providing default controls/options - -**Define default options** - -Since theme.json acts as a configuration tool, there are numerous ways to define at a granular level what options are available. This section will use duotone as an example since it showcases a feature that cuts across a few blocks and allows for varying levels of access. - -*Duotone with Core options and customization available for each image related block:* - -```json -{ -"version": 2, - "settings": { - "color": { - "customDuotone": true, - "duotone": [ - ] - } - } -} -``` - -*Duotone with theme defined color options, Core options, and customization available for each image related block:* -```json -{ - "version": 2, - "settings": { - "color": { - "duotone": [ - { - "colors": [ "#000000", "#ffffff" ], - "slug": "foreground-and-background", - "name": "Foreground and background" - }, - { - "colors": [ "#000000", "#ff0200" ], - "slug": "foreground-and-secondary", - "name": "Foreground and secondary" - }, - { - "colors": [ "#000000", "#7f5dee" ], - "slug": "foreground-and-tertiary", - "name": "Foreground and tertiary" - }, - ] - } - } -} -``` - -*Duotone with defined default options and all customization available for the Post Featured Image block:* - -```json -{ - "schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "color": { - "custom": true, - "customDuotone": true - }, - "blocks": { - "core/post-featured-image": { - "color": { - "duotone": [ - { - "colors": [ "#282828", "#ff5837" ], - "slug": "black-and-orange", - "name": "Black and Orange" - }, - { - "colors": [ "#282828", "#0288d1" ], - "slug": "black-and-blue", - "name": "Black and Blue" - } - ], - "customDuotone": true, - "custom": true - } - } - } - } -} -``` - -*Duotone with only defined default options and core options available for the Post Featured Image block (no customization):* - -```json -{ - "schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "color": { - "custom": true, - "customDuotone": true - }, - "blocks": { - "core/post-featured-image": { - "color": { - "duotone": [ - { - "colors": [ "#282828", "#ff5837" ], - "slug": "black-and-orange", - "name": "Black and Orange" - }, - { - "colors": [ "#282828", "#0288d1" ], - "slug": "black-and-blue", - "name": "Black and Blue" - } - ], - "customDuotone": false, - "custom": false - } - } - } - } -} -``` - -## Limiting interface options with theme.json - -**Limit options on a per block basis** - -Beyond defining default values, using theme.json allows you to also remove options entirely and instead rely on what the theme has set in place. Below is a visual showing two extremes with the same paragraph block: - -![Image of restricted interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20comparison%20visual.png?raw=true) - -Continuing the examples with duotone, this means you could allow full access to all Duotone functionality for Image blocks and only limit the Post Featured Image block like so: - -```json -{ - "schema": "https://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "color": { - "custom": true, - "customDuotone": true - }, - "blocks": { - "core/image": { - "color": { - "duotone": [], - "customDuotone": true, - "custom": true - } - }, - "core/post-featured-image": { - "color": { - "duotone": [], - "customDuotone": false, - "custom": false - } - } - } - } -} -``` - -You can read more about how best to [turn on/off options with theme.json here](/docs/how-to-guides/themes/theme-json.md). - -**Disable inherit default layout** - -To disable the “Inherit default layout” setting for container blocks like the Group block, remove the following section: - -```json -"layout": { - "contentSize": null, - "wideSize": null -}, -``` - -**Limit options globally** - -When using theme.json in a block or classic theme, these settings will stop the default color and typography controls from being enabled globally, greatly limiting what’s possible: - -```json -{ - "$schema": "http://schemas.wp.org/trunk/theme.json", - "version": 2, - "settings": { - "layout": { - "contentSize": "750px" - }, - "color": { - "background": false, - "custom": false, - "customDuotone": false, - "customGradient": false, - "defaultGradients": false, - "defaultPalette": false, - "text": false - }, - "typography": { - "customFontSize": false, - "dropCap": false, - "fontStyle": false, - "fontWeight": false, - "letterSpacing": false, - "lineHeight": false, - "textDecoration": false, - "textTransform": false - } - } -} -``` - -To enable something from the above, just set whatever value you want to change to `true` for more granularity. - -## Limiting interface options with theme.json filters - -The theme.json file is a great way to control interface options, but it only allows for global or block-level modifications, which can be limiting in some scenarios. - -For instance, in the previous section, color and typography controls were disabled globally using theme.json. But let's say you want to enable color settings for users who are Administrators. - -To provide more flexibility, WordPress 6.1 introduced server-side filters allowing you to customize theme.json data at four different data layers. - -- [`wp_theme_json_data_default`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_default/) - Hooks into the default data provided by WordPress -- [`wp_theme_json_data_blocks`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_blocks/) - Hooks into the data provided by blocks. -- [`wp_theme_json_data_theme`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_theme/) - Hooks into the data provided by the current theme. -- [`wp_theme_json_data_user`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_user/) - Hooks into the data provided by the user. - -In the following example, the data from the current theme's theme.json file is updated using the `wp_theme_json_data_theme` filter. Color controls are restored if the current user is an Administrator. - -```php -// Disable color controls for all users except Administrators. -function example_filter_theme_json_data_theme( $theme_json ){ - $is_administrator = current_user_can( 'edit_theme_options' ); - - if ( $is_administrator ) { - $new_data = array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'background' => true, - 'custom' => true, - 'customDuotone' => true, - 'customGradient' => true, - 'defaultGradients' => true, - 'defaultPalette' => true, - 'text' => true, - ), - ), - ); - } - - return $theme_json->update_with( $new_data ); -} -add_filter( 'wp_theme_json_data_theme', 'example_filter_theme_json_data_theme' ); -``` - -The filter receives an instance of the `WP_Theme_JSON_Data class` with the data for the respective layer. Then, you pass new data in a valid theme.json-like structure to the `update_with( $new_data )` method. A theme.json version number is required in `$new_data`. - - -## Limiting interface options with client-side filters - -WordPress 6.2 introduced a new client-side filter allowing you to modify block-level [theme.json settings](/docs/reference-guides/theme-json-reference/theme-json-living.md#settings) before the editor is rendered. - -The filter is called `blockEditor.useSetting.before` and can be used in the JavaScript code as follows: - -```js -import { addFilter } from '@wordpress/hooks'; - -/** - * Limit the Column block's spacing options to pixels. - */ -addFilter( - 'blockEditor.useSetting.before', - 'example/useSetting.before', - ( settingValue, settingName, clientId, blockName ) => { - if ( blockName === 'core/column' && settingName === 'spacing.units' ) { - return [ 'px' ]; - } - return settingValue; - } -); -``` - -This example will restrict the available spacing units for the Column block to just pixels. As discussed above, a similar restriction could be applied using theme.json filters or directly in a theme’s theme.json file using block-level settings. - -However, the `blockEditor.useSetting.before` filter is unique because it allows you to modify settings according to the block’s location, neighboring blocks, the current user’s role, and more. The possibilities for customization are extensive. - -In the following example, text color controls are disabled for the Heading block whenever the block is placed inside of a Media & Text block. - -```js -import { select } from '@wordpress/data'; -import { addFilter } from '@wordpress/hooks'; - -/** - * Disable text color controls on Heading blocks when placed inside of Media & Text blocks. - */ -addFilter( - 'blockEditor.useSetting.before', - 'example/useSetting.before', - ( settingValue, settingName, clientId, blockName ) => { - if ( blockName === 'core/heading' ) { - const { getBlockParents, getBlockName } = select( 'core/block-editor' ); - const blockParents = getBlockParents( clientId, true ); - const inMediaText = blockParents.some( ( ancestorId ) => getBlockName( ancestorId ) === 'core/media-text' ); - - if ( inMediaText && settingName === 'color.text' ) { - return false; - } - } - - return settingValue; - } -); -``` - -## Remove access to functionality - -**Remove access to the template editor** - -Whether you’re using [theme.json in a Classic Theme](https://developer.wordpress.org/themes/block-themes/converting-a-classic-theme-to-a-block-theme/#adding-theme-json-in-classic-themes) or Block Theme, you can add the following to your functions.php file to remove access to the Template Editor that is available when editing posts or pages: - -`remove_theme_support( 'block-templates');` - -This prevents both the ability to both create new block templates or edit them from within the Post Editor. - -**Create an allow or disallow list to limit block options** - -There might be times when you don’t want access to a block at all to be available for users. To control what’s available in the inserter, you can take two approaches: [an allow list](/docs/reference-guides/filters/block-filters.md#using-an-allow-list) that disables all blocks except those on the list or a [deny list that unregisters specific blocks](/docs/reference-guides/filters/block-filters.md#using-a-deny-list). - -**Disable pattern directory** - -To fully remove patterns bundled with WordPress core from being accessed in the Inserter, the following can be added to your functions.php file: - -`remove_theme_support( 'core-block-patterns' );` - -## Utilizing patterns - -**Prioritize starter patterns for any post type** - -When a user creates new content, regardless of post type, they are met with an empty canvas. However, that experience can be improved thanks to the option to have patterns from a specific type prioritized upon creation of a new piece of content. The modal appears each time the user creates a new item when there are patterns on their website that declare support for the `core/post-content` block types. By default, WordPress does not include any of these patterns, so the modal will not appear without at least two of these post content patterns being added. - -To opt into this, include `core/post-content` in the Block Types for your pattern. From there you can control which post types the pattern should show up for via the Post Types option. [Here's an example of a pattern](https://gist.github.com/annezazu/ead4c4965345251ec999b716c0c84f32) that would appear when creating a new post. - -Read more about this functionality in the [Page creation patterns in WordPress 6.0 dev note](https://make.wordpress.org/core/2022/05/03/page-creation-patterns-in-wordpress-6-0/) and [note that WordPress 6.1 brought this functionality to all post types](https://make.wordpress.org/core/2022/10/10/miscellaneous-editor-changes-for-wordpress-6-1/#start-content-patterns-for-all-post-types). - -**Prioritize starter patterns for template creation** - -In the same way patterns can be prioritized for new posts or pages, the same experience can be added to the template creation process. When patterns declare support for the 'templateTypes' property, the patterns will appear anytime a template that matches the designation is created, along with the options to start from a blank state or use the current fallback of the template. By default, WordPress does not include any of these patterns. - -To opt into this, a pattern needs to specify a property called `templateTypes`, which is an array containing the templates where the patterns can be used as the full content. Here's an example of a pattern that would appear when creating a 404 template: - -``` -register_block_pattern( - 'wp-my-theme/404-template-pattern', - array( - 'title' => __( '404 Only template pattern', 'wp-my-theme' ), - 'templateTypes' => array( '404' ), - 'content' => '

404 pattern

', - ) -); -``` - -Read more about this functionality in the [Patterns on the create a new template modal in the WordPress 6.3 dev note](https://make.wordpress.org/core/2023/07/18/miscellaneous-editor-changes-in-wordpress-6-3/#patterns-on-the-create-a-new-template-modal). - -**Lock patterns** - -As mentioned in the prior section on Locking APIs, aspects of patterns themselves can be locked so that the important aspects of the design can be preserved. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways. You can build these patterns in the editor itself, including adding locking options, before [following the documentation to register them](/docs/reference-guides/block-api/block-patterns.md). - -**Prioritize specific patterns from the Pattern Directory** - -With WordPress 6.0 themes can register patterns from [Pattern Directory](https://wordpress.org/patterns/) through theme.json. To accomplish this, themes should use the new patterns top level key in theme.json. Within this field, themes can list patterns to register from the Pattern Directory. The patterns field is an array of pattern slugs from the Pattern Directory. Pattern slugs can be extracted by the url in a single pattern view at the Pattern Directory. Example: This url https://wordpress.org/patterns/pattern/partner-logos the slug is partner-logos. -```json -{ - "version": 2, - "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] -} -``` - -Note that this field requires using [version 2 of theme.json](/docs/reference-guides/theme-json-reference/theme-json-living.md). The content creator will then find the respective Pattern in the inserter “Patterns” tab in the categories that match the categories from the Pattern Directory. - -## Combining approaches - -Keep in mind that the above approaches can be combined as you see fit. For example, you can provide custom patterns to use when creating a new page while also limiting the amount of customization that can be done to aspects of them, like only allowing certain preset colors to be used for the background of a Cover block or locking down what blocks can be deleted. When considering the approaches to take, think about the specific ways you might want to both open up the experience and curate it. - -## Additional Resources - -- [Builder Basics – Working with Templates in Full Site Editing (Part 3)](https://wordpress.tv/2022/05/24/nick-diego-builder-basics-working-with-templates-in-full-site-editing-part-3/) -- [Core Editor Improvement: Curated experiences with locking APIs & theme.json](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/) -- [Learn WordPress session on Curating the Editor Experience](https://wordpress.tv/2022/07/22/nick-diego-curating-the-editor-experience/) diff --git a/docs/how-to-guides/curating-the-editor-experience/README.md b/docs/how-to-guides/curating-the-editor-experience/README.md new file mode 100644 index 00000000000000..3b348d67dee26d --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/README.md @@ -0,0 +1,25 @@ +# Curating the Editor Experience + +Curating the editing experience in WordPress is important because it allows you to streamline the editing process, ensuring consistency and alignment with the site's style and branding guidelines. It also makes it easier for users to create and manage content effectively without accidental modifications or layout changes. This leads to a more efficient and personalized experience. + +The purpose of this guide is to offer various ways you can lock down and curate the experience of using WordPress, especially with the introduction of more design tools and the Site Editor. + +In this section, you will learn: + +1. [**Block locking**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/block-locking): how to restrict user interactions with specific blocks in the Editor for better content control +1. [**Patterns**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/patterns): about creating and implementing predefined block layouts to ensure design and content uniformity +1. [**theme.json**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/theme-json): to configure global styles and settings for your theme using the theme.json file +1. [**Filters and hooks**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/filters-and-hooks): about the essential filters and hooks used to modify the Editor +1. [**Disabling Editor functionality**](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/disable-editor-functionality): about additional ways to selectively disable features or components in the Editor to streamline the user experience + +## Combining approaches + +Remember that the approaches provided in the documentation above can be combined as you see fit. For example, you can provide custom patterns to use when creating a new page while also limiting the amount of customization that can be done to aspects of them, like only allowing specific preset colors to be used for the background of a Cover block or locking down what blocks can be deleted. + +When considering the approaches to take, think about the specific ways you might want to both open up the experience and curate it. + +## Additional resources + +- [Builder Basics – Working with Templates in Full Site Editing (Part 3)](https://wordpress.tv/2022/05/24/nick-diego-builder-basics-working-with-templates-in-full-site-editing-part-3/) +- [Core Editor Improvement: Curated experiences with locking APIs & theme.json](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/) +- [Learn WordPress session on Curating the Editor Experience](https://wordpress.tv/2022/07/22/nick-diego-curating-the-editor-experience/) diff --git a/docs/how-to-guides/curating-the-editor-experience/block-locking.md b/docs/how-to-guides/curating-the-editor-experience/block-locking.md new file mode 100644 index 00000000000000..83f26ea87a479e --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/block-locking.md @@ -0,0 +1,69 @@ +# Block Locking API + +The Block Locking API allows you to restrict actions on specific blocks within the Editor. This API can be used to prevent users from moving, removing, or editing certain blocks, ensuring layout consistency and content integrity. + +## Lock the ability to move or remove specific blocks + +Users can lock and unlock blocks via the Editor. The locking UI has options for preventing blocks from being moved within the content canvas or removed: + +![Image of locking interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20interface.png?raw=true) + +Keep in mind that you can apply locking options to blocks nested inside of a containing block by turning on the "Apply to all blocks inside" option. However, you cannot mass lock blocks otherwise. + +## Lock the ability to edit certain blocks + +Alongside the ability to lock moving or removing blocks, the [Navigation Block](https://github.com/WordPress/gutenberg/pull/44739) and [Reusable block](https://github.com/WordPress/gutenberg/pull/39950) have an additional capability: lock the ability to edit the contents of the block. This locks the ability to make changes to any blocks inside of either block type. + +## Apply block locking to patterns or templates + +When building patterns or templates, theme authors can use these same UI tools to set the default locked state of blocks. For example, a theme author could lock various pieces of a header. Keep in mind that by default, users with editing access can unlock these blocks. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways and here’s more context on [creating a template with locked blocks](https://make.wordpress.org/core/2022/02/09/core-editor-improvement-curated-experiences-with-locking-apis-theme-json/). You can build these patterns in the Editor itself, including adding locking options, before following the [documentation to register them](/docs/reference-guides/block-api/block-patterns.md). + +## Apply content-only editing in patterns or templates + +This functionality was introduced in WordPress 6.1. In contrast to block locking, which disables the ability to move or remove blocks, content-only editing is both designed for use at the pattern or template level and hides all design tools, while still allowing for the ability to edit the content of the blocks. This provides a great way to simplify the interface for users and preserve a design. When this option is added, the following changes occur: + +- Non-content child blocks (containers, spacers, columns, etc) are hidden from list view, un-clickable on the canvas, and entirely un-editable. +- The Inspector will display a list of all child 'content' blocks. Clicking a block in this list reveals its settings panel. +- The main List View only shows content blocks, all at the same level regardless of actual nesting. +- Children blocks within the overall content locked container are automatically move / remove locked. +- Additional child blocks cannot be inserted, further preserving the design and layout. +- There is a link in the block toolbar to ‘Modify’ that a user can toggle on/off to have access to the broader design tools. Currently, it's not possibly to programmatically remove this option. + +This option can be applied to Columns, Cover, and Group blocks as well as third-party blocks that have the templateLock attribute in its block.json. To adopt this functionality, you need to use `"templateLock":"contentOnly"`. [Here's an example of a pattern](https://gist.github.com/annezazu/d62acd2514cea558be6cea97fe28ff3c) with this functionality in place. For more information, please [review the relevant documentation](/docs/reference-guides/block-api/block-templates.md#locking). + +Note: There is no UI in place to manage content locking and it must be managed at the code level. + +## Change permissions to control locking ability + +Agencies and plugin authors can offer an even more curated experience by limiting which users have [permission to lock and unlock blocks](https://make.wordpress.org/core/2022/05/05/block-locking-settings-in-wordpress-6-0/). By default, anyone who is an administrator will have access to lock and unlock blocks. + +Developers can add a filter to the [block_editor_settings_all](https://developer.wordpress.org/reference/hooks/block_editor_settings_all/) hook to configure permissions around locking blocks. The hook passes two parameters to the callback function: + +- `$settings` - An array of configurable settings for the Editor. +- `$context` - An instance of WP_Block_Editor_Context, an object that contains information about the current Editor. + +Specifically, developers can alter the `$settings['canLockBlocks']` value by setting it to `true` or `false`, typically by running through one or more conditional checks. + +The following example disables block locking permissions for all users when editing a page: + +```php +add_filter( 'block_editor_settings_all', function( $settings, $context ) { + if ( $context->post && 'page' === $context->post->post_type ) { + $settings['canLockBlocks'] = false; + } + + return $settings; +}, 10, 2 ); +``` + +Another common use case may be to only allow users who can edit the visual design of the site (theme editing) to lock or unlock blocks. Now, the best option would be to test against the `edit_theme_options` capability, as shown in the following code snippet: + +```php +add_filter( 'block_editor_settings_all', function( $settings ) { + $settings['canLockBlocks'] = current_user_can( 'edit_theme_options' ); + + return $settings; +} ); +``` + +Developers may use any type of conditional check to determine who can lock/unlock blocks. This is merely a small sampling of what is possible via the filter hook. \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md new file mode 100644 index 00000000000000..34b23776914804 --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md @@ -0,0 +1,89 @@ +## Disable Editor functionality + +This page is dedicated to the many ways you can disable specific functionality in the Post Editor and Site Editor that are not covered in other areas of the curation documentation. + +## Restrict block options + +There might be times when you don’t want access to a block at all to be available for users. To control what’s available in the inserter, you can take two approaches: [an allow list](/docs/reference-guides/filters/block-filters.md#using-an-allow-list) that disables all blocks except those on the list or a [deny list that unregisters specific blocks](/docs/reference-guides/filters/block-filters.md#using-a-deny-list). + +## Disable the Pattern Directory + +To fully remove patterns bundled with WordPress core from being accessed in the Inserter, the following can be added to your `functions.php` file: + +```php +function example_theme_support() { + remove_theme_support( 'core-block-patterns' ); +} +add_action( 'after_setup_theme', 'example_theme_support' ); +``` + +## Disable block variations + +Some Core blocks are actually [block variations](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/). A great example is the Row and Stack blocks, which are actually variations of the Group block. If you want to disable these "blocks", you actually need to disable the respective variations. + +Block variations are registered using JavaScript and need to be disabled with JavaScript. The code below will disable the Row variation. + +```js +wp.domReady( () => { + wp.blocks.unregisterBlockVariation( 'core/group', 'group-row' ); +}); +``` + +Assuming the code was placed in a `disable-variations.js` file located in the root of your theme folder, you can enqueue this file in the theme's `functions.php` using the code below. + +```php +function example_disable_variations_script() { + wp_enqueue_script( + 'example-disable-variations-script', + get_template_directory_uri() . '/disable-variations.js', + array( 'wp-dom-ready' ), + wp_get_theme()->get( 'Version' ), + true + ); +} +add_action( 'enqueue_block_editor_assets', 'example_disable_variations_script' ); +``` + +## Disable block styles + +There are a few Core blocks that include their own [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/). An example is the Image block, which includes a block style for rounded images called "Rounded". You many not want your users to round images, or you might prefer to use the border-radius control instead of the block style. Either way, it's easy to disable any unwanted block styles. + +Unlike block variations, you can register styles in either JavaScript or PHP. If a style was registered in JavaScript, it must be disabled with JavaScript. If registered using PHP, the style can be disabled with either. All Core block styles are registed in JavaScript. + +So, you would use the following code to disable the "Rounded" block style for the Image block. + +```js +wp.domReady( () => { + wp.blocks.unregisterBlockStyle( 'core/image', 'rounded' ); +}); +``` + +This JavaScript should be enqueued much like the block variation example above. Refer to the [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/) documentation for how to register and unregister styles using PHP. + +## Disable access to the Template Editor + +Whether you’re using theme.json in a Classic or Block theme, you can add the following to your `functions.php` file to remove access to the Template Editor that is available when editing posts or pages: + +```php +function example_theme_support() { + remove_theme_support( 'block-templates'); +} +add_action( 'after_setup_theme', 'example_theme_support' ); +``` + +This prevents both the ability to create new block templates or edit them from within the Post Editor. + +## Disable access to the Code Editor + +The Code Editor allows you to view the underlying block markup for a page or post. While this view is handy for experienced users, you can inadvertently break block markup by editing content. Add the following to your `functions.php` file to restrict access. + +```php +function example_restrict_code_editor_access( $settings, $context ) { + $settings[ 'codeEditingEnabled' ] = false; + + return $settings; +} +add_filter( 'block_editor_settings_all', 'example_restrict_code_editor_access', 10, 2 ); +``` + +This code prevents all users from accessing the Code Editor. You could also add [capability](https://wordpress.org/documentation/article/roles-and-capabilities/) checks to disable access for specific users. \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md b/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md new file mode 100644 index 00000000000000..fbc4de12586396 --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md @@ -0,0 +1,109 @@ +# Filters and hooks + +The Editor provides numerous filters and hooks that allow you to modify the editing experience. Here are a few. + +## Server-side theme.json filters + +The theme.json file is a great way to control interface options, but it only allows for global or block-level modifications, which can be limiting in some scenarios. + +For instance, in the previous section, color and typography controls were disabled globally using theme.json. But let's say you want to enable color settings for users who are Administrators. + +To provide more flexibility, WordPress 6.1 introduced server-side filters allowing you to customize theme.json data at four different data layers. + +- [`wp_theme_json_data_default`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_default/) - Hooks into the default data provided by WordPress +- [`wp_theme_json_data_blocks`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_blocks/) - Hooks into the data provided by blocks. +- [`wp_theme_json_data_theme`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_theme/) - Hooks into the data provided by the current theme. +- [`wp_theme_json_data_user`](https://developer.wordpress.org/reference/hooks/wp_theme_json_data_user/) - Hooks into the data provided by the user. + +In the following example, the data from the current theme's theme.json file is updated using the `wp_theme_json_data_theme` filter. Color controls are restored if the current user is an Administrator. + +```php +// Disable color controls for all users except Administrators. +function example_filter_theme_json_data_theme( $theme_json ){ + $is_administrator = current_user_can( 'edit_theme_options' ); + + if ( $is_administrator ) { + $new_data = array( + 'version' => 2, + 'settings' => array( + 'color' => array( + 'background' => true, + 'custom' => true, + 'customDuotone' => true, + 'customGradient' => true, + 'defaultGradients' => true, + 'defaultPalette' => true, + 'text' => true, + ), + ), + ); + } + + return $theme_json->update_with( $new_data ); +} +add_filter( 'wp_theme_json_data_theme', 'example_filter_theme_json_data_theme' ); +``` + +The filter receives an instance of the `WP_Theme_JSON_Data class` with the data for the respective layer. Then, you pass new data in a valid theme.json-like structure to the `update_with( $new_data )` method. A theme.json version number is required in `$new_data`. + + +## Client-side (Editor) filters + +WordPress 6.2 introduced a new client-side filter allowing you to modify block-level [theme.json settings](/docs/reference-guides/theme-json-reference/theme-json-living.md#settings) before the Editor is rendered. + +The filter is called `blockEditor.useSetting.before` and can be used in the JavaScript code as follows: + +```js +import { addFilter } from '@wordpress/hooks'; + +/** + * Limit the Column block's spacing options to pixels. + */ +addFilter( + 'blockEditor.useSetting.before', + 'example/useSetting.before', + ( settingValue, settingName, clientId, blockName ) => { + if ( blockName === 'core/column' && settingName === 'spacing.units' ) { + return [ 'px' ]; + } + return settingValue; + } +); +``` + +This example will restrict the available spacing units for the Column block to just pixels. As discussed above, a similar restriction could be applied using theme.json filters or directly in a theme’s theme.json file using block-level settings. + +However, the `blockEditor.useSetting.before` filter is unique because it allows you to modify settings according to the block’s location, neighboring blocks, the current user’s role, and more. The possibilities for customization are extensive. + +In the following example, text color controls are disabled for the Heading block whenever the block is placed inside of a Media & Text block. + +```js +import { select } from '@wordpress/data'; +import { addFilter } from '@wordpress/hooks'; + +/** + * Disable text color controls on Heading blocks when placed inside of Media & Text blocks. + */ +addFilter( + 'blockEditor.useSetting.before', + 'example/useSetting.before', + ( settingValue, settingName, clientId, blockName ) => { + if ( blockName === 'core/heading' ) { + const { getBlockParents, getBlockName } = select( 'core/block-editor' ); + const blockParents = getBlockParents( clientId, true ); + const inMediaText = blockParents.some( ( ancestorId ) => getBlockName( ancestorId ) === 'core/media-text' ); + + if ( inMediaText && settingName === 'color.text' ) { + return false; + } + } + + return settingValue; + } +); +``` + +## Additional resources + +- [How to modify theme.json data using server-side filters](https://developer.wordpress.org/news/2023/07/05/how-to-modify-theme-json-data-using-server-side-filters/) (WordPress Developer Blog) +- [Curating the Editor experience with client-side filters](https://developer.wordpress.org/news/2023/05/24/curating-the-editor-experience-with-client-side-filters/) (WordPress Developer Blog) \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/patterns.md b/docs/how-to-guides/curating-the-editor-experience/patterns.md new file mode 100644 index 00000000000000..fbe5143298cdba --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/patterns.md @@ -0,0 +1,91 @@ +# Patterns + +Block [patterns](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-patterns/) are one of the best ways to provide users with unique and curated editing experiences. + +## Prioritize starter patterns for any post type + +When a user creates new content, regardless of post type, they are met with an empty canvas. However, that experience can be improved thanks to the option to have patterns from a specific type prioritized upon creation of a new piece of content. The modal appears each time the user creates a new item when there are patterns on their website that declare support for the `core/post-content` block types. By default, WordPress does not include any of these patterns, so the modal will not appear without at least two of these post content patterns being added. + +To opt into this, include `core/post-content` in the Block Types for your pattern. From there, you can control which post types the pattern should show up for via the Post Types option. Here's an example of a pattern that would appear when creating a new post. + +```php + + + +

Details

+ + + +

Directions

+ + + +

RSVP

+ + + +

To RSVP, please join the #fse-outreach-experiment in Make Slack.

+ + + + + + + +
+

We hope to see you there!

+
+ +``` + +Read more about this functionality in the [Page creation patterns in WordPress 6.0 dev note](https://make.wordpress.org/core/2022/05/03/page-creation-patterns-in-wordpress-6-0/) and [note that WordPress 6.1 brought this functionality to all post types](https://make.wordpress.org/core/2022/10/10/miscellaneous-editor-changes-for-wordpress-6-1/#start-content-patterns-for-all-post-types). + +## Prioritize starter patterns for template creation + +In the same way patterns can be prioritized for new posts or pages, the same experience can be added to the template creation process. When patterns declare support for the 'templateTypes' property, the patterns will appear anytime a template that matches the designation is created, along with the options to start from a blank state or use the current fallback of the template. By default, WordPress does not include any of these patterns. + +To opt into this, a pattern needs to specify a property called `templateTypes`, which is an array containing the templates where the patterns can be used as the full content. Here's an example of a pattern that would appear when creating a 404 template: + +```php +register_block_pattern( + 'wp-my-theme/404-template-pattern', + array( + 'title' => __( '404 Only template pattern', 'wp-my-theme' ), + 'templateTypes' => array( '404' ), + 'content' => '

404 pattern

', + ) +); +``` + +Read more about this functionality in the [Patterns on the create a new template modal in the WordPress 6.3 dev note](https://make.wordpress.org/core/2023/07/18/miscellaneous-editor-changes-in-wordpress-6-3/#patterns-on-the-create-a-new-template-modal). + +## Lock patterns + +As mentioned in the prior section on Locking APIs, aspects of patterns themselves can be locked so that the important aspects of the design can be preserved. [Here’s an example of a pattern](https://gist.github.com/annezazu/acee30f8b6e8995e1b1a52796e6ef805) with various blocks locked in different ways. You can build these patterns in the editor itself, including adding locking options, before [following the documentation to register them](/docs/reference-guides/block-api/block-patterns.md). + +## Prioritize specific patterns from the Pattern Directory + +With WordPress 6.0 themes can register patterns from [Pattern Directory](https://wordpress.org/patterns/) through theme.json. To accomplish this, themes should use the new patterns top level key in theme.json. Within this field, themes can list patterns to register from the Pattern Directory. The patterns field is an array of pattern slugs from the Pattern Directory. Pattern slugs can be extracted by the url in a single pattern view at the Pattern Directory. Example: This url https://wordpress.org/patterns/pattern/partner-logos the slug is partner-logos. + +```json +{ + "version": 2, + "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] +} +``` + +Note that this field requires using [version 2 of theme.json](/docs/reference-guides/theme-json-reference/theme-json-living.md). The content creator will then find the respective Pattern in the inserter “Patterns” tab in the categories that match the categories from the Pattern Directory. + +## Additional resources + +- [Using template patterns to build multiple homepage designs](https://developer.wordpress.org/news/2023/04/13/using-template-patterns-to-build-multiple-homepage-designs/) (WordPress Developer Blog) \ No newline at end of file diff --git a/docs/how-to-guides/curating-the-editor-experience/theme-json.md b/docs/how-to-guides/curating-the-editor-experience/theme-json.md new file mode 100644 index 00000000000000..c8f51ea3e91106 --- /dev/null +++ b/docs/how-to-guides/curating-the-editor-experience/theme-json.md @@ -0,0 +1,210 @@ +# theme.json + +A theme's theme.json file is one of the best ways to curate the Editor experience and will likely be the first tool you use before reaching for more sophisticated solutions. + +## Providing default controls/options + +Since theme.json acts as a configuration tool, there are numerous ways to define at a granular level what options are available. This section will use duotone as an example since it showcases a feature that cuts across a few blocks and allows for varying levels of access. + +*Duotone with Core options and customization available for each image related block:* + +```json +{ +"version": 2, + "settings": { + "color": { + "customDuotone": true, + "duotone": [ + ] + } + } +} +``` + +*Duotone with theme defined color options, Core options, and customization available for each image related block:* + +```json +{ + "version": 2, + "settings": { + "color": { + "duotone": [ + { + "colors": [ "#000000", "#ffffff" ], + "slug": "foreground-and-background", + "name": "Foreground and background" + }, + { + "colors": [ "#000000", "#ff0200" ], + "slug": "foreground-and-secondary", + "name": "Foreground and secondary" + }, + { + "colors": [ "#000000", "#7f5dee" ], + "slug": "foreground-and-tertiary", + "name": "Foreground and tertiary" + }, + ] + } + } +} +``` + +*Duotone with defined default options and all customization available for the Post Featured Image block:* + +```json +{ + "schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": true, + "customDuotone": true + }, + "blocks": { + "core/post-featured-image": { + "color": { + "duotone": [ + { + "colors": [ "#282828", "#ff5837" ], + "slug": "black-and-orange", + "name": "Black and Orange" + }, + { + "colors": [ "#282828", "#0288d1" ], + "slug": "black-and-blue", + "name": "Black and Blue" + } + ], + "customDuotone": true, + "custom": true + } + } + } + } +} +``` + +*Duotone with only defined default options and core options available for the Post Featured Image block (no customization):* + +```json +{ + "schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": true, + "customDuotone": true + }, + "blocks": { + "core/post-featured-image": { + "color": { + "duotone": [ + { + "colors": [ "#282828", "#ff5837" ], + "slug": "black-and-orange", + "name": "Black and Orange" + }, + { + "colors": [ "#282828", "#0288d1" ], + "slug": "black-and-blue", + "name": "Black and Blue" + } + ], + "customDuotone": false, + "custom": false + } + } + } + } +} +``` + +## Limiting interface options with theme.json + +### Limit options on a per-block basis + +Beyond defining default values, using theme.json allows you to also remove options entirely and instead rely on what the theme has set in place. Below is a visual showing two extremes with the same paragraph block: + +![Image of restricted interface](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/Locking%20comparison%20visual.png?raw=true) + +Continuing the examples with duotone, this means you could allow full access to all Duotone functionality for Image blocks and only limit the Post Featured Image block like so: + +```json +{ + "schema": "https://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "color": { + "custom": true, + "customDuotone": true + }, + "blocks": { + "core/image": { + "color": { + "duotone": [], + "customDuotone": true, + "custom": true + } + }, + "core/post-featured-image": { + "color": { + "duotone": [], + "customDuotone": false, + "custom": false + } + } + } + } +} +``` + +You can read more about how best to [turn on/off options with theme.json here](/docs/how-to-guides/themes/theme-json.md). + +### Disable inherit default layout + +To disable the “Inherit default layout” setting for container blocks like the Group block, remove the following section: + +```json +"layout": { + "contentSize": null, + "wideSize": null +}, +``` + +### Limit options globally + +When using theme.json in a block or classic theme, these settings will stop the default color and typography controls from being enabled globally, greatly limiting what’s possible: + +```json +{ + "$schema": "http://schemas.wp.org/trunk/theme.json", + "version": 2, + "settings": { + "layout": { + "contentSize": "750px" + }, + "color": { + "background": false, + "custom": false, + "customDuotone": false, + "customGradient": false, + "defaultGradients": false, + "defaultPalette": false, + "text": false + }, + "typography": { + "customFontSize": false, + "dropCap": false, + "fontStyle": false, + "fontWeight": false, + "letterSpacing": false, + "lineHeight": false, + "textDecoration": false, + "textTransform": false + } + } +} +``` + +To enable something from the above, just set whatever value you want to change to `true` for more granularity. \ No newline at end of file diff --git a/docs/manifest.json b/docs/manifest.json index 86a889406ce919..b17343872ea71d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -212,9 +212,39 @@ { "title": "Curating the Editor Experience", "slug": "curating-the-editor-experience", - "markdown_source": "../docs/how-to-guides/curating-the-editor-experience.md", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/README.md", "parent": "how-to-guides" }, + { + "title": "Block Locking API", + "slug": "block-locking", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/block-locking.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "Patterns", + "slug": "patterns", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/patterns.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "theme.json", + "slug": "theme-json", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/theme-json.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "Filters and hooks", + "slug": "filters-and-hooks", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md", + "parent": "curating-the-editor-experience" + }, + { + "title": "DisableEditorFunctionality", + "slug": "disable-editor-functionality", + "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md", + "parent": "curating-the-editor-experience" + }, { "title": "Enqueueing assets in the Editor", "slug": "enqueueing-assets-in-the-editor", diff --git a/docs/toc.json b/docs/toc.json index 961fc88fae4f52..2a0eb6470bd082 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -99,7 +99,25 @@ } ] }, - { "docs/how-to-guides/curating-the-editor-experience.md": [] }, + { + "docs/how-to-guides/curating-the-editor-experience/README.md": [ + { + "docs/how-to-guides/curating-the-editor-experience/block-locking.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/patterns.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/theme-json.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/filters-and-hooks.md": [] + }, + { + "docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md": [] + } + ] + }, { "docs/how-to-guides/enqueueing-assets-in-the-editor.md": [] }, { "docs/how-to-guides/feature-flags.md": [] }, { "docs/how-to-guides/format-api.md": [] }, From 1f7609a67acef3a5b5febb90844fb878fe2b9777 Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Wed, 27 Dec 2023 14:46:53 -0600 Subject: [PATCH 06/17] Improve the static vs dynamic rendering comment in the block tutorial. (#57284) * Improve the static vs dynamic rendering comment. * Further clarify the sentence. --- docs/getting-started/tutorial.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md index e70b4aba9234eb..1f3fd2b2aca9d7 100644 --- a/docs/getting-started/tutorial.md +++ b/docs/getting-started/tutorial.md @@ -690,7 +690,9 @@ In the next section, however, you will add static rendering to the block. This e ## Adding static rendering -A block can be dynamically rendered, statically rendered, or both. The block you have built so far is dynamically rendered. The HTML output of the block is not actually stored in the database, only the block markup and the associated attributes. +A block can utilize dynamic rendering, static rendering, or both. The block you have built so far is dynamically rendered. Its block markup and associated attributes are stored in the database, but its HTML output is not. + +Statically rendered blocks will always store the block markup, attributes, and output in the database. Blocks can also store static output in the database while being further enhanced dynamically on the front end, a combination of both methods. You will see the following if you switch to the Code editor from within the Editor. From 31706d5c6555a82d567941cd102328caa295ca40 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 27 Dec 2023 19:23:04 -0500 Subject: [PATCH 07/17] fix: Prevent duplicative offline status indicators (#57130) * refactor: Rename `useIsConnected` to `useNetworkConnectivity` Attempt to better communicate the Hook intent. * test: Add `useNetworkConnectivity` Hook tests * docs: Document `useNetworkConnectivity` Hook * refactor: Rename `withIsConnected` to `withNetworkConnectivity` * fix: Optimistically presume network connectivity Prior to making the asynchronous request to the host app across the bridge, it is a better UX to presume network connectivity is present rather than displaying network connectivity messages briefly. * test: Create realistic default `requestConnectionStatus` mock * fix: Prevent duplicative offline status indicators Hoist the `OfflineStatus` indicator from the block list to the editor. The block list is leveraged for inner blocks, which means it rendered nested `OfflineStatus` indicators for blocks with inner blocks. Additionally, the `editor` package feels like an appropriate location for the offline detection component. --------- Co-authored-by: Derek Blank --- .../src/components/block-list/index.native.js | 5 ----- .../edit-post/src/components/layout/index.native.js | 10 +++++++++- packages/editor/src/components/index.native.js | 1 + .../src/components/offline-status/index.native.js | 0 .../src/components/offline-status/style.native.scss | 0 5 files changed, 10 insertions(+), 6 deletions(-) rename packages/{block-editor => editor}/src/components/offline-status/index.native.js (100%) rename packages/{block-editor => editor}/src/components/offline-status/style.native.scss (100%) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 4c4d7914b18066..810e23e4c1442a 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -30,7 +30,6 @@ import { import { BlockDraggableWrapper } from '../block-draggable'; import { useEditorWrapperStyles } from '../../hooks/use-editor-wrapper-styles'; import { store as blockEditorStore } from '../../store'; -import OfflineStatus from '../offline-status'; const identity = ( x ) => x; @@ -236,10 +235,6 @@ export default function BlockList( { onLayout={ onLayout } testID="block-list-wrapper" > - { - // eslint-disable-next-line no-undef - __DEV__ && - } { isRootList ? ( + { + // eslint-disable-next-line no-undef + __DEV__ && + } { isHtmlView ? this.renderHTML() : this.renderVisual() } { ! isHtmlView && Platform.OS === 'android' && ( diff --git a/packages/editor/src/components/index.native.js b/packages/editor/src/components/index.native.js index 127f15fbfdaf53..f9855fe8c6629f 100644 --- a/packages/editor/src/components/index.native.js +++ b/packages/editor/src/components/index.native.js @@ -9,5 +9,6 @@ export { default as EditorProvider } from './provider'; // Other Components. export { default as EditorHelpTopics } from './editor-help'; +export { default as OfflineStatus } from './offline-status'; export * from './deprecated'; diff --git a/packages/block-editor/src/components/offline-status/index.native.js b/packages/editor/src/components/offline-status/index.native.js similarity index 100% rename from packages/block-editor/src/components/offline-status/index.native.js rename to packages/editor/src/components/offline-status/index.native.js diff --git a/packages/block-editor/src/components/offline-status/style.native.scss b/packages/editor/src/components/offline-status/style.native.scss similarity index 100% rename from packages/block-editor/src/components/offline-status/style.native.scss rename to packages/editor/src/components/offline-status/style.native.scss From 1fface7813dab1d1d7d7e6b0a9c326a96c96f0c1 Mon Sep 17 00:00:00 2001 From: Ramon Date: Thu, 28 Dec 2023 15:37:24 +1100 Subject: [PATCH 08/17] Media: send numerical post id when uploading image (#57388) * Always send a numerical post ID. Templates and template parts use string ids comprising "theme + // + template name". * Don't assign a post at all if id is not a number or wp_id (for templates) doesn't exist. --- packages/editor/src/utils/media-upload/index.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/editor/src/utils/media-upload/index.js b/packages/editor/src/utils/media-upload/index.js index 3edd4fec51d4be..f1f60b7c1e99a0 100644 --- a/packages/editor/src/utils/media-upload/index.js +++ b/packages/editor/src/utils/media-upload/index.js @@ -31,17 +31,24 @@ export default function mediaUpload( { onError = noop, onFileChange, } ) { - const { getCurrentPostId, getEditorSettings } = select( editorStore ); + const { getCurrentPost, getEditorSettings } = select( editorStore ); const wpAllowedMimeTypes = getEditorSettings().allowedMimeTypes; maxUploadFileSize = maxUploadFileSize || getEditorSettings().maxUploadFileSize; - const currentPostId = getCurrentPostId(); + const currentPost = getCurrentPost(); + // Templates and template parts' numerical ID is stored in `wp_id`. + const currentPostId = + typeof currentPost?.id === 'number' + ? currentPost.id + : currentPost?.wp_id; + const postData = currentPostId ? { post: currentPostId } : {}; + uploadMedia( { allowedTypes, filesList, onFileChange, additionalData: { - ...( ! isNaN( currentPostId ) ? { post: currentPostId } : {} ), + ...postData, ...additionalData, }, maxUploadFileSize, From a50e4c719c91289de1d46e54722b44ba12546b8f Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 28 Dec 2023 00:36:04 -0500 Subject: [PATCH 09/17] feat: Announce offline status (#57178) * refactor: Rename `useIsConnected` to `useNetworkConnectivity` Attempt to better communicate the Hook intent. * test: Add `useNetworkConnectivity` Hook tests * docs: Document `useNetworkConnectivity` Hook * refactor: Rename `withIsConnected` to `withNetworkConnectivity` * fix: Optimistically presume network connectivity Prior to making the asynchronous request to the host app across the bridge, it is a better UX to presume network connectivity is present rather than displaying network connectivity messages briefly. * test: Create realistic default `requestConnectionStatus` mock * fix: Prevent duplicative offline status indicators Hoist the `OfflineStatus` indicator from the block list to the editor. The block list is leveraged for inner blocks, which means it rendered nested `OfflineStatus` indicators for blocks with inner blocks. Additionally, the `editor` package feels like an appropriate location for the offline detection component. * feat: Announce offline status Improve the UX for screen reader users by announcing the state of network connectivity whenever it changes. * test: Add automated `OfflineStatus` tests * refactor: Fix typo * Add missing closing tag and fix indentation from merge --------- Co-authored-by: Derek Blank --- .../components/offline-status/index.native.js | 61 +++++++++- .../offline-status/test/index.native.js | 108 ++++++++++++++++++ 2 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 packages/editor/src/components/offline-status/test/index.native.js diff --git a/packages/editor/src/components/offline-status/index.native.js b/packages/editor/src/components/offline-status/index.native.js index 0447791e69a7e1..b136fdae1d5b29 100644 --- a/packages/editor/src/components/offline-status/index.native.js +++ b/packages/editor/src/components/offline-status/index.native.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Text, View } from 'react-native'; +import { AccessibilityInfo, Text, View } from 'react-native'; /** * WordPress dependencies @@ -9,19 +9,63 @@ import { Text, View } from 'react-native'; import { usePreferredColorSchemeStyle, useNetworkConnectivity, + usePrevious, } from '@wordpress/compose'; import { Icon } from '@wordpress/components'; import { offline as offlineIcon } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ import styles from './style.native.scss'; +/** + * Conditionally announces messages for screen reader users. This Hook provides + * two benefits over React Native's `accessibilityLiveRegion`: + * + * 1. It works on both iOS and Android. + * 2. It allows announcing a secondary message when the component is inactive. + * + * @param {string} message The message to announce. + * @param {Object} options Options for the Hook. + * @param {boolean} [options.isActive] Whether the message should be announced. + * @param {string} [options.inactiveMessage] The message to announce when inactive. + */ +function useAccessibilityLiveRegion( message, { isActive, inactiveMessage } ) { + const { announceForAccessibility } = AccessibilityInfo; + const prevIsActive = usePrevious( isActive ); + + useEffect( () => { + const unconditionalMessage = typeof isActive === 'undefined'; + const initialRender = typeof prevIsActive === 'undefined'; + + if ( + unconditionalMessage || + ( isActive && ! prevIsActive && ! initialRender ) + ) { + announceForAccessibility( message ); + } else if ( ! isActive && prevIsActive && inactiveMessage ) { + announceForAccessibility( inactiveMessage ); + } + }, [ + message, + isActive, + prevIsActive, + inactiveMessage, + announceForAccessibility, + ] ); +} + const OfflineStatus = () => { const { isConnected } = useNetworkConnectivity(); + useAccessibilityLiveRegion( __( 'Network connection re-established' ), { + isActive: isConnected, + inactiveMessage: __( 'Network connection lost, working offline' ), + } ); + const containerStyle = usePreferredColorSchemeStyle( styles.offline, styles.offline__dark @@ -38,9 +82,18 @@ const OfflineStatus = () => { ); return ! isConnected ? ( - - - { __( 'Working Offline' ) } + + + + { __( 'Working Offline' ) } + ) : null; }; diff --git a/packages/editor/src/components/offline-status/test/index.native.js b/packages/editor/src/components/offline-status/test/index.native.js new file mode 100644 index 00000000000000..7e8787c4033b01 --- /dev/null +++ b/packages/editor/src/components/offline-status/test/index.native.js @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import { act, render, screen } from 'test/helpers'; + +/** + * WordPress dependencies + */ +import { + requestConnectionStatus, + subscribeConnectionStatus, +} from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import OfflineStatus from '../index'; +import { AccessibilityInfo } from 'react-native'; + +jest.mock( '../style.native.scss', () => ( { + 'offline--icon': { + fill: '', + }, +} ) ); + +describe( 'when network connectivity is unavailable', () => { + beforeAll( () => { + requestConnectionStatus.mockImplementation( ( callback ) => { + callback( false ); + return { remove: jest.fn() }; + } ); + } ); + + it( 'should display a helpful message', () => { + render( ); + + expect( screen.getByText( 'Working Offline' ) ).toBeVisible(); + } ); + + it( 'should display an accessible message', () => { + render( ); + + expect( + screen.getByLabelText( 'Network connection lost, working offline' ) + ).toBeVisible(); + } ); + + it( 'should announce network status', () => { + render( ); + + expect( + AccessibilityInfo.announceForAccessibility + ).toHaveBeenCalledWith( 'Network connection lost, working offline' ); + } ); + + it( 'should announce changes to network status', () => { + let subscriptionCallback; + subscribeConnectionStatus.mockImplementation( ( callback ) => { + subscriptionCallback = callback; + return { remove: jest.fn() }; + } ); + render( ); + + act( () => subscriptionCallback( { isConnected: false } ) ); + + expect( + AccessibilityInfo.announceForAccessibility + ).toHaveBeenCalledWith( 'Network connection lost, working offline' ); + } ); +} ); + +describe( 'when network connectivity is available', () => { + beforeAll( () => { + requestConnectionStatus.mockImplementation( ( callback ) => { + callback( true ); + return { remove: jest.fn() }; + } ); + } ); + + it( 'should not display a helpful message', () => { + render( ); + + expect( screen.queryByText( 'Working Offline' ) ).toBeNull(); + } ); + + it( 'should not announce network status', () => { + render( ); + + expect( + AccessibilityInfo.announceForAccessibility + ).not.toHaveBeenCalled(); + } ); + + it( 'should announce changes to network status', () => { + let subscriptionCallback; + subscribeConnectionStatus.mockImplementation( ( callback ) => { + subscriptionCallback = callback; + return { remove: jest.fn() }; + } ); + render( ); + + act( () => subscriptionCallback( { isConnected: false } ) ); + + expect( + AccessibilityInfo.announceForAccessibility + ).toHaveBeenCalledWith( 'Network connection lost, working offline' ); + } ); +} ); From d7c222c09ebb94949bc205ca6ef708d201b59796 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Thu, 28 Dec 2023 11:47:50 +0400 Subject: [PATCH 10/17] Fix flaky 'Post publish button' e2e test (#57407) --- .../specs/editor/various/publish-button.spec.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/e2e/specs/editor/various/publish-button.spec.js b/test/e2e/specs/editor/various/publish-button.spec.js index b1e4b07a28580f..631ddcd0fe61ba 100644 --- a/test/e2e/specs/editor/various/publish-button.spec.js +++ b/test/e2e/specs/editor/various/publish-button.spec.js @@ -42,18 +42,20 @@ test.describe( 'Post publish button', () => { topBar.getByRole( 'button', { name: 'Publish' } ) ).toBeEnabled(); - const postId = new URL( page.url() ).searchParams.get( 'post' ); const deferred = defer(); await page.route( ( url ) => - url.searchParams.has( - 'rest_route', - encodeURIComponent( `/wp/v2/posts/${ postId }` ) + url.href.includes( + `rest_route=${ encodeURIComponent( '/wp/v2/posts/' ) }` ), - async ( route ) => { - await deferred; - await route.continue(); + async ( route, request ) => { + if ( request.method() === 'POST' ) { + await deferred; + await route.continue(); + } else { + await route.continue(); + } } ); From e41b9f16fb50995d307486a80bf58e939c7ef0c1 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:29:34 +0900 Subject: [PATCH 11/17] Docs: Update sample code to fix React warning error on Tutorial page (#57412) --- docs/getting-started/tutorial.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md index 1f3fd2b2aca9d7..b6b3efcf4384a7 100644 --- a/docs/getting-started/tutorial.md +++ b/docs/getting-started/tutorial.md @@ -483,7 +483,7 @@ export default function Edit( { attributes, setAttributes } ) { 'Starting year', 'copyright-date-block' ) } - value={ startingYear } + value={ startingYear || '' } onChange={ ( value ) => setAttributes( { startingYear: value } ) } @@ -496,6 +496,10 @@ export default function Edit( { attributes, setAttributes } ) { } ``` +
+ You may have noticed that the value property has a value of startingYear || ''. The symbol || is called the Logical OR (logical disjunction) operator. This prevents warnings in React when the startingYear is empty. See Controlled and uncontrolled components for details. +
+ Save the file and refresh the Editor. Confirm that a text field now exists in the Settings panel. Add a starting year and confirm that when you update the page, the value is saved. ![A live look at editing the new Starting Year field in the Settings Sidebar](https://developer.wordpress.org/files/2023/12/block-tutorial-11.gif) @@ -522,7 +526,7 @@ export default function Edit( { attributes, setAttributes } ) { setAttributes( { startingYear: value } ) } @@ -601,7 +605,7 @@ export default function Edit( { attributes, setAttributes } ) { setAttributes( { startingYear: value } ) } From 573d7f05bfb46b630970462ef056bf78a780ffd0 Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Thu, 28 Dec 2023 04:54:58 -0600 Subject: [PATCH 12/17] Fix incorrect heading level in Editor curation docs (#57409) * Fix incorrect heading level. * Update manifest. --- .../disable-editor-functionality.md | 2 +- docs/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md index 34b23776914804..23803888f95221 100644 --- a/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md +++ b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md @@ -1,4 +1,4 @@ -## Disable Editor functionality +# Disable Editor functionality This page is dedicated to the many ways you can disable specific functionality in the Post Editor and Site Editor that are not covered in other areas of the curation documentation. diff --git a/docs/manifest.json b/docs/manifest.json index b17343872ea71d..db3ac4ff5ca57d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -240,7 +240,7 @@ "parent": "curating-the-editor-experience" }, { - "title": "DisableEditorFunctionality", + "title": "Disable Editor functionality", "slug": "disable-editor-functionality", "markdown_source": "../docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md", "parent": "curating-the-editor-experience" From 970e9250a4bdd399634439f15431e86b7b1d5385 Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Thu, 28 Dec 2023 11:56:26 +0100 Subject: [PATCH 13/17] NumberControl: Make increment and decrement buttons keyboard accessible. (#57402) * Make increment and decrement buttons keyboard accessible. * Fix typo in documentation. * Add changelog entry. --- packages/components/CHANGELOG.md | 3 ++- packages/components/src/number-control/README.md | 4 ++-- packages/components/src/number-control/index.tsx | 8 ++------ packages/components/src/number-control/types.ts | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 026ad72de90eae..3e3a29728490bd 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,7 +4,8 @@ ### Bug Fix -- `DropdownMenu V2 `: better fallback on browsers that don't support CSS subgrid([#57327](https://github.com/WordPress/gutenberg/pull/57327)). +- `NumberControl`: Make increment and decrement buttons keyboard accessible. ([#57402](https://github.com/WordPress/gutenberg/pull/57402)). +- `DropdownMenu V2`: better fallback on browsers that don't support CSS subgrid([#57327](https://github.com/WordPress/gutenberg/pull/57327)). - `FontSizePicker`: Use Button API for keeping focus on reset ([#57221](https://github.com/WordPress/gutenberg/pull/57221)). - `FontSizePicker`: Fix Reset button focus loss ([#57196](https://github.com/WordPress/gutenberg/pull/57196)). - `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). diff --git a/packages/components/src/number-control/README.md b/packages/components/src/number-control/README.md index 1a78cd25bb457c..3a54351cd29258 100644 --- a/packages/components/src/number-control/README.md +++ b/packages/components/src/number-control/README.md @@ -46,9 +46,9 @@ If `isDragEnabled` is true, this controls the amount of `px` to have been dragge ### spinControls - The type of spin controls to display. These are butons that allow the user to + The type of spin controls to display. These are buttons that allow the user to quickly increment and decrement the number. - + - 'none' - Do not show spin controls. - 'native' - Use browser's native HTML `input` controls. - 'custom' - Use plus and minus icon buttons. diff --git a/packages/components/src/number-control/index.tsx b/packages/components/src/number-control/index.tsx index 320ef4cb87d1da..57d811d69939b4 100644 --- a/packages/components/src/number-control/index.tsx +++ b/packages/components/src/number-control/index.tsx @@ -247,9 +247,7 @@ function UnforwardedNumberControl( className={ spinButtonClasses } icon={ plusIcon } size="small" - aria-hidden="true" - aria-label={ __( 'Increment' ) } - tabIndex={ -1 } + label={ __( 'Increment' ) } onClick={ buildSpinButtonClickHandler( 'up' ) } @@ -258,9 +256,7 @@ function UnforwardedNumberControl( className={ spinButtonClasses } icon={ resetIcon } size="small" - aria-hidden="true" - aria-label={ __( 'Decrement' ) } - tabIndex={ -1 } + label={ __( 'Decrement' ) } onClick={ buildSpinButtonClickHandler( 'down' ) } diff --git a/packages/components/src/number-control/types.ts b/packages/components/src/number-control/types.ts index 98ee8e0a672f08..8d198e777bd557 100644 --- a/packages/components/src/number-control/types.ts +++ b/packages/components/src/number-control/types.ts @@ -15,7 +15,7 @@ export type NumberControlProps = Omit< */ hideHTMLArrows?: boolean; /** - * The type of spin controls to display. These are butons that allow the + * The type of spin controls to display. These are buttons that allow the * user to quickly increment and decrement the number. * * - 'none' - Do not show spin controls. From 9c8001a46b4ab970a7d7abb1c403a05a464ad853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:00:33 +0100 Subject: [PATCH 14/17] DataViews: update names for `DropdownMenuRadioItemCustom` (#57416) --- packages/dataviews/src/add-filter.js | 4 ++-- packages/dataviews/src/dropdown-menu-helper.js | 5 ++++- packages/dataviews/src/filter-summary.js | 4 ++-- packages/dataviews/src/view-table.js | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index a9d8d78509dc46..2f001050cc1e51 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -117,7 +117,7 @@ export default function AddFilter( { filters, view, onChangeView } ) { return ( { @@ -172,7 +172,7 @@ export default function AddFilter( { filters, view, onChangeView } ) { ] ) => ( { + function DropdownMenuRadioItemCustom( + { checked, name, value, onChange, onClick, ...props }, + ref + ) { const onClickHandler = ( e ) => { onClick?.( e ); onChange?.( { ...e, target: { ...e.target, value } } ); diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index e255561660648c..f09c15c8eb1374 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -103,7 +103,7 @@ export default function FilterSummary( { filter, view, onChangeView } ) { return ( @@ -150,7 +150,7 @@ export default function FilterSummary( { filter, view, onChangeView } ) { ( [ operator, { label, key } ] ) => ( { diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 200da945a101a1..6ca851c5d5690d 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -167,7 +167,7 @@ function HeaderMenu( { field, view, onChangeView } ) { return ( { @@ -222,7 +222,7 @@ function HeaderMenu( { field, view, onChangeView } ) { ] ) => ( Date: Thu, 28 Dec 2023 20:24:07 +0900 Subject: [PATCH 15/17] Navigation Block: Use dom.focus for focus control (#57362) * Navigation Block: Use dom.focus for focus control * Try adding a focus trap test for Safari * Don't use dom package --- .../class-wp-navigation-block-renderer.php | 17 ++++++++++------- packages/block-library/src/navigation/view.js | 6 +++--- .../navigation-frontend-interactivity.spec.js | 6 +++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php index 189e5a695c23a7..ea94128e1dde29 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -428,11 +428,11 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $responsive_dialog_directives = ''; $close_button_directives = ''; if ( $should_load_view_script ) { - $open_button_directives = ' + $open_button_directives = ' data-wp-on--click="actions.openMenuOnClick" data-wp-on--keydown="actions.handleMenuKeydown" '; - $responsive_container_directives = ' + $responsive_container_directives = ' data-wp-class--has-modal-open="state.isMenuOpen" data-wp-class--is-menu-open="state.isMenuOpen" data-wp-watch="callbacks.initMenu" @@ -440,15 +440,17 @@ private static function get_responsive_container_markup( $attributes, $inner_blo data-wp-on--focusout="actions.handleMenuFocusout" tabindex="-1" '; - $responsive_dialog_directives = ' + $responsive_dialog_directives = ' data-wp-bind--aria-modal="state.ariaModal" data-wp-bind--aria-label="state.ariaLabel" data-wp-bind--role="state.roleAttribute" - data-wp-watch="callbacks.focusFirstElement" '; - $close_button_directives = ' + $close_button_directives = ' data-wp-on--click="actions.closeMenuOnClick" '; + $responsive_container_content_directives = ' + data-wp-watch="callbacks.focusFirstElement" + '; } return sprintf( @@ -457,7 +459,7 @@ private static function get_responsive_container_markup( $attributes, $inner_blo
-
+
%2$s
@@ -475,7 +477,8 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $open_button_directives, $responsive_container_directives, $responsive_dialog_directives, - $close_button_directives + $close_button_directives, + $responsive_container_content_directives ); } diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index ba8e6d1a6683a4..fb3919168a2677 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -180,9 +180,9 @@ const { state, actions } = store( 'core/navigation', { focusFirstElement() { const { ref } = getElement(); if ( state.isMenuOpen ) { - ref.querySelector( - '.wp-block-navigation-item > *:first-child' - ).focus(); + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + focusableElements?.[ 0 ]?.focus(); } }, }, diff --git a/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js b/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js index ac6094c3d3eac3..1d54f25b1a7cc9 100644 --- a/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js +++ b/test/e2e/specs/editor/blocks/navigation-frontend-interactivity.spec.js @@ -112,7 +112,11 @@ test.describe( 'Navigation block - Frontend interactivity', () => { // Test: overlay menu focuses on first element after opening await expect( overlayMenuFirstElement ).toBeFocused(); - // Not Tested: overlay menu traps focus + // Test: overlay menu traps focus + await pageUtils.pressKeys( 'Tab', { times: 2, delay: 50 } ); + await expect( closeMenuButton ).toBeFocused(); + await pageUtils.pressKeys( 'Shift+Tab', { times: 2, delay: 50 } ); + await expect( overlayMenuFirstElement ).toBeFocused(); // Test: overlay menu closes on click on close menu button await closeMenuButton.click(); From ec2c51dd32aacda848f851c52b1ab63ebe6f6f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:50:01 +0100 Subject: [PATCH 16/17] DataViews: hide actions menu upon selecting a layout (#57418) --- packages/dataviews/src/dropdown-menu-helper.js | 4 ++-- packages/dataviews/src/view-actions.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/dropdown-menu-helper.js b/packages/dataviews/src/dropdown-menu-helper.js index f953d36e4b43b6..0c473c50cd0c10 100644 --- a/packages/dataviews/src/dropdown-menu-helper.js +++ b/packages/dataviews/src/dropdown-menu-helper.js @@ -29,7 +29,7 @@ const radioCheck = ( */ export const DropdownMenuRadioItemCustom = forwardRef( function DropdownMenuRadioItemCustom( - { checked, name, value, onChange, onClick, ...props }, + { checked, name, value, hideOnClick, onChange, onClick, ...props }, ref ) { const onClickHandler = ( e ) => { @@ -42,7 +42,7 @@ export const DropdownMenuRadioItemCustom = forwardRef( role="menuitemradio" name={ name } aria-checked={ checked } - hideOnClick={ false } + hideOnClick={ !! hideOnClick } prefix={ checked ? ( diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 458032d68ac70c..9130f72694b289 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -54,6 +54,7 @@ function ViewTypeMenu( { view, onChangeView, supportedLayouts } ) { value={ availableView.type } name="view-actions-available-view" checked={ availableView.type === view.type } + hideOnClick={ true } onChange={ ( e ) => { onChangeView( { ...view, From 1f9b753b325faaa94107e7e98a4b67132c91e620 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+c4rl0sbr4v0@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:24:51 +0100 Subject: [PATCH 17/17] Server directive processing: Stop processing non-interactive blocks (#56302) * Initial commit * It works, but is stripping comments * Added an extra return for debugging * Refactor to use string instead of arrays to compare * Use hidden textarea to save comments in production html * Use divs as delimiters, comments not working for interactive innner components * Back to array for references and comment delimiters * Something is working :-) * Refactor, process working * experiment replacing inner blocks * now working if there is only 1 interactive block * now working with 2 interactive blocks * Commented stopping at first custom element, after innerblocks is not being processed otherwise * Try using bookmarks, fix autoclosing tag not working * Not using bookmarks anymore, just processing inner inside processing function * Seems to be working, still needs a good test battery * Fix interactivity API directive processing * Remove tests that will be updated in next commits * Add markup tests * Use correct div group structure * Small refactor * remove not needed block name in interactive markers * Add more markup tests * refactor tests * Make inner blocks optional * Move directives declaration to declare them only if needed * Refactor more code, thanks to @darerodz * Remove only the first ocurrence * Improve comments and format * Update tests to use HTML API, fix non interactive blocks not being parsed * Unmark children of interactive blocks according to @DAreRodz comment * Add a p tag check test * Execute directives by priority * Remove gutenberg name from tests and use camelCase for context property * Fix empty style attribute error * Add test for directive ordering * Fix evaluate should only execute anonymous functions test * Fix wp-style tests * Improve tests * Test that we don't process non-interactive blocks * Stop unmarking children of interactive blocks * Move directives inside gutenberg_process_interactive_html --------- Co-authored-by: Luis Herranz Co-authored-by: David Arenas --- .../class-wp-directive-processor.php | 138 +++------ .../directive-processing.php | 282 ++++++++++++++++-- .../interactivity-api/directives/wp-style.php | 2 +- .../directive-processing-test.php | 274 +++++++++++------ .../directives/wp-style-test.php | 27 +- 5 files changed, 506 insertions(+), 217 deletions(-) diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index cf55a048bb9fa5..bb70068aa9482b 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -13,47 +13,42 @@ /** * This processor is built on top of the HTML Tag Processor and augments its * capabilities to process the Interactivity API directives. - * - * IMPORTANT DISCLAIMER: This code is highly experimental and its only purpose - * is to provide a way to test the server-side rendering of the Interactivity - * API. Most of this code will be discarded once the HTML Processor is - * available. Please restrain from investing unnecessary time and effort trying - * to improve this code. */ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 { + /** + * String containing the current root block. + * + * @var string + */ + public static $root_block = null; /** - * An array of root blocks. + * Array containing the direct children of interactive blocks. * * @var array */ - public static $root_block = null; + public static $children_of_interactive_block = array(); /** - * Add a root block to the variable. + * Sets the current root block. * * @param array $block The block to add. - * - * @return void */ public static function mark_root_block( $block ) { self::$root_block = md5( serialize( $block ) ); } /** - * Remove a root block to the variable. - * - * @return void + * Resets the root block. */ public static function unmark_root_block() { self::$root_block = null; } /** - * Check if block is a root block. + * Checks if block is a root block. * * @param array $block The block to check. - * * @return bool True if block is a root block, false otherwise. */ public static function is_marked_as_root_block( $block ) { @@ -61,17 +56,36 @@ public static function is_marked_as_root_block( $block ) { } /** - * Check if a root block has already been defined. + * Checks if a root block has already been defined. * - * @return bool True if block is a root block, false otherwise. + * @return bool True if there is a root block, false otherwise. */ public static function has_root_block() { return isset( self::$root_block ); } + /** + * Stores a reference to a direct children of an interactive block to be able + * to identify it later. + * + * @param array $block The block to add. + */ + public static function mark_children_of_interactive_block( $block ) { + self::$children_of_interactive_block[] = md5( serialize( $block ) ); + } /** - * Find the matching closing tag for an opening tag. + * Checks if block is marked as children of an interactive block. + * + * @param array $block The block to check. + * @return bool True if block is a children of an interactive block, false otherwise. + */ + public static function is_marked_as_children_of_interactive_block( $block ) { + return in_array( md5( serialize( $block ) ), self::$children_of_interactive_block, true ); + } + + /** + * Finds the matching closing tag for an opening tag. * * When called while on an open tag, traverse the HTML until we find the * matching closing tag, respecting any in-between content, including nested @@ -111,76 +125,7 @@ public function next_balanced_closer() { } /** - * Traverses the HTML searching for Interactivity API directives and processing - * them. - * - * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. - * @param string $prefix Attribute prefix. - * @param string[] $directives Directives. - * - * @return WP_Directive_Processor The modified instance of the - * WP_Directive_Processor. - */ - public function process_rendered_html( $tags, $prefix, $directives ) { - $context = new WP_Directive_Context(); - $tag_stack = array(); - - while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $tags->get_tag(); - - // Is this a tag that closes the latest opening tag? - if ( $tags->is_tag_closer() ) { - if ( 0 === count( $tag_stack ) ) { - continue; - } - - list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); - if ( $latest_opening_tag_name === $tag_name ) { - array_pop( $tag_stack ); - - // If the matching opening tag didn't have any directives, we move on. - if ( 0 === count( $attributes ) ) { - continue; - } - } - } else { - $attributes = array(); - foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { - /* - * Removes the part after the double hyphen before looking for - * the directive processor inside `$directives`, e.g., "wp-bind" - * from "wp-bind--src" and "wp-context" from "wp-context" etc... - */ - list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); - if ( array_key_exists( $type, $directives ) ) { - $attributes[] = $type; - } - } - - /* - * If this is an open tag, and if it either has directives, or if - * we're inside a tag that does, take note of this tag and its - * directives so we can call its directive processor once we - * encounter the matching closing tag. - */ - if ( - ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - foreach ( $attributes as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); - } - } - - return $tags; - } - - /** - * Return the content between two balanced tags. + * Returns the content between two balanced tags. * * When called on an opening tag, return the HTML content found between that * opening tag and its matching closing tag. @@ -206,14 +151,13 @@ public function get_inner_html() { } /** - * Set the content between two balanced tags. + * Sets the content between two balanced tags. * * When called on an opening tag, set the HTML content found between that * opening tag and its matching closing tag. * * @param string $new_html The string to replace the content between the * matching tags with. - * * @return bool Whether the content was successfully replaced. */ public function set_inner_html( $new_html ) { @@ -237,7 +181,7 @@ public function set_inner_html( $new_html ) { } /** - * Return a pair of bookmarks for the current opening tag and the matching + * Returns a pair of bookmarks for the current opening tag and the matching * closing tag. * * @return array|false A pair of bookmarks, or false if there's no matching @@ -267,12 +211,12 @@ public function get_balanced_tag_bookmarks() { } /** - * Whether a given HTML element is void (e.g.
). + * Checks whether a given HTML element is void (e.g.
). + * + * @see https://html.spec.whatwg.org/#elements-2 * * @param string $tag_name The element in question. * @return bool True if the element is void. - * - * @see https://html.spec.whatwg.org/#elements-2 */ public static function is_html_void_element( $tag_name ) { switch ( $tag_name ) { @@ -297,7 +241,7 @@ public static function is_html_void_element( $tag_name ) { } /** - * Extract and return the directive type and the the part after the double + * Extracts and return the directive type and the the part after the double * hyphen from an attribute name (if present), in an array format. * * Examples: @@ -307,7 +251,7 @@ public static function is_html_void_element( $tag_name ) { * 'wp-thing--and--thang' => array( 'wp-thing', 'and--thang' ) * * @param string $name The attribute name. - * @return array The resulting array + * @return array The resulting array. */ public static function parse_attribute_name( $name ) { return explode( '--', $name, 2 ); diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index 064fc8ea62cbb2..075d31d577634c 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -1,16 +1,16 @@ 'gutenberg_interactivity_process_wp_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', - ); - $tags = new WP_Directive_Processor( $block_content ); - $tags = $tags->process_rendered_html( $tags, 'data-wp-', $directives ); - return $tags->get_updated_html(); + // Parse our own block delimiters for interactive and non-interactive blocks. + $parsed_blocks = parse_blocks( $block_content ); + $context = new WP_Directive_Context(); + $processed_content = ''; + + foreach ( $parsed_blocks as $parsed_block ) { + if ( 'core/interactivity-wrapper' === $parsed_block['blockName'] ) { + $processed_content .= gutenberg_process_interactive_block( $parsed_block, $context ); + } elseif ( 'core/non-interactivity-wrapper' === $parsed_block['blockName'] ) { + $processed_content .= gutenberg_process_non_interactive_block( $parsed_block, $context ); + } else { + $processed_content .= $parsed_block['innerHTML']; + } + } + return $processed_content; + } + + return $block_content; +} +add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 20, 2 ); +/** + * Marks the block as a children of an interactive block. + * + * @param array $parsed_block The parsed block. + * @param array $source_block The source block. + * @param WP_Block $parent_block The parent block. + */ +function gutenberg_mark_chidren_of_interactive_block( $parsed_block, $source_block, $parent_block ) { + if ( + isset( $parent_block ) && + isset( $parent_block->block_type->supports['interactivity'] ) && + $parent_block->block_type->supports['interactivity'] + ) { + WP_Directive_Processor::mark_children_of_interactive_block( $source_block ); } + return $parsed_block; +} +add_filter( 'render_block_data', 'gutenberg_mark_chidren_of_interactive_block', 100, 3 ); +/** + * Adds a comment delimiter to mark if the block is interactive or not. + * + * @param string $block_content The block content. + * @param array $block The full block, including name and attributes. + * @param WP_Block $block_instance The block instance. + */ +function gutenberg_mark_block_interactivity( $block_content, $block, $block_instance ) { + if ( + isset( $block_instance->block_type->supports['interactivity'] ) && + $block_instance->block_type->supports['interactivity'] + ) { + // Wraps the interactive block with a comment delimiter to be able to + // process it later. + return get_comment_delimited_block_content( + 'core/interactivity-wrapper', + array(), + $block_content + ); + } elseif ( WP_Directive_Processor::is_marked_as_children_of_interactive_block( $block ) ) { + // Wraps the non-interactive block with a comment delimiter to be able to + // skip it later. + return get_comment_delimited_block_content( + 'core/non-interactivity-wrapper', + array(), + $block_content + ); + } return $block_content; } -add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 ); +add_filter( 'render_block', 'gutenberg_mark_block_interactivity', 10, 3 ); + +/** + * Traverses the HTML of an interactive block, searching for Interactivity API + * directives and processing them. For the inner blocks, it calls the + * corresponding function depending on the wrapper type. + * + * @param array $interactive_block The interactive block to process. + * @param WP_Directive_Context $context The context to use when processing. + * + * @return string The processed HTML. + */ +function gutenberg_process_interactive_block( $interactive_block, $context ) { + $block_index = 0; + $content = ''; + $interactive_inner_blocks = array(); + + foreach ( $interactive_block['innerContent'] as $inner_content ) { + if ( is_string( $inner_content ) ) { + $content .= $inner_content; + } else { + // This is an inner block. It may be an interactive block or a + // non-interactive block. + $content .= ''; + $interactive_inner_blocks[] = $interactive_block['innerBlocks'][ $block_index++ ]; + } + } + + return gutenberg_process_interactive_html( $content, $context, $interactive_inner_blocks ); +} + +/** + * Returns the HTML of a non-interactive block without processing the + * directives. For the inner blocks, it calls the corresponding function + * depending on the wrapper type. + * + * @param array $non_interactive_block The non-interactive block to process. + * @param WP_Directive_Context $context The context to use when processing. + * + * @return string The processed HTML. + */ +function gutenberg_process_non_interactive_block( $non_interactive_block, $context ) { + $block_index = 0; + $content = ''; + foreach ( $non_interactive_block['innerContent'] as $inner_content ) { + if ( is_string( $inner_content ) ) { + // This content belongs to a non interactive block and therefore it cannot + // contain directives. We add the HTML directly to the final output. + $content .= $inner_content; + } else { + // This is an inner block. It may be an interactive block or a + // non-interactive block. + $inner_block = $non_interactive_block['innerBlocks'][ $block_index++ ]; + + if ( 'core/interactivity-wrapper' === $inner_block['blockName'] ) { + $content .= gutenberg_process_interactive_block( $inner_block, $context ); + } elseif ( 'core/non-interactivity-wrapper' === $inner_block['blockName'] ) { + $content .= gutenberg_process_non_interactive_block( $inner_block, $context ); + } + } + } + return $content; +} + +/** + * Processes interactive HTML by applying directives to the HTML tags. + * + * It uses the WP_Directive_Processor class to parse the HTML and apply the + * directives. If a tag contains a 'WP-INNER-BLOCKS' string and there are inner + * blocks to process, the function processes these inner blocks and replaces the + * 'WP-INNER-BLOCKS' tag in the HTML with those blocks. + * + * @param string $html The HTML to process. + * @param mixed $context The context to use when processing. + * @param array $inner_blocks The inner blocks to process. + * + * @return string The processed HTML. + */ +function gutenberg_process_interactive_html( $html, $context, $inner_blocks = array() ) { + static $directives = array( + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + ); + + $tags = new WP_Directive_Processor( $html ); + $prefix = 'data-wp-'; + $tag_stack = array(); + $inner_processed_blocks = array(); + $inner_blocks_index = 0; + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // Processes the inner blocks. + if ( str_contains( $tag_name, 'WP-INNER-BLOCKS' ) && ! empty( $inner_blocks ) && ! $tags->is_tag_closer() ) { + if ( 'core/interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context ); + } elseif ( 'core/non-interactivity-wrapper' === $inner_blocks[ $inner_blocks_index ]['blockName'] ) { + $inner_processed_blocks[ strtolower( $tag_name ) ] = gutenberg_process_non_interactive_block( $inner_blocks[ $inner_blocks_index++ ], $context ); + } + } + if ( $tags->is_tag_closer() ) { + if ( 0 === count( $tag_stack ) ) { + continue; + } + list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); + if ( $latest_opening_tag_name === $tag_name ) { + array_pop( $tag_stack ); + // If the matching opening tag didn't have any directives, we move on. + if ( 0 === count( $attributes ) ) { + continue; + } + } + } else { + $attributes = array(); + foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { + /* + * Removes the part after the double hyphen before looking for + * the directive processor inside `$directives`, e.g., "wp-bind" + * from "wp-bind--src" and "wp-context" from "wp-context" etc... + */ + list( $type ) = $tags::parse_attribute_name( $name ); + if ( array_key_exists( $type, $directives ) ) { + $attributes[] = $type; + } + } + + /* + * If this is an open tag, and if it either has directives, or if + * we're inside a tag that does, take note of this tag and its + * directives so we can call its directive processor once we + * encounter the matching closing tag. + */ + if ( + ! $tags::is_html_void_element( $tag_name ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + // Extract all directive names. They'll be used later on. + $directive_names = array_keys( $directives ); + $directive_names_rev = array_reverse( $directive_names ); + + /* + * Sort attributes by the order they appear in the `$directives` + * argument, considering it as the priority order in which + * directives should be processed. Note that the order is reversed + * for tag closers. + */ + $sorted_attrs = array_intersect( + $tags->is_tag_closer() + ? $directive_names_rev + : $directive_names, + $attributes + ); + + foreach ( $sorted_attrs as $attribute ) { + call_user_func( $directives[ $attribute ], $tags, $context ); + } + } + + $processed_html = $tags->get_updated_html(); + + // Replaces the inner block tags with the content of each inner block + // processed. + if ( ! empty( $inner_processed_blocks ) ) { + foreach ( $inner_processed_blocks as $inner_block_tag => $inner_block_content ) { + if ( str_contains( $processed_html, $inner_block_tag ) ) { + $processed_html = str_replace( '<' . $inner_block_tag . '>', $inner_block_content, $processed_html ); + } + } + } + return $processed_html; +} /** - * Resolve the reference using the store and the context from the provided path. + * Resolves the reference using the store and the context from the provided + * path. * * @param string $path Path. * @param array $context Context data. @@ -71,15 +304,14 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr ); /* - * Check first if the directive path is preceded by a negator operator (!), + * Checks first if the directive path is preceded by a negator operator (!), * indicating that the value obtained from the Interactivity Store (or the * passed context) using the subsequent path should be negated. */ $should_negate_value = '!' === $path[0]; - - $path = $should_negate_value ? substr( $path, 1 ) : $path; - $path_segments = explode( '.', $path ); - $current = $store; + $path = $should_negate_value ? substr( $path, 1 ) : $path; + $path_segments = explode( '.', $path ); + $current = $store; foreach ( $path_segments as $p ) { if ( isset( $current[ $p ] ) ) { $current = $current[ $p ]; @@ -89,7 +321,7 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr } /* - * Check if $current is an anonymous function or an arrow function, and if + * Checks if $current is an anonymous function or an arrow function, and if * so, call it passing the store. Other types of callables are ignored on * purpose, as arbitrary strings or arrays could be wrongly evaluated as * "callables". @@ -100,6 +332,6 @@ function gutenberg_interactivity_evaluate_reference( $path, array $context = arr $current = call_user_func( $current, $store ); } - // Return the opposite if it has a negator operator (!). + // Returns the opposite if it has a negator operator (!). return $should_negate_value ? ! $current : $current; } diff --git a/lib/experimental/interactivity-api/directives/wp-style.php b/lib/experimental/interactivity-api/directives/wp-style.php index 9c37f9082c2c0b..e5d7b269ace7cf 100644 --- a/lib/experimental/interactivity-api/directives/wp-style.php +++ b/lib/experimental/interactivity-api/directives/wp-style.php @@ -28,7 +28,7 @@ function gutenberg_interactivity_process_wp_style( $tags, $context ) { $expr = $tags->get_attribute( $attr ); $style_value = gutenberg_interactivity_evaluate_reference( $expr, $context->get_context() ); if ( $style_value ) { - $style_attr = $tags->get_attribute( 'style' ); + $style_attr = $tags->get_attribute( 'style' ) ?? ''; $style_attr = gutenberg_interactivity_set_style( $style_attr, $style_name, $style_value ); $tags->set_attribute( 'style', $style_attr ); } else { diff --git a/phpunit/experimental/interactivity-api/directive-processing-test.php b/phpunit/experimental/interactivity-api/directive-processing-test.php index 46ef0284df15d9..99218f69a8fb79 100644 --- a/phpunit/experimental/interactivity-api/directive-processing-test.php +++ b/phpunit/experimental/interactivity-api/directive-processing-test.php @@ -4,81 +4,97 @@ * * @package Gutenberg * @subpackage Interactivity API - * - * @phpcs:disable Generic.Files.OneObjectStructurePerFile.MultipleFound */ +class Tests_Process_Directives extends WP_UnitTestCase { + public function set_up() { + parent::set_up(); -class Helper_Class { - // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - public function process_foo_test( $tags, $context ) { - } - - public function increment( $store ) { - return $store['state']['count'] + $store['context']['count']; - } - - public static function static_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; - } -} + register_block_type( + 'test/context-level-1', + array( + 'render_callback' => function ( $attributes, $content ) { + return '
' . $content . '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); -function gutenberg_test_process_directives_helper_increment( $store ) { - return $store['state']['count'] + $store['context']['count']; -} + register_block_type( + 'test/context-level-2', + array( + 'render_callback' => function ( $attributes, $content ) { + return '
' . $content . '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); -/** - * Tests for the gutenberg_interactivity_process_rendered_html function. - * - * @group interactivity-api - * @covers gutenberg_interactivity_process_rendered_html - */ -class Tests_Process_Directives extends WP_UnitTestCase { - public function test_correctly_call_attribute_directive_processor_on_closing_tag() { - - // PHPUnit cannot stub functions, only classes. - $test_helper = $this->createMock( Helper_Class::class ); - - $test_helper->expects( $this->exactly( 2 ) ) - ->method( 'process_foo_test' ) - ->with( - $this->callback( - function ( $p ) { - return 'DIV' === $p->get_tag() && ( - // Either this is a closing tag... - $p->is_tag_closer() || - // ...or it is an open tag, and has the directive attribute set. - ( ! $p->is_tag_closer() && 'abc' === $p->get_attribute( 'foo-test' ) ) - ); - } - ) - ); - - $directives = array( - 'foo-test' => array( $test_helper, 'process_foo_test' ), + register_block_type( + 'test/context-read-only', + array( + 'render_callback' => function () { + return '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) ); - $markup = '
Example:
This is a test>
Here is a nested div
'; - $tags = new WP_Directive_Processor( $markup ); - $tags->process_rendered_html( $tags, 'foo-', $directives ); - } + register_block_type( + 'test/non-interactive-with-directive', + array( + 'render_callback' => function () { + return ''; + }, + ) + ); - public function test_directives_with_double_hyphen_processed_correctly() { - $test_helper = $this->createMock( Helper_Class::class ); - $test_helper->expects( $this->atLeastOnce() ) - ->method( 'process_foo_test' ); + register_block_type( + 'test/context-level-with-manual-inner-block-rendering', + array( + 'render_callback' => function ( $attributes, $content, $block ) { + $inner_blocks_html = ''; + foreach ( $block->inner_blocks as $inner_block ) { + $inner_blocks_html .= $inner_block->render(); + } + return '
' . $inner_blocks_html . '
'; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) + ); - $directives = array( - 'foo-test' => array( $test_helper, 'process_foo_test' ), + register_block_type( + 'test/directives-ordering', + array( + 'render_callback' => function () { + return ''; + }, + 'supports' => array( + 'interactivity' => true, + ), + ) ); + } - $markup = '
'; - $tags = new WP_Directive_Processor( $markup ); - $tags->process_rendered_html( $tags, 'foo-', $directives ); + public function tear_down() { + unregister_block_type( 'test/context-level-1' ); + unregister_block_type( 'test/context-level-2' ); + unregister_block_type( 'test/context-read-only' ); + unregister_block_type( 'test/non-interactive-with-directive' ); + unregister_block_type( 'test/context-level-with-manual-inner-block-rendering' ); + unregister_block_type( 'test/directives-ordering' ); + parent::tear_down(); } public function test_interactivity_process_directives_in_root_blocks() { - $block_content = '' . '

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

' . @@ -87,15 +103,11 @@ public function test_interactivity_process_directives_in_root_blocks() { '

Welcome to WordPress.

' . ''; - $parsed_block = parse_blocks( $block_content )[0]; - - $source_block = $parsed_block; - - $rendered_content = render_block( $parsed_block ); - + $parsed_block = parse_blocks( $block_content )[0]; + $source_block = $parsed_block; + $rendered_content = render_block( $parsed_block ); $parsed_block_second = parse_blocks( $block_content )[1]; - - $fake_parent_block = array(); + $fake_parent_block = array(); // Test that root block is intially emtpy. $this->assertEmpty( WP_Directive_Processor::$root_block ); @@ -117,16 +129,103 @@ public function test_interactivity_process_directives_in_root_blocks() { gutenberg_process_directives_in_root_blocks( $rendered_content, $parsed_block ); $this->assertEmpty( WP_Directive_Processor::$root_block ); } -} + public function test_directive_processing_of_interactive_block() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_directive_processing_two_interactive_blocks_at_same_level() { + $post_content = '
'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-2-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-2', $value ); + } + + public function test_directives_are_processed_at_tag_end() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-2-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-2', $value ); + $p->next_tag( array( 'class_name' => 'read-only-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_non_interactive_children_of_interactive_is_rendered() { + $post_content = '

Welcome

'; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'level-1-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag( array( 'class_name' => 'read-only-input-1' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + $p->next_tag(); + $this->assertSame( 'P', $p->get_tag() ); + $p->next_tag( array( 'class_name' => 'level-1-input-2' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'level-1', $value ); + } + + public function test_non_interactive_blocks_are_not_processed() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'non-interactive-with-directive' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( null, $value ); + } + + public function test_non_interactive_blocks_with_manual_inner_block_rendering_are_not_processed() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag( array( 'class_name' => 'non-interactive-with-directive' ) ); + $value = $p->get_attribute( 'value' ); + $this->assertSame( null, $value ); + } + + public function test_directives_ordering() { + $post_content = ''; + $rendered_blocks = do_blocks( $post_content ); + $p = new WP_HTML_Tag_Processor( $rendered_blocks ); + $p->next_tag(); + + $value = $p->get_attribute( 'class' ); + $this->assertSame( 'other-class some-class', $value ); + + $value = $p->get_attribute( 'value' ); + $this->assertSame( 'some-value', $value ); + + $value = $p->get_attribute( 'style' ); + $this->assertSame( 'display: none;', $value ); + } -/** - * Tests for the gutenberg_interactivity_evaluate_reference function. - * - * @group interactivity-api - * @covers gutenberg_interactivity_evaluate_reference - */ -class Tests_Utils_Evaluate extends WP_UnitTestCase { public function test_evaluate_function_should_access_state() { // Init a simple store. wp_store( @@ -142,6 +241,7 @@ public function test_evaluate_function_should_access_state() { ), ) ); + $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.core.number' ) ); $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.core.bool' ) ); $this->assertSame( 'hi', gutenberg_interactivity_evaluate_reference( 'state.core.nested.string' ) ); @@ -158,10 +258,12 @@ public function test_evaluate_function_should_access_passed_context() { ), ), ); + $this->assertSame( 2, gutenberg_interactivity_evaluate_reference( 'context.local.number', $context ) ); $this->assertFalse( gutenberg_interactivity_evaluate_reference( 'context.local.bool', $context ) ); $this->assertTrue( gutenberg_interactivity_evaluate_reference( '!context.local.bool', $context ) ); $this->assertSame( 'bye', gutenberg_interactivity_evaluate_reference( 'context.local.nested.string', $context ) ); + // Previously defined state is also accessible. $this->assertSame( 1, gutenberg_interactivity_evaluate_reference( 'state.core.number' ) ); $this->assertTrue( gutenberg_interactivity_evaluate_reference( 'state.core.bool' ) ); @@ -174,7 +276,6 @@ public function test_evaluate_function_should_return_null_for_unresolved_paths() public function test_evaluate_function_should_execute_anonymous_functions() { $context = new WP_Directive_Context( array( 'count' => 2 ) ); - $helper = new Helper_Class(); wp_store( array( @@ -182,14 +283,13 @@ public function test_evaluate_function_should_execute_anonymous_functions() { 'count' => 3, ), 'selectors' => array( - 'anonymous_function' => function ( $store ) { + 'anonymous_function' => function ( $store ) { return $store['state']['count'] + $store['context']['count']; }, // Other types of callables should not be executed. - 'function_name' => 'gutenberg_test_process_directives_helper_increment', - 'class_method' => array( $helper, 'increment' ), - 'class_static_method' => 'Helper_Class::static_increment', - 'class_static_method_as_array' => array( 'Helper_Class', 'static_increment' ), + 'function_name' => 'gutenberg_test_process_directives_helper_increment', + 'class_method' => array( $this, 'increment' ), + 'class_static_method' => array( 'Tests_Process_Directives', 'static_increment' ), ), ) ); @@ -200,16 +300,12 @@ public function test_evaluate_function_should_execute_anonymous_functions() { gutenberg_interactivity_evaluate_reference( 'selectors.function_name', $context->get_context() ) ); $this->assertSame( - array( $helper, 'increment' ), + array( $this, 'increment' ), gutenberg_interactivity_evaluate_reference( 'selectors.class_method', $context->get_context() ) ); $this->assertSame( - 'Helper_Class::static_increment', + array( 'Tests_Process_Directives', 'static_increment' ), gutenberg_interactivity_evaluate_reference( 'selectors.class_static_method', $context->get_context() ) ); - $this->assertSame( - array( 'Helper_Class', 'static_increment' ), - gutenberg_interactivity_evaluate_reference( 'selectors.class_static_method_as_array', $context->get_context() ) - ); } } diff --git a/phpunit/experimental/interactivity-api/directives/wp-style-test.php b/phpunit/experimental/interactivity-api/directives/wp-style-test.php index 8942559b2fe89f..51468bd8a28141 100644 --- a/phpunit/experimental/interactivity-api/directives/wp-style-test.php +++ b/phpunit/experimental/interactivity-api/directives/wp-style-test.php @@ -14,16 +14,16 @@ */ class Tests_Directives_WpStyle extends WP_UnitTestCase { public function test_directive_adds_style() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); + $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); $context = $context_before; gutenberg_interactivity_process_wp_style( $tags, $context ); $this->assertSame( - '
Test
', + '
Test
', $tags->get_updated_html() ); $this->assertStringContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); @@ -31,11 +31,11 @@ public function test_directive_adds_style() { } public function test_directive_ignores_empty_style() { - $markup = '
Test
'; + $markup = '
Test
'; $tags = new WP_HTML_Tag_Processor( $markup ); $tags->next_tag(); - $context_before = new WP_Directive_Context( array( 'myblock' => array( 'color' => 'green' ) ) ); + $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); $context = $context_before; gutenberg_interactivity_process_wp_style( $tags, $context ); @@ -43,4 +43,21 @@ public function test_directive_ignores_empty_style() { $this->assertStringNotContainsString( 'color: green;', $tags->get_attribute( 'style' ) ); $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-style directive changed context' ); } + + public function test_directive_works_without_style_attribute() { + $markup = '
Test
'; + $tags = new WP_HTML_Tag_Processor( $markup ); + $tags->next_tag(); + + $context_before = new WP_Directive_Context( array( 'color' => 'green' ) ); + $context = $context_before; + gutenberg_interactivity_process_wp_style( $tags, $context ); + + $this->assertSame( + '
Test
', + $tags->get_updated_html() + ); + $this->assertSame( 'color: green;', $tags->get_attribute( 'style' ) ); + $this->assertSame( $context_before->get_context(), $context->get_context(), 'data-wp-style directive changed context' ); + } }