From 5660271d6fac40319454e8246c0dbf99fa70518c Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 2 Feb 2023 16:05:51 +0100 Subject: [PATCH 01/48] Enable React StricMode again (#47639) * Enable React StricMode again * Enable concurrent and strict mode in customize-widgets, too --- .../src/components/focus-control/index.js | 13 ++++-- packages/customize-widgets/src/index.js | 17 ++++---- packages/edit-post/src/editor.js | 42 ++++++++++--------- .../edit-site/src/components/app/index.js | 24 ++++++----- .../index.js | 40 ++++++++++-------- .../specs/widgets/customizing-widgets.spec.js | 7 +++- 6 files changed, 81 insertions(+), 62 deletions(-) diff --git a/packages/customize-widgets/src/components/focus-control/index.js b/packages/customize-widgets/src/components/focus-control/index.js index b449edb651137c..682f3b2a20b662 100644 --- a/packages/customize-widgets/src/components/focus-control/index.js +++ b/packages/customize-widgets/src/components/focus-control/index.js @@ -52,21 +52,26 @@ export default function FocusControl( { api, sidebarControls, children } ) { focusWidget( widgetId ); } + let previewBound = false; + function handleReady() { api.previewer.preview.bind( 'focus-control-for-setting', handleFocus ); + previewBound = true; } api.previewer.bind( 'ready', handleReady ); return () => { api.previewer.unbind( 'ready', handleReady ); - api.previewer.preview.unbind( - 'focus-control-for-setting', - handleFocus - ); + if ( previewBound ) { + api.previewer.preview.unbind( + 'focus-control-for-setting', + handleFocus + ); + } }; }, [ api, focusWidget ] ); diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index 8ec1447306d2de..29e7e91186b17d 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { render } from '@wordpress/element'; +import { StrictMode, createRoot } from '@wordpress/element'; import { registerCoreBlocks, __experimentalGetCoreBlocks, @@ -91,13 +91,14 @@ export function initialize( editorName, blockEditorSettings ) { } } ); - render( - , - container + createRoot( container ).render( + + + ); } ); } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 03620a5021b4a2..093bfce4bc3b34 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -9,7 +9,7 @@ import { store as editorStore, experiments as editorExperiments, } from '@wordpress/editor'; -import { useMemo } from '@wordpress/element'; +import { StrictMode, useMemo } from '@wordpress/element'; import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; @@ -173,24 +173,28 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { } return ( - - - - - - - - - - - + + + + + + + + + + + + + ); } diff --git a/packages/edit-site/src/components/app/index.js b/packages/edit-site/src/components/app/index.js index 479aebf0d5361e..7f355aee2a39e5 100644 --- a/packages/edit-site/src/components/app/index.js +++ b/packages/edit-site/src/components/app/index.js @@ -3,6 +3,7 @@ */ import { SlotFillProvider, Popover } from '@wordpress/components'; import { UnsavedChangesWarning } from '@wordpress/editor'; +import { StrictMode } from '@wordpress/element'; import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; import { store as noticesStore } from '@wordpress/notices'; import { useDispatch } from '@wordpress/data'; @@ -31,16 +32,17 @@ export default function App() { } return ( - - - - - - - - - - - + + + + + + + + + + + + ); } diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index 917899d7dd99be..988e558db80a1f 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -9,7 +9,7 @@ import { store as coreStore, useResourcePermissions, } from '@wordpress/core-data'; -import { useMemo } from '@wordpress/element'; +import { StrictMode, useMemo } from '@wordpress/element'; import { BlockEditorKeyboardShortcuts, CopyHandler, @@ -99,22 +99,26 @@ export default function WidgetAreasBlockEditorProvider( { ); return ( - - - - - - { children } - - - - + + + + + + + { children } + + + + + ); } diff --git a/test/e2e/specs/widgets/customizing-widgets.spec.js b/test/e2e/specs/widgets/customizing-widgets.spec.js index 776770fecbefe4..01b6cf7e13ea70 100644 --- a/test/e2e/specs/widgets/customizing-widgets.spec.js +++ b/test/e2e/specs/widgets/customizing-widgets.spec.js @@ -262,6 +262,7 @@ test.describe( 'Widgets Customizer', () => { await expect( firstParagraphBlock ).toBeFocused(); // Expect to focus on a already focused widget. + await paragraphWidget.click(); // noop click on the widget text to unfocus the editor and hide toolbar await editParagraphWidget.click(); await expect( firstParagraphBlock ).toBeFocused(); @@ -272,6 +273,8 @@ test.describe( 'Widgets Customizer', () => { const editHeadingWidget = headingWidget.locator( 'role=button[name="Click to edit this widget."i]' ); + + await headingWidget.click(); // noop click on the widget text to unfocus the editor and hide toolbar await editHeadingWidget.click(); const headingBlock = page.locator( @@ -463,9 +466,9 @@ test.describe( 'Widgets Customizer', () => { await page.keyboard.press( 'Escape' ); await expect( page.locator( - '*[aria-live="polite"][aria-relevant="additions text"] >> text=/^You are currently in navigation mode./' + 'css=.block-editor-block-list__layout.is-navigate-mode' ) - ).toHaveCount( 1 ); + ).toBeVisible(); await expect( paragraphBlock ).toBeVisible(); } ); From b0d4954b28c94ce74b95bc6ae2549e283c847d9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 10:12:12 -0500 Subject: [PATCH 02/48] Bump http-cache-semantics from 4.1.0 to 4.1.1 (#47664) Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/kornelski/http-cache-semantics/releases) - [Commits](https://github.com/kornelski/http-cache-semantics/commits) --- updated-dependencies: - dependency-name: http-cache-semantics dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index f914bfc45c8d35..e13c69fad3e12f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23180,12 +23180,6 @@ "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -37226,9 +37220,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-deceiver": { From 70904e39da1d7d88c0eec68d85bc337c9a5929f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Feb 2023 10:17:38 -0500 Subject: [PATCH 03/48] Bump jszip from 3.6.0 to 3.10.1 (#47663) Bumps [jszip](https://github.com/Stuk/jszip) from 3.6.0 to 3.10.1. - [Release notes](https://github.com/Stuk/jszip/releases) - [Changelog](https://github.com/Stuk/jszip/blob/main/CHANGES.md) - [Commits](https://github.com/Stuk/jszip/compare/v3.6.0...v3.10.1) --- updated-dependencies: - dependency-name: jszip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 63 ++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index e13c69fad3e12f..1fe6f5f0ef07aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23262,8 +23262,7 @@ "immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", - "dev": true + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" }, "inflight": { "version": "1.0.6", @@ -23515,35 +23514,6 @@ "verror": "1.10.0" } }, - "jszip": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", - "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", - "dev": true, - "requires": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "set-immediate-shim": "~1.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, "keypather": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/keypather/-/keypather-1.10.2.tgz", @@ -23616,7 +23586,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, "requires": { "immediate": "~3.0.5" } @@ -24967,8 +24936,7 @@ "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" }, "setprototypeof": { "version": "1.1.1", @@ -37531,6 +37499,12 @@ "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.3.tgz", "integrity": "sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA==" }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "import-fresh": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", @@ -42748,6 +42722,18 @@ } } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "junk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", @@ -43091,6 +43077,15 @@ } } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } + }, "lilconfig": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", From 20ff09a108c0aa87ea4911a1ae7894bced91bd63 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 3 Feb 2023 01:01:44 +0900 Subject: [PATCH 04/48] Pattern Inserter: Fix unintended preview panel display when hovering mouse over pattern (#47693) --- packages/block-editor/src/components/inserter/menu.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index f235274d3c40f1..51026ec939dd5e 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -113,6 +113,13 @@ function InserterMenu( [ onToggleInsertionPoint, setHoveredItem ] ); + const onHoverPattern = useCallback( + ( item ) => { + onToggleInsertionPoint( !! item ); + }, + [ onToggleInsertionPoint ] + ); + const onClickPatternCategory = useCallback( ( patternCategory ) => { setSelectedPatternCategory( patternCategory ); @@ -296,7 +303,7 @@ function InserterMenu( From d113597a9d718e22aee030cc94e5631ba84a6a3c Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Thu, 2 Feb 2023 16:07:01 +0000 Subject: [PATCH 05/48] Navigation: Only show the deleted menu warning if a ref is specified (#47699) Co-authored-by: scruffian --- .../src/navigation/edit/menu-inspector-controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/navigation/edit/menu-inspector-controls.js b/packages/block-library/src/navigation/edit/menu-inspector-controls.js index 34c7faf4e00374..5eb32b5c8eac76 100644 --- a/packages/block-library/src/navigation/edit/menu-inspector-controls.js +++ b/packages/block-library/src/navigation/edit/menu-inspector-controls.js @@ -50,7 +50,7 @@ const MainContent = ( { return

{ __( 'Select or create a menu' ) }

; } - if ( isNavigationMenuMissing ) { + if ( currentMenuId && isNavigationMenuMissing ) { return ; } From 5535a3a59da845a92c72df9e4c66cc03f5af5656 Mon Sep 17 00:00:00 2001 From: Madhu Dollu Date: Thu, 2 Feb 2023 21:56:31 +0530 Subject: [PATCH 06/48] Shadow: move shadow to own panel (#47634) * move shadow into newly created effects panel * rename effects to shadow * rename label shadows to shadow in popover --- .../components/global-styles/context-menu.js | 16 +++++++++-- .../components/global-styles/screen-border.js | 5 ---- .../global-styles/screen-effects.js | 28 +++++++++++++++++++ .../components/global-styles/shadow-panel.js | 2 +- .../src/components/global-styles/ui.js | 5 ++++ 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 packages/edit-site/src/components/global-styles/screen-effects.js diff --git a/packages/edit-site/src/components/global-styles/context-menu.js b/packages/edit-site/src/components/global-styles/context-menu.js index cedb17a41699b6..96251c94d8c082 100644 --- a/packages/edit-site/src/components/global-styles/context-menu.js +++ b/packages/edit-site/src/components/global-styles/context-menu.js @@ -12,6 +12,7 @@ import { import { typography, border, + shadow, color, layout, chevronLeft, @@ -32,11 +33,13 @@ import { useHasVariationsPanel } from './variations-panel'; import { NavigationButtonAsItem } from './navigation-button'; import { IconWithCurrentColor } from './icon-with-current-color'; import { ScreenVariations } from './screen-variations'; +import { useHasShadowControl } from './shadow-panel'; function ContextMenu( { name, parentMenu = '' } ) { const hasTypographyPanel = useHasTypographyPanel( name ); const hasColorPanel = useHasColorPanel( name ); const hasBorderPanel = useHasBorderPanel( name ); + const hasEffectsPanel = useHasShadowControl( name ); const hasDimensionsPanel = useHasDimensionsPanel( name ); const hasLayoutPanel = hasDimensionsPanel; const hasVariationsPanel = useHasVariationsPanel( name, parentMenu ); @@ -85,9 +88,18 @@ function ContextMenu( { name, parentMenu = '' } ) { - { __( 'Border & Shadow' ) } + { __( 'Border' ) } + + ) } + { hasEffectsPanel && ( + + { __( 'Shadow' ) } ) } { hasLayoutPanel && ( diff --git a/packages/edit-site/src/components/global-styles/screen-border.js b/packages/edit-site/src/components/global-styles/screen-border.js index b0d07638242675..8312483cce6b2f 100644 --- a/packages/edit-site/src/components/global-styles/screen-border.js +++ b/packages/edit-site/src/components/global-styles/screen-border.js @@ -10,12 +10,10 @@ import ScreenHeader from './header'; import BorderPanel, { useHasBorderPanel } from './border-panel'; import BlockPreviewPanel from './block-preview-panel'; import { getVariationClassName } from './utils'; -import ShadowPanel, { useHasShadowControl } from './shadow-panel'; function ScreenBorder( { name, variation = '' } ) { const hasBorderPanel = useHasBorderPanel( name ); const variationClassName = getVariationClassName( variation ); - const hasShadowPanel = useHasShadowControl( name ); return ( <> @@ -23,9 +21,6 @@ function ScreenBorder( { name, variation = '' } ) { { hasBorderPanel && ( ) } - { hasShadowPanel && ( - - ) } ); } diff --git a/packages/edit-site/src/components/global-styles/screen-effects.js b/packages/edit-site/src/components/global-styles/screen-effects.js new file mode 100644 index 00000000000000..be3eb922e50303 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-effects.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import ScreenHeader from './header'; +import BlockPreviewPanel from './block-preview-panel'; +import { getVariationClassName } from './utils'; +import ShadowPanel, { useHasShadowControl } from './shadow-panel'; + +function ScreenEffects( { name, variation = '' } ) { + const variationClassName = getVariationClassName( variation ); + const hasShadowPanel = useHasShadowControl( name ); + return ( + <> + + + { hasShadowPanel && ( + + ) } + + ); +} + +export default ScreenEffects; diff --git a/packages/edit-site/src/components/global-styles/shadow-panel.js b/packages/edit-site/src/components/global-styles/shadow-panel.js index 7d7b6a381564ba..7b3aadfe6a2d65 100644 --- a/packages/edit-site/src/components/global-styles/shadow-panel.js +++ b/packages/edit-site/src/components/global-styles/shadow-panel.js @@ -129,7 +129,7 @@ function ShadowPopoverContainer( { shadow, onShadowChange } ) { return (
- { __( 'Shadows' ) } + { __( 'Shadow' ) } + + + + From 53a4d512fc9e296c78bb86101932bd1a4a0c074e Mon Sep 17 00:00:00 2001 From: Andy Peatling Date: Thu, 2 Feb 2023 08:34:19 -0800 Subject: [PATCH 07/48] Pattern Explorer Modal: Select the first category as the initial category (#47661) * Pass the first pattern category as the initially selected category in the pattern explorer modal. * If there is a selected category, make sure we use that instead. * Update packages/block-editor/src/components/inserter/block-patterns-tab.js Co-authored-by: George Mamadashvili --------- Co-authored-by: George Mamadashvili --- .../block-editor/src/components/inserter/block-patterns-tab.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab.js b/packages/block-editor/src/components/inserter/block-patterns-tab.js index 3e0b9407543ab9..ea1332dc63d0d7 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab.js @@ -195,6 +195,7 @@ function BlockPatternsTabs( { } ) { const [ showPatternsExplorer, setShowPatternsExplorer ] = useState( false ); const categories = usePatternsCategories( rootClientId ); + const initialCategory = selectedCategory || categories[ 0 ]; const isMobile = useViewportMatch( 'medium', '<' ); return ( <> @@ -261,7 +262,7 @@ function BlockPatternsTabs( { ) } { showPatternsExplorer && ( setShowPatternsExplorer( false ) } /> From 0636e977cc48225a8c3a2b7ee8cbb6c86c50b2cc Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Fri, 3 Feb 2023 03:09:42 +0900 Subject: [PATCH 08/48] BoxControl: Convert to TypeScript (#47622) * Rename index.tsx * BoxControl: Add types * Rename all-input-control.tsx * Rename unit-control.tsx * Add types * Rename axial-input-controls.tsx * Add types to axial input controls * Rename input-controls.tsx * Add types for input-controls * Rename icon.tsx * Add types to icon * Add types to LinkedButton * Add types for styles * Improve types in utils * Remove from tsconfig * Convert tests * Add main JSDoc * Add changelog * Add description for `id` prop * Fix lint errors * Remove copypasta in types * Update formatting in readme * Add todo comment * Fixup optional * Add default value * Make onChange required * Add default reset value to readme * Add allowed `side` values to readme * Fix default value for `label` in readme --- packages/components/CHANGELOG.md | 1 + packages/components/src/box-control/README.md | 43 +++---- ...input-control.js => all-input-control.tsx} | 16 ++- ...t-controls.js => axial-input-controls.tsx} | 53 ++++---- .../src/box-control/{icon.js => icon.tsx} | 13 +- .../src/box-control/{index.js => index.tsx} | 51 ++++++-- .../{input-controls.js => input-controls.tsx} | 42 +++--- .../{linked-button.js => linked-button.tsx} | 5 +- ...n-styles.js => box-control-icon-styles.ts} | 2 +- ...ontrol-styles.js => box-control-styles.ts} | 12 +- ...es.js => box-control-visualizer-styles.ts} | 8 +- .../box-control/test/{index.js => index.tsx} | 21 +-- packages/components/src/box-control/types.ts | 121 ++++++++++++++++++ .../{unit-control.js => unit-control.tsx} | 11 +- .../src/box-control/{utils.js => utils.ts} | 85 ++++++------ .../src/utils/hooks/use-controlled-state.js | 4 +- packages/components/tsconfig.json | 1 - 17 files changed, 347 insertions(+), 142 deletions(-) rename packages/components/src/box-control/{all-input-control.js => all-input-control.tsx} (74%) rename packages/components/src/box-control/{axial-input-controls.js => axial-input-controls.tsx} (71%) rename packages/components/src/box-control/{icon.js => icon.tsx} (69%) rename packages/components/src/box-control/{index.js => index.tsx} (78%) rename packages/components/src/box-control/{input-controls.js => input-controls.tsx} (69%) rename packages/components/src/box-control/{linked-button.js => linked-button.tsx} (80%) rename packages/components/src/box-control/styles/{box-control-icon-styles.js => box-control-icon-styles.ts} (94%) rename packages/components/src/box-control/styles/{box-control-styles.js => box-control-styles.ts} (81%) rename packages/components/src/box-control/styles/{box-control-visualizer-styles.js => box-control-visualizer-styles.ts} (87%) rename packages/components/src/box-control/test/{index.js => index.tsx} (93%) create mode 100644 packages/components/src/box-control/types.ts rename packages/components/src/box-control/{unit-control.js => unit-control.tsx} (89%) rename packages/components/src/box-control/{utils.js => utils.ts} (66%) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index df0efd8ebeabc7..eb47fcb30a2a7a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -24,6 +24,7 @@ - `ColorPalette`, `BorderControl`, `GradientPicker`: refine types and logic around single vs multiple palettes ([#47384](https://github.com/WordPress/gutenberg/pull/47384)). - `Button`: Convert to TypeScript ([#46997](https://github.com/WordPress/gutenberg/pull/46997)). +- `BoxControl`: Convert to TypeScript ([#47622](https://github.com/WordPress/gutenberg/pull/47622)). - `QueryControls`: Convert to TypeScript ([#46721](https://github.com/WordPress/gutenberg/pull/46721)). - `Notice`: refactor to TypeScript ([47118](https://github.com/WordPress/gutenberg/pull/47118)). diff --git a/packages/components/src/box-control/README.md b/packages/components/src/box-control/README.md index edf3993679033f..83ccd50a6f1ffc 100644 --- a/packages/components/src/box-control/README.md +++ b/packages/components/src/box-control/README.md @@ -30,82 +30,73 @@ const Example = () => { ``` ## Props -### allowReset +### `allowReset`: `boolean` If this property is true, a button to reset the box control is rendered. -- Type: `Boolean` - Required: No - Default: `true` -### splitOnAxis +### `splitOnAxis`: `boolean` If this property is true, when the box control is unlinked, vertical and horizontal controls can be used instead of updating individual sides. -- Type: `Boolean` - Required: No - Default: `false` -### inputProps +### `inputProps`: `object` -Props for the internal [InputControl](../input-control) components. +Props for the internal [UnitControl](../unit-control) components. -- Type: `Object` - Required: No +- Default: `{ min: 0 }` -### label +### `label`: `string` -Heading label for BoxControl. +Heading label for the control. -- Type: `String` - Required: No -- Default: `Box Control` +- Default: `__( 'Box Control' )` -### onChange +### `onChange`: `(next: BoxControlValue) => void` A callback function when an input value changes. -- Type: `Function` - Required: Yes -### resetValues +### `resetValues`: `object` The `top`, `right`, `bottom`, and `left` box dimension values to use when the control is reset. -- Type: `Object` - Required: No +- Default: `{ top: undefined, right: undefined, bottom: undefined, left: undefined }` -### sides +### `sides`: `string[]` -Collection of sides to allow control of. If omitted or empty, all sides will be available. +Collection of sides to allow control of. If omitted or empty, all sides will be available. Allowed values are "top", "right", "bottom", "left", "vertical", and "horizontal". -- Type: `Array` - Required: No -### units +### `units`: `WPUnitControlUnit[]` Collection of available units which are compatible with [UnitControl](../unit-control). -- Type: `Array` - Required: No -### values +### `values`: `object` The `top`, `right`, `bottom`, and `left` box dimension values. -- Type: `Object` - Required: No -### onMouseOver +### `onMouseOver`: `function` A handler for onMouseOver events. -- Type: `Function` - Required: No -### onMouseOut +### `onMouseOut`: `function` A handler for onMouseOut events. -- Type: `Function` - Required: No diff --git a/packages/components/src/box-control/all-input-control.js b/packages/components/src/box-control/all-input-control.tsx similarity index 74% rename from packages/components/src/box-control/all-input-control.js rename to packages/components/src/box-control/all-input-control.tsx index fcde202fd7de8f..b66e10fdb4ce3f 100644 --- a/packages/components/src/box-control/all-input-control.js +++ b/packages/components/src/box-control/all-input-control.tsx @@ -1,6 +1,8 @@ /** * Internal dependencies */ +import type { UnitControlProps } from '../unit-control/types'; +import type { BoxControlInputControlProps } from './types'; import UnitControl from './unit-control'; import { LABELS, @@ -22,18 +24,20 @@ export default function AllInputControl( { selectedUnits, setSelectedUnits, ...props -} ) { +}: BoxControlInputControlProps ) { const allValue = getAllValue( values, selectedUnits, sides ); const hasValues = isValuesDefined( values ); const isMixed = hasValues && isValuesMixed( values, selectedUnits, sides ); - const allPlaceholder = isMixed ? LABELS.mixed : null; + const allPlaceholder = isMixed ? LABELS.mixed : undefined; - const handleOnFocus = ( event ) => { + const handleOnFocus: React.FocusEventHandler< HTMLInputElement > = ( + event + ) => { onFocus( event, { side: 'all' } ); }; - const handleOnChange = ( next ) => { - const isNumeric = ! isNaN( parseFloat( next ) ); + const handleOnChange: UnitControlProps[ 'onChange' ] = ( next ) => { + const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); const nextValue = isNumeric ? next : undefined; const nextValues = applyValueToSides( values, nextValue, sides ); @@ -42,7 +46,7 @@ export default function AllInputControl( { // Set selected unit so it can be used as fallback by unlinked controls // when individual sides do not have a value containing a unit. - const handleOnUnitChange = ( unit ) => { + const handleOnUnitChange: UnitControlProps[ 'onUnitChange' ] = ( unit ) => { const newUnits = applyValueToSides( selectedUnits, unit, sides ); setSelectedUnits( newUnits ); }; diff --git a/packages/components/src/box-control/axial-input-controls.js b/packages/components/src/box-control/axial-input-controls.tsx similarity index 71% rename from packages/components/src/box-control/axial-input-controls.js rename to packages/components/src/box-control/axial-input-controls.tsx index 99a09d4ba366c7..627cfce408583a 100644 --- a/packages/components/src/box-control/axial-input-controls.js +++ b/packages/components/src/box-control/axial-input-controls.tsx @@ -5,8 +5,10 @@ import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; import UnitControl from './unit-control'; import { LABELS } from './utils'; import { Layout } from './styles/box-control-styles'; +import type { BoxControlInputControlProps } from './types'; -const groupedSides = [ 'vertical', 'horizontal' ]; +const groupedSides = [ 'vertical', 'horizontal' ] as const; +type GroupedSide = typeof groupedSides[ number ]; export default function AxialInputControls( { onChange, @@ -18,15 +20,17 @@ export default function AxialInputControls( { setSelectedUnits, sides, ...props -} ) { - const createHandleOnFocus = ( side ) => ( event ) => { - if ( ! onFocus ) { - return; - } - onFocus( event, { side } ); - }; +}: BoxControlInputControlProps ) { + const createHandleOnFocus = + ( side: GroupedSide ) => + ( event: React.FocusEvent< HTMLInputElement > ) => { + if ( ! onFocus ) { + return; + } + onFocus( event, { side } ); + }; - const createHandleOnHoverOn = ( side ) => () => { + const createHandleOnHoverOn = ( side: GroupedSide ) => () => { if ( ! onHoverOn ) { return; } @@ -44,7 +48,7 @@ export default function AxialInputControls( { } }; - const createHandleOnHoverOff = ( side ) => () => { + const createHandleOnHoverOff = ( side: GroupedSide ) => () => { if ( ! onHoverOff ) { return; } @@ -62,12 +66,12 @@ export default function AxialInputControls( { } }; - const createHandleOnChange = ( side ) => ( next ) => { + const createHandleOnChange = ( side: GroupedSide ) => ( next?: string ) => { if ( ! onChange ) { return; } const nextValues = { ...values }; - const isNumeric = ! isNaN( parseFloat( next ) ); + const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); const nextValue = isNumeric ? next : undefined; if ( side === 'vertical' ) { @@ -83,21 +87,22 @@ export default function AxialInputControls( { onChange( nextValues ); }; - const createHandleOnUnitChange = ( side ) => ( next ) => { - const newUnits = { ...selectedUnits }; + const createHandleOnUnitChange = + ( side: GroupedSide ) => ( next?: string ) => { + const newUnits = { ...selectedUnits }; - if ( side === 'vertical' ) { - newUnits.top = next; - newUnits.bottom = next; - } + if ( side === 'vertical' ) { + newUnits.top = next; + newUnits.bottom = next; + } - if ( side === 'horizontal' ) { - newUnits.left = next; - newUnits.right = next; - } + if ( side === 'horizontal' ) { + newUnits.left = next; + newUnits.right = next; + } - setSelectedUnits( newUnits ); - }; + setSelectedUnits( newUnits ); + }; // Filter sides if custom configuration provided, maintaining default order. const filteredSides = sides?.length diff --git a/packages/components/src/box-control/icon.js b/packages/components/src/box-control/icon.tsx similarity index 69% rename from packages/components/src/box-control/icon.js rename to packages/components/src/box-control/icon.tsx index 2a7db9972c5b13..6cb893648d68ab 100644 --- a/packages/components/src/box-control/icon.js +++ b/packages/components/src/box-control/icon.tsx @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { WordPressComponentProps } from '../ui/context'; import { Root, Viewbox, @@ -9,6 +10,7 @@ import { BottomStroke, LeftStroke, } from './styles/box-control-icon-styles'; +import type { BoxControlIconProps, BoxControlProps } from './types'; const BASE_ICON_SIZE = 24; @@ -17,11 +19,14 @@ export default function BoxControlIcon( { side = 'all', sides, ...props -} ) { - const isSideDisabled = ( value ) => - sides?.length && ! sides.includes( value ); +}: WordPressComponentProps< BoxControlIconProps, 'span' > ) { + const isSideDisabled = ( + value: NonNullable< BoxControlProps[ 'sides' ] >[ number ] + ) => sides?.length && ! sides.includes( value ); - const hasSide = ( value ) => { + const hasSide = ( + value: NonNullable< BoxControlProps[ 'sides' ] >[ number ] + ) => { if ( isSideDisabled( value ) ) { return false; } diff --git a/packages/components/src/box-control/index.js b/packages/components/src/box-control/index.tsx similarity index 78% rename from packages/components/src/box-control/index.js rename to packages/components/src/box-control/index.tsx index d8a0fdf4c67483..cf267aff44352e 100644 --- a/packages/components/src/box-control/index.js +++ b/packages/components/src/box-control/index.tsx @@ -29,6 +29,11 @@ import { isValuesDefined, } from './utils'; import { useControlledState } from '../utils/hooks'; +import type { + BoxControlIconProps, + BoxControlProps, + BoxControlValue, +} from './types'; const defaultInputProps = { min: 0, @@ -36,12 +41,38 @@ const defaultInputProps = { const noop = () => {}; -function useUniqueId( idProp ) { +function useUniqueId( idProp?: string ) { const instanceId = useInstanceId( BoxControl, 'inspector-box-control' ); return idProp || instanceId; } -export default function BoxControl( { + +/** + * BoxControl components let users set values for Top, Right, Bottom, and Left. + * This can be used as an input control for values like `padding` or `margin`. + * + * ```jsx + * import { __experimentalBoxControl as BoxControl } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const Example = () => { + * const [ values, setValues ] = useState( { + * top: '50px', + * left: '10%', + * right: '10%', + * bottom: '50px', + * } ); + * + * return ( + * setValues( nextValues ) } + * /> + * ); + * }; + * ``` + */ +function BoxControl( { id: idProp, inputProps = defaultInputProps, onChange = noop, @@ -54,7 +85,7 @@ export default function BoxControl( { resetValues = DEFAULT_VALUES, onMouseOver, onMouseOut, -} ) { +}: BoxControlProps ) { const [ values, setValues ] = useControlledState( valuesProp, { fallback: DEFAULT_VALUES, } ); @@ -67,14 +98,14 @@ export default function BoxControl( { ! hasInitialValue || ! isValuesMixed( inputValues ) || hasOneSide ); - const [ side, setSide ] = useState( + const [ side, setSide ] = useState< BoxControlIconProps[ 'side' ] >( getInitialSide( isLinked, splitOnAxis ) ); // Tracking selected units via internal state allows filtering of CSS unit // only values from being saved while maintaining preexisting unit selection // behaviour. Filtering CSS only values prevents invalid style values. - const [ selectedUnits, setSelectedUnits ] = useState( { + const [ selectedUnits, setSelectedUnits ] = useState< BoxControlValue >( { top: parseQuantityAndUnitFromRawValue( valuesProp?.top )[ 1 ], right: parseQuantityAndUnitFromRawValue( valuesProp?.right )[ 1 ], bottom: parseQuantityAndUnitFromRawValue( valuesProp?.bottom )[ 1 ], @@ -89,11 +120,14 @@ export default function BoxControl( { setSide( getInitialSide( ! isLinked, splitOnAxis ) ); }; - const handleOnFocus = ( event, { side: nextSide } ) => { + const handleOnFocus = ( + _event: React.FocusEvent< HTMLInputElement >, + { side: nextSide }: { side: typeof side } + ) => { setSide( nextSide ); }; - const handleOnChange = ( nextValues ) => { + const handleOnChange = ( nextValues: BoxControlValue ) => { onChange( nextValues ); setValues( nextValues ); setIsDirty( true ); @@ -132,7 +166,7 @@ export default function BoxControl( { + ) } { ( props ) => ( - - ) } - - - { ( props ) => ( - + ) } { ( props ) => ( - - ) } - - - { ( props ) => ( - + ) } { ( props ) => ( - + ) } { ( props ) => ( - - ) } - - - { ( props ) => ( - + ) } { ( props ) => ( - + ) } @@ -112,27 +79,24 @@ Aside from the documented callback functions, any props specified will be passed `TreeGrid` should always have children. -###### onFocusRow( event: Event, startRow: HTMLElement, destinationRow: HTMLElement ) +###### `onFocusRow`: `( event: KeyboardEvent, startRow: HTMLElement, destinationRow: HTMLElement ) => void` Callback that fires when focus is shifted from one row to another via the Up and Down keys. Callback is also fired on Home and End keys which move focus from the beginning row to the end row. The callback is passed the event, the start row element that the focus was on originally, and the destination row element after the focus has moved. -- Type: `Function` - Required: No -###### onCollapseRow( row: HTMLElement ) +###### `onCollapseRow`: `( row: HTMLElement ) => void` A callback that passes in the row element to be collapsed. -- Type: `Function` - Required: No -###### onExpandRow( row: HTMLElement ) +###### `onExpandRow`: `( row: HTMLElement ) => void` A callback that passes in the row element to be expanded. -- Type: `Function` - Required: No #### TreeGridRow @@ -141,32 +105,28 @@ A callback that passes in the row element to be expanded. Additional props other than those specified below will be passed to the `tr` element rendered by `TreeGridRow`, so for example, it is possible to also set a `className` on a row. -###### level +###### `level`: `number` An integer value designating the level in the hierarchical tree structure. Counting starts at 1. A value of `1` indicates the root level of the structure. -- Type: `Number` - Required: Yes -###### positionInSet +###### `positionInSet`: `number` An integer value that represents the position in the set. A set is the count of elements at a specific level. Counting starts at 1. -- Type: `Number` - Required: Yes -###### setSize +###### `setSize`: `number` -An integer value that represents the total number of items in the set ... that is the total number of items at this specific level of the hierarchy. +An integer value that represents the total number of items in the set, at this specific level of the hierarchy. -- Type: `Number` - Required: Yes -###### isExpanded +###### `isExpanded`: `boolean` An optional value that designates whether a row is expanded or collapsed. Currently this value only sets the correct aria-expanded property on a row, it has no other built-in behavior. -- Type: `Boolean` - Required: No ### TreeGridCell @@ -182,14 +142,14 @@ An optional value that designates whether a row is expanded or collapsed. Curren ```jsx { ( props ) => ( - ) } ``` -Props passed as an argument to the render prop must be passed to the child focusable component/element within the cell. If a component is used, it must correctly handle the `onFocus`, `tabIndex`, and `ref` props, passing these to the element it renders. These props are used to handle the roving tab index functionality of the tree grid. +Props passed as an argument to the render prop must be passed to the child focusable component/element within the cell. If a component is used, it must correctly handle the `onFocus`, `tabIndex`, and `ref` props, passing these to the element it renders. These props are used to handle the roving tabindex functionality of the tree grid. ## Related components diff --git a/packages/components/src/tree-grid/cell.js b/packages/components/src/tree-grid/cell.js deleted file mode 100644 index dd4e2654d64e5e..00000000000000 --- a/packages/components/src/tree-grid/cell.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * WordPress dependencies - */ -import { forwardRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import TreeGridItem from './item'; - -export default forwardRef( function TreeGridCell( - { children, withoutGridItem = false, ...props }, - ref -) { - return ( - - { withoutGridItem ? ( - children - ) : ( - { children } - ) } - - ); -} ); diff --git a/packages/components/src/tree-grid/cell.tsx b/packages/components/src/tree-grid/cell.tsx new file mode 100644 index 00000000000000..f33e8e8d9c6eb1 --- /dev/null +++ b/packages/components/src/tree-grid/cell.tsx @@ -0,0 +1,41 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import TreeGridItem from './item'; +import type { WordPressComponentProps } from '../ui/context'; +import type { TreeGridCellProps } from './types'; + +function UnforwardedTreeGridCell( + { + children, + withoutGridItem = false, + ...props + }: WordPressComponentProps< TreeGridCellProps, 'td', false >, + ref: React.ForwardedRef< any > +) { + return ( + + { withoutGridItem ? ( + <>{ children } + ) : ( + { children } + ) } + + ); +} + +/** + * `TreeGridCell` is used to create a tree hierarchy. + * It is not a visually styled component, but instead helps with adding + * keyboard navigation and roving tab index behaviors to tree grid structures. + * + * @see {@link https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html} + */ +export const TreeGridCell = forwardRef( UnforwardedTreeGridCell ); + +export default TreeGridCell; diff --git a/packages/components/src/tree-grid/index.js b/packages/components/src/tree-grid/index.tsx similarity index 69% rename from packages/components/src/tree-grid/index.js rename to packages/components/src/tree-grid/index.tsx index 81bc5cfe9fa535..7087bafc86f70b 100644 --- a/packages/components/src/tree-grid/index.js +++ b/packages/components/src/tree-grid/index.tsx @@ -9,24 +9,22 @@ import { UP, DOWN, LEFT, RIGHT, HOME, END } from '@wordpress/keycodes'; * Internal dependencies */ import RovingTabIndexContainer from './roving-tab-index'; +import type { TreeGridProps } from './types'; +import type { WordPressComponentProps } from '../ui/context'; /** * Return focusables in a row element, excluding those from other branches * nested within the row. * - * @param {Element} rowElement The DOM element representing the row. + * @param rowElement The DOM element representing the row. * - * @return {Array | undefined} The array of focusables in the row. + * @return The array of focusables in the row. */ -function getRowFocusables( rowElement ) { +function getRowFocusables( rowElement: HTMLElement ) { const focusablesInRow = focus.focusable.find( rowElement, { sequential: true, } ); - if ( ! focusablesInRow || ! focusablesInRow.length ) { - return; - } - return focusablesInRow.filter( ( focusable ) => { return focusable.closest( '[role="row"]' ) === rowElement; } ); @@ -35,16 +33,8 @@ function getRowFocusables( rowElement ) { /** * Renders both a table and tbody element, used to create a tree hierarchy. * - * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/components/src/tree-grid/README.md - * @param {Object} props Component props. - * @param {WPElement} props.children Children to be rendered. - * @param {Function} props.onExpandRow Callback to fire when row is expanded. - * @param {Function} props.onCollapseRow Callback to fire when row is collapsed. - * @param {Function} props.onFocusRow Callback to fire when moving focus to a different row. - * @param {string} props.applicationAriaLabel Label to use for the application role. - * @param {Object} ref A ref to the underlying DOM table element. */ -function TreeGrid( +function UnforwardedTreeGrid( { children, onExpandRow = () => {}, @@ -52,11 +42,12 @@ function TreeGrid( onFocusRow = () => {}, applicationAriaLabel, ...props - }, - ref + }: WordPressComponentProps< TreeGridProps, 'table', false >, + /** A ref to the underlying DOM table element. */ + ref: React.ForwardedRef< HTMLTableElement > ) { const onKeyDown = useCallback( - ( event ) => { + ( event: React.KeyboardEvent< HTMLTableElement > ) => { const { keyCode, metaKey, ctrlKey, altKey } = event; // The shift key is intentionally absent from the following list, @@ -65,7 +56,9 @@ function TreeGrid( if ( hasModifierKeyPressed || - ! [ UP, DOWN, LEFT, RIGHT, HOME, END ].includes( keyCode ) + ! ( [ UP, DOWN, LEFT, RIGHT, HOME, END ] as number[] ).includes( + keyCode + ) ) { return; } @@ -75,21 +68,33 @@ function TreeGrid( const { activeElement } = document; const { currentTarget: treeGridElement } = event; - if ( ! treeGridElement.contains( activeElement ) ) { + + if ( + ! activeElement || + ! treeGridElement.contains( activeElement ) + ) { return; } // Calculate the columnIndex of the active element. - const activeRow = activeElement.closest( '[role="row"]' ); + const activeRow = + activeElement.closest< HTMLElement >( '[role="row"]' ); + + if ( ! activeRow ) { + return; + } + const focusablesInRow = getRowFocusables( activeRow ); - const currentColumnIndex = focusablesInRow.indexOf( activeElement ); + const currentColumnIndex = focusablesInRow.indexOf( + activeElement as HTMLElement + ); const canExpandCollapse = 0 === currentColumnIndex; const cannotFocusNextColumn = canExpandCollapse && activeRow.getAttribute( 'aria-expanded' ) === 'false' && keyCode === RIGHT; - if ( [ LEFT, RIGHT ].includes( keyCode ) ) { + if ( ( [ LEFT, RIGHT ] as number[] ).includes( keyCode ) ) { // Calculate to the next element. let nextIndex; if ( keyCode === LEFT ) { @@ -116,22 +121,25 @@ function TreeGrid( // If a row is focused, and it is collapsed, moves to the parent row (if there is one). const level = Math.max( parseInt( - activeRow?.getAttribute( 'aria-level' ) ?? 1, + activeRow?.getAttribute( 'aria-level' ) ?? '1', 10 ) - 1, 1 ); const rows = Array.from( - treeGridElement.querySelectorAll( '[role="row"]' ) + treeGridElement.querySelectorAll< HTMLElement >( + '[role="row"]' + ) ); let parentRow = activeRow; const currentRowIndex = rows.indexOf( activeRow ); for ( let i = currentRowIndex; i >= 0; i-- ) { + const ariaLevel = + rows[ i ].getAttribute( 'aria-level' ); + if ( - parseInt( - rows[ i ].getAttribute( 'aria-level' ), - 10 - ) === level + ariaLevel !== null && + parseInt( ariaLevel, 10 ) === level ) { parentRow = rows[ i ]; break; @@ -172,10 +180,12 @@ function TreeGrid( // Prevent key use for anything else. This ensures Voiceover // doesn't try to handle key navigation. event.preventDefault(); - } else if ( [ UP, DOWN ].includes( keyCode ) ) { + } else if ( ( [ UP, DOWN ] as number[] ).includes( keyCode ) ) { // Calculate the rowIndex of the next row. const rows = Array.from( - treeGridElement.querySelectorAll( '[role="row"]' ) + treeGridElement.querySelectorAll< HTMLElement >( + '[role="row"]' + ) ); const currentRowIndex = rows.indexOf( activeRow ); let nextRowIndex; @@ -226,10 +236,12 @@ function TreeGrid( // Prevent key use for anything else. This ensures Voiceover // doesn't try to handle key navigation. event.preventDefault(); - } else if ( [ HOME, END ].includes( keyCode ) ) { + } else if ( ( [ HOME, END ] as number[] ).includes( keyCode ) ) { // Calculate the rowIndex of the next row. const rows = Array.from( - treeGridElement.querySelectorAll( '[role="row"]' ) + treeGridElement.querySelectorAll< HTMLElement >( + '[role="row"]' + ) ); const currentRowIndex = rows.indexOf( activeRow ); let nextRowIndex; @@ -306,7 +318,71 @@ function TreeGrid( /* eslint-enable jsx-a11y/no-noninteractive-element-to-interactive-role */ } -export default forwardRef( TreeGrid ); +/** + * `TreeGrid` is used to create a tree hierarchy. + * It is not a visually styled component, but instead helps with adding + * keyboard navigation and roving tab index behaviors to tree grid structures. + * + * A tree grid is a hierarchical 2 dimensional UI component, for example it could be + * used to implement a file system browser. + * + * A tree grid allows the user to navigate using arrow keys. + * Up/down to navigate vertically across rows, and left/right to navigate horizontally + * between focusables in a row. + * + * The `TreeGrid` renders both a `table` and `tbody` element, and is intended to be used + * with `TreeGridRow` (`tr`) and `TreeGridCell` (`td`) to build out a grid. + * + * ```jsx + * function TreeMenu() { + * return ( + * + * + * + * { ( props ) => ( + * + * ) } + * + * + * { ( props ) => ( + * + * ) } + * + * + * + * + * { ( props ) => ( + * + * ) } + * + * + * { ( props ) => ( + * + * ) } + * + * + * + * + * { ( props ) => ( + * + * ) } + * + * + * { ( props ) => ( + * + * ) } + * + * + * + * ); + * } + * ``` + * + * @see {@link https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html} + */ +export const TreeGrid = forwardRef( UnforwardedTreeGrid ); + +export default TreeGrid; export { default as TreeGridRow } from './row'; export { default as TreeGridCell } from './cell'; export { default as TreeGridItem } from './item'; diff --git a/packages/components/src/tree-grid/item.js b/packages/components/src/tree-grid/item.js deleted file mode 100644 index 1d79b5640731b3..00000000000000 --- a/packages/components/src/tree-grid/item.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { forwardRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import RovingTabIndexItem from './roving-tab-index-item'; - -export default forwardRef( function TreeGridItem( - { children, ...props }, - ref -) { - return ( - - { children } - - ); -} ); diff --git a/packages/components/src/tree-grid/item.tsx b/packages/components/src/tree-grid/item.tsx new file mode 100644 index 00000000000000..7e32fb346248fa --- /dev/null +++ b/packages/components/src/tree-grid/item.tsx @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import RovingTabIndexItem from './roving-tab-index-item'; +import type { RovingTabIndexItemProps } from './types'; + +function UnforwardedTreeGridItem( + { children, ...props }: RovingTabIndexItemProps, + ref: React.ForwardedRef< any > +) { + return ( + + { children } + + ); +} + +/** + * `TreeGridItem` is used to create a tree hierarchy. + * It is not a visually styled component, but instead helps with adding + * keyboard navigation and roving tab index behaviors to tree grid structures. + * + * @see {@link https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html} + */ +export const TreeGridItem = forwardRef( UnforwardedTreeGridItem ); + +export default TreeGridItem; diff --git a/packages/components/src/tree-grid/roving-tab-index-context.js b/packages/components/src/tree-grid/roving-tab-index-context.ts similarity index 52% rename from packages/components/src/tree-grid/roving-tab-index-context.js rename to packages/components/src/tree-grid/roving-tab-index-context.ts index 90c3ca16704d74..a0ff4ae486df45 100644 --- a/packages/components/src/tree-grid/roving-tab-index-context.js +++ b/packages/components/src/tree-grid/roving-tab-index-context.ts @@ -3,7 +3,15 @@ */ import { createContext, useContext } from '@wordpress/element'; -const RovingTabIndexContext = createContext(); +const RovingTabIndexContext = createContext< + | { + lastFocusedElement: HTMLElement | undefined; + setLastFocusedElement: React.Dispatch< + React.SetStateAction< HTMLElement | undefined > + >; + } + | undefined +>( undefined ); export const useRovingTabIndexContext = () => useContext( RovingTabIndexContext ); export const RovingTabIndexProvider = RovingTabIndexContext.Provider; diff --git a/packages/components/src/tree-grid/roving-tab-index-item.js b/packages/components/src/tree-grid/roving-tab-index-item.js deleted file mode 100644 index 1ac76950d038d5..00000000000000 --- a/packages/components/src/tree-grid/roving-tab-index-item.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * WordPress dependencies - */ -import { useRef, forwardRef } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { useRovingTabIndexContext } from './roving-tab-index-context'; - -export default forwardRef( function RovingTabIndexItem( - { children, as: Component, ...props }, - forwardedRef -) { - const localRef = useRef(); - const ref = forwardedRef || localRef; - const { lastFocusedElement, setLastFocusedElement } = - useRovingTabIndexContext(); - let tabIndex; - - if ( lastFocusedElement ) { - tabIndex = lastFocusedElement === ref.current ? 0 : -1; - } - - const onFocus = ( event ) => setLastFocusedElement( event.target ); - const allProps = { ref, tabIndex, onFocus, ...props }; - - if ( typeof children === 'function' ) { - return children( allProps ); - } - - return { children }; -} ); diff --git a/packages/components/src/tree-grid/roving-tab-index-item.tsx b/packages/components/src/tree-grid/roving-tab-index-item.tsx new file mode 100644 index 00000000000000..6bcea5862bf160 --- /dev/null +++ b/packages/components/src/tree-grid/roving-tab-index-item.tsx @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { useRef, forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useRovingTabIndexContext } from './roving-tab-index-context'; +import type { RovingTabIndexItemProps } from './types'; + +export const RovingTabIndexItem = forwardRef( + function UnforwardedRovingTabIndexItem( + { children, as: Component, ...props }: RovingTabIndexItemProps, + forwardedRef: React.ForwardedRef< any > + ) { + const localRef = useRef< any >(); + const ref = forwardedRef || localRef; + // @ts-expect-error - We actually want to throw an error if this is undefined. + const { lastFocusedElement, setLastFocusedElement } = + useRovingTabIndexContext(); + let tabIndex; + + if ( lastFocusedElement ) { + tabIndex = + lastFocusedElement === + // TODO: The original implementation simply used `ref.current` here, assuming + // that a forwarded ref would always be an object, which is not necessarily true. + // This workaround maintains the original runtime behavior in a type-safe way, + // but should be revisited. + ( 'current' in ref ? ref.current : undefined ) + ? 0 + : -1; + } + + const onFocus: React.FocusEventHandler< HTMLElement > = ( event ) => + setLastFocusedElement?.( event.target ); + const allProps = { ref, tabIndex, onFocus, ...props }; + + if ( typeof children === 'function' ) { + return children( allProps ); + } + + if ( ! Component ) return null; + + return { children }; + } +); + +export default RovingTabIndexItem; diff --git a/packages/components/src/tree-grid/roving-tab-index.js b/packages/components/src/tree-grid/roving-tab-index.tsx similarity index 76% rename from packages/components/src/tree-grid/roving-tab-index.js rename to packages/components/src/tree-grid/roving-tab-index.tsx index b390c70f291d4d..6d6d989fbc3ffa 100644 --- a/packages/components/src/tree-grid/roving-tab-index.js +++ b/packages/components/src/tree-grid/roving-tab-index.tsx @@ -12,12 +12,14 @@ import { RovingTabIndexProvider } from './roving-tab-index-context'; * Provider for adding roving tab index behaviors to tree grid structures. * * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/components/src/tree-grid/README.md - * - * @param {Object} props Component props. - * @param {WPElement} props.children Children to be rendered */ -export default function RovingTabIndex( { children } ) { - const [ lastFocusedElement, setLastFocusedElement ] = useState(); +export default function RovingTabIndex( { + children, +}: { + children: React.ReactNode; +} ) { + const [ lastFocusedElement, setLastFocusedElement ] = + useState< HTMLElement >(); // Use `useMemo` to avoid creation of a new object for the providerValue // on every render. Only create a new object when the `lastFocusedElement` diff --git a/packages/components/src/tree-grid/row.js b/packages/components/src/tree-grid/row.js deleted file mode 100644 index 744055194ad226..00000000000000 --- a/packages/components/src/tree-grid/row.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * WordPress dependencies - */ -import { forwardRef } from '@wordpress/element'; - -function TreeGridRow( - { children, level, positionInSet, setSize, isExpanded, ...props }, - ref -) { - return ( - // Disable reason: Due to an error in the ARIA 1.1 specification, the - // aria-posinset and aria-setsize properties are not supported on row - // elements. This is being corrected in ARIA 1.2. Consequently, the - // linting rule fails when validating this markup. - // - // eslint-disable-next-line jsx-a11y/role-supports-aria-props - - { children } - - ); -} - -export default forwardRef( TreeGridRow ); diff --git a/packages/components/src/tree-grid/row.tsx b/packages/components/src/tree-grid/row.tsx new file mode 100644 index 00000000000000..a4b69cd46e1797 --- /dev/null +++ b/packages/components/src/tree-grid/row.tsx @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../ui/context'; +import type { TreeGridRowProps } from './types'; + +function UnforwardedTreeGridRow( + { + children, + level, + positionInSet, + setSize, + isExpanded, + ...props + }: WordPressComponentProps< TreeGridRowProps, 'tr', false >, + ref: React.ForwardedRef< HTMLTableRowElement > +) { + return ( + + { children } + + ); +} + +/** + * `TreeGridRow` is used to create a tree hierarchy. + * It is not a visually styled component, but instead helps with adding + * keyboard navigation and roving tab index behaviors to tree grid structures. + * + * @see {@link https://www.w3.org/TR/wai-aria-practices/examples/treegrid/treegrid-1.html} + */ +export const TreeGridRow = forwardRef( UnforwardedTreeGridRow ); + +export default TreeGridRow; diff --git a/packages/components/src/tree-grid/stories/index.js b/packages/components/src/tree-grid/stories/index.js deleted file mode 100644 index e1a75448f39791..00000000000000 --- a/packages/components/src/tree-grid/stories/index.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * WordPress dependencies - */ -import { Fragment } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import TreeGrid, { TreeGridRow, TreeGridCell } from '../'; -import { Button } from '../../'; - -export default { - title: 'Components (Experimental)/TreeGrid', - component: TreeGrid, -}; - -const groceries = [ - { - name: 'Fruit', - types: [ - { - name: 'Apple', - }, - { - name: 'Orange', - }, - { - name: 'Pear', - }, - ], - }, - { - name: 'Vegetable', - types: [ - { - name: 'Cucumber', - }, - { - name: 'Parsnip', - }, - { - name: 'Pumpkin', - }, - ], - }, -]; - -const Descender = ( { level } ) => { - if ( level === 1 ) { - return ''; - } - const indentation = '\u00A0'.repeat( ( level - 1 ) * 4 ); - - return ; -}; - -const Rows = ( { items, level = 1 } ) => { - return items.map( ( item, index ) => { - const hasChildren = !! item.types && !! item.types.length; - return ( - - - - { ( props ) => ( - <> - - - - ) } - - - { ( props ) => ( - - ) } - - - { ( props ) => ( - - ) } - - - { hasChildren && ( - - ) } - - ); - } ); -}; - -export const _default = () => { - return ( - - - - ); -}; diff --git a/packages/components/src/tree-grid/stories/index.tsx b/packages/components/src/tree-grid/stories/index.tsx new file mode 100644 index 00000000000000..6ae39be6e7caf0 --- /dev/null +++ b/packages/components/src/tree-grid/stories/index.tsx @@ -0,0 +1,144 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { Fragment } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import TreeGrid, { TreeGridRow, TreeGridCell } from '..'; +import { Button } from '../../button'; +import InputControl from '../../input-control'; + +const meta: ComponentMeta< typeof TreeGrid > = { + title: 'Components (Experimental)/TreeGrid', + component: TreeGrid, + subcomponents: { TreeGridRow, TreeGridCell }, + argTypes: { + children: { control: { type: null } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + }, +}; +export default meta; + +const groceries = [ + { + name: 'Fruit', + types: [ + { + name: 'Apple', + }, + { + name: 'Orange', + }, + { + name: 'Pear', + }, + ], + }, + { + name: 'Vegetable', + types: [ + { + name: 'Cucumber', + }, + { + name: 'Parsnip', + }, + { + name: 'Pumpkin', + }, + ], + }, +]; + +const Descender = ( { level }: { level: number } ) => { + if ( level === 1 ) { + return null; + } + const indentation = '\u00A0'.repeat( ( level - 1 ) * 4 ); + + return ; +}; + +type Item = { + name: string; + types?: Item[]; +}; + +const Rows = ( { + items = [], + level = 1, +}: { + items?: Item[]; + level?: number; +} ) => { + return ( + <> + { items.map( ( item, index ) => { + const hasChildren = !! item.types && !! item.types.length; + return ( + + + + { ( props ) => ( + <> + + + + ) } + + + { ( props ) => ( + + ) } + + + { ( props ) => ( + + ) } + + + { hasChildren && ( + + ) } + + ); + } ) } + + ); +}; + +const Template: ComponentStory< typeof TreeGrid > = ( args ) => ( + +); + +export const Default = Template.bind( {} ); +Default.args = { + children: , +}; diff --git a/packages/components/src/tree-grid/test/__snapshots__/cell.js.snap b/packages/components/src/tree-grid/test/__snapshots__/cell.tsx.snap similarity index 100% rename from packages/components/src/tree-grid/test/__snapshots__/cell.js.snap rename to packages/components/src/tree-grid/test/__snapshots__/cell.tsx.snap diff --git a/packages/components/src/tree-grid/test/__snapshots__/index.js.snap b/packages/components/src/tree-grid/test/__snapshots__/index.tsx.snap similarity index 100% rename from packages/components/src/tree-grid/test/__snapshots__/index.js.snap rename to packages/components/src/tree-grid/test/__snapshots__/index.tsx.snap diff --git a/packages/components/src/tree-grid/test/__snapshots__/roving-tab-index-item.js.snap b/packages/components/src/tree-grid/test/__snapshots__/roving-tab-index-item.tsx.snap similarity index 100% rename from packages/components/src/tree-grid/test/__snapshots__/roving-tab-index-item.js.snap rename to packages/components/src/tree-grid/test/__snapshots__/roving-tab-index-item.tsx.snap diff --git a/packages/components/src/tree-grid/test/__snapshots__/roving-tab-index.js.snap b/packages/components/src/tree-grid/test/__snapshots__/roving-tab-index.tsx.snap similarity index 100% rename from packages/components/src/tree-grid/test/__snapshots__/roving-tab-index.js.snap rename to packages/components/src/tree-grid/test/__snapshots__/roving-tab-index.tsx.snap diff --git a/packages/components/src/tree-grid/test/__snapshots__/row.js.snap b/packages/components/src/tree-grid/test/__snapshots__/row.tsx.snap similarity index 100% rename from packages/components/src/tree-grid/test/__snapshots__/row.js.snap rename to packages/components/src/tree-grid/test/__snapshots__/row.tsx.snap diff --git a/packages/components/src/tree-grid/test/cell.js b/packages/components/src/tree-grid/test/cell.tsx similarity index 81% rename from packages/components/src/tree-grid/test/cell.js rename to packages/components/src/tree-grid/test/cell.tsx index 7e62f751512d24..e0791d6206dc7b 100644 --- a/packages/components/src/tree-grid/test/cell.js +++ b/packages/components/src/tree-grid/test/cell.tsx @@ -11,12 +11,15 @@ import { forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import TreeGrid from '../'; +import TreeGrid from '..'; import TreeGridCell from '../cell'; -const TestButton = forwardRef( ( { ...props }, ref ) => ( - -) ); +const TestButton = forwardRef( + ( + { ...props }: React.ComponentPropsWithoutRef< 'button' >, + ref: React.ForwardedRef< HTMLButtonElement > + ) => +); describe( 'TreeGridCell', () => { it( 'requires TreeGrid to be declared as a parent component somewhere in the component hierarchy', () => { diff --git a/packages/components/src/tree-grid/test/index.js b/packages/components/src/tree-grid/test/index.tsx similarity index 95% rename from packages/components/src/tree-grid/test/index.js rename to packages/components/src/tree-grid/test/index.tsx index 679000b91204bc..74294be24f8b5e 100644 --- a/packages/components/src/tree-grid/test/index.js +++ b/packages/components/src/tree-grid/test/index.tsx @@ -12,13 +12,16 @@ import { forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import TreeGrid from '../'; +import TreeGrid from '..'; import TreeGridRow from '../row'; import TreeGridCell from '../cell'; -const TestButton = forwardRef( ( { ...props }, ref ) => ( - -) ); +const TestButton = forwardRef( + ( + { ...props }: React.ComponentPropsWithoutRef< 'button' >, + ref: React.ForwardedRef< HTMLButtonElement > + ) => +); describe( 'TreeGrid', () => { const originalGetClientRects = window.Element.prototype.getClientRects; @@ -35,6 +38,7 @@ describe( 'TreeGrid', () => { ] ); beforeAll( () => { + // @ts-expect-error - This is just a mock window.Element.prototype.getClientRects = jest.fn( mockedGetClientRects ); } ); @@ -162,7 +166,11 @@ describe( 'TreeGrid', () => { } ); describe( 'onFocusRow', () => { - const TestTree = ( { onFocusRow } ) => ( + const TestTree = ( { + onFocusRow, + }: { + onFocusRow: React.ComponentProps< typeof TreeGrid >[ 'onFocusRow' ]; + } ) => ( diff --git a/packages/components/src/tree-grid/test/roving-tab-index-item.js b/packages/components/src/tree-grid/test/roving-tab-index-item.tsx similarity index 85% rename from packages/components/src/tree-grid/test/roving-tab-index-item.js rename to packages/components/src/tree-grid/test/roving-tab-index-item.tsx index 0f2c6bd58c7c60..fe426d1bb5c535 100644 --- a/packages/components/src/tree-grid/test/roving-tab-index-item.js +++ b/packages/components/src/tree-grid/test/roving-tab-index-item.tsx @@ -14,9 +14,12 @@ import { forwardRef } from '@wordpress/element'; import RovingTabIndex from '../roving-tab-index'; import RovingTabIndexItem from '../roving-tab-index-item'; -const TestButton = forwardRef( ( { ...props }, ref ) => ( - -) ); +const TestButton = forwardRef( + ( + { ...props }: React.ComponentPropsWithoutRef< 'button' >, + ref: React.ForwardedRef< HTMLButtonElement > + ) => +); describe( 'RovingTabIndexItem', () => { it( 'requires RovingTabIndex to be declared as a parent component somewhere in the component hierarchy', () => { @@ -52,7 +55,7 @@ describe( 'RovingTabIndexItem', () => { const { container } = render( - { ( props ) => ( + { ( props: React.ComponentProps< typeof TestButton > ) => ( Click Me! diff --git a/packages/components/src/tree-grid/test/roving-tab-index.js b/packages/components/src/tree-grid/test/roving-tab-index.tsx similarity index 100% rename from packages/components/src/tree-grid/test/roving-tab-index.js rename to packages/components/src/tree-grid/test/roving-tab-index.tsx diff --git a/packages/components/src/tree-grid/test/row.js b/packages/components/src/tree-grid/test/row.tsx similarity index 85% rename from packages/components/src/tree-grid/test/row.js rename to packages/components/src/tree-grid/test/row.tsx index 77b092c0735224..d0cdf2c9b7aeeb 100644 --- a/packages/components/src/tree-grid/test/row.js +++ b/packages/components/src/tree-grid/test/row.tsx @@ -13,7 +13,7 @@ describe( 'TreeGridRow', () => { const { container } = render( - + @@ -29,9 +29,9 @@ describe( 'TreeGridRow', () => { diff --git a/packages/components/src/tree-grid/types.ts b/packages/components/src/tree-grid/types.ts new file mode 100644 index 00000000000000..47c2739f43dc7c --- /dev/null +++ b/packages/components/src/tree-grid/types.ts @@ -0,0 +1,116 @@ +export type TreeGridRowProps = { + /** + * The children to be rendered in the row. + */ + children: React.ReactNode; + /** + * An integer value designating the level in the hierarchical tree structure. + * Counting starts at 1. A value of `1` indicates the root level of the structure. + */ + level: NonNullable< React.AriaAttributes[ 'aria-level' ] >; + /** + * An integer value that represents the position in the set. + * A set is the count of elements at a specific level. Counting starts at 1. + */ + positionInSet: NonNullable< React.AriaAttributes[ 'aria-posinset' ] >; + /** + * An integer value that represents the total number of items in the set, + * at this specific level of the hierarchy. + */ + setSize: NonNullable< React.AriaAttributes[ 'aria-setsize' ] >; + /** + * An optional value that designates whether a row is expanded or collapsed. + * Currently this value only sets the correct aria-expanded property on a row, + * it has no other built-in behavior. + */ + isExpanded?: boolean; +}; + +type RovingTabIndexItemPassThruProps = { + ref: React.ForwardedRef< any >; + tabIndex?: number; + onFocus: React.FocusEventHandler< any >; + [ key: string ]: any; +}; + +export type RovingTabIndexItemProps = { + /** + * A render function that receives the props necessary to make it participate in the + * roving tabindex. Any extra props will also be passed through to this function. + * + * Props passed as an argument to the render prop must be passed to the child + * focusable component/element within the cell. If a component is used, it must + * correctly handle the `onFocus`, `tabIndex`, and `ref` props, passing these to the + * element it renders. These props are used to handle the roving tabindex functionality + * of the tree grid. + * + * ```jsx + * + * { ( props ) => ( + * + * ) } + * + * ``` + */ + children?: ( props: RovingTabIndexItemPassThruProps ) => JSX.Element; + /** + * If `children` is not a function, this component will be used instead. + */ + as?: React.ComponentType< RovingTabIndexItemPassThruProps >; + [ key: string ]: any; +}; + +export type TreeGridCellProps = + | ( { + /** + * Render `children` without wrapping it in a `TreeGridItem` component. + * This means that `children` will not participate in the roving tabindex. + * + * @default false + */ + withoutGridItem?: false; + } & NonNullable< Pick< RovingTabIndexItemProps, 'children' > > ) + | { + children: React.ReactNode; + withoutGridItem: true; + }; + +export type TreeGridProps = { + /** + * Label to use for the element with the `application` role. + */ + applicationAriaLabel?: string; + /** + * The children to be rendered in the tree grid. + */ + children: React.ReactNode; + /** + * Callback to fire when row is expanded. + * + * @default noop + */ + onExpandRow?: ( row: HTMLElement ) => void; + /** + * Callback to fire when row is collapsed. + * + * @default noop + */ + onCollapseRow?: ( row: HTMLElement ) => void; + /** + * Callback that fires when focus is shifted from one row to another via + * the Up and Down keys. Callback is also fired on Home and End keys which + * move focus from the beginning row to the end row. + * + * The callback is passed the event, the start row element that the focus was on + * originally, and the destination row element after the focus has moved. + * + * @default noop + */ + onFocusRow?: ( + event: React.KeyboardEvent< HTMLTableElement >, + startRow: HTMLElement, + destinationRow: HTMLElement + ) => void; +}; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index d9828676beb10e..131dd444a7f725 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -61,7 +61,6 @@ "src/navigation", "src/palette-edit", "src/panel/body.js", - "src/toolbar", - "src/tree-grid" + "src/toolbar" ] } diff --git a/packages/dom/src/focusable.js b/packages/dom/src/focusable.js index 75e4f72f0c1f77..5050952879f60f 100644 --- a/packages/dom/src/focusable.js +++ b/packages/dom/src/focusable.js @@ -95,7 +95,7 @@ function isValidFocusableArea( element ) { * not sequentially focusable. * https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute * - * @return {Element[]} Focusable elements. + * @return {HTMLElement[]} Focusable elements. */ export function find( context, { sequential = false } = {} ) { /* eslint-disable jsdoc/no-undefined-types */ From f1f3b963b31681280da93c6e2df4087b0232090a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 3 Feb 2023 12:50:54 +0000 Subject: [PATCH 23/48] Add link control code owner (#47733) --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa19138a958f1d..cc39bde3d2d93f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -44,6 +44,7 @@ /packages/interface /packages/media-utils /packages/server-side-render +/packages/block-editor/src/components/link-control @getdave # Widgets /packages/edit-widgets @draganescu @talldan @noisysocks @tellthemachines @adamziel @kevin940726 From 4051e8635034790a304f4701c17ac3b19c6dadf7 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 3 Feb 2023 15:55:18 +0200 Subject: [PATCH 24/48] Components: Remove unnecessary `act()` from `ToolsPanel` tests (#47691) * Components: Remove unnecessary act() from ToolsPanel tests * Fix type annotation Co-authored-by: Lena Morita --------- Co-authored-by: Lena Morita --- packages/components/src/tools-panel/test/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/components/src/tools-panel/test/index.js b/packages/components/src/tools-panel/test/index.js index 01879497da4bd8..c6b9c88a14b3ec 100644 --- a/packages/components/src/tools-panel/test/index.js +++ b/packages/components/src/tools-panel/test/index.js @@ -1,8 +1,8 @@ /** * External dependencies */ -import { render, screen, fireEvent, within } from '@testing-library/react'; -import { act } from 'react-test-renderer'; +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -169,19 +169,19 @@ const getMenuButton = () => { /** * Helper to find the menu button and simulate a user click. * - * @return {HTMLElement} The menuButton. */ const openDropdownMenu = async () => { + const user = userEvent.setup(); const menuButton = getMenuButton(); - fireEvent.click( menuButton ); - await act( () => Promise.resolve() ); + await user.click( menuButton ); return menuButton; }; // Opens dropdown then selects the menu item by label before simulating a click. const selectMenuItem = async ( label ) => { + const user = userEvent.setup(); const menuItem = await screen.findByText( label ); - fireEvent.click( menuItem ); + await user.click( menuItem ); }; describe( 'ToolsPanel', () => { From 12fcb43c3116cd368e208cc8fe7b0fbd9f12907c Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Fri, 3 Feb 2023 17:57:19 +0400 Subject: [PATCH 25/48] Site Logo: Simplify method for getting block editor settings (#47736) --- packages/block-library/src/site-logo/edit.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index a8ebde6c9c26db..07b44fd877844c 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -78,18 +78,15 @@ const SiteLogo = ( { 'is-transient': isBlobURL( logoUrl ), } ); const { imageEditing, maxWidth, title } = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); + const settings = select( blockEditorStore ).getSettings(); const siteEntities = select( coreStore ).getEntityRecord( 'root', '__unstableBase' ); return { title: siteEntities?.name, - ...Object.fromEntries( - Object.entries( getSettings() ).filter( ( [ key ] ) => - [ 'imageEditing', 'maxWidth' ].includes( key ) - ) - ), + imageEditing: settings.imageEditing, + maxWidth: settings.maxWidth, }; }, [] ); From 09353514e73e61f4abdf80216320d4ff0bca8a9e Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Fri, 3 Feb 2023 16:22:13 +0200 Subject: [PATCH 26/48] Lodash: Remove from `@wordpress/keycodes` package (#47737) * Lodash: Refactor away from _.get() in @wordpress/keycodes * Lodash: Refactor away from _.mapValues() in @wordpress/keycodes * Lodash: Remove dependency from @wordpress/keycodes --- package-lock.json | 3 +- packages/keycodes/package.json | 3 +- packages/keycodes/src/index.js | 269 +++++++++++++++++++-------------- 3 files changed, 156 insertions(+), 119 deletions(-) diff --git a/package-lock.json b/package-lock.json index ac2f9a01ac06b5..084850e16bce3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18163,8 +18163,7 @@ "requires": { "@babel/runtime": "^7.16.0", "@wordpress/i18n": "file:packages/i18n", - "change-case": "^4.1.2", - "lodash": "^4.17.21" + "change-case": "^4.1.2" } }, "@wordpress/lazy-import": { diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 51672821ad93c7..3a71c79c5955a3 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -29,8 +29,7 @@ "dependencies": { "@babel/runtime": "^7.16.0", "@wordpress/i18n": "file:../i18n", - "change-case": "^4.1.2", - "lodash": "^4.17.21" + "change-case": "^4.1.2" }, "publishConfig": { "access": "public" diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js index fe21171c0c7a96..2aa1cb70fa3f3c 100644 --- a/packages/keycodes/src/index.js +++ b/packages/keycodes/src/index.js @@ -13,7 +13,6 @@ * External dependencies */ import { capitalCase } from 'change-case'; -import { get, mapValues } from 'lodash'; /** * WordPress dependencies @@ -45,6 +44,8 @@ import { isAppleOS } from './platform'; */ /** @typedef {(event: KeyboardEvent, character: string, isApple?: () => boolean) => boolean} WPEventKeyHandler */ +/** @typedef {( isApple: () => boolean ) => WPModifierPart[]} WPModifier */ + /** * Keycode for BACKSPACE key. */ @@ -147,6 +148,25 @@ export const ZERO = 48; export { isAppleOS }; +/** + * Map the values of an object with a specified callback and return the result object. + * + * @template T + * + * @param {T} object Object to map values of. + * @param {( value: any ) => any} mapFn Mapping function + * + * @return {any} Active modifier constants. + */ +function mapValues( object, mapFn ) { + return Object.fromEntries( + Object.entries( object ).map( ( [ key, value ] ) => [ + key, + mapFn( value ), + ] ) + ); +} + /** * Object that contains functions that return the available modifier * depending on platform. @@ -185,14 +205,19 @@ export const modifiers = { * @type {WPModifierHandler>} Keyed map of functions to raw * shortcuts. */ -export const rawShortcut = mapValues( modifiers, ( modifier ) => { - return /** @type {WPKeyHandler} */ ( - character, - _isApple = isAppleOS - ) => { - return [ ...modifier( _isApple ), character.toLowerCase() ].join( '+' ); - }; -} ); +export const rawShortcut = mapValues( + modifiers, + ( /** @type {WPModifier} */ modifier ) => { + return /** @type {WPKeyHandler} */ ( + character, + _isApple = isAppleOS + ) => { + return [ ...modifier( _isApple ), character.toLowerCase() ].join( + '+' + ); + }; + } +); /** * Return an array of the parts of a keyboard shortcut chord for display. @@ -207,42 +232,45 @@ export const rawShortcut = mapValues( modifiers, ( modifier ) => { * @type {WPModifierHandler>} Keyed map of functions to * shortcut sequences. */ -export const displayShortcutList = mapValues( modifiers, ( modifier ) => { - return /** @type {WPKeyHandler} */ ( - character, - _isApple = isAppleOS - ) => { - const isApple = _isApple(); - const replacementKeyMap = { - [ ALT ]: isApple ? '⌥' : 'Alt', - [ CTRL ]: isApple ? '⌃' : 'Ctrl', // Make sure ⌃ is the U+2303 UP ARROWHEAD unicode character and not the caret character. - [ COMMAND ]: '⌘', - [ SHIFT ]: isApple ? '⇧' : 'Shift', +export const displayShortcutList = mapValues( + modifiers, + ( /** @type {WPModifier} */ modifier ) => { + return /** @type {WPKeyHandler} */ ( + character, + _isApple = isAppleOS + ) => { + const isApple = _isApple(); + const replacementKeyMap = { + [ ALT ]: isApple ? '⌥' : 'Alt', + [ CTRL ]: isApple ? '⌃' : 'Ctrl', // Make sure ⌃ is the U+2303 UP ARROWHEAD unicode character and not the caret character. + [ COMMAND ]: '⌘', + [ SHIFT ]: isApple ? '⇧' : 'Shift', + }; + + const modifierKeys = modifier( _isApple ).reduce( + ( accumulator, key ) => { + const replacementKey = replacementKeyMap[ key ] ?? key; + // If on the Mac, adhere to platform convention and don't show plus between keys. + if ( isApple ) { + return [ ...accumulator, replacementKey ]; + } + + return [ ...accumulator, replacementKey, '+' ]; + }, + /** @type {string[]} */ ( [] ) + ); + + // Symbols (~`,.) are removed by the default regular expression, + // so override the rule to allow symbols used for shortcuts. + // see: https://github.com/blakeembrey/change-case#options + const capitalizedCharacter = capitalCase( character, { + stripRegexp: /[^A-Z0-9~`,\.\\\-]/gi, + } ); + + return [ ...modifierKeys, capitalizedCharacter ]; }; - - const modifierKeys = modifier( _isApple ).reduce( - ( accumulator, key ) => { - const replacementKey = get( replacementKeyMap, key, key ); - // If on the Mac, adhere to platform convention and don't show plus between keys. - if ( isApple ) { - return [ ...accumulator, replacementKey ]; - } - - return [ ...accumulator, replacementKey, '+' ]; - }, - /** @type {string[]} */ ( [] ) - ); - - // Symbols (~`,.) are removed by the default regular expression, - // so override the rule to allow symbols used for shortcuts. - // see: https://github.com/blakeembrey/change-case#options - const capitalizedCharacter = capitalCase( character, { - stripRegexp: /[^A-Z0-9~`,\.\\\-]/gi, - } ); - - return [ ...modifierKeys, capitalizedCharacter ]; - }; -} ); + } +); /** * An object that contains functions to display shortcuts. @@ -259,7 +287,7 @@ export const displayShortcutList = mapValues( modifiers, ( modifier ) => { */ export const displayShortcut = mapValues( displayShortcutList, - ( shortcutList ) => { + ( /** @type {WPKeyHandler} */ shortcutList ) => { return /** @type {WPKeyHandler} */ ( character, _isApple = isAppleOS @@ -281,32 +309,38 @@ export const displayShortcut = mapValues( * @type {WPModifierHandler>} Keyed map of functions to * shortcut ARIA labels. */ -export const shortcutAriaLabel = mapValues( modifiers, ( modifier ) => { - return /** @type {WPKeyHandler} */ ( - character, - _isApple = isAppleOS - ) => { - const isApple = _isApple(); - const replacementKeyMap = { - [ SHIFT ]: 'Shift', - [ COMMAND ]: isApple ? 'Command' : 'Control', - [ CTRL ]: 'Control', - [ ALT ]: isApple ? 'Option' : 'Alt', - /* translators: comma as in the character ',' */ - ',': __( 'Comma' ), - /* translators: period as in the character '.' */ - '.': __( 'Period' ), - /* translators: backtick as in the character '`' */ - '`': __( 'Backtick' ), - /* translators: tilde as in the character '~' */ - '~': __( 'Tilde' ), +export const shortcutAriaLabel = mapValues( + modifiers, + ( /** @type {WPModifier} */ modifier ) => { + return /** @type {WPKeyHandler} */ ( + character, + _isApple = isAppleOS + ) => { + const isApple = _isApple(); + /** @type {Record} */ + const replacementKeyMap = { + [ SHIFT ]: 'Shift', + [ COMMAND ]: isApple ? 'Command' : 'Control', + [ CTRL ]: 'Control', + [ ALT ]: isApple ? 'Option' : 'Alt', + /* translators: comma as in the character ',' */ + ',': __( 'Comma' ), + /* translators: period as in the character '.' */ + '.': __( 'Period' ), + /* translators: backtick as in the character '`' */ + '`': __( 'Backtick' ), + /* translators: tilde as in the character '~' */ + '~': __( 'Tilde' ), + }; + + return [ ...modifier( _isApple ), character ] + .map( ( key ) => + capitalCase( replacementKeyMap[ key ] ?? key ) + ) + .join( isApple ? ' ' : ' + ' ); }; - - return [ ...modifier( _isApple ), character ] - .map( ( key ) => capitalCase( get( replacementKeyMap, key, key ) ) ) - .join( isApple ? ' ' : ' + ' ); - }; -} ); + } +); /** * From a given KeyboardEvent, returns an array of active modifier constants for @@ -346,51 +380,56 @@ function getEventModifiers( event ) { * @type {WPModifierHandler} Keyed map of functions * to match events. */ -export const isKeyboardEvent = mapValues( modifiers, ( getModifiers ) => { - return /** @type {WPEventKeyHandler} */ ( - event, - character, - _isApple = isAppleOS - ) => { - const mods = getModifiers( _isApple ); - const eventMods = getEventModifiers( event ); - - const modsDiff = mods.filter( ( mod ) => ! eventMods.includes( mod ) ); - const eventModsDiff = eventMods.filter( - ( mod ) => ! mods.includes( mod ) - ); - - if ( modsDiff.length > 0 || eventModsDiff.length > 0 ) { - return false; - } - - let key = event.key.toLowerCase(); - - if ( ! character ) { - return mods.includes( /** @type {WPModifierPart} */ ( key ) ); - } - - if ( event.altKey && character.length === 1 ) { - key = String.fromCharCode( event.keyCode ).toLowerCase(); - } - - // Replace some characters to match the key indicated - // by the shortcut on Windows. - if ( ! _isApple() ) { - if ( - event.shiftKey && - character.length === 1 && - event.code === 'Comma' - ) { - key = ','; +export const isKeyboardEvent = mapValues( + modifiers, + ( /** @type {WPModifier} */ getModifiers ) => { + return /** @type {WPEventKeyHandler} */ ( + event, + character, + _isApple = isAppleOS + ) => { + const mods = getModifiers( _isApple ); + const eventMods = getEventModifiers( event ); + + const modsDiff = mods.filter( + ( mod ) => ! eventMods.includes( mod ) + ); + const eventModsDiff = eventMods.filter( + ( mod ) => ! mods.includes( mod ) + ); + + if ( modsDiff.length > 0 || eventModsDiff.length > 0 ) { + return false; + } + + let key = event.key.toLowerCase(); + + if ( ! character ) { + return mods.includes( /** @type {WPModifierPart} */ ( key ) ); + } + + if ( event.altKey && character.length === 1 ) { + key = String.fromCharCode( event.keyCode ).toLowerCase(); } - } - // For backwards compatibility. - if ( character === 'del' ) { - character = 'delete'; - } + // Replace some characters to match the key indicated + // by the shortcut on Windows. + if ( ! _isApple() ) { + if ( + event.shiftKey && + character.length === 1 && + event.code === 'Comma' + ) { + key = ','; + } + } - return key === character.toLowerCase(); - }; -} ); + // For backwards compatibility. + if ( character === 'del' ) { + character = 'delete'; + } + + return key === character.toLowerCase(); + }; + } +); From 581eabf6aef1ff0f244e101e8ecd0c463c95e1fd Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 3 Feb 2023 18:06:33 +0100 Subject: [PATCH 27/48] Fix multi entities saved state in the post editor (#47734) --- .../src/editor/publish-post.ts | 12 +++++------- .../edit-post/src/components/layout/style.scss | 4 ++++ .../entities-saved-states/style.scss | 1 + .../components/interface-skeleton/style.scss | 18 ++++++++++++++++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/e2e-test-utils-playwright/src/editor/publish-post.ts b/packages/e2e-test-utils-playwright/src/editor/publish-post.ts index 552a0cef37233e..f036b7c626cf1f 100644 --- a/packages/e2e-test-utils-playwright/src/editor/publish-post.ts +++ b/packages/e2e-test-utils-playwright/src/editor/publish-post.ts @@ -11,18 +11,16 @@ import type { Editor } from './index'; */ export async function publishPost( this: Editor ) { await this.page.click( 'role=button[name="Publish"i]' ); - const publishEditorPanel = this.page.locator( - 'role=region[name="Editor publish"i]' + const entitiesSaveButton = this.page.locator( + 'role=region[name="Editor publish"i] >> role=button[name="Save"i]' ); - const isPublishEditorVisible = await publishEditorPanel.isVisible(); + const isEntitiesSavePanelVisible = await entitiesSaveButton.isVisible(); // Save any entities. - if ( isPublishEditorVisible ) { + if ( isEntitiesSavePanelVisible ) { // Handle saving entities. - await this.page.click( - 'role=region[name="Editor publish"i] >> role=button[name="Save"i]' - ); + await entitiesSaveButton.click(); } // Handle saving just the post. diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 473dae279473e9..229ab58a4e14b0 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -95,3 +95,7 @@ bottom: 0; } } + +.edit-post-layout .entities-saved-states__panel-header { + height: $header-height + $border-width; +} diff --git a/packages/editor/src/components/entities-saved-states/style.scss b/packages/editor/src/components/entities-saved-states/style.scss index aaf11c1aa9a6f9..4a2c8700245ed7 100644 --- a/packages/editor/src/components/entities-saved-states/style.scss +++ b/packages/editor/src/components/entities-saved-states/style.scss @@ -14,6 +14,7 @@ } .entities-saved-states__panel-header { + box-sizing: border-box; background: $white; padding-left: $grid-unit-10; padding-right: $grid-unit-10; diff --git a/packages/interface/src/components/interface-skeleton/style.scss b/packages/interface/src/components/interface-skeleton/style.scss index 05752e9ff32164..57d88626636dde 100644 --- a/packages/interface/src/components/interface-skeleton/style.scss +++ b/packages/interface/src/components/interface-skeleton/style.scss @@ -174,12 +174,26 @@ html.interface-interface-skeleton__html-container { bottom: auto; left: auto; right: 0; - width: $sidebar-width; color: $gray-900; + background: $white; + width: 100vw; + + @include break-medium() { + width: $sidebar-width; + } &:focus, &:focus-within { - top: auto; + top: $admin-bar-height-big; + + @include break-medium() { + border-left: $border-width solid $gray-300; + top: $admin-bar-height; + + .is-fullscreen-mode & { + top: 0; + } + } bottom: 0; } } From ddcaa70b640697294ac0ecc9876c91f8e119617b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 3 Feb 2023 18:50:51 +0100 Subject: [PATCH 28/48] Extract the getSupportedStyles selector to the blocks store as a private selector (#47606) --- package-lock.json | 3 +- packages/blocks/package.json | 1 + .../blocks/src/api/raw-handling/test/utils.js | 55 ++----- packages/blocks/src/experiments.js | 10 ++ packages/blocks/src/store/index.js | 3 + .../blocks/src/store/private-selectors.js | 155 ++++++++++++++++++ .../src/store/test/private-selectors.js | 151 +++++++++++++++++ .../components/global-styles/border-panel.js | 10 +- .../components/global-styles/color-utils.js | 4 +- .../global-styles/dimensions-panel.js | 14 +- .../src/components/global-styles/hooks.js | 107 ++---------- .../global-styles/screen-background-color.js | 4 +- .../global-styles/screen-button-color.js | 4 +- .../components/global-styles/screen-colors.js | 12 +- .../global-styles/screen-heading-color.js | 4 +- .../global-styles/screen-link-color.js | 4 +- .../global-styles/screen-text-color.js | 4 +- .../components/global-styles/shadow-panel.js | 4 +- .../global-styles/typography-panel.js | 42 ++--- .../push-changes-to-global-styles/index.js | 47 +++--- 20 files changed, 432 insertions(+), 206 deletions(-) create mode 100644 packages/blocks/src/experiments.js create mode 100644 packages/blocks/src/store/private-selectors.js create mode 100644 packages/blocks/src/store/test/private-selectors.js diff --git a/package-lock.json b/package-lock.json index 084850e16bce3a..32dd3a8216dfab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17400,6 +17400,7 @@ "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", + "@wordpress/experiments": "file:packages/experiments", "@wordpress/hooks": "file:packages/hooks", "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", @@ -29543,7 +29544,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", "dev": true }, "code-point-at": { diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 0fe9ad0b796265..977daff1b3648c 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -37,6 +37,7 @@ "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", + "@wordpress/experiments": "file:../experiments", "@wordpress/hooks": "file:../hooks", "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", diff --git a/packages/blocks/src/api/raw-handling/test/utils.js b/packages/blocks/src/api/raw-handling/test/utils.js index 56c38bbb38c965..d02d8e7d0602be 100644 --- a/packages/blocks/src/api/raw-handling/test/utils.js +++ b/packages/blocks/src/api/raw-handling/test/utils.js @@ -3,47 +3,15 @@ */ import deepFreeze from 'deep-freeze'; +/** + * WordPress dependencies + */ +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; + /** * Internal dependencies */ import { getBlockContentSchemaFromTransforms, isPlain } from '../utils'; -import { store as mockStore } from '../../../store'; -import { STORE_NAME as mockStoreName } from '../../../store/constants'; - -jest.mock( '@wordpress/data', () => { - return { - select: jest.fn( ( store ) => { - switch ( store ) { - case [ mockStoreName ]: - case mockStore: { - return { - hasBlockSupport: ( blockName, supports ) => { - return ( - blockName === 'core/paragraph' && - supports === 'anchor' - ); - }, - }; - } - } - } ), - combineReducers: () => { - const mock = jest.fn(); - return mock; - }, - createReduxStore: () => { - const mock = jest.fn(); - return mock; - }, - register: () => { - const mock = jest.fn(); - return mock; - }, - createRegistryControl() { - return jest.fn(); - }, - }; -} ); describe( 'isPlain', () => { it( 'should return true for plain text', () => { @@ -65,6 +33,19 @@ describe( 'isPlain', () => { } ); describe( 'getBlockContentSchema', () => { + beforeAll( () => { + registerBlockType( 'core/paragraph', { + title: 'Paragraph', + supports: { + anchor: true, + }, + } ); + } ); + + afterAll( () => { + unregisterBlockType( 'core/paragraph' ); + } ); + const myContentSchema = { strong: {}, em: {}, diff --git a/packages/blocks/src/experiments.js b/packages/blocks/src/experiments.js new file mode 100644 index 00000000000000..914280321d5320 --- /dev/null +++ b/packages/blocks/src/experiments.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', + '@wordpress/blocks' + ); diff --git a/packages/blocks/src/store/index.js b/packages/blocks/src/store/index.js index 6d2dc7c822dbde..dcdfa60d1cb79a 100644 --- a/packages/blocks/src/store/index.js +++ b/packages/blocks/src/store/index.js @@ -8,8 +8,10 @@ import { createReduxStore, register } from '@wordpress/data'; */ import reducer from './reducer'; import * as selectors from './selectors'; +import * as privateSelectors from './private-selectors'; import * as actions from './actions'; import { STORE_NAME } from './constants'; +import { unlock } from '../experiments'; /** * Store definition for the blocks namespace. @@ -25,3 +27,4 @@ export const store = createReduxStore( STORE_NAME, { } ); register( store ); +unlock( store ).registerPrivateSelectors( privateSelectors ); diff --git a/packages/blocks/src/store/private-selectors.js b/packages/blocks/src/store/private-selectors.js new file mode 100644 index 00000000000000..2f4fb1dbea3866 --- /dev/null +++ b/packages/blocks/src/store/private-selectors.js @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; +import { get } from 'lodash'; + +/** + * Internal dependencies + */ +import { getBlockType } from './selectors'; +import { __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY } from '../api/constants'; + +const ROOT_BLOCK_SUPPORTS = [ + 'background', + 'backgroundColor', + 'color', + 'linkColor', + 'buttonColor', + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontWeight', + 'lineHeight', + 'padding', + 'contentSize', + 'wideSize', + 'blockGap', + 'textDecoration', + 'textTransform', + 'letterSpacing', +]; + +/** + * Filters the list of supported styles for a given element. + * + * @param {string[]} blockSupports list of supported styles. + * @param {string|undefined} name block name. + * @param {string|undefined} element element name. + * + * @return {string[]} filtered list of supported styles. + */ +function filterElementBlockSupports( blockSupports, name, element ) { + return blockSupports.filter( ( support ) => { + if ( support === 'fontSize' && element === 'heading' ) { + return false; + } + + // This is only available for links + if ( support === 'textDecoration' && ! name && element !== 'link' ) { + return false; + } + + // This is only available for heading + if ( + support === 'textTransform' && + ! name && + ! [ 'heading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ].includes( + element + ) + ) { + return false; + } + + // This is only available for headings + if ( + support === 'letterSpacing' && + ! name && + ! [ 'heading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ].includes( + element + ) + ) { + return false; + } + + return true; + } ); +} + +/** + * Returns the list of supported styles for a given block name and element. + */ +export const getSupportedStyles = createSelector( + ( state, name, element ) => { + if ( ! name ) { + return filterElementBlockSupports( + ROOT_BLOCK_SUPPORTS, + name, + element + ); + } + + const blockType = getBlockType( state, name ); + + if ( ! blockType ) { + return []; + } + + const supportKeys = []; + + // Check for blockGap support. + // Block spacing support doesn't map directly to a single style property, so needs to be handled separately. + // Also, only allow `blockGap` support if serialization has not been skipped, to be sure global spacing can be applied. + if ( + blockType?.supports?.spacing?.blockGap && + blockType?.supports?.spacing?.__experimentalSkipSerialization !== + true && + ! blockType?.supports?.spacing?.__experimentalSkipSerialization?.some?.( + ( spacingType ) => spacingType === 'blockGap' + ) + ) { + supportKeys.push( 'blockGap' ); + } + + // check for shadow support + if ( blockType?.supports?.shadow ) { + supportKeys.push( 'shadow' ); + } + + Object.keys( STYLE_PROPERTY ).forEach( ( styleName ) => { + if ( ! STYLE_PROPERTY[ styleName ].support ) { + return; + } + + // Opting out means that, for certain support keys like background color, + // blocks have to explicitly set the support value false. If the key is + // unset, we still enable it. + if ( STYLE_PROPERTY[ styleName ].requiresOptOut ) { + if ( + STYLE_PROPERTY[ styleName ].support[ 0 ] in + blockType.supports && + get( + blockType.supports, + STYLE_PROPERTY[ styleName ].support + ) !== false + ) { + supportKeys.push( styleName ); + return; + } + } + + if ( + get( + blockType.supports, + STYLE_PROPERTY[ styleName ].support, + false + ) + ) { + supportKeys.push( styleName ); + } + } ); + + return filterElementBlockSupports( supportKeys, name, element ); + }, + ( state, name ) => [ state.blockTypes[ name ] ] +); diff --git a/packages/blocks/src/store/test/private-selectors.js b/packages/blocks/src/store/test/private-selectors.js new file mode 100644 index 00000000000000..55cc270976ca0b --- /dev/null +++ b/packages/blocks/src/store/test/private-selectors.js @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { getSupportedStyles } from '../private-selectors'; + +const keyBlocksByName = ( blocks ) => + blocks.reduce( + ( result, block ) => ( { ...result, [ block.name ]: block } ), + {} + ); + +describe( 'private selectors', () => { + describe( 'getSupportedStyles', () => { + const getState = ( blocks ) => { + return deepFreeze( { + blockTypes: keyBlocksByName( blocks ), + } ); + }; + + it( 'return the list of globally supported panels (no block name)', () => { + const supports = getSupportedStyles( getState( [] ) ); + + expect( supports ).toEqual( [ + 'background', + 'backgroundColor', + 'color', + 'linkColor', + 'buttonColor', + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontWeight', + 'lineHeight', + 'padding', + 'contentSize', + 'wideSize', + 'blockGap', + ] ); + } ); + + it( 'return the list of globally supported panels including link specific styles', () => { + const supports = getSupportedStyles( getState( [] ), null, 'link' ); + + expect( supports ).toEqual( [ + 'background', + 'backgroundColor', + 'color', + 'linkColor', + 'buttonColor', + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontWeight', + 'lineHeight', + 'padding', + 'contentSize', + 'wideSize', + 'blockGap', + 'textDecoration', + ] ); + } ); + + it( 'return the list of globally supported panels including heading specific styles', () => { + const supports = getSupportedStyles( + getState( [] ), + null, + 'heading' + ); + + expect( supports ).toEqual( [ + 'background', + 'backgroundColor', + 'color', + 'linkColor', + 'buttonColor', + 'fontFamily', + 'fontStyle', + 'fontWeight', + 'lineHeight', + 'padding', + 'contentSize', + 'wideSize', + 'blockGap', + 'textTransform', + 'letterSpacing', + ] ); + } ); + + it( 'return an empty list for unknown blocks', () => { + const supports = getSupportedStyles( + getState( [] ), + 'unkown/block' + ); + + expect( supports ).toEqual( [] ); + } ); + + it( 'return empty by default for blocks without support keys', () => { + const supports = getSupportedStyles( + getState( [ + { + name: 'core/example-block', + supports: {}, + }, + ] ), + 'core/example-block' + ); + + expect( supports ).toEqual( [] ); + } ); + + it( 'return the allowed styles according to the blocks support keys', () => { + const supports = getSupportedStyles( + getState( [ + { + name: 'core/example-block', + supports: { + typography: { + __experimentalFontFamily: true, + __experimentalFontStyle: true, + __experimentalFontWeight: true, + __experimentalTextDecoration: true, + __experimentalTextTransform: true, + __experimentalLetterSpacing: true, + fontSize: true, + lineHeight: true, + }, + }, + }, + ] ), + 'core/example-block' + ); + + expect( supports ).toEqual( [ + 'fontFamily', + 'fontSize', + 'fontStyle', + 'fontWeight', + 'lineHeight', + 'textDecoration', + 'textTransform', + 'letterSpacing', + ] ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/border-panel.js b/packages/edit-site/src/components/global-styles/border-panel.js index 3cec93c6115efb..541accde55b509 100644 --- a/packages/edit-site/src/components/global-styles/border-panel.js +++ b/packages/edit-site/src/components/global-styles/border-panel.js @@ -18,7 +18,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { getSupportedGlobalStylesPanels, useColorsPerOrigin } from './hooks'; +import { useSupportedStyles, useColorsPerOrigin } from './hooks'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); @@ -35,7 +35,7 @@ export function useHasBorderPanel( name ) { } function useHasBorderColorControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( useGlobalSetting( 'border.color', name )[ 0 ] && supports.includes( 'borderColor' ) @@ -43,7 +43,7 @@ function useHasBorderColorControl( name ) { } function useHasBorderRadiusControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( useGlobalSetting( 'border.radius', name )[ 0 ] && supports.includes( 'borderRadius' ) @@ -51,7 +51,7 @@ function useHasBorderRadiusControl( name ) { } function useHasBorderStyleControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( useGlobalSetting( 'border.style', name )[ 0 ] && supports.includes( 'borderStyle' ) @@ -59,7 +59,7 @@ function useHasBorderStyleControl( name ) { } function useHasBorderWidthControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( useGlobalSetting( 'border.width', name )[ 0 ] && supports.includes( 'borderWidth' ) diff --git a/packages/edit-site/src/components/global-styles/color-utils.js b/packages/edit-site/src/components/global-styles/color-utils.js index 6ff99bbfe678d8..a80684344e7a0d 100644 --- a/packages/edit-site/src/components/global-styles/color-utils.js +++ b/packages/edit-site/src/components/global-styles/color-utils.js @@ -2,10 +2,10 @@ * Internal dependencies */ -import { getSupportedGlobalStylesPanels } from './hooks'; +import { useSupportedStyles } from './hooks'; export function useHasColorPanel( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( supports.includes( 'color' ) || supports.includes( 'backgroundColor' ) || diff --git a/packages/edit-site/src/components/global-styles/dimensions-panel.js b/packages/edit-site/src/components/global-styles/dimensions-panel.js index d3c895b0c28f64..dbff7bae1e7a5d 100644 --- a/packages/edit-site/src/components/global-styles/dimensions-panel.js +++ b/packages/edit-site/src/components/global-styles/dimensions-panel.js @@ -27,7 +27,7 @@ import { Icon, positionCenter, stretchWide } from '@wordpress/icons'; /** * Internal dependencies */ -import { getSupportedGlobalStylesPanels } from './hooks'; +import { useSupportedStyles } from './hooks'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); @@ -53,42 +53,42 @@ export function useHasDimensionsPanel( name ) { } function useHasContentSize( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ settings ] = useGlobalSetting( 'layout.contentSize', name ); return settings && supports.includes( 'contentSize' ); } function useHasWideSize( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ settings ] = useGlobalSetting( 'layout.wideSize', name ); return settings && supports.includes( 'wideSize' ); } function useHasPadding( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ settings ] = useGlobalSetting( 'spacing.padding', name ); return settings && supports.includes( 'padding' ); } function useHasMargin( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ settings ] = useGlobalSetting( 'spacing.margin', name ); return settings && supports.includes( 'margin' ); } function useHasGap( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ settings ] = useGlobalSetting( 'spacing.blockGap', name ); return settings && supports.includes( 'blockGap' ); } function useHasMinHeight( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ settings ] = useGlobalSetting( 'dimensions.minHeight', name ); return settings && supports.includes( 'minHeight' ); diff --git a/packages/edit-site/src/components/global-styles/hooks.js b/packages/edit-site/src/components/global-styles/hooks.js index dd85c8896c6a5c..f9de6dc4ae335a 100644 --- a/packages/edit-site/src/components/global-styles/hooks.js +++ b/packages/edit-site/src/components/global-styles/hooks.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { get } from 'lodash'; import { colord, extend } from 'colord'; import a11yPlugin from 'colord/plugins/a11y'; @@ -10,107 +9,20 @@ import a11yPlugin from 'colord/plugins/a11y'; */ import { _x } from '@wordpress/i18n'; import { useMemo } from '@wordpress/element'; -import { - getBlockType, - __EXPERIMENTAL_STYLE_PROPERTY as STYLE_PROPERTY, -} from '@wordpress/blocks'; +import { store as blocksStore } from '@wordpress/blocks'; import { experiments as blockEditorExperiments } from '@wordpress/block-editor'; /** * Internal dependencies */ import { unlock } from '../../experiments'; +import { useSelect } from '@wordpress/data'; const { useGlobalSetting } = unlock( blockEditorExperiments ); // Enable colord's a11y plugin. extend( [ a11yPlugin ] ); -const ROOT_BLOCK_SUPPORTS = [ - 'background', - 'backgroundColor', - 'color', - 'linkColor', - 'buttonColor', - 'fontFamily', - 'fontSize', - 'fontStyle', - 'fontWeight', - 'lineHeight', - 'textDecoration', - 'padding', - 'contentSize', - 'wideSize', - 'blockGap', -]; - -export function getSupportedGlobalStylesPanels( name ) { - if ( ! name ) { - return ROOT_BLOCK_SUPPORTS; - } - - const blockType = getBlockType( name ); - - if ( ! blockType ) { - return []; - } - - const supportKeys = []; - - // Check for blockGap support. - // Block spacing support doesn't map directly to a single style property, so needs to be handled separately. - // Also, only allow `blockGap` support if serialization has not been skipped, to be sure global spacing can be applied. - if ( - blockType?.supports?.spacing?.blockGap && - blockType?.supports?.spacing?.__experimentalSkipSerialization !== - true && - ! blockType?.supports?.spacing?.__experimentalSkipSerialization?.some?.( - ( spacingType ) => spacingType === 'blockGap' - ) - ) { - supportKeys.push( 'blockGap' ); - } - - // check for shadow support - if ( blockType?.supports?.shadow ) { - supportKeys.push( 'shadow' ); - } - - Object.keys( STYLE_PROPERTY ).forEach( ( styleName ) => { - if ( ! STYLE_PROPERTY[ styleName ].support ) { - return; - } - - // Opting out means that, for certain support keys like background color, - // blocks have to explicitly set the support value false. If the key is - // unset, we still enable it. - if ( STYLE_PROPERTY[ styleName ].requiresOptOut ) { - if ( - STYLE_PROPERTY[ styleName ].support[ 0 ] in - blockType.supports && - get( - blockType.supports, - STYLE_PROPERTY[ styleName ].support - ) !== false - ) { - return supportKeys.push( styleName ); - } - } - - if ( - get( - blockType.supports, - STYLE_PROPERTY[ styleName ].support, - false - ) - ) { - return supportKeys.push( styleName ); - } - } ); - - return supportKeys; -} - export function useColorsPerOrigin( name ) { const [ customColors ] = useGlobalSetting( 'color.palette.custom', name ); const [ themeColors ] = useGlobalSetting( 'color.palette.theme', name ); @@ -240,3 +152,18 @@ export function useColorRandomizer( name ) { ? [ randomizeColors ] : []; } + +export function useSupportedStyles( name, element ) { + const { supportedPanels } = useSelect( + ( select ) => { + return { + supportedPanels: unlock( + select( blocksStore ) + ).getSupportedStyles( name, element ), + }; + }, + [ name, element ] + ); + + return supportedPanels; +} diff --git a/packages/edit-site/src/components/global-styles/screen-background-color.js b/packages/edit-site/src/components/global-styles/screen-background-color.js index ad0a5124f03a37..352ee897705c88 100644 --- a/packages/edit-site/src/components/global-styles/screen-background-color.js +++ b/packages/edit-site/src/components/global-styles/screen-background-color.js @@ -17,7 +17,7 @@ import { */ import ScreenHeader from './header'; import { - getSupportedGlobalStylesPanels, + useSupportedStyles, useColorsPerOrigin, useGradientsPerOrigin, } from './hooks'; @@ -27,7 +27,7 @@ const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); function ScreenBackgroundColor( { name, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ areCustomSolidsEnabled ] = useGlobalSetting( 'color.custom', name ); const [ areCustomGradientsEnabled ] = useGlobalSetting( 'color.customGradient', diff --git a/packages/edit-site/src/components/global-styles/screen-button-color.js b/packages/edit-site/src/components/global-styles/screen-button-color.js index 2ee286e574f470..f734ad8f000d64 100644 --- a/packages/edit-site/src/components/global-styles/screen-button-color.js +++ b/packages/edit-site/src/components/global-styles/screen-button-color.js @@ -11,14 +11,14 @@ import { * Internal dependencies */ import ScreenHeader from './header'; -import { getSupportedGlobalStylesPanels, useColorsPerOrigin } from './hooks'; +import { useSupportedStyles, useColorsPerOrigin } from './hooks'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); function ScreenButtonColor( { name, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const colorsPerOrigin = useColorsPerOrigin( name ); const [ areCustomSolidsEnabled ] = useGlobalSetting( 'color.custom', name ); const [ isBackgroundEnabled ] = useGlobalSetting( diff --git a/packages/edit-site/src/components/global-styles/screen-colors.js b/packages/edit-site/src/components/global-styles/screen-colors.js index d4a5ba84ce646a..0a6b213ed2363d 100644 --- a/packages/edit-site/src/components/global-styles/screen-colors.js +++ b/packages/edit-site/src/components/global-styles/screen-colors.js @@ -18,7 +18,7 @@ import { experiments as blockEditorExperiments } from '@wordpress/block-editor'; import ScreenHeader from './header'; import Palette from './palette'; import { NavigationButtonAsItem } from './navigation-button'; -import { getSupportedGlobalStylesPanels } from './hooks'; +import { useSupportedStyles } from './hooks'; import Subtitle from './subtitle'; import ColorIndicatorWrapper from './color-indicator-wrapper'; import BlockPreviewPanel from './block-preview-panel'; @@ -30,7 +30,7 @@ const { useGlobalStyle } = unlock( blockEditorExperiments ); function BackgroundColorItem( { name, parentMenu, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; const urlPrefix = variation ? `/variations/${ variation }` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasSupport = supports.includes( 'backgroundColor' ) || supports.includes( 'background' ); @@ -67,7 +67,7 @@ function BackgroundColorItem( { name, parentMenu, variation = '' } ) { function TextColorItem( { name, parentMenu, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; const urlPrefix = variation ? `/variations/${ variation }` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasSupport = supports.includes( 'color' ); const [ color ] = useGlobalStyle( prefix + 'color.text', name ); @@ -98,7 +98,7 @@ function TextColorItem( { name, parentMenu, variation = '' } ) { function LinkColorItem( { name, parentMenu, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; const urlPrefix = variation ? `/variations/${ variation }` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasSupport = supports.includes( 'linkColor' ); const [ color ] = useGlobalStyle( prefix + 'elements.link.color.text', @@ -138,7 +138,7 @@ function LinkColorItem( { name, parentMenu, variation = '' } ) { function HeadingColorItem( { name, parentMenu, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; const urlPrefix = variation ? `/variations/${ variation }` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasSupport = supports.includes( 'color' ); const [ color ] = useGlobalStyle( prefix + 'elements.heading.color.text', @@ -176,7 +176,7 @@ function HeadingColorItem( { name, parentMenu, variation = '' } ) { function ButtonColorItem( { name, parentMenu, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; const urlPrefix = variation ? `/variations/${ variation }` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasSupport = supports.includes( 'buttonColor' ); const [ color ] = useGlobalStyle( prefix + 'elements.button.color.text', diff --git a/packages/edit-site/src/components/global-styles/screen-heading-color.js b/packages/edit-site/src/components/global-styles/screen-heading-color.js index 16b2aad6b3f1c3..fa1ff332e2c47b 100644 --- a/packages/edit-site/src/components/global-styles/screen-heading-color.js +++ b/packages/edit-site/src/components/global-styles/screen-heading-color.js @@ -17,7 +17,7 @@ import { useState } from '@wordpress/element'; */ import ScreenHeader from './header'; import { - getSupportedGlobalStylesPanels, + useSupportedStyles, useColorsPerOrigin, useGradientsPerOrigin, } from './hooks'; @@ -28,7 +28,7 @@ const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); function ScreenHeadingColor( { name, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; const [ selectedLevel, setCurrentTab ] = useState( 'heading' ); - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ areCustomSolidsEnabled ] = useGlobalSetting( 'color.custom', name ); const [ areCustomGradientsEnabled ] = useGlobalSetting( 'color.customGradient', diff --git a/packages/edit-site/src/components/global-styles/screen-link-color.js b/packages/edit-site/src/components/global-styles/screen-link-color.js index 6403ff97e035a1..692859a79c08e8 100644 --- a/packages/edit-site/src/components/global-styles/screen-link-color.js +++ b/packages/edit-site/src/components/global-styles/screen-link-color.js @@ -12,14 +12,14 @@ import { TabPanel } from '@wordpress/components'; * Internal dependencies */ import ScreenHeader from './header'; -import { getSupportedGlobalStylesPanels, useColorsPerOrigin } from './hooks'; +import { useSupportedStyles, useColorsPerOrigin } from './hooks'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); function ScreenLinkColor( { name, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ areCustomSolidsEnabled ] = useGlobalSetting( 'color.custom', name ); const colorsPerOrigin = useColorsPerOrigin( name ); const [ isLinkEnabled ] = useGlobalSetting( 'color.link', name ); diff --git a/packages/edit-site/src/components/global-styles/screen-text-color.js b/packages/edit-site/src/components/global-styles/screen-text-color.js index 03dc46059333b3..17401fc8a6195c 100644 --- a/packages/edit-site/src/components/global-styles/screen-text-color.js +++ b/packages/edit-site/src/components/global-styles/screen-text-color.js @@ -11,14 +11,14 @@ import { * Internal dependencies */ import ScreenHeader from './header'; -import { getSupportedGlobalStylesPanels, useColorsPerOrigin } from './hooks'; +import { useSupportedStyles, useColorsPerOrigin } from './hooks'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); function ScreenTextColor( { name, variation = '' } ) { const prefix = variation ? `variations.${ variation }.` : ''; - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ areCustomSolidsEnabled ] = useGlobalSetting( 'color.custom', name ); const [ isTextEnabled ] = useGlobalSetting( 'color.text', name ); const colorsPerOrigin = useColorsPerOrigin( name ); diff --git a/packages/edit-site/src/components/global-styles/shadow-panel.js b/packages/edit-site/src/components/global-styles/shadow-panel.js index 7b3aadfe6a2d65..e71017668330e5 100644 --- a/packages/edit-site/src/components/global-styles/shadow-panel.js +++ b/packages/edit-site/src/components/global-styles/shadow-panel.js @@ -27,14 +27,14 @@ import { experiments as blockEditorExperiments } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { getSupportedGlobalStylesPanels } from './hooks'; +import { useSupportedStyles } from './hooks'; import { IconWithCurrentColor } from './icon-with-current-color'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); export function useHasShadowControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return supports.includes( 'shadow' ); } diff --git a/packages/edit-site/src/components/global-styles/typography-panel.js b/packages/edit-site/src/components/global-styles/typography-panel.js index de88bf71114422..114ed4fb3ba67c 100644 --- a/packages/edit-site/src/components/global-styles/typography-panel.js +++ b/packages/edit-site/src/components/global-styles/typography-panel.js @@ -20,7 +20,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { getSupportedGlobalStylesPanels } from './hooks'; +import { useSupportedStyles } from './hooks'; import { unlock } from '../../experiments'; const { useGlobalSetting, useGlobalStyle } = unlock( blockEditorExperiments ); @@ -30,7 +30,7 @@ export function useHasTypographyPanel( name ) { const hasLineHeight = useHasLineHeightControl( name ); const hasFontAppearance = useHasAppearanceControl( name ); const hasLetterSpacing = useHasLetterSpacingControl( name ); - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( hasFontFamily || hasLineHeight || @@ -41,7 +41,7 @@ export function useHasTypographyPanel( name ) { } function useHasFontFamilyControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const [ fontFamiliesPerOrigin ] = useGlobalSetting( 'typography.fontFamilies', name @@ -54,7 +54,7 @@ function useHasFontFamilyControl( name ) { } function useHasLineHeightControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); return ( useGlobalSetting( 'typography.lineHeight', name )[ 0 ] && supports.includes( 'lineHeight' ) @@ -62,7 +62,7 @@ function useHasLineHeightControl( name ) { } function useHasAppearanceControl( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasFontStyles = useGlobalSetting( 'typography.fontStyle', name )[ 0 ] && supports.includes( 'fontStyle' ); @@ -73,7 +73,7 @@ function useHasAppearanceControl( name ) { } function useAppearanceControlLabel( name ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); const hasFontStyles = useGlobalSetting( 'typography.fontStyle', name )[ 0 ] && supports.includes( 'fontStyle' ); @@ -90,27 +90,19 @@ function useAppearanceControlLabel( name ) { } function useHasLetterSpacingControl( name, element ) { - const setting = useGlobalSetting( 'typography.letterSpacing', name )[ 0 ]; - if ( ! setting ) { - return false; - } - if ( ! name && element === 'heading' ) { - return true; - } - const supports = getSupportedGlobalStylesPanels( name ); - return supports.includes( 'letterSpacing' ); + const supports = useSupportedStyles( name, element ); + return ( + useGlobalSetting( 'typography.letterSpacing', name )[ 0 ] && + supports.includes( 'letterSpacing' ) + ); } function useHasTextTransformControl( name, element ) { - const setting = useGlobalSetting( 'typography.textTransform', name )[ 0 ]; - if ( ! setting ) { - return false; - } - if ( ! name && element === 'heading' ) { - return true; - } - const supports = getSupportedGlobalStylesPanels( name ); - return supports.includes( 'textTransform' ); + const supports = useSupportedStyles( name, element ); + return ( + useGlobalSetting( 'typography.textTransform', name )[ 0 ] && + supports.includes( 'textTransform' ) + ); } function useHasTextDecorationControl( name, element ) { @@ -188,7 +180,7 @@ export default function TypographyPanel( { headingLevel, variation = '', } ) { - const supports = getSupportedGlobalStylesPanels( name ); + const supports = useSupportedStyles( name ); let prefix = ''; if ( element === 'heading' ) { prefix = `elements.${ headingLevel }.`; diff --git a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js index c23b184876eb93..c02658517fdd63 100644 --- a/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js +++ b/packages/edit-site/src/hooks/push-changes-to-global-styles/index.js @@ -26,7 +26,7 @@ import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ -import { getSupportedGlobalStylesPanels } from '../../components/global-styles/hooks'; +import { useSupportedStyles } from '../../components/global-styles/hooks'; import { unlock } from '../../experiments'; const { GlobalStylesContext } = unlock( blockEditorExperiments ); @@ -90,22 +90,30 @@ const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = { 'typography.fontFamily': 'fontFamily', }; -function getChangesToPush( name, attributes ) { - return getSupportedGlobalStylesPanels( name ).flatMap( ( key ) => { - if ( ! STYLE_PROPERTY[ key ] ) { - return []; - } - const { value: path } = STYLE_PROPERTY[ key ]; - const presetAttributeKey = path.join( '.' ); - const presetAttributeValue = - attributes[ - STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[ presetAttributeKey ] - ]; - const value = presetAttributeValue - ? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }` - : get( attributes.style, path ); - return value ? [ { path, value } ] : []; - } ); +function useChangesToPush( name, attributes ) { + const supports = useSupportedStyles( name ); + + return useMemo( + () => + supports.flatMap( ( key ) => { + if ( ! STYLE_PROPERTY[ key ] ) { + return []; + } + const { value: path } = STYLE_PROPERTY[ key ]; + const presetAttributeKey = path.join( '.' ); + const presetAttributeValue = + attributes[ + STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE[ + presetAttributeKey + ] + ]; + const value = presetAttributeValue + ? `var:preset|${ STYLE_PATH_TO_CSS_VAR_INFIX[ presetAttributeKey ] }|${ presetAttributeValue }` + : get( attributes.style, path ); + return value ? [ { path, value } ] : []; + } ), + [ supports, name, attributes ] + ); } function cloneDeep( object ) { @@ -117,10 +125,7 @@ function PushChangesToGlobalStylesControl( { attributes, setAttributes, } ) { - const changes = useMemo( - () => getChangesToPush( name, attributes ), - [ name, attributes ] - ); + const changes = useChangesToPush( name, attributes ); const { user: userConfig, setUserConfig } = useContext( GlobalStylesContext ); From 7d8c488d572addeb993957652868e80284c44569 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Fri, 3 Feb 2023 21:48:30 +0200 Subject: [PATCH 29/48] marks not persistent system actions for nesting effects (#47633) --- packages/block-library/src/navigation-link/edit.js | 3 +++ packages/block-library/src/navigation-submenu/edit.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index a52349c1580234..fda1268715c328 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -261,6 +261,9 @@ export default function NavigationLinkEdit( { useEffect( () => { // If block has inner blocks, transform to Submenu. if ( hasChildren ) { + // This side-effect should not create an undo level as those should + // only be created via user interactions. + __unstableMarkNextChangeAsNotPersistent(); transformToSubmenu(); } }, [ hasChildren ] ); diff --git a/packages/block-library/src/navigation-submenu/edit.js b/packages/block-library/src/navigation-submenu/edit.js index bfb830a9caed2a..866333a061de84 100644 --- a/packages/block-library/src/navigation-submenu/edit.js +++ b/packages/block-library/src/navigation-submenu/edit.js @@ -358,6 +358,9 @@ export default function NavigationSubmenuEdit( { useEffect( () => { // If block becomes empty, transform to Navigation Link. if ( ! hasChildren && prevHasChildren ) { + // This side-effect should not create an undo level as those should + // only be created via user interactions. + __unstableMarkNextChangeAsNotPersistent(); transformToLink(); } }, [ hasChildren, prevHasChildren ] ); From ab1f2a08ab4f5fd234017a336a3d3293d39c44d4 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Fri, 3 Feb 2023 16:57:19 -0500 Subject: [PATCH 30/48] Mobile Release v1.88.0 (#47711) * Release script: Update react-native-editor version to 1.88.0 * Release script: Update with changes from 'npm run core preios' * Update changelog --------- Co-authored-by: jhnstn --- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 2 ++ packages/react-native-editor/ios/Gemfile.lock | 1 + packages/react-native-editor/ios/Podfile.lock | 12 ++++++------ packages/react-native-editor/package.json | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index 424e774264802f..6a514e589095c9 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.87.3", + "version": "1.88.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 15161992e6a4b2..608b51ca9d93a7 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.87.3", + "version": "1.88.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index b342e1fcbab9ae..30c376d8688fe8 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased + +## 1.88.0 - [*] Bump Android `minSdkVersion` to 24 [#47604] - [*] Update React Native Reanimated to 2.9.1-wp-3 [#47574] - [*] Bump Aztec version to `1.6.3` [#47610] diff --git a/packages/react-native-editor/ios/Gemfile.lock b/packages/react-native-editor/ios/Gemfile.lock index f03dae68673787..e368373d7cbb74 100644 --- a/packages/react-native-editor/ios/Gemfile.lock +++ b/packages/react-native-editor/ios/Gemfile.lock @@ -90,6 +90,7 @@ GEM PLATFORMS arm64-darwin-21 arm64-darwin-22 + ruby DEPENDENCIES cocoapods (~> 1.11, >= 1.11.2) diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 557b12ae6f73af..39fbc77bc585a4 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.69.4) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.87.3): + - Gutenberg (1.88.0): - React-Core (= 0.69.4) - React-CoreModules (= 0.69.4) - React-RCTImage (= 0.69.4) @@ -331,7 +331,7 @@ PODS: - SDWebImageWebPCoder (~> 0.8.4) - RNGestureHandler (2.3.2-wp-2): - React-Core - - RNReanimated (2.9.1-wp-2): + - RNReanimated (2.9.1-wp-3): - DoubleConversion - FBLazyVector - FBReactNativeSpec @@ -362,7 +362,7 @@ PODS: - React-Core - RNSVG (9.13.6): - React-Core - - RNTAztecView (1.87.3): + - RNTAztecView (1.88.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.8) - SDWebImage (5.11.1): @@ -545,7 +545,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 2ff441cbe6e58c1778d8a5cf3311831a6a8c0809 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 3d02b25ca00c2d456734d0bcff864cbc62f6ae1a - Gutenberg: f5c4ab1e1462614a3c22a98a21370a9c35a3a49a + Gutenberg: b2a0a2aa6ed4c20879d8913534fedb3ac49092fd libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a RCTRequired: bd9d2ab0fda10171fcbcf9ba61a7df4dc15a28f4 @@ -585,10 +585,10 @@ SPEC CHECKSUMS: RNCMaskedView: c298b644a10c0c142055b3ae24d83879ecb13ccd RNFastImage: 1f2cab428712a4baaf78d6169eaec7f622556dd7 RNGestureHandler: 3e0ea0c115175e66680032904339696bab928ca3 - RNReanimated: 8b189a09da0345d84b33b8cde57a57f8ed847352 + RNReanimated: bea6acb5fdcbd8ca27641180579d09e3434f803c RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b RNSVG: 36a7359c428dcb7c6bce1cc546fbfebe069809b0 - RNTAztecView: 79caa4df08250dca4ab2dca2eddfd25c85a1ae2a + RNTAztecView: 7ab44d4b7a55b3b2e41feab2b4d2c4a1552ea575 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: 7d11d598f14c82c727c08b56bd35fbeb7dafb504 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 21df9c364294c3..2601a1871f5800 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.87.3", + "version": "1.88.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 4f94f097ecf52d145db4563e2da3352c19cb02e2 Mon Sep 17 00:00:00 2001 From: Matthew Reishus Date: Fri, 3 Feb 2023 18:37:03 -0600 Subject: [PATCH 31/48] Fix unbalanced parenthesis in Element README (#47700) * Fix unbalanced parenthesis in Element README * Use npm run docs:build to autoformat --- packages/element/README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/element/README.md b/packages/element/README.md index 0c1a99f2c302b7..aaf145b4e46efd 100755 --- a/packages/element/README.md +++ b/packages/element/README.md @@ -38,11 +38,9 @@ Let's render a customized greeting into an empty element: ); } - wp.element.createRoot(document.getElementById( 'greeting' )) - .render( - wp.element.createElement( Greeting, { toWhom: 'World' } ) - ) - ); + wp.element + .createRoot( document.getElementById( 'greeting' ) ) + .render( wp.element.createElement( Greeting, { toWhom: 'World' } ) ); ``` From 4ef110ca0f1177a720b0d233454bde837ae455a4 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Sun, 5 Feb 2023 03:31:48 +0800 Subject: [PATCH 32/48] Show a pointer/hint in the settings tab informing the user about the styles tab (#47670) * Show a pointer/hint in the settings tab informing the user about the styles tab * Add a spoken message when the notice is dismissed * Update margin around text and button * Remove spoken message * Rename to hint and add focus management code * Relabel dismiss button --- package-lock.json | 1 + packages/block-editor/package.json | 1 + .../settings-tab-hint.js | 52 +++++++++++++++++++ .../inspector-controls-tabs/settings-tab.js | 2 + .../inspector-controls-tabs/style.scss | 20 +++++++ 5 files changed, 76 insertions(+) create mode 100644 packages/block-editor/src/components/inspector-controls-tabs/settings-tab-hint.js diff --git a/package-lock.json b/package-lock.json index 32dd3a8216dfab..3893e6fe012573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17309,6 +17309,7 @@ "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/notices": "file:packages/notices", + "@wordpress/preferences": "file:packages/preferences", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/shortcode": "file:packages/shortcode", "@wordpress/style-engine": "file:packages/style-engine", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 4416bb686c9af9..a160a757074c79 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -54,6 +54,7 @@ "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", "@wordpress/notices": "file:../notices", + "@wordpress/preferences": "file:../preferences", "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "@wordpress/style-engine": "file:../style-engine", diff --git a/packages/block-editor/src/components/inspector-controls-tabs/settings-tab-hint.js b/packages/block-editor/src/components/inspector-controls-tabs/settings-tab-hint.js new file mode 100644 index 00000000000000..4fc829817b4e0f --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls-tabs/settings-tab-hint.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { focus } from '@wordpress/dom'; +import { useRef } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { close } from '@wordpress/icons'; +import { store as preferencesStore } from '@wordpress/preferences'; + +const PREFERENCE_NAME = 'isInspectorControlsTabsHintVisible'; + +export default function InspectorControlsTabsHint() { + const isInspectorControlsTabsHintVisible = useSelect( + ( select ) => + select( preferencesStore ).get( 'core', PREFERENCE_NAME ) ?? true, + [] + ); + + const ref = useRef(); + + const { set: setPreference } = useDispatch( preferencesStore ); + if ( ! isInspectorControlsTabsHintVisible ) { + return null; + } + + return ( +
+
+ { __( + "Looking for other block settings? They've moved to the styles tab." + ) } +
+
+ ); +} diff --git a/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js b/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js index ec34035b754a91..bd462837442fe9 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/settings-tab.js @@ -4,6 +4,7 @@ import AdvancedControls from './advanced-controls-panel'; import PositionControls from './position-controls-panel'; import { default as InspectorControls } from '../inspector-controls'; +import SettingsTabHint from './settings-tab-hint'; const SettingsTab = ( { showAdvancedControls = false } ) => ( <> @@ -14,6 +15,7 @@ const SettingsTab = ( { showAdvancedControls = false } ) => ( ) } + ); diff --git a/packages/block-editor/src/components/inspector-controls-tabs/style.scss b/packages/block-editor/src/components/inspector-controls-tabs/style.scss index f863f8f844d720..da83073a45590a 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/style.scss +++ b/packages/block-editor/src/components/inspector-controls-tabs/style.scss @@ -13,3 +13,23 @@ } } } + +.block-editor-inspector-controls-tabs__hint { + align-items: top; + background: $gray-100; + border-radius: $radius-block-ui; + color: $gray-900; + display: flex; + flex-direction: row; + margin: $grid-unit-20; +} + +.block-editor-inspector-controls-tabs__hint-content { + margin: $grid-unit-15 0 $grid-unit-15 $grid-unit-15; +} + +.block-editor-inspector-controls-tabs__hint-dismiss { + // The dismiss button has a lot of empty space through its padding. + // Apply margin to visually align the icon with the top of the text to its left. + margin: $grid-unit-05 $grid-unit-05 $grid-unit-05 0; +} From c5104afba77d138b67e6e5923163f89a6a2a881c Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Mon, 6 Feb 2023 09:38:27 +1100 Subject: [PATCH 33/48] Add tests for gutenberg_render_layout_support_flag (#47719) --- phpunit/block-supports/layout-test.php | 84 ++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/phpunit/block-supports/layout-test.php b/phpunit/block-supports/layout-test.php index c7e14ffe84dbc0..7ec06401c01706 100644 --- a/phpunit/block-supports/layout-test.php +++ b/phpunit/block-supports/layout-test.php @@ -380,4 +380,88 @@ public function data_gutenberg_get_layout_style() { ), ); } + + /** + * Check that gutenberg_render_layout_support_flag() renders the correct classnames on the wrapper. + * + * @dataProvider data_layout_support_flag_renders_classnames_on_wrapper + * + * @covers ::gutenberg_render_layout_support_flag + * + * @param array $args Dataset to test. + * @param string $expected_output The expected output. + */ + public function test_layout_support_flag_renders_classnames_on_wrapper( $args, $expected_output ) { + $actual_output = gutenberg_render_layout_support_flag( $args['block_content'], $args['block'] ); + $this->assertEquals( $expected_output, $actual_output ); + } + + /** + * Data provider for test_layout_support_flag_renders_classnames_on_wrapper. + * + * @return array + */ + public function data_layout_support_flag_renders_classnames_on_wrapper() { + return array( + 'single wrapper block layout with flow type' => array( + 'args' => array( + 'block_content' => '
', + 'block' => array( + 'blockName' => 'core/group', + 'attrs' => array( + 'layout' => array( + 'type' => 'default', + ), + ), + 'innerBlocks' => array(), + 'innerHTML' => '
', + 'innerContent' => array( + '
', + ), + ), + ), + 'expected_output' => '
', + ), + 'single wrapper block layout with constrained type' => array( + 'args' => array( + 'block_content' => '
', + 'block' => array( + 'blockName' => 'core/group', + 'attrs' => array( + 'layout' => array( + 'type' => 'constrained', + ), + ), + 'innerBlocks' => array(), + 'innerHTML' => '
', + 'innerContent' => array( + '
', + ), + ), + ), + 'expected_output' => '
', + ), + 'multiple wrapper block layout with flow type' => array( + 'args' => array( + 'block_content' => '
', + 'block' => array( + 'blockName' => 'core/group', + 'attrs' => array( + 'layout' => array( + 'type' => 'default', + ), + ), + 'innerBlocks' => array(), + 'innerHTML' => '
', + 'innerContent' => array( + '
', + ' ', + '
', + ), + ), + ), + 'expected_output' => '
', + ), + ); + } } From 862fe670f9a9890e9ebae67ceb2e28bd5bd856f8 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:55:14 +0900 Subject: [PATCH 34/48] Table block: don't render empty sections in editor (#47753) --- packages/block-library/src/table/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index af3d7d39900717..313bd6e3ce9f55 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -398,7 +398,7 @@ function TableEdit( { }, ]; - const renderedSections = [ 'head', 'body', 'foot' ].map( ( name ) => ( + const renderedSections = sections.map( ( name ) => ( { attributes[ name ].map( ( { cells }, rowIndex ) => (
From b0f945db28ce128a7ec2ab98419bd0a6f36297b0 Mon Sep 17 00:00:00 2001 From: Ari Stathopoulos Date: Mon, 6 Feb 2023 10:47:14 +0200 Subject: [PATCH 35/48] Make process_blocks_custom_css method protected (#47725) --- lib/class-wp-theme-json-gutenberg.php | 2 +- phpunit/class-wp-theme-json-test.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index f3ed36dce18e57..277a967c208242 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -971,7 +971,7 @@ public function get_settings() { * * @return string The processed CSS. */ - public function process_blocks_custom_css( $css, $selector ) { + protected function process_blocks_custom_css( $css, $selector ) { $processed_css = ''; // Split CSS nested rules. diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 4aca6ad5c3bf1f..423b8a6ff41546 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -2010,8 +2010,10 @@ public function test_process_blocks_custom_css( $input, $expected ) { 'styles' => array(), ) ); + $reflection = new ReflectionMethod( $theme_json, 'process_blocks_custom_css' ); + $reflection->setAccessible( true ); - $this->assertEquals( $expected, $theme_json->process_blocks_custom_css( $input['css'], $input['selector'] ) ); + $this->assertEquals( $expected, $reflection->invoke( $theme_json, $input['css'], $input['selector'] ) ); } /** From 81eecb5c4135e67b2a709943192e6482fcf8c1d7 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:41:33 +0200 Subject: [PATCH 36/48] Lodash: Refactor away from _.mapValues() in data registry (#47742) --- packages/data/src/registry.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 7a5668505b7127..66e0ad7d5298c6 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { mapValues } from 'lodash'; - /** * WordPress dependencies */ @@ -198,14 +193,19 @@ export function createRegistry( storeConfigs = {}, parent = null ) { // Deprecated // TODO: Remove this after `use()` is removed. function withPlugins( attributes ) { - return mapValues( attributes, ( attribute, key ) => { - if ( typeof attribute !== 'function' ) { - return attribute; - } - return function () { - return registry[ key ].apply( null, arguments ); - }; - } ); + return Object.fromEntries( + Object.entries( attributes ).map( ( [ key, attribute ] ) => { + if ( typeof attribute !== 'function' ) { + return [ key, attribute ]; + } + return [ + key, + function () { + return registry[ key ].apply( null, arguments ); + }, + ]; + } ) + ); } /** From dc4818fc0cc726af9a645eba2d1b3c2330ffa871 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Mon, 6 Feb 2023 11:42:05 +0200 Subject: [PATCH 37/48] Lodash: Refactor away from _.get() in resolvers cache middleware (#47743) --- packages/data/src/resolvers-cache-middleware.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/data/src/resolvers-cache-middleware.js b/packages/data/src/resolvers-cache-middleware.js index 1bb0fbaec98a4e..68945db2535752 100644 --- a/packages/data/src/resolvers-cache-middleware.js +++ b/packages/data/src/resolvers-cache-middleware.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { get } from 'lodash'; - /** * Internal dependencies */ @@ -27,11 +22,10 @@ const createResolversCacheMiddleware = .getCachedResolvers( reducerKey ); Object.entries( resolvers ).forEach( ( [ selectorName, resolversByArgs ] ) => { - const resolver = get( registry.stores, [ - reducerKey, - 'resolvers', - selectorName, - ] ); + const resolver = + registry.stores?.[ reducerKey ]?.resolvers?.[ + selectorName + ]; if ( ! resolver || ! resolver.shouldInvalidate ) { return; } From 58ba1a8d1947574224c45686d59c300d57fd962e Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 6 Feb 2023 12:04:32 +0200 Subject: [PATCH 38/48] [Block Editor]: Lock __experimentalBlockInspectorAnimation setting (#47740) --- ...-rest-block-editor-settings-controller.php | 6 ---- .../src/components/block-inspector/index.js | 2 +- packages/block-editor/src/store/defaults.js | 7 ++++ .../block-editor/src/store/private-actions.js | 5 ++- .../src/navigation-link/index.php | 32 ------------------- .../src/navigation-submenu/index.php | 32 ------------------- .../block-library/src/navigation/index.php | 32 ------------------- .../provider/use-block-editor-settings.js | 1 - 8 files changed, 12 insertions(+), 105 deletions(-) diff --git a/lib/experimental/class-wp-rest-block-editor-settings-controller.php b/lib/experimental/class-wp-rest-block-editor-settings-controller.php index d38f73be71d208..7bca2a9e8e9967 100644 --- a/lib/experimental/class-wp-rest-block-editor-settings-controller.php +++ b/lib/experimental/class-wp-rest-block-editor-settings-controller.php @@ -168,12 +168,6 @@ public function get_item_schema() { 'context' => array( 'mobile' ), ), - '__experimentalBlockInspectorAnimation' => array( - 'description' => __( 'Whether to enable animation when showing and hiding the block inspector.', 'gutenberg' ), - 'type' => 'object', - 'context' => array( 'site-editor' ), - ), - 'alignWide' => array( 'description' => __( 'Enable/Disable Wide/Full Alignments.', 'gutenberg' ), 'type' => 'boolean', diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index e7a769f51cec75..f80bfddb74018e 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -178,7 +178,7 @@ const BlockInspector = ( { showNoBlockSelectedMessage = true } ) => { if ( blockType ) { const globalBlockInspectorAnimationSettings = select( blockEditorStore ).getSettings() - .__experimentalBlockInspectorAnimation; + .blockInspectorAnimation; return globalBlockInspectorAnimationSettings?.[ blockType.name ]; diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 18f189dab41054..44095cff573f54 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -170,6 +170,13 @@ export const SETTINGS_DEFAULTS = { __unstableGalleryWithImageBlocks: false, __unstableIsPreviewMode: false, + // This setting is `private` now with `lock` API. + blockInspectorAnimation: { + 'core/navigation': { enterDirection: 'leftToRight' }, + 'core/navigation-submenu': { enterDirection: 'rightToLeft' }, + 'core/navigation-link': { enterDirection: 'rightToLeft' }, + }, + generateAnchors: false, // gradients setting is not used anymore now defaults are passed from theme.json on the server and core has its own defaults. // The setting is only kept for backward compatibility purposes. diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index faf227edc0de6f..2d33ea82cb9b63 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -11,7 +11,10 @@ import { Platform } from '@wordpress/element'; * * @see https://github.com/WordPress/gutenberg/pull/46131 */ -const privateSettings = [ 'inserterMediaCategories' ]; +const privateSettings = [ + 'inserterMediaCategories', + 'blockInspectorAnimation', +]; /** * Action that updates the block editor settings and diff --git a/packages/block-library/src/navigation-link/index.php b/packages/block-library/src/navigation-link/index.php index 34db86d3b341e3..1164fa995b28cf 100644 --- a/packages/block-library/src/navigation-link/index.php +++ b/packages/block-library/src/navigation-link/index.php @@ -371,35 +371,3 @@ function register_block_core_navigation_link() { ); } add_action( 'init', 'register_block_core_navigation_link' ); - -/** - * Enables animation of the block inspector for the Navigation Link block. - * - * See: - * - https://github.com/WordPress/gutenberg/pull/46342 - * - https://github.com/WordPress/gutenberg/issues/45884 - * - * @param array $settings Default editor settings. - * @return array Filtered editor settings. - */ -function block_core_navigation_link_enable_inspector_animation( $settings ) { - $current_animation_settings = _wp_array_get( - $settings, - array( '__experimentalBlockInspectorAnimation' ), - array() - ); - - $settings['__experimentalBlockInspectorAnimation'] = array_merge( - $current_animation_settings, - array( - 'core/navigation-link' => - array( - 'enterDirection' => 'rightToLeft', - ), - ) - ); - - return $settings; -} - -add_filter( 'block_editor_settings_all', 'block_core_navigation_link_enable_inspector_animation' ); diff --git a/packages/block-library/src/navigation-submenu/index.php b/packages/block-library/src/navigation-submenu/index.php index 870fda57b5ef5e..be6046076e76e1 100644 --- a/packages/block-library/src/navigation-submenu/index.php +++ b/packages/block-library/src/navigation-submenu/index.php @@ -289,35 +289,3 @@ function register_block_core_navigation_submenu() { ); } add_action( 'init', 'register_block_core_navigation_submenu' ); - -/** - * Enables animation of the block inspector for the Navigation Submenu block. - * - * See: - * - https://github.com/WordPress/gutenberg/pull/46342 - * - https://github.com/WordPress/gutenberg/issues/45884 - * - * @param array $settings Default editor settings. - * @return array Filtered editor settings. - */ -function block_core_navigation_submenu_enable_inspector_animation( $settings ) { - $current_animation_settings = _wp_array_get( - $settings, - array( '__experimentalBlockInspectorAnimation' ), - array() - ); - - $settings['__experimentalBlockInspectorAnimation'] = array_merge( - $current_animation_settings, - array( - 'core/navigation-submenu' => - array( - 'enterDirection' => 'rightToLeft', - ), - ) - ); - - return $settings; -} - -add_filter( 'block_editor_settings_all', 'block_core_navigation_submenu_enable_inspector_animation' ); diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 06cc21aa87351b..01c64cf9e5a66d 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -874,35 +874,3 @@ function block_core_navigation_typographic_presets_backcompatibility( $parsed_bl } add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' ); - -/** - * Enables animation of the block inspector for the Navigation block. - * - * See: - * - https://github.com/WordPress/gutenberg/pull/46342 - * - https://github.com/WordPress/gutenberg/issues/45884 - * - * @param array $settings Default editor settings. - * @return array Filtered editor settings. - */ -function block_core_navigation_enable_inspector_animation( $settings ) { - $current_animation_settings = _wp_array_get( - $settings, - array( '__experimentalBlockInspectorAnimation' ), - array() - ); - - $settings['__experimentalBlockInspectorAnimation'] = array_merge( - $current_animation_settings, - array( - 'core/navigation' => - array( - 'enterDirection' => 'leftToRight', - ), - ) - ); - - return $settings; -} - -add_filter( 'block_editor_settings_all', 'block_core_navigation_enable_inspector_animation' ); diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 8fbd2cdd548239..8b9d9e5ba86d77 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -21,7 +21,6 @@ const EMPTY_BLOCKS_LIST = []; const BLOCK_EDITOR_SETTINGS = [ '__experimentalBlockDirectory', - '__experimentalBlockInspectorAnimation', '__experimentalDiscussionSettings', '__experimentalFeatures', '__experimentalGlobalStylesBaseStyles', From fdc4aa758e9dc82b07af73fc6eb9d0aba6459804 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Mon, 6 Feb 2023 11:10:56 +0100 Subject: [PATCH 39/48] useBlockSync: change subscribed.current on unsubscribe (#47752) --- .../block-editor/src/components/provider/use-block-sync.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/provider/use-block-sync.js b/packages/block-editor/src/components/provider/use-block-sync.js index e17a9054f5d123..6e9b2c0474e15a 100644 --- a/packages/block-editor/src/components/provider/use-block-sync.js +++ b/packages/block-editor/src/components/provider/use-block-sync.js @@ -265,6 +265,9 @@ export default function useBlockSync( { previousAreBlocksDifferent = areBlocksDifferent; } ); - return () => unsubscribe(); + return () => { + subscribed.current = false; + unsubscribe(); + }; }, [ registry, clientId ] ); } From 312d524360168b37af42b05520cdd52e132a04e4 Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Mon, 6 Feb 2023 21:48:17 +1100 Subject: [PATCH 40/48] CustomSelectControl: Privatise __experimentalShowSelectedHint using @wordpress/experiments (#47229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What? Part of https://github.com/WordPress/gutenberg/issues/47196. Uses `@wordpress/experiments` (https://github.com/WordPress/gutenberg/pull/46131) to make `__experimentalShowSelectedHint` in `CustomSelectControl` private. ## Why? We don't want to add any new experimental APIs to 6.2 as part of an effort to no longer expose experimental APIs in Core. ## How? https://github.com/WordPress/gutenberg/blob/trunk/docs/contributors/code/coding-guidelines.md#experimental-react-component-properties ## Testing Instructions 1. Use a block theme with more than 5 font sizes or manually edit `theme.json` to contain more than 5 font sizes in `settings.typography.fontSizes`. 2. Open the site editor. Appearance → Editor → Edit. 3. Go to Styles → Typography → Headings. 4. Select a heading level. 5. Toggle off the custom font size picker. 6. You should see a hint alongside the selected font size preset. Co-authored-by: Adam Zieliński --- package-lock.json | 1 + package.json | 3 +- .../block-tools/selected-block-popover.js | 2 +- .../off-canvas-editor/block-contents.js | 2 +- .../src/components/provider/index.js | 2 +- packages/block-editor/src/experiments.js | 12 +----- packages/block-editor/src/hooks/dimensions.js | 2 +- packages/block-editor/src/hooks/position.js | 8 +++- packages/block-editor/src/lock-unlock.js | 10 +++++ packages/block-editor/src/store/index.js | 2 +- packages/block-editor/tsconfig.json | 1 + packages/block-library/tsconfig.json | 1 + packages/components/package.json | 1 + .../src/custom-select-control/index.js | 9 ++++ .../custom-select-control/stories/index.js | 2 +- .../src/custom-select-control/test/index.js | 4 +- packages/components/src/experiments.js | 20 +++++++++ packages/components/src/index.js | 5 ++- packages/components/tsconfig.json | 1 + packages/data/tsconfig.json | 1 + packages/experiments/package.json | 1 + packages/experiments/src/implementation.js | 43 +++++++++++++++---- packages/experiments/tsconfig.json | 9 ++++ storybook/main.js | 24 +++-------- test/native/setup.js | 8 ++++ ...s-gutenberg-plugin.js => gutenberg-env.js} | 7 +++ test/unit/jest.config.js | 2 +- tools/webpack/shared.js | 3 ++ tsconfig.json | 2 + typings/gutenberg-env/index.d.ts | 2 + 30 files changed, 141 insertions(+), 49 deletions(-) create mode 100644 packages/block-editor/src/lock-unlock.js create mode 100644 packages/components/src/experiments.js create mode 100644 packages/experiments/tsconfig.json rename test/unit/config/{is-gutenberg-plugin.js => gutenberg-env.js} (73%) diff --git a/package-lock.json b/package-lock.json index 3893e6fe012573..b320c8955e3e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17444,6 +17444,7 @@ "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", "@wordpress/escape-html": "file:packages/escape-html", + "@wordpress/experiments": "file:packages/experiments", "@wordpress/hooks": "file:packages/hooks", "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", diff --git a/package.json b/package.json index 2ecfed4c20fda7..2d6e7ab238d0fc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "npm": ">=6.9.0 <7" }, "config": { - "IS_GUTENBERG_PLUGIN": true + "IS_GUTENBERG_PLUGIN": true, + "ALLOW_EXPERIMENT_REREGISTRATION": true }, "dependencies": { "@wordpress/a11y": "file:packages/a11y", diff --git a/packages/block-editor/src/components/block-tools/selected-block-popover.js b/packages/block-editor/src/components/block-tools/selected-block-popover.js index 6ebd743f22d2f8..b4d14296e823d4 100644 --- a/packages/block-editor/src/components/block-tools/selected-block-popover.js +++ b/packages/block-editor/src/components/block-tools/selected-block-popover.js @@ -21,7 +21,7 @@ import { store as blockEditorStore } from '../../store'; import BlockPopover from '../block-popover'; import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; import Inserter from '../inserter'; -import { unlock } from '../../experiments'; +import { unlock } from '../../lock-unlock'; function selector( select ) { const { diff --git a/packages/block-editor/src/components/off-canvas-editor/block-contents.js b/packages/block-editor/src/components/off-canvas-editor/block-contents.js index 4048f25b49c989..78e97a6c0cf335 100644 --- a/packages/block-editor/src/components/off-canvas-editor/block-contents.js +++ b/packages/block-editor/src/components/off-canvas-editor/block-contents.js @@ -12,7 +12,7 @@ import { forwardRef, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { unlock } from '../../experiments'; +import { unlock } from '../../lock-unlock'; import ListViewBlockSelectButton from './block-select-button'; import BlockDraggable from '../block-draggable'; import { store as blockEditorStore } from '../../store'; diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 323ce68765c40a..dbd646426718de 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -11,7 +11,7 @@ import withRegistryProvider from './with-registry-provider'; import useBlockSync from './use-block-sync'; import { store as blockEditorStore } from '../../store'; import { BlockRefsProvider } from './block-refs-provider'; -import { unlock } from '../../experiments'; +import { unlock } from '../../lock-unlock'; /** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ diff --git a/packages/block-editor/src/experiments.js b/packages/block-editor/src/experiments.js index d954fc7515530d..2e59430ae78b2f 100644 --- a/packages/block-editor/src/experiments.js +++ b/packages/block-editor/src/experiments.js @@ -1,21 +1,11 @@ -/** - * WordPress dependencies - */ -import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments'; - /** * Internal dependencies */ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; +import { lock } from './lock-unlock'; import OffCanvasEditor from './components/off-canvas-editor'; -export const { lock, unlock } = - __dangerousOptInToUnstableAPIsOnlyForCoreModules( - 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', - '@wordpress/block-editor' - ); - /** * Experimental @wordpress/block-editor APIs. */ diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index e520f5dd7b4acd..14384d19b0000b 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -59,7 +59,7 @@ import { } from './child-layout'; import useSetting from '../components/use-setting'; import { store as blockEditorStore } from '../store'; -import { unlock } from '../experiments'; +import { unlock } from '../lock-unlock'; export const DIMENSIONS_SUPPORT_KEY = 'dimensions'; export const SPACING_SUPPORT_KEY = 'spacing'; diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index d2924a84db4397..c5176304fb958d 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -8,7 +8,10 @@ import classnames from 'classnames'; */ import { __, sprintf } from '@wordpress/i18n'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; -import { BaseControl, CustomSelectControl } from '@wordpress/components'; +import { + BaseControl, + experiments as componentsExperiments, +} from '@wordpress/components'; import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { @@ -26,8 +29,11 @@ import BlockList from '../components/block-list'; import useSetting from '../components/use-setting'; import InspectorControls from '../components/inspector-controls'; import { cleanEmptyObject } from './utils'; +import { unlock } from '../lock-unlock'; import { store as blockEditorStore } from '../store'; +const { CustomSelectControl } = unlock( componentsExperiments ); + const POSITION_SUPPORT_KEY = 'position'; const OPTION_CLASSNAME = diff --git a/packages/block-editor/src/lock-unlock.js b/packages/block-editor/src/lock-unlock.js new file mode 100644 index 00000000000000..09199196e9cf05 --- /dev/null +++ b/packages/block-editor/src/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', + '@wordpress/block-editor' + ); diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 346979e6106457..ed17b387ba5884 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -12,7 +12,7 @@ import * as privateActions from './private-actions'; import * as privateSelectors from './private-selectors'; import * as actions from './actions'; import { STORE_NAME } from './constants'; -import { unlock } from '../experiments'; +import { unlock } from '../lock-unlock'; /** * Block editor data store configuration. diff --git a/packages/block-editor/tsconfig.json b/packages/block-editor/tsconfig.json index 79adc0b78c6164..37432183cfc3c1 100644 --- a/packages/block-editor/tsconfig.json +++ b/packages/block-editor/tsconfig.json @@ -16,6 +16,7 @@ { "path": "../dom" }, { "path": "../element" }, { "path": "../escape-html" }, + { "path": "../experiments" }, { "path": "../hooks" }, { "path": "../html-entities" }, { "path": "../i18n" }, diff --git a/packages/block-library/tsconfig.json b/packages/block-library/tsconfig.json index f24b0524d7c7b0..dedc1d7db2daf0 100644 --- a/packages/block-library/tsconfig.json +++ b/packages/block-library/tsconfig.json @@ -21,6 +21,7 @@ { "path": "../dom" }, { "path": "../element" }, { "path": "../escape-html" }, + { "path": "../experiments" }, { "path": "../hooks" }, { "path": "../html-entities" }, { "path": "../i18n" }, diff --git a/packages/components/package.json b/packages/components/package.json index 531b1a6fa46e58..f31816feecb484 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -45,6 +45,7 @@ "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", "@wordpress/escape-html": "file:../escape-html", + "@wordpress/experiments": "file:../experiments", "@wordpress/hooks": "file:../hooks", "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", diff --git a/packages/components/src/custom-select-control/index.js b/packages/components/src/custom-select-control/index.js index 326bd42ac53b3a..10b9f0a307c010 100644 --- a/packages/components/src/custom-select-control/index.js +++ b/packages/components/src/custom-select-control/index.js @@ -257,3 +257,12 @@ export default function CustomSelectControl( props ) { ); } + +export function StableCustomSelectControl( props ) { + return ( + + ); +} diff --git a/packages/components/src/custom-select-control/stories/index.js b/packages/components/src/custom-select-control/stories/index.js index 4891bcf2109378..68f179864f86d5 100644 --- a/packages/components/src/custom-select-control/stories/index.js +++ b/packages/components/src/custom-select-control/stories/index.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import CustomSelectControl from '../'; +import CustomSelectControl from '..'; export default { title: 'Components/CustomSelectControl', diff --git a/packages/components/src/custom-select-control/test/index.js b/packages/components/src/custom-select-control/test/index.js index 413760b63ebed7..3ef6d52ab1f6ec 100644 --- a/packages/components/src/custom-select-control/test/index.js +++ b/packages/components/src/custom-select-control/test/index.js @@ -4,9 +4,9 @@ import { render, fireEvent, screen } from '@testing-library/react'; /** - * WordPress dependencies + * Internal dependencies */ -import { CustomSelectControl } from '@wordpress/components'; +import CustomSelectControl from '..'; describe( 'CustomSelectControl', () => { it( 'Captures the keypress event and does not let it propagate', () => { diff --git a/packages/components/src/experiments.js b/packages/components/src/experiments.js new file mode 100644 index 00000000000000..4133f094077d23 --- /dev/null +++ b/packages/components/src/experiments.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments'; + +/** + * Internal dependencies + */ +import { default as CustomSelectControl } from './custom-select-control'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.', + '@wordpress/components' + ); + +export const experiments = {}; +lock( experiments, { + CustomSelectControl, +} ); diff --git a/packages/components/src/index.js b/packages/components/src/index.js index ee20e323a363e9..3cf3a97f044549 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -59,7 +59,7 @@ export { useCompositeState as __unstableUseCompositeState, } from './composite'; export { ConfirmDialog as __experimentalConfirmDialog } from './confirm-dialog'; -export { default as CustomSelectControl } from './custom-select-control'; +export { StableCustomSelectControl as CustomSelectControl } from './custom-select-control'; export { default as Dashicon } from './dashicon'; export { default as DateTimePicker, DatePicker, TimePicker } from './date-time'; export { default as __experimentalDimensionControl } from './dimension-control'; @@ -212,3 +212,6 @@ export { } from './higher-order/with-focus-return'; export { default as withNotices } from './higher-order/with-notices'; export { default as withSpokenMessages } from './higher-order/with-spoken-messages'; + +// Experiments. +export { experiments } from './experiments'; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 131dd444a7f725..5c65c848174a58 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -25,6 +25,7 @@ { "path": "../dom" }, { "path": "../element" }, { "path": "../escape-html" }, + { "path": "../experiments" }, { "path": "../hooks" }, { "path": "../html-entities" }, { "path": "../i18n" }, diff --git a/packages/data/tsconfig.json b/packages/data/tsconfig.json index c604c1785853c0..fc80d7ed5fc8ff 100644 --- a/packages/data/tsconfig.json +++ b/packages/data/tsconfig.json @@ -10,6 +10,7 @@ { "path": "../compose" }, { "path": "../deprecated" }, { "path": "../element" }, + { "path": "../experiments" }, { "path": "../is-shallow-equal" }, { "path": "../priority-queue" }, { "path": "../redux-routine" } diff --git a/packages/experiments/package.json b/packages/experiments/package.json index 4d71c3980150a6..e45d4c4a569864 100644 --- a/packages/experiments/package.json +++ b/packages/experiments/package.json @@ -25,6 +25,7 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0" diff --git a/packages/experiments/src/implementation.js b/packages/experiments/src/implementation.js index 143411ba640442..26502a47231112 100644 --- a/packages/experiments/src/implementation.js +++ b/packages/experiments/src/implementation.js @@ -10,20 +10,23 @@ * The list of core modules allowed to opt-in to the experimental APIs. */ const CORE_MODULES_USING_EXPERIMENTS = [ - '@wordpress/data', - '@wordpress/editor', - '@wordpress/blocks', '@wordpress/block-editor', + '@wordpress/block-library', + '@wordpress/blocks', + '@wordpress/components', '@wordpress/customize-widgets', - '@wordpress/edit-site', + '@wordpress/data', '@wordpress/edit-post', + '@wordpress/edit-site', '@wordpress/edit-widgets', - '@wordpress/block-library', + '@wordpress/editor', ]; /** * A list of core modules that already opted-in to * the experiments package. + * + * @type {string[]} */ const registeredExperiments = []; @@ -44,6 +47,24 @@ const registeredExperiments = []; const requiredConsent = 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.'; +/** @type {boolean} */ +let allowReRegistration; +// Use try/catch to force "false" if the environment variable is not explicitly +// set to true (e.g. when building WordPress core). +try { + allowReRegistration = process.env.ALLOW_EXPERIMENT_REREGISTRATION ?? false; +} catch ( error ) { + allowReRegistration = false; +} + +/** + * Called by a @wordpress package wishing to opt-in to accessing or exposing + * private experimental APIs. + * + * @param {string} consent The consent string. + * @param {string} moduleName The name of the module that is opting in. + * @return {{lock: typeof lock, unlock: typeof unlock}} An object containing the lock and unlock functions. + */ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( consent, moduleName @@ -57,7 +78,13 @@ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( 'your product will inevitably break on one of the next WordPress releases.' ); } - if ( registeredExperiments.includes( moduleName ) ) { + if ( + ! allowReRegistration && + registeredExperiments.includes( moduleName ) + ) { + // This check doesn't play well with Story Books / Hot Module Reloading + // and isn't included in the Gutenberg plugin. It only matters in the + // WordPress core release. throw new Error( `You tried to opt-in to unstable APIs as module "${ moduleName }" which is already registered. ` + 'This feature is only for JavaScript modules shipped with WordPress core. ' + @@ -104,8 +131,8 @@ export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( * // { a: 1 } * ``` * - * @param {Object|Function} object The object to bind the private data to. - * @param {any} privateData The private data to bind to the object. + * @param {any} object The object to bind the private data to. + * @param {any} privateData The private data to bind to the object. */ function lock( object, privateData ) { if ( ! object ) { diff --git a/packages/experiments/tsconfig.json b/packages/experiments/tsconfig.json new file mode 100644 index 00000000000000..671d4a5eba4403 --- /dev/null +++ b/packages/experiments/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "types": [ "gutenberg-env" ] + }, + "include": [ "src/**/*" ] +} diff --git a/storybook/main.js b/storybook/main.js index 3397c831f4da3f..92f2d7b4998e5d 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -5,8 +5,6 @@ const stories = [ '../packages/icons/src/**/stories/*.@(js|tsx|mdx)', ].filter( Boolean ); -const customEnvVariables = {}; - module.exports = { core: { builder: 'webpack5', @@ -30,20 +28,10 @@ module.exports = { emotionAlias: false, storyStoreV7: true, }, - // Workaround: - // https://github.com/storybookjs/storybook/issues/12270 - webpackFinal: async ( config ) => { - // Find the DefinePlugin. - const plugin = config.plugins.find( ( p ) => { - return p.definitions && p.definitions[ 'process.env' ]; - } ); - // Add custom env variables. - Object.keys( customEnvVariables ).forEach( ( key ) => { - plugin.definitions[ 'process.env' ][ key ] = JSON.stringify( - customEnvVariables[ key ] - ); - } ); - - return config; - }, + env: ( config ) => ( { + ...config, + // Inject the `ALLOW_EXPERIMENT_REREGISTRATION` global, used by + // @wordpress/experiments. + ALLOW_EXPERIMENT_REREGISTRATION: true, + } ), }; diff --git a/test/native/setup.js b/test/native/setup.js index 5f1801a338985f..b9e32a51d28799 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -8,6 +8,14 @@ import { Image, NativeModules as RNNativeModules } from 'react-native'; // testing environment: https://github.com/facebook/react-native/blob/6c19dc3266b84f47a076b647a1c93b3c3b69d2c5/Libraries/Core/setUpNavigator.js#L17 global.navigator = global.navigator ?? {}; +/** + * Whether to allow the same experiment to be registered multiple times. + * This is useful for development purposes, but should be set to false + * during the unit tests to ensure the Gutenberg plugin can be cleanly + * merged into WordPress core where this is false. + */ +global.process.env.ALLOW_EXPERIMENT_REREGISTRATION = true; + // Set up the app runtime globals for the test environment, which includes // modifying the above `global.navigator` require( '../../packages/react-native-editor/src/globals' ); diff --git a/test/unit/config/is-gutenberg-plugin.js b/test/unit/config/gutenberg-env.js similarity index 73% rename from test/unit/config/is-gutenberg-plugin.js rename to test/unit/config/gutenberg-env.js index 7af5e4ad9a8647..72527ecb725a3c 100644 --- a/test/unit/config/is-gutenberg-plugin.js +++ b/test/unit/config/gutenberg-env.js @@ -22,4 +22,11 @@ global.process.env = { // eslint-disable-next-line @wordpress/is-gutenberg-plugin IS_GUTENBERG_PLUGIN: String( process.env.npm_package_config_IS_GUTENBERG_PLUGIN ) === 'true', + /** + * Whether to allow the same experiment to be registered multiple times. + * This is useful for development purposes, but should be set to false + * during the unit tests to ensure the Gutenberg plugin can be cleanly + * merged into WordPress core where this is false. + */ + ALLOW_EXPERIMENT_REREGISTRATION: false, }; diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 77d7818bacc852..0f0480072e569a 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -17,7 +17,7 @@ module.exports = { preset: '@wordpress/jest-preset-default', setupFiles: [ '/test/unit/config/global-mocks.js', - '/test/unit/config/is-gutenberg-plugin.js', + '/test/unit/config/gutenberg-env.js', ], setupFilesAfterEnv: [ '/test/unit/config/testing-library.js' ], testURL: 'http://localhost', diff --git a/tools/webpack/shared.js b/tools/webpack/shared.js index a8079439528659..414ac4adf940b3 100644 --- a/tools/webpack/shared.js +++ b/tools/webpack/shared.js @@ -66,6 +66,9 @@ const plugins = [ // Inject the `IS_GUTENBERG_PLUGIN` global, used for feature flagging. 'process.env.IS_GUTENBERG_PLUGIN': process.env.npm_package_config_IS_GUTENBERG_PLUGIN, + // Inject the `ALLOW_EXPERIMENT_REREGISTRATION` global, used by @wordpress/experiments. + 'process.env.ALLOW_EXPERIMENT_REREGISTRATION': + process.env.npm_package_config_ALLOW_EXPERIMENT_REREGISTRATION, } ), mode === 'production' && new ReadableJsAssetsWebpackPlugin(), ]; diff --git a/tsconfig.json b/tsconfig.json index 633406a0154d56..10bb351d891807 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,8 +21,10 @@ { "path": "packages/element" }, { "path": "packages/escape-html" }, { "path": "packages/eslint-plugin" }, + { "path": "packages/experiments" }, { "path": "packages/hooks" }, { "path": "packages/html-entities" }, + { "path": "packages/html-entities" }, { "path": "packages/i18n" }, { "path": "packages/icons" }, { "path": "packages/is-shallow-equal" }, diff --git a/typings/gutenberg-env/index.d.ts b/typings/gutenberg-env/index.d.ts index 7fe6d587446f59..834c307515893b 100644 --- a/typings/gutenberg-env/index.d.ts +++ b/typings/gutenberg-env/index.d.ts @@ -1,5 +1,7 @@ interface Environment { NODE_ENV: unknown; + IS_GUTENBERG_PLUGIN?: boolean; + ALLOW_EXPERIMENT_REREGISTRATION?: boolean; } interface Process { env: Environment; From 006337e472b3d2f48566a87eca0ab1400b6285cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Tue, 10 Jan 2023 12:54:33 +0200 Subject: [PATCH 41/48] Zoom out mode: scale iframe instead of contents --- .../src/components/iframe/index.js | 33 +++++++++---------- packages/components/src/popover/index.tsx | 22 +++++++++++-- packages/components/src/popover/utils.ts | 27 +++++++++++++++ .../components/block-editor/editor-canvas.js | 1 + 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 71ac295979cec7..3d289ba2defcfe 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -108,6 +108,7 @@ function Iframe( { tabIndex = 0, scale = 1, frameSize = 0, + expand = false, readonly, forwardedRef: ref, ...props @@ -251,6 +252,20 @@ function Iframe( { { tabIndex >= 0 && before }
Test
Test