diff --git a/.eslintrc.js b/.eslintrc.js index a81369b61c5f1..fd7e9183781ea 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -294,7 +294,7 @@ module.exports = { 'packages/e2e-test-utils-playwright/**/*.[tj]s', ], extends: [ - 'plugin:eslint-plugin-playwright/playwright-test', + 'plugin:@wordpress/eslint-plugin/test-playwright', 'plugin:@typescript-eslint/base', ], parserOptions: { @@ -308,7 +308,6 @@ module.exports = { rules: { '@wordpress/no-global-active-element': 'off', '@wordpress/no-global-get-selection': 'off', - 'playwright/no-page-pause': 'error', 'no-restricted-syntax': [ 'error', { diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 5c4c7b068108f..27a1d7290f2b5 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -66,13 +66,13 @@ jobs: - name: Compare performance with base branch if: github.event_name == 'push' # The base hash used here need to be a commit that is compatible with the current WP version - # The current one is 34af5829ac9edb31833167ff6a3b51bea982999c and it needs to be updated every WP major release. + # The current one is bd2a881101727b03b0be09382b34841af5a3c03e and it needs to be updated every WP major release. # It is used as a base comparison point to avoid fluctuation in the performance metrics. run: | WP_VERSION=$(awk -F ': ' '/^Tested up to/{print $2}' readme.txt) IFS=. read -ra WP_VERSION_ARRAY <<< "$WP_VERSION" WP_MAJOR="${WP_VERSION_ARRAY[0]}.${WP_VERSION_ARRAY[1]}" - ./bin/plugin/cli.js perf $GITHUB_SHA 34af5829ac9edb31833167ff6a3b51bea982999c --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + ./bin/plugin/cli.js perf $GITHUB_SHA bd2a881101727b03b0be09382b34841af5a3c03e --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - name: Compare performance with custom branches if: github.event_name == 'workflow_dispatch' @@ -95,7 +95,7 @@ jobs: CODEHEALTH_PROJECT_TOKEN: ${{ secrets.CODEHEALTH_PROJECT_TOKEN }} run: | COMMITTED_AT=$(git show -s $GITHUB_SHA --format="%cI") - ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA 34af5829ac9edb31833167ff6a3b51bea982999c $COMMITTED_AT + ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA bd2a881101727b03b0be09382b34841af5a3c03e $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index dd1f188e0ae2f..52a6d8f9833f2 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -30,12 +30,14 @@ jobs: environment: WordPress packages steps: - name: Checkout (for CLI) + if: ${{ github.event.inputs.release_type != 'wp' }} uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: path: cli ref: trunk - name: Checkout (for publishing) + if: ${{ github.event.inputs.release_type != 'wp' }} uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: path: publish @@ -43,6 +45,14 @@ jobs: ref: trunk token: ${{ secrets.GUTENBERG_TOKEN }} + - name: Checkout (for publishing WP major version) + if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + path: publish + ref: wp/${{ github.event.inputs.wp_version }} + token: ${{ secrets.GUTENBERG_TOKEN }} + - name: Configure git user name and email (for publishing) run: | cd publish @@ -50,11 +60,19 @@ jobs: git config user.email gutenberg@wordpress.org - name: Setup Node.js + if: ${{ github.event.inputs.release_type != 'wp' }} uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version-file: 'cli/.nvmrc' registry-url: 'https://registry.npmjs.org' + - name: Setup Node.js (for WP major version) + if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + with: + node-version-file: 'publish/.nvmrc' + registry-url: 'https://registry.npmjs.org' + - name: Publish development packages to npm ("next" dist-tag) if: ${{ github.event.inputs.release_type == 'development' }} run: | @@ -73,7 +91,7 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Publish packages to npm for WP major ("wp/${{ github.event.inputs.wp_version || 'X.Y' }}" dist-tag) + - name: Publish packages to npm for WP major version ("wp/${{ github.event.inputs.wp_version || 'X.Y' }}" dist-tag) if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} run: | cd publish diff --git a/bin/cherry-pick.mjs b/bin/cherry-pick.mjs index baf8d42962f8e..91ded84489e68 100644 --- a/bin/cherry-pick.mjs +++ b/bin/cherry-pick.mjs @@ -118,19 +118,10 @@ async function fetchPRs() { id, number, title, - closed_at, pull_request, - } ) ).sort( ( a, b ) => { - /* - * `closed_at` and `pull_request.merged_at` are _usually_ the same, - * but let's prefer the latter if it's available. - */ - if ( a?.pull_request?.merged_at && b?.pull_request?.merged_at ) { - return new Date( a?.pull_request?.merged_at ) - new Date( b?.pull_request?.merged_at ); - } - return new Date( a.closed_at ) - new Date( b.closed_at ); - } ); - + } ) ) + .filter( ( { pull_request } ) => !! pull_request?.merged_at ) + .sort( ( a, b ) => new Date( a?.pull_request?.merged_at ) - new Date( b?.pull_request?.merged_at ) ); console.log( 'Found the following PRs to cherry-pick (sorted by closed date in ascending order): ' ); PRs.forEach( ( { number, title } ) => diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 02cb5f7afdd81..59d77c3fe9610 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -72,7 +72,7 @@ const LABEL_TYPE_MAPPING = { '[Type] Project Management': 'Tools', '[Package] Scripts': 'Tools', '[Type] Build Tooling': 'Tools', - 'Automated Testing': 'Tools', + '[Type] Automated Testing': 'Tools', '[Package] Dependency Extraction Webpack Plugin': 'Tools', '[Type] Code Quality': 'Code Quality', '[Type] Accessibility (a11y)': 'Accessibility', @@ -128,7 +128,7 @@ const LABEL_FEATURE_MAPPING = { 'New Block': 'Block Library', '[Package] E2E Tests': 'Testing', '[Package] E2E Test Utils': 'Testing', - 'Automated Testing': 'Testing', + '[Type] Automated Testing': 'Testing', 'CSS Styling': 'CSS & Styling', 'developer-docs': 'Documentation', '[Type] Developer Documentation': 'Documentation', diff --git a/bin/plugin/commands/packages.js b/bin/plugin/commands/packages.js index 87ecc6eb89cfc..4cf509764436c 100644 --- a/bin/plugin/commands/packages.js +++ b/bin/plugin/commands/packages.js @@ -150,6 +150,7 @@ async function runNpmReleaseBranchSyncStep( pluginReleaseBranch, config ) { */ await repo .raw( 'rm', '-r', '.' ) + .fetch( 'origin', pluginReleaseBranch, [ '--depth=1' ] ) .raw( 'checkout', `origin/${ pluginReleaseBranch }`, '--', '.' ); const { commit: commitHash } = await repo.commit( diff --git a/bin/plugin/commands/test/__snapshots__/changelog.js.snap b/bin/plugin/commands/test/__snapshots__/changelog.js.snap index 571019ea3dca9..9ecb797fa5683 100644 --- a/bin/plugin/commands/test/__snapshots__/changelog.js.snap +++ b/bin/plugin/commands/test/__snapshots__/changelog.js.snap @@ -89,6 +89,7 @@ exports[`getChangelog verify that the changelog is properly formatted 1`] = ` ### Performance +- Add search performance measure and make other measures more stable. ([33848](https://github.com/WordPress/gutenberg/pull/33848)) - Avoid double parsing the content when loading the editor. ([33727](https://github.com/WordPress/gutenberg/pull/33727)) #### Block Library @@ -170,7 +171,6 @@ exports[`getChangelog verify that the changelog is properly formatted 1`] = ` - Scripts: Update webpack to v5 (try 2). ([33818](https://github.com/WordPress/gutenberg/pull/33818)) #### Testing -- Add search performance measure and make other measures more stable. ([33848](https://github.com/WordPress/gutenberg/pull/33848)) - E2E: Block Hierarchy Navigation wait for the column to be highlighted. ([33721](https://github.com/WordPress/gutenberg/pull/33721)) diff --git a/changelog.txt b/changelog.txt index 0149b4c2b7bf5..0f4f5bcf9bbcf 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,277 @@ == Changelog == -= 16.5.0-rc.1 = += 16.6.0-rc.1 = + +## Changelog + +### Features + +#### Interactivity API +- Add Slot and Fill directives. ([53958](https://github.com/WordPress/gutenberg/pull/53958)) +- Query block: Client-side pagination. ([53812](https://github.com/WordPress/gutenberg/pull/53812)) +- Update `data-wp-bind` directive logic. ([54003](https://github.com/WordPress/gutenberg/pull/54003)) + + +### Enhancements + +- Bundle ObserveTyping within the BlockList component. ([53875](https://github.com/WordPress/gutenberg/pull/53875)) +- Default appender: Hide the dashed indicator until ancestor is selected. ([53761](https://github.com/WordPress/gutenberg/pull/53761)) +- Register the block editor keyboard shortcuts automatically when using BlockEditorProvider. ([53910](https://github.com/WordPress/gutenberg/pull/53910)) +- [Commands]: Add toggle list view command in site editor. ([53983](https://github.com/WordPress/gutenberg/pull/53983)) + +#### Components +- Bundle SlotFillProvider within BlockEditorProvider. ([53940](https://github.com/WordPress/gutenberg/pull/53940)) +- Make the Popover.Slot optional. ([53889](https://github.com/WordPress/gutenberg/pull/53889)) +- Popover: Update `@floating-ui` to latest version, remove custom fix for iframe positioning and scaling. ([46845](https://github.com/WordPress/gutenberg/pull/46845)) +- `AlignmentMatrixControl`: Replace `act()` with `userEvent`. ([53703](https://github.com/WordPress/gutenberg/pull/53703)) +- `ProgressBar`: Add transition to determinate indicator. ([53877](https://github.com/WordPress/gutenberg/pull/53877)) + +#### Block Library +- Blocks: Move bootstrapped block types to Redux state. ([53807](https://github.com/WordPress/gutenberg/pull/53807)) +- Capture toolbars in navigation block. ([53697](https://github.com/WordPress/gutenberg/pull/53697)) +- Content Block: Change placeholder and end-to-end test to refer to Content block. ([53902](https://github.com/WordPress/gutenberg/pull/53902)) +- Make mid size parameter settable for Query Pagination block. ([51216](https://github.com/WordPress/gutenberg/pull/51216)) + +#### Block Editor +- Capture toolbars in quote block. ([53699](https://github.com/WordPress/gutenberg/pull/53699)) +- Improve writing flow for lists by capturing list item toolbars. ([53306](https://github.com/WordPress/gutenberg/pull/53306)) +- RichTextValue: Typescript Adjustment. ([54002](https://github.com/WordPress/gutenberg/pull/54002)) + +#### Typography +- Font Face: Prepare for merge into Core. ([53858](https://github.com/WordPress/gutenberg/pull/53858)) +- Renames "Fonts Library" to "Font Library". ([53780](https://github.com/WordPress/gutenberg/pull/53780)) + +#### Post Editor +- Edit Post: Use hooks instead of HoCs in `TaxonomyPanel`. ([53773](https://github.com/WordPress/gutenberg/pull/53773)) + +#### List View +- Add keyboard shortcut for duplicating blocks. ([53559](https://github.com/WordPress/gutenberg/pull/53559)) + +#### Patterns +- Add a custom taxonomy for user created patterns. ([53163](https://github.com/WordPress/gutenberg/pull/53163)) + + +### New APIs + +#### Interactivity API +- Router with region-based client-side navigation. ([53733](https://github.com/WordPress/gutenberg/pull/53733)) + + +### Bug Fixes + +- Add missing aria roles for block locking toolbar and menu buttons. ([53734](https://github.com/WordPress/gutenberg/pull/53734)) +- Block Editor: Fix cleanup in the 'useNavModeExit' hook. ([53795](https://github.com/WordPress/gutenberg/pull/53795)) +- Command Palette: Fix crash on block-related commands. ([53923](https://github.com/WordPress/gutenberg/pull/53923)) +- Date: Add relative time translations for moment.js. ([53931](https://github.com/WordPress/gutenberg/pull/53931)) +- Date: Update translation domains for strings to be translatable. ([53995](https://github.com/WordPress/gutenberg/pull/53995)) +- Iframe: Set character encoding to utf-8. ([53519](https://github.com/WordPress/gutenberg/pull/53519)) +- Replace horizontal ellipsis icon with vertical ellipsis icon. ([52731](https://github.com/WordPress/gutenberg/pull/52731)) +- Toggle Distraction free mode mode based on compatibility. ([54030](https://github.com/WordPress/gutenberg/pull/54030)) +- Warning: Introduce `SCRIPT_DEBUG` to make the package compatible with webpack 5. ([50122](https://github.com/WordPress/gutenberg/pull/50122)) +- [Commands]: Fix `move to` command condition for registering. ([54049](https://github.com/WordPress/gutenberg/pull/54049)) +- [Commands]: Fix block editor commands availability. ([53994](https://github.com/WordPress/gutenberg/pull/53994)) +- [Format library]: Fix `language` popover position. ([53841](https://github.com/WordPress/gutenberg/pull/53841)) + +#### Block Library +- Add init.js module for the Footnotes block. ([53763](https://github.com/WordPress/gutenberg/pull/53763)) +- Adding center align css for social icon issue. ([43120](https://github.com/WordPress/gutenberg/pull/43120)) +- Cover block: Fix exception when adding video background. ([53961](https://github.com/WordPress/gutenberg/pull/53961)) +- List View: Allow replacing template part when a block isn't selected. ([53757](https://github.com/WordPress/gutenberg/pull/53757)) +- Post Navigation Link: Remove unnecessary space between arrows and label. ([53572](https://github.com/WordPress/gutenberg/pull/53572)) +- Search block: Fix width input field. ([53952](https://github.com/WordPress/gutenberg/pull/53952)) +- Simplify check for no posts in query-no-results block. ([53772](https://github.com/WordPress/gutenberg/pull/53772)) +- Site Logo: Remove line-height for the anchor element. ([53909](https://github.com/WordPress/gutenberg/pull/53909)) + +#### Components +- Always render the fallback Popover anchor within the Popover's parent element. ([53982](https://github.com/WordPress/gutenberg/pull/53982)) +- Fix the cleanup method for SandBox. ([53796](https://github.com/WordPress/gutenberg/pull/53796)) +- PaletteEdit: Fix component height. ([54000](https://github.com/WordPress/gutenberg/pull/54000)) + +#### Post Editor +- Edit Post: Fix tab border conflicts in the Document Overview panel. ([53711](https://github.com/WordPress/gutenberg/pull/53711)) +- EditPostPreferencesModal: Fix intermittently failing tests. ([53814](https://github.com/WordPress/gutenberg/pull/53814)) +- getInsertionPoint: Fix type check for the state value. ([53793](https://github.com/WordPress/gutenberg/pull/53793)) + +#### npm Packages +- Workflow: Run Learn directly from GitHub action when publishing to npm targeting WP core. ([53762](https://github.com/WordPress/gutenberg/pull/53762)) +- Workflows: Fix issues with the npm publishing workflow when using locally. ([53565](https://github.com/WordPress/gutenberg/pull/53565)) + +#### Themes +- Command Palette: Proper handling of page/post links in all themes. ([53718](https://github.com/WordPress/gutenberg/pull/53718)) +- Fix query loop bugs by correctly relying on the main query and removing problematic workaround. ([49904](https://github.com/WordPress/gutenberg/pull/49904)) + +#### Block Editor +- Fix: Indicator style when block moving mode. ([53972](https://github.com/WordPress/gutenberg/pull/53972)) + +#### Icons +- Fix invalid namespaces. ([53955](https://github.com/WordPress/gutenberg/pull/53955)) + +#### Patterns +- Disable the preview option button when editing. ([53913](https://github.com/WordPress/gutenberg/pull/53913)) + +#### Global Styles +- Gallery: Re-enable block spacing at block level while still hiding in global styles. ([53900](https://github.com/WordPress/gutenberg/pull/53900)) + +#### Layout +- BlockList: Ensure element styles (and svg) are always appended at the end of the document. ([53859](https://github.com/WordPress/gutenberg/pull/53859)) + +#### Interactivity API +- Add "supports.interactivity" to Image block. ([53850](https://github.com/WordPress/gutenberg/pull/53850)) + +#### Style Variations +- Block Styles: Fix misplaced preview popover on RTL site. ([53726](https://github.com/WordPress/gutenberg/pull/53726)) + +#### List View +- Recalculate window list when expanded state changes (fix logic for long nested lists). ([53716](https://github.com/WordPress/gutenberg/pull/53716)) + +#### Widgets Editor +- Block Widget: Fix content cutoff in the keyboard shortcut modal. ([53638](https://github.com/WordPress/gutenberg/pull/53638)) + +#### Rich Text +- Fix cleanup in `useRemoveBrowserShortcuts`. ([52225](https://github.com/WordPress/gutenberg/pull/52225)) + + +### Accessibility + +- Edit site: Add missing label to post status password protected input field. ([52885](https://github.com/WordPress/gutenberg/pull/52885)) +- [a11y] Fix: Aria-haspop, aria-expanded attributes on the link format button. ([53691](https://github.com/WordPress/gutenberg/pull/53691)) + +#### Site Editor +- Add missing aria roles to the 'Create template part' menu item. ([53754](https://github.com/WordPress/gutenberg/pull/53754)) +- Unify the delete button style in the dropdown menu with red. ([52597](https://github.com/WordPress/gutenberg/pull/52597)) + +#### Block Library +- Add missing aria roles to the 'Replace template part' menu item. ([53755](https://github.com/WordPress/gutenberg/pull/53755)) + +#### Patterns +- Add missing aria roles to the 'Create pattern' menu item. ([53739](https://github.com/WordPress/gutenberg/pull/53739)) + +#### List View +- [a11y] Fix: Aria-haspop and aria-expanded attributes on list view button. ([53693](https://github.com/WordPress/gutenberg/pull/53693)) + +#### Block Editor +- [a11y] Fix: Aria-haspop and aria-expanded attributes on the inserter button. ([53692](https://github.com/WordPress/gutenberg/pull/53692)) + + +### Performance + +- Revert "Switch performance tests to Playwright (#52022)". ([53741](https://github.com/WordPress/gutenberg/pull/53741)) +- StartPageOptions: Load and parse patterns only after establishing the need for them. ([53673](https://github.com/WordPress/gutenberg/pull/53673)) +- Switch performance tests to Playwright: Take 2. ([53768](https://github.com/WordPress/gutenberg/pull/53768)) + + +### Experiments + +#### Block API +- Auto-inserting blocks: Add block inspector panel. ([52969](https://github.com/WordPress/gutenberg/pull/52969)) + + +### Documentation + +- Add juanmaguitar as codeowner of /packages/interactivity/docs. ([53845](https://github.com/WordPress/gutenberg/pull/53845)) +- Add new How-to Guide for enqueueing assets in the Editor. ([53828](https://github.com/WordPress/gutenberg/pull/53828)) +- Adds example for useBlockProps hook. ([53646](https://github.com/WordPress/gutenberg/pull/53646)) +- Adds explanatory text to view.js template. ([53870](https://github.com/WordPress/gutenberg/pull/53870)) +- Clarification for `parent` and `ancestor` hierarchical relationships. ([53855](https://github.com/WordPress/gutenberg/pull/53855)) +- Docs: Extend the information about using `render` with `block.json`. ([53973](https://github.com/WordPress/gutenberg/pull/53973)) +- Docs: Remove duplicate sections from FAQ page. ([53830](https://github.com/WordPress/gutenberg/pull/53830)) +- Document the naming convention for `block-library` PHP functions. ([53777](https://github.com/WordPress/gutenberg/pull/53777)) +- Fix 'lerna' links in the release documentation. ([53770](https://github.com/WordPress/gutenberg/pull/53770)) +- Fix typo in code sample for Interactivity API. ([53916](https://github.com/WordPress/gutenberg/pull/53916)) +- MenuItem: Add Storybook stories. ([53613](https://github.com/WordPress/gutenberg/pull/53613)) +- Shortcut: Add Storybook stories. ([53627](https://github.com/WordPress/gutenberg/pull/53627)) +- Storybook: Add back subcomponents to props table. ([53751](https://github.com/WordPress/gutenberg/pull/53751)) +- Storybook: Fix default source visibility. ([53749](https://github.com/WordPress/gutenberg/pull/53749)) +- Storybook: Show main story before description. ([53753](https://github.com/WordPress/gutenberg/pull/53753)) +- Update local instructions on the dev env documentation. ([53924](https://github.com/WordPress/gutenberg/pull/53924)) +- Update the Block Variations API doc. ([53817](https://github.com/WordPress/gutenberg/pull/53817)) +- Update to node 16 and npm 8 in the getting started with code contribution doc. ([53912](https://github.com/WordPress/gutenberg/pull/53912)) +- docs: Fix report-flaky-test link. ([53848](https://github.com/WordPress/gutenberg/pull/53848)) + + +### Code Quality + +- Components: Update Popover per reviews. ([53907](https://github.com/WordPress/gutenberg/pull/53907)) +- Edit Site: Rename `CanvasSpinner` to `CanvasLoader`. ([53728](https://github.com/WordPress/gutenberg/pull/53728)) +- Enforce valid function names in the packages/block-library/src/*/*.php files. ([53438](https://github.com/WordPress/gutenberg/pull/53438)) +- Fonts Library: Update properties name from snake case to camel case to match the rest of the properties. ([53746](https://github.com/WordPress/gutenberg/pull/53746)) + +#### Post Editor +- Editor: Fix the 'useSelect' warning in the 'useIsDirty' hook. ([53759](https://github.com/WordPress/gutenberg/pull/53759)) +- Fix browser console error when changing device preview mode. ([53969](https://github.com/WordPress/gutenberg/pull/53969)) +- Refactor latest content selectors in 'CopyContentMenuItem' components. ([53676](https://github.com/WordPress/gutenberg/pull/53676)) + +#### Components +- Remove unnecessary utils. ([53679](https://github.com/WordPress/gutenberg/pull/53679)) +- SlotFill: Refactor ``. ([53272](https://github.com/WordPress/gutenberg/pull/53272)) +- Storybook: Update TypeScript types. ([53748](https://github.com/WordPress/gutenberg/pull/53748)) + +#### List View +- Fix warning error when the gallery block has the same image URLs. ([53809](https://github.com/WordPress/gutenberg/pull/53809)) + +#### Typography +- Font Face API: Use `gutenberg_get_global_settings` instead of private API. ([53805](https://github.com/WordPress/gutenberg/pull/53805)) + + +### Tools + +- Try: Change PR label enforcer automation not to work on draft PRs by default. ([53417](https://github.com/WordPress/gutenberg/pull/53417)) + +#### Testing +- Attempt to fix intermittent end-to-end test failure. ([53905](https://github.com/WordPress/gutenberg/pull/53905)) +- Fonts Library: Test improvements. ([53702](https://github.com/WordPress/gutenberg/pull/53702)) +- Make fonts test files use Core approach. ([53856](https://github.com/WordPress/gutenberg/pull/53856)) +- Migrate shortcut help end-to-end tests to Playwright. ([53832](https://github.com/WordPress/gutenberg/pull/53832)) +- Relocates Font Face and Fonts Library PHP files into Core's fonts directory. ([53747](https://github.com/WordPress/gutenberg/pull/53747)) +- `ColorPalette`: Refine test query. ([53704](https://github.com/WordPress/gutenberg/pull/53704)) +- end-to-end Playwright Utils: Automatically detect canvas type. ([53744](https://github.com/WordPress/gutenberg/pull/53744)) +- test: Automate mobile editor tests. ([53991](https://github.com/WordPress/gutenberg/pull/53991)) + +#### Build Tooling +- Update Jest to latest version, and use optimized JSDOM. ([53736](https://github.com/WordPress/gutenberg/pull/53736)) + +#### Plugin +- Backport themes `is_block_theme` collection param from core. ([53846](https://github.com/WordPress/gutenberg/pull/53846)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @JEverhart383: Fix typo in code sample for Interactivity API. ([53916](https://github.com/WordPress/gutenberg/pull/53916)) +- @krokodok: Make mid size parameter settable for Query Pagination block. ([51216](https://github.com/WordPress/gutenberg/pull/51216)) +- @mklute101: Update local instructions on the dev env documentation. ([53924](https://github.com/WordPress/gutenberg/pull/53924)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @andrewserong @anton-vlasenko @bangank36 @brookewp @ciampo @colorful-tones @DAreRodz @dcalhoun @derekblank @ellatrix @felixarntz @geriux @glendaviesnz @gziolo @hellofromtonya @jasmussen @jblz @JEverhart383 @jordesign @jorgefilipecosta @jsnajdr @juanmaguitar @krokodok @luisherranz @Mamaduka @margolisj @matiasbenedetto @mburridge @mirka @mklute101 @mokagio @ndiego @ntsekouras @oandregal @ocean90 @ockham @priethor @ramonjd @richtabor @SiobhyB @Smit2808 @stokesman @t-hamano @torounit @tyxla @walbo @WunderBart @youknowriad + + += 16.5.1 = + + + +## Changelog + +### Bug Fixes + +#### Block Editor +- Pass entire link value on toggle of setting on Link Preview. ([53949](https://github.com/WordPress/gutenberg/pull/53949)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@getdave + + += 16.5.0 = @@ -13,166 +284,139 @@ ### Enhancements -#### Post Editor -- Command Palette: - - Add new block commands. ([52509](https://github.com/WordPress/gutenberg/pull/52509)) - - Add support for registering commands without icons. ([53647](https://github.com/WordPress/gutenberg/pull/53647)) - - Update the `Preview in a new tab` command to reuse the preview target tab when available. ([53242](https://github.com/WordPress/gutenberg/pull/53242)) - - Update command palette styling. ([53117](https://github.com/WordPress/gutenberg/pull/53117)) - - Improve command palette rendering on smaller viewports. ([53661](https://github.com/WordPress/gutenberg/pull/53661)) -- Replace `withPluginContext` in `PluginPostPublishPanel` ([53302](https://github.com/WordPress/gutenberg/pull/53302)) and `PluginPrePublishPanel` ([53304](https://github.com/WordPress/gutenberg/pull/53304)). -- Replace HoCs with hooks in: - - `PluginDocumentSettingPanel` ([53290](https://github.com/WordPress/gutenberg/pull/53290)) - - `PostPendingStatusCheck` ([53389](https://github.com/WordPress/gutenberg/pull/53389)) - - `PostPendingStatus` ([53387](https://github.com/WordPress/gutenberg/pull/53387)) -- Top-align Publish row in the post panel. ([53573](https://github.com/WordPress/gutenberg/pull/53573)) -- Dependencies: Bump `remove-accents` to 0.5.0. ([53420](https://github.com/WordPress/gutenberg/pull/53420)) +#### Commands +- Add block-related commands. ([52509](https://github.com/WordPress/gutenberg/pull/52509)) +- Add support for registering commands without icons. ([53647](https://github.com/WordPress/gutenberg/pull/53647)) +- Update the "Preview in a new tab" command to reuse the preview target tab when available. ([53242](https://github.com/WordPress/gutenberg/pull/53242)) +- Update command palette styling. ([53117](https://github.com/WordPress/gutenberg/pull/53117)) +- Improve command palette rendering on smaller viewports. ([53661](https://github.com/WordPress/gutenberg/pull/53661)) +- Tweak existing commands to establish consistency with command language. ([53496](https://github.com/WordPress/gutenberg/pull/53496)) +- End the command palette description with a period in the keyboard shortcut modal. ([53635](https://github.com/WordPress/gutenberg/pull/53635)) #### Components - Button: Remove default border from the destructive button. ([53607](https://github.com/WordPress/gutenberg/pull/53607)) - LineHeightControl: Allow for more granular control of decimal places. ([52902](https://github.com/WordPress/gutenberg/pull/52902)) - Snackbar: Design and motion improvements. ([53248](https://github.com/WordPress/gutenberg/pull/53248)) - Modal: - - Add `headerActions` prop to render buttons in the header. ([53328](https://github.com/WordPress/gutenberg/pull/53328)) - - Nuance outside interactions. ([52994](https://github.com/WordPress/gutenberg/pull/52994)) -- ProgressBar: - - Use gray 300 for track color. ([53349](https://github.com/WordPress/gutenberg/pull/53349)) - - Use the theme system accent for indicator color. ([53347](https://github.com/WordPress/gutenberg/pull/53347)) - - Use theme accent color variable. ([53632](https://github.com/WordPress/gutenberg/pull/53632)) -- Expose `Theme` via private APIs. ([53262](https://github.com/WordPress/gutenberg/pull/53262)) -- Move accent colors to theme context. ([53631](https://github.com/WordPress/gutenberg/pull/53631)) + - Add `headerActions` prop to enable buttons or other elements to be injected in the header. ([53328](https://github.com/WordPress/gutenberg/pull/53328)) + - Enhance overlay interactions, enabling outside interactions without dismissal. ([52994](https://github.com/WordPress/gutenberg/pull/52994)) +- ProgressBar: Update colors, including gray 300 for track color ([53349](https://github.com/WordPress/gutenberg/pull/53349)), theme system accent for indicator color ([53347](https://github.com/WordPress/gutenberg/pull/53347)), and the theme accent color variable. ([53632](https://github.com/WordPress/gutenberg/pull/53632)). #### Block Library -- Details block: Add accordion and toggle keywords. ([53501](https://github.com/WordPress/gutenberg/pull/53501)) - Column block: - - Add stretch alignment. ([53325](https://github.com/WordPress/gutenberg/pull/53325)) - - Exit on enter. ([53311](https://github.com/WordPress/gutenberg/pull/53311)) + - Add a `stretch` option to block's vertical alignment options. ([53325](https://github.com/WordPress/gutenberg/pull/53325)) + - Exit upon pressing enter in an empty paragraph at the end of the block. ([53311](https://github.com/WordPress/gutenberg/pull/53311)) - Classic block: Increase dimensions of modal and allow toggling fullscreen. ([53449](https://github.com/WordPress/gutenberg/pull/53449)) -- File block: Add spacing support. ([45107](https://github.com/WordPress/gutenberg/pull/45107)) -- Footnotes block: Add typography, dimensions, and border block supports. ([53044](https://github.com/WordPress/gutenberg/pull/53044)) +- Details block: + - Add `accordion` and `toggle` keywords to improve block's discoverability. ([53501](https://github.com/WordPress/gutenberg/pull/53501)) + - Add layout and block spacing options. ([53282](https://github.com/WordPress/gutenberg/pull/53282)) +- File block: Add block spacing options. ([45107](https://github.com/WordPress/gutenberg/pull/45107)) - Image block: Add aspect ratio support to lightbox. ([52765](https://github.com/WordPress/gutenberg/pull/52765)) +- Post Content block: Add color controls. ([51326](https://github.com/WordPress/gutenberg/pull/51326)) - Remove "post" from block titles. ([53492](https://github.com/WordPress/gutenberg/pull/53492)) #### Patterns -- Open detail view when duplicating pattern. ([53214](https://github.com/WordPress/gutenberg/pull/53214)) -- Prevent convert modal closing block options menu. ([53707](https://github.com/WordPress/gutenberg/pull/53707)) -- Skip migration logs in the Patterns screen. ([53626](https://github.com/WordPress/gutenberg/pull/53626)) +- Open detail view when duplicating a pattern. ([53214](https://github.com/WordPress/gutenberg/pull/53214)) +- Prevent the "create pattern" modal from closing the block options menu when it is closed. ([53707](https://github.com/WordPress/gutenberg/pull/53707)) +- Skip migration logs in the patterns screen. ([53626](https://github.com/WordPress/gutenberg/pull/53626)) +- Add missing full stop to string. ([53544](https://github.com/WordPress/gutenberg/pull/53544)) #### Global Styles -- Global styles revisions: Add a reset to default revision. ([52965](https://github.com/WordPress/gutenberg/pull/52965)) -- Global styles revisions: Reduce visibility check from 2 to 1 revision. ([53281](https://github.com/WordPress/gutenberg/pull/53281)) -- Post Content: Add color controls. ([51326](https://github.com/WordPress/gutenberg/pull/51326)) +- Add a reset to default global styles revision ([52965](https://github.com/WordPress/gutenberg/pull/52965)) and reduce visibility check from two to one revision ([53281](https://github.com/WordPress/gutenberg/pull/53281)). #### Media - Adjust size of image previews in list view. ([53649](https://github.com/WordPress/gutenberg/pull/53649)) -- List View: Add media previews to list view for gallery and image blocks. ([53381](https://github.com/WordPress/gutenberg/pull/53381)) +- Add media previews to list view for gallery and image blocks. ([53381](https://github.com/WordPress/gutenberg/pull/53381)) #### Site Editor -- Command Palette: Order template results in Site Editor. ([53286](https://github.com/WordPress/gutenberg/pull/53286)) -- Edit Site: Use progress bar for loading screen. ([53032](https://github.com/WordPress/gutenberg/pull/53032)) - -#### Data Layer -- Data: Warn if the 'useSelect' hook returns different values when called with the same state and parameters. ([53666](https://github.com/WordPress/gutenberg/pull/53666)) +- Expose `Theme` via private APIs ([53262](https://github.com/WordPress/gutenberg/pull/53262)), which was necessary to use the progress bar component for the site editor loading screen ([53032](https://github.com/WordPress/gutenberg/pull/53032)). #### Block Editor - Add `Opens in new Tab` control into Link Preview. ([53566](https://github.com/WordPress/gutenberg/pull/53566)) - -#### Interactivity API -- Update deepsignal version. ([53549](https://github.com/WordPress/gutenberg/pull/53549)) - -#### Code Editor -- Tweak, and add, more consistent commands. ([53496](https://github.com/WordPress/gutenberg/pull/53496)) - -#### Themes +- Dependencies: Bump `remove-accents` to 0.5.0. ([53420](https://github.com/WordPress/gutenberg/pull/53420)) +- Top-align Publish row in the post panel. ([53573](https://github.com/WordPress/gutenberg/pull/53573)) - Allow layout controls to be disabled per block from theme.json. ([53378](https://github.com/WordPress/gutenberg/pull/53378)) - -#### Plugins API -- Plugins: Introduce the 'usePluginContext' hook. ([53291](https://github.com/WordPress/gutenberg/pull/53291)) - -#### Layout -- Add layout and block spacing to details block. ([53282](https://github.com/WordPress/gutenberg/pull/53282)) - -#### Typography - Fluid typography: Add min and max viewport width configurable options. ([53081](https://github.com/WordPress/gutenberg/pull/53081)) ### New APIs #### Extensibility -- Make useBlockEditingMode() public. ([52094](https://github.com/WordPress/gutenberg/pull/52094)) +- Make `useBlockEditingMode()` public. ([52094](https://github.com/WordPress/gutenberg/pull/52094)) ### Bug Fixes -- Command palette: Fix metrics for resting and no results view. ([53497](https://github.com/WordPress/gutenberg/pull/53497)) -- Fix top toolbar in the post editor with custom fields in Safari. ([53688](https://github.com/WordPress/gutenberg/pull/53688)) -- Improve metrics on post publish view buttons. ([53245](https://github.com/WordPress/gutenberg/pull/53245)) -- Set top toolbar size dynamically. ([53526](https://github.com/WordPress/gutenberg/pull/53526)) -- Support container queries in editor CSS. ([49915](https://github.com/WordPress/gutenberg/pull/49915)) +#### Commands +- Style tweaks to fix metrics for resting and no results view in command palette. ([53497](https://github.com/WordPress/gutenberg/pull/53497)) +- Order template results in Site Editor, to fix some templates not displaying. ([53286](https://github.com/WordPress/gutenberg/pull/53286)) +- Don't allow access to Styles-related pages via the command palette in the hybrid theme. ([53123](https://github.com/WordPress/gutenberg/pull/53123)) #### Block Library -- Button block: Memoize link value passed to the LinkControl. ([53507](https://github.com/WordPress/gutenberg/pull/53507)) -- Cover block: Fix flickering when inserted in templates and also fix isDark calculation bugs. ([53253](https://github.com/WordPress/gutenberg/pull/53253)) -- Footnotes: - - Autosave is not slashing JSON. ([53664](https://github.com/WordPress/gutenberg/pull/53664)) +- Button block: Avoid losing user changes when the `ButtonEdit` component re-renders. ([53507](https://github.com/WordPress/gutenberg/pull/53507)) +- Cover block: Fix flickering when inserted in templates and also fix `isDark` calculation bugs. ([53253](https://github.com/WordPress/gutenberg/pull/53253)) +- Footnotes block: + - Ensure autosave works and escapes quotes as expected. ([53664](https://github.com/WordPress/gutenberg/pull/53664)) - Fix accidental override. ([53663](https://github.com/WordPress/gutenberg/pull/53663)) - Fix recursion into updating attributes when attributes is not an object. ([53257](https://github.com/WordPress/gutenberg/pull/53257)) + - Remove Footnotes when interactive formatting is disabled. ([53474](https://github.com/WordPress/gutenberg/pull/53474)) - Image block: - Fix image stretching with only height. ([53443](https://github.com/WordPress/gutenberg/pull/53443)) - Don't render `DimensionsTool` if it is not resizable. ([53181](https://github.com/WordPress/gutenberg/pull/53181)) - Fix stretched images constrained by max-width. ([53274](https://github.com/WordPress/gutenberg/pull/53274)) - Clear aspect ratio when wide aligned. ([53439](https://github.com/WordPress/gutenberg/pull/53439)) - - Dimensions Tool: Change the conditions underwhich we display the scale control. ([53334](https://github.com/WordPress/gutenberg/pull/53334)) - - Aspect Ratio: Reset height when selecting the original aspect ratio. ([53339](https://github.com/WordPress/gutenberg/pull/53339)) -- Latest Posts block: Make latest-posts ssr categories handling more defensive. ([53659](https://github.com/WordPress/gutenberg/pull/53659)) + - Change the conditions under which we display the scale control. ([53334](https://github.com/WordPress/gutenberg/pull/53334)) + - Reset height when selecting the original aspect ratio. ([53339](https://github.com/WordPress/gutenberg/pull/53339)) +- Latest Posts block: Make categories handling more defensive to prevent multisite error. ([53659](https://github.com/WordPress/gutenberg/pull/53659)) +- Media & Text block: Fix deprecation with `isStackOnMobile` default value changed. ([49538](https://github.com/WordPress/gutenberg/pull/49538)) - Inject theme stylesheet value as template part theme attribute. ([53423](https://github.com/WordPress/gutenberg/pull/53423)) -- Patterns: Add `delete_posts` to the wp_block (patterns) capabilities. ([53405](https://github.com/WordPress/gutenberg/pull/53405)) - Block serialization: Correctly compare default attribute values. ([53521](https://github.com/WordPress/gutenberg/pull/53521)) #### Block Editor -- Fix Synced Patterns' color in quick inserter. ([53327](https://github.com/WordPress/gutenberg/pull/53327)) -- Hide pattern previews on hover in inserter. ([53331](https://github.com/WordPress/gutenberg/pull/53331)) - LinkControl: Prevent overflow when the title is a URL. ([53356](https://github.com/WordPress/gutenberg/pull/53356)) -- Safari: Fix ArrowUp on empty paragraph. ([53341](https://github.com/WordPress/gutenberg/pull/53341)) -- Safari: Fix Shift+Click multi select. ([53440](https://github.com/WordPress/gutenberg/pull/53440)) -- Selection: Restore focus after dragging out of the block repeatedly. ([53429](https://github.com/WordPress/gutenberg/pull/53429)) -- Writing flow: Avoid merging paragraph into Columns. ([53508](https://github.com/WordPress/gutenberg/pull/53508)) -- Writing flow: Fix vertical arrow keys not moving. ([53454](https://github.com/WordPress/gutenberg/pull/53454)) +- Fix broken flows on Safari, including `ArrowUp` functionality in an empty paragraph ([53341](https://github.com/WordPress/gutenberg/pull/53341)) and multi-selection upon shift plus click ([53440](https://github.com/WordPress/gutenberg/pull/53440)). +- Restore focus after dragging out of the block repeatedly. ([53429](https://github.com/WordPress/gutenberg/pull/53429)) +- Avoid merging paragraph into a Columns block. ([53508](https://github.com/WordPress/gutenberg/pull/53508)) +- Prevent vertical arrow keys getting stuck in view. ([53454](https://github.com/WordPress/gutenberg/pull/53454)) +- Set top toolbar size dynamically. ([53526](https://github.com/WordPress/gutenberg/pull/53526)) +- Support container queries in editor CSS. ([49915](https://github.com/WordPress/gutenberg/pull/49915)) +- Copy tag name on internal paste. ([48254](https://github.com/WordPress/gutenberg/pull/48254)) #### Site Editor - Add missing i18n in `HomeTemplateDetails`. ([53543](https://github.com/WordPress/gutenberg/pull/53543)) -- Adds site editor mobile block settings and styles. ([53412](https://github.com/WordPress/gutenberg/pull/53412)) -- Edit Site: Fix site editor canvas edit mode button. ([53730](https://github.com/WordPress/gutenberg/pull/53730)) +- Add buttons for block settings and styles in smaller viewport. ([53412](https://github.com/WordPress/gutenberg/pull/53412)) +- Ensure canvas edit mode button occupies the entire frame canvas. ([53730](https://github.com/WordPress/gutenberg/pull/53730)) - Fix document actions label helper method. ([52974](https://github.com/WordPress/gutenberg/pull/52974)) - Fix document title alignment in command palette button. ([53224](https://github.com/WordPress/gutenberg/pull/53224)) #### Post Editor -- Fix crash by moving editor style logic into a hook with useMemo. ([53596](https://github.com/WordPress/gutenberg/pull/53596)) +- Address crash by moving editor style logic into a hook with `useMemo`. ([53596](https://github.com/WordPress/gutenberg/pull/53596)) - Fix support of sticky position in non-iframed post editor. ([53540](https://github.com/WordPress/gutenberg/pull/53540)) -- Fix the typo when setting the preview device type to 'Desktop'. ([53409](https://github.com/WordPress/gutenberg/pull/53409)) -- getInsertionPoint: Avoid returning a different object on every call. ([53722](https://github.com/WordPress/gutenberg/pull/53722)) +- Correct typo when setting the preview device type to 'Desktop'. ([53409](https://github.com/WordPress/gutenberg/pull/53409)) +- Avoid returning a different object on every call to `getInsertionPoint`. ([53722](https://github.com/WordPress/gutenberg/pull/53722)) +- Fix top toolbar in the post editor with custom fields in Safari. ([53688](https://github.com/WordPress/gutenberg/pull/53688)) +- Improve metrics on post publish view buttons. ([53245](https://github.com/WordPress/gutenberg/pull/53245)) #### Page Content Focus - Fix missing Replace button in content-locked Image blocks. ([53410](https://github.com/WordPress/gutenberg/pull/53410)) -- Site Editor: Fix BlockPreview in Template panel when editing a page. ([53550](https://github.com/WordPress/gutenberg/pull/53550)) -- Use template.blocks in BlockPreview if it exists. ([53611](https://github.com/WordPress/gutenberg/pull/53611)) +- Fix BlockPreview in Template panel when editing a page in the site editor. ([53550](https://github.com/WordPress/gutenberg/pull/53550)) +- Use `template.blocks` in BlockPreview if it exists. ([53611](https://github.com/WordPress/gutenberg/pull/53611)) #### Navigation Menus -- Fix: #52886 Make all the 'Loading' strings consistent. ([52901](https://github.com/WordPress/gutenberg/pull/52901)) -- Fix: Title is not copied correctly when duplicating navigation. ([53610](https://github.com/WordPress/gutenberg/pull/53610)) -- Revert Fix entity cache misses for single posts due to string as recordKey. ([53419](https://github.com/WordPress/gutenberg/pull/53419)) +- Make all the 'Loading' strings consistent. ([52901](https://github.com/WordPress/gutenberg/pull/52901)) +- Fix title not being copied correctly when duplicating navigation. ([53610](https://github.com/WordPress/gutenberg/pull/53610)) +- Remove "go to" for terms and posts. ([53408](https://github.com/WordPress/gutenberg/pull/53408)) #### Typography - Fallback to default max viewport if layout wide size is fluid. ([53551](https://github.com/WordPress/gutenberg/pull/53551)) - Fix typo and add tests for fonts install endpoint. ([53644](https://github.com/WordPress/gutenberg/pull/53644)) #### Patterns +- Fix Synced Patterns' color in quick inserter. ([53327](https://github.com/WordPress/gutenberg/pull/53327)) +- Hide pattern previews on hover in inserter. ([53331](https://github.com/WordPress/gutenberg/pull/53331)) +- Ensure it's possible to delete draft patterns. ([53405](https://github.com/WordPress/gutenberg/pull/53405)) - Fix pattern creation button in list view dropdown menu. ([53562](https://github.com/WordPress/gutenberg/pull/53562)) -- Fix: Sync status overlaps for some languages in Patterns post type page. ([53243](https://github.com/WordPress/gutenberg/pull/53243)) - -#### Rich Text -- Copy tag name on internal paste. ([48254](https://github.com/WordPress/gutenberg/pull/48254)) -- RichText: Remove 'Footnotes' when interactive formatting is disabled. ([53474](https://github.com/WordPress/gutenberg/pull/53474)) +- Prevent sync status overlapping for some languages in patterns. ([53243](https://github.com/WordPress/gutenberg/pull/53243)) #### Global Styles - Fix push-to-global-styles clearing of attributes, border fallbacks, link hover colors, and behaviors. ([51621](https://github.com/WordPress/gutenberg/pull/51621)) @@ -183,33 +427,18 @@ - Include namespace in layout classname for non-core blocks. ([53404](https://github.com/WordPress/gutenberg/pull/53404)) #### Interactivity API -- Add short-cirtuit to `useSignalEffect`. ([53358](https://github.com/WordPress/gutenberg/pull/53358)) +- Add short-circuit to `useSignalEffect`. ([53358](https://github.com/WordPress/gutenberg/pull/53358)) - Add support for underscores and leading dashes in the suffix part of the directive. ([53337](https://github.com/WordPress/gutenberg/pull/53337)) +- Update deepsignal version. ([53549](https://github.com/WordPress/gutenberg/pull/53549)) #### Components -- Button: add `:Disabled` selector to reset hover color for disabled buttons. ([53411](https://github.com/WordPress/gutenberg/pull/53411)) - -#### Template Editor -- Remove "go to" for terms and posts. ([53408](https://github.com/WordPress/gutenberg/pull/53408)) - -#### Custom Fields -- Insert path and query args to form before submitting. ([53324](https://github.com/WordPress/gutenberg/pull/53324)) - -#### Themes -- Don't allow access to Styles-related pages via the command palette in the hybrid theme. ([53123](https://github.com/WordPress/gutenberg/pull/53123)) - -#### Block Validation/Deprecation -- Media & Text Block: Fix deprecation with `isStackOnMobile` default value changed. ([49538](https://github.com/WordPress/gutenberg/pull/49538)) - -#### npm Packages -- Add some missing package dependencies. ([41486](https://github.com/WordPress/gutenberg/pull/41486)) +- Button: Add `:Disabled` selector to reset hover color for disabled buttons. ([53411](https://github.com/WordPress/gutenberg/pull/53411)) +- Preferences Modal: Insert path and query args to form before submitting. ([53324](https://github.com/WordPress/gutenberg/pull/53324)) ### Accessibility - Type labels GH Action: Fix accessibility issues in error message. ([53371](https://github.com/WordPress/gutenberg/pull/53371)) - -#### Block Library - Add accessible description of current Navigation block state. ([53469](https://github.com/WordPress/gutenberg/pull/53469)) - Implement accessible version of Navigation overlay preview toggle control. ([53462](https://github.com/WordPress/gutenberg/pull/53462)) - Search Block: Fix unintended wrapping of button text in "Button only" style. ([53373](https://github.com/WordPress/gutenberg/pull/53373)) @@ -219,156 +448,145 @@ - Compute presets from `theme.json`: Skip those without classes or variables. ([53574](https://github.com/WordPress/gutenberg/pull/53574)) - Switch performance tests to Playwright. ([52022](https://github.com/WordPress/gutenberg/pull/52022)) - -#### Block Editor - Fix memory leaks in ` + ); +} + +/** + * BlockCanvas component is a component used to display the canvas of the block editor. + * What we call the canvas is an iframe containing the block list that you can manipulate. + * The component is also responsible of wiring up all the necessary hooks to enable + * the keyboard navigation across blocks in the editor and inject content styles into the iframe. + * + * @example + * + * ```jsx + * function MyBlockEditor() { + * const [ blocks, updateBlocks ] = useState([]); + * return ( + * + * + * + * ); + * } + * ``` + * + * @param {Object} props Component props. + * @param {string} props.height Canvas height, defaults to 300px. + * @param {Array} props.styles Content styles to inject into the iframe. + * @param {WPElement} props.children Content of the canvas, defaults to the BlockList component. + * @return {WPElement} Block Breadcrumb. + */ +function BlockCanvas( { children, height, styles } ) { + return ( + + { children } + + ); +} + +export default BlockCanvas; diff --git a/packages/block-editor/src/components/block-list/block-outline.native.js b/packages/block-editor/src/components/block-list/block-outline.native.js index 753b16f94a755..83c6a58bac365 100644 --- a/packages/block-editor/src/components/block-list/block-outline.native.js +++ b/packages/block-editor/src/components/block-list/block-outline.native.js @@ -13,7 +13,7 @@ import { usePreferredColorSchemeStyle } from '@wordpress/compose'; */ import styles from './block.scss'; -const BLOCKS_WITH_OUTLINE = [ 'core/social-link', 'core/missing' ]; +const TEXT_BLOCKS_WITH_OUTLINE = [ 'core/missing' ]; function BlockOutline( { blockCategory, @@ -22,7 +22,9 @@ function BlockOutline( { isSelected, name, } ) { - const textBlockWithOutline = BLOCKS_WITH_OUTLINE.includes( name ); + const textBlockWithOutline = TEXT_BLOCKS_WITH_OUTLINE.includes( name ); + const socialBlockWithOutline = name.includes( 'core/social-link' ); + const hasBlockTextCategory = blockCategory === 'text' && ! textBlockWithOutline; const hasBlockMediaCategory = @@ -47,6 +49,7 @@ function BlockOutline( { ( ( hasBlockTextCategory && hasInnerBlocks ) || ( ! hasBlockTextCategory && hasInnerBlocks ) || ( ! hasBlockTextCategory && isRootList ) || + socialBlockWithOutline || textBlockWithOutline ); return ( diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index a163ddaa78955..52f2621773077 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -85,7 +85,6 @@ .block-editor-block-list__block.is-highlighted, .block-editor-block-list__block.is-highlighted ~ .is-multi-selected, &.is-navigate-mode .block-editor-block-list__block.is-selected, - & .is-block-moving-mode.block-editor-block-list__block.has-child-selected, .block-editor-block-list__block:not([contenteditable]):focus { outline: none; @@ -113,8 +112,6 @@ // Moving blocks using keyboard (Ellipsis > Move). & .is-block-moving-mode.block-editor-block-list__block.is-selected { - box-shadow: none; - outline: none; &::after { content: ""; @@ -130,6 +127,8 @@ top: -$default-block-margin * 0.5; border-radius: $radius-block-ui; border-top: 4px solid $gray-400; + bottom: auto; + box-shadow: none; } } diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 04b767d9568b7..a9d5f15f12f81 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -31,7 +31,6 @@ import BlockListBlock from './block'; import BlockListAppender from '../block-list-appender'; import { useInBetweenInserter } from './use-in-between-inserter'; import { store as blockEditorStore } from '../../store'; -import { usePreParsePatterns } from '../../utils/pre-parse-patterns'; import { LayoutProvider, defaultLayout } from './layout'; import { useBlockSelectionClearer } from '../block-selection-clearer'; import { useInnerBlocksProps } from '../inner-blocks'; @@ -39,6 +38,7 @@ import { BlockEditContextProvider, DEFAULT_BLOCK_EDIT_CONTEXT, } from '../block-edit/context'; +import { useTypingObserver } from '../observe-typing'; const elementContext = createContext(); @@ -104,7 +104,7 @@ function Root( { className, ...settings } ) { ref: useMergeRefs( [ useBlockSelectionClearer(), useInBetweenInserter(), - setElement, + useTypingObserver(), ] ), className: classnames( 'is-root-container', className, { 'is-outline-mode': isOutlineMode, @@ -118,13 +118,14 @@ function Root( { className, ...settings } ) {
+ { /* Ensure element and layout styles are always at the end of the document */ } +
); } export default function BlockList( settings ) { - usePreParsePatterns(); return ( diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js index 6a215363c3901..a0175c4d4ae58 100644 --- a/packages/block-editor/src/components/block-popover/inbetween.js +++ b/packages/block-editor/src/components/block-popover/inbetween.js @@ -81,10 +81,10 @@ function BlockPopoverInbetween( { return undefined; } - const { ownerDocument } = previousElement || nextElement; + const contextElement = previousElement || nextElement; return { - ownerDocument, + contextElement, getBoundingClientRect() { const previousRect = previousElement ? previousElement.getBoundingClientRect() @@ -215,7 +215,8 @@ function BlockPopoverInbetween( { focusOnMount={ false } // Render in the old slot if needed for backward compatibility, // otherwise render in place (not in the default popover slot). - __unstableSlotName={ __unstablePopoverSlot || null } + __unstableSlotName={ __unstablePopoverSlot } + inline={ ! __unstablePopoverSlot } // Forces a remount of the popover when its position changes // This makes sure the popover doesn't animate from its previous position. key={ nextClientId + '--' + rootClientId } diff --git a/packages/block-editor/src/components/block-popover/index.js b/packages/block-editor/src/components/block-popover/index.js index 5382949684abd..13e6ba4d9e7f8 100644 --- a/packages/block-editor/src/components/block-popover/index.js +++ b/packages/block-editor/src/components/block-popover/index.js @@ -142,7 +142,7 @@ function BlockPopover( return new window.DOMRect( left, top, width, height ); }, - ownerDocument: selectedElement.ownerDocument, + contextElement: selectedElement, }; }, [ bottomClientId, @@ -163,7 +163,8 @@ function BlockPopover( anchor={ popoverAnchor } // Render in the old slot if needed for backward compatibility, // otherwise render in place (not in the default popover slot). - __unstableSlotName={ __unstablePopoverSlot || null } + __unstableSlotName={ __unstablePopoverSlot } + inline={ ! __unstablePopoverSlot } placement="top-start" resize={ false } flip={ false } diff --git a/packages/block-editor/src/components/block-settings-menu-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js index ec8fa46d4859d..53b3835fad1a1 100644 --- a/packages/block-editor/src/components/block-settings-menu-controls/index.js +++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js @@ -29,19 +29,15 @@ const BlockSettingsMenuControlsSlot = ( { clientIds = null, __unstableDisplayLocation, } ) => { - const { selectedBlocks, selectedClientIds, canRemove } = useSelect( + const { selectedBlocks, selectedClientIds } = useSelect( ( select ) => { - const { - getBlockNamesByClientId, - getSelectedBlockClientIds, - canRemoveBlocks, - } = select( blockEditorStore ); + const { getBlockNamesByClientId, getSelectedBlockClientIds } = + select( blockEditorStore ); const ids = clientIds !== null ? clientIds : getSelectedBlockClientIds(); return { selectedBlocks: getBlockNamesByClientId( ids ), selectedClientIds: ids, - canRemove: canRemoveBlocks( ids ), }; }, [ clientIds ] @@ -55,8 +51,7 @@ const BlockSettingsMenuControlsSlot = ( { const convertToGroupButtonProps = useConvertToGroupButtonProps( selectedClientIds ); const { isGroupable, isUngroupable } = convertToGroupButtonProps; - const showConvertToGroupButton = - ( isGroupable || isUngroupable ) && canRemove; + const showConvertToGroupButton = isGroupable || isUngroupable; return ( &, .is-layout-flow.block-editor-block-list__block:not(.is-selected) > &, // Legacy groups have an inner container so need to be targeted separately @@ -69,6 +70,13 @@ } } + // Hide the dashed outline in 2-level nested cases, so for example the dashed + // empty column is only shown when the columns block is selected. + .block-editor-block-list__block:not(.is-selected) > .block-editor-block-list__block > &::after { + border: none; + } + + // Drop zone. &.is-drag-over .block-editor-button-block-appender { background-color: var(--wp-admin-theme-color); box-shadow: inset 0 0 0 $border-width $light-gray-placeholder; diff --git a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap index 039d35937294b..3fa7e6fd690e8 100644 --- a/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap +++ b/packages/block-editor/src/components/color-palette/test/__snapshots__/control.js.snap @@ -199,34 +199,44 @@ exports[`ColorPaletteControl matches the snapshot 1`] = `
-
( - + ); diff --git a/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js b/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js index 197dca486d0e3..5c0aa5dc68fc9 100644 --- a/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js +++ b/packages/block-editor/src/components/convert-to-group-buttons/use-convert-to-group-button-props.js @@ -34,55 +34,28 @@ export default function useConvertToGroupButtonProps( selectedClientIds ) { return useSelect( ( select ) => { const { - getBlockRootClientId, getBlocksByClientId, - canInsertBlockType, getSelectedBlockClientIds, + isUngroupable, + isGroupable, } = select( blockEditorStore ); const { getGroupingBlockName, getBlockType } = select( blocksStore ); const clientIds = selectedClientIds?.length ? selectedClientIds : getSelectedBlockClientIds(); - const groupingBlockName = getGroupingBlockName(); - - const rootClientId = clientIds?.length - ? getBlockRootClientId( clientIds[ 0 ] ) - : undefined; - - const groupingBlockAvailable = canInsertBlockType( - groupingBlockName, - rootClientId - ); - const blocksSelection = getBlocksByClientId( clientIds ); - const isSingleBlockSelected = blocksSelection.length === 1; const [ firstSelectedBlock ] = blocksSelection; - // A block is ungroupable if it is a single grouping block with inner blocks. - // If a block has an `ungroup` transform, it is also ungroupable, without the - // requirement of being the default grouping block. - // Do we have a single grouping Block selected and does that group have inner blocks? - const isUngroupable = - isSingleBlockSelected && - ( firstSelectedBlock.name === groupingBlockName || - getBlockType( firstSelectedBlock.name )?.transforms - ?.ungroup ) && - !! firstSelectedBlock.innerBlocks.length; - - // Do we have - // 1. Grouping block available to be inserted? - // 2. One or more blocks selected - const isGroupable = - groupingBlockAvailable && blocksSelection.length; - + const _isUngroupable = + clientIds.length === 1 && isUngroupable( clientIds[ 0 ] ); return { clientIds, - isGroupable, - isUngroupable, + isGroupable: isGroupable( clientIds ), + isUngroupable: _isUngroupable, blocksSelection, - groupingBlockName, + groupingBlockName: getGroupingBlockName(), onUngroup: - isUngroupable && + _isUngroupable && getBlockType( firstSelectedBlock.name )?.transforms ?.ungroup, }; diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 19c1b61058016..5c9f640e64b22 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -31,6 +31,30 @@ import { useWritingFlow } from '../writing-flow'; import { useCompatibilityStyles } from './use-compatibility-styles'; import { store as blockEditorStore } from '../../store'; +function bubbleEvent( event, Constructor, frame ) { + const init = {}; + + for ( const key in event ) { + init[ key ] = event[ key ]; + } + + if ( event instanceof frame.ownerDocument.defaultView.MouseEvent ) { + const rect = frame.getBoundingClientRect(); + init.clientX += rect.left; + init.clientY += rect.top; + } + + const newEvent = new Constructor( event.type, init ); + if ( init.defaultPrevented ) { + newEvent.preventDefault(); + } + const cancelled = ! frame.dispatchEvent( newEvent ); + + if ( cancelled ) { + event.preventDefault(); + } +} + /** * Bubbles some event types (keydown, keypress, and dragover) to parent document * document to ensure that the keyboard shortcuts and drag and drop work. @@ -39,42 +63,30 @@ import { store as blockEditorStore } from '../../store'; * should be context dependent, e.g. actions on blocks like Cmd+A should not * work globally outside the block editor. * - * @param {Document} doc Document to attach listeners to. + * @param {Document} iframeDocument Document to attach listeners to. */ -function bubbleEvents( doc ) { - const { defaultView } = doc; - const { frameElement } = defaultView; - - function bubbleEvent( event ) { - const prototype = Object.getPrototypeOf( event ); - const constructorName = prototype.constructor.name; - const Constructor = window[ constructorName ]; - - const init = {}; - - for ( const key in event ) { - init[ key ] = event[ key ]; +function useBubbleEvents( iframeDocument ) { + return useRefEffect( ( body ) => { + const { defaultView } = iframeDocument; + const { frameElement } = defaultView; + const eventTypes = [ 'dragover', 'mousemove' ]; + const handlers = {}; + for ( const name of eventTypes ) { + handlers[ name ] = ( event ) => { + const prototype = Object.getPrototypeOf( event ); + const constructorName = prototype.constructor.name; + const Constructor = window[ constructorName ]; + bubbleEvent( event, Constructor, frameElement ); + }; + body.addEventListener( name, handlers[ name ] ); } - if ( event instanceof defaultView.MouseEvent ) { - const rect = frameElement.getBoundingClientRect(); - init.clientX += rect.left; - init.clientY += rect.top; - } - - const newEvent = new Constructor( event.type, init ); - const cancelled = ! frameElement.dispatchEvent( newEvent ); - - if ( cancelled ) { - event.preventDefault(); - } - } - - const eventTypes = [ 'dragover', 'mousemove' ]; - - for ( const name of eventTypes ) { - doc.addEventListener( name, bubbleEvent ); - } + return () => { + for ( const name of eventTypes ) { + body.removeEventListener( name, handlers[ name ] ); + } + }; + } ); } function Iframe( { @@ -117,7 +129,6 @@ function Iframe( { const { documentElement } = contentDocument; iFrameDocument = contentDocument; - bubbleEvents( contentDocument ); clearerRef( documentElement ); // Ideally ALL classes that are added through get_body_class should @@ -182,6 +193,7 @@ function Iframe( { const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ + useBubbleEvents( iframeDocument ), contentRef, clearerRef, writingFlowRef, @@ -225,6 +237,7 @@ function Iframe( { { + // This stopPropagation call ensures React doesn't create a syncthetic event to bubble this event + // which would result in two React events being bubbled throught the iframe. + event.stopPropagation(); + const { defaultView } = iframeDocument; + const { frameElement } = defaultView; + bubbleEvent( + event, + window.KeyboardEvent, + frameElement + ); + } } > { contentResizeListener } diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 7e46698e2b61b..7be30aa5a858b 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -17,6 +17,7 @@ export { default as __experimentalBlockAlignmentMatrixControl } from './block-al export { default as BlockBreadcrumb } from './block-breadcrumb'; export { default as __experimentalUseBlockOverlayActive } from './block-content-overlay'; export { BlockContextProvider } from './block-context'; +export { default as BlockCanvas } from './block-canvas'; export { default as BlockControls, BlockFormatControls, diff --git a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js index 44f99428a31bf..8eba8a1d2223d 100644 --- a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js +++ b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js @@ -1,9 +1,10 @@ /** * WordPress dependencies */ -import { useLayoutEffect, useMemo } from '@wordpress/element'; +import { useLayoutEffect, useMemo, useState } from '@wordpress/element'; import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; +import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies @@ -15,6 +16,14 @@ import { getLayoutType } from '../../layouts'; const pendingSettingsUpdates = new WeakMap(); +function useShallowMemo( value ) { + const [ prevValue, setPrevValue ] = useState( value ); + if ( ! isShallowEqual( prevValue, value ) ) { + setPrevValue( value ); + } + return prevValue; +} + /** * This hook is a side effect which updates the block-editor store when changes * happen to inner block settings. The given props are transformed into a @@ -70,16 +79,12 @@ export default function useNestedSettingsUpdate( [ clientId ] ); - // Memoize allowedBlocks and prioritisedInnerBlocks based on the contents - // of the arrays. Implementors often pass a new array on every render, + // Implementors often pass a new array on every render, // and the contents of the arrays are just strings, so the entire array - // can be passed as dependencies. - - const _allowedBlocks = useMemo( - () => allowedBlocks, - // eslint-disable-next-line react-hooks/exhaustive-deps - allowedBlocks - ); + // can be passed as dependencies but We need to include the length of the array, + // otherwise if the arrays change length but the first elements are equal the comparison, + // does not works as expected. + const _allowedBlocks = useShallowMemo( allowedBlocks ); const _prioritizedInserterBlocks = useMemo( () => prioritizedInserterBlocks, diff --git a/packages/block-editor/src/components/inspector-controls/block-support-slot-container.js b/packages/block-editor/src/components/inspector-controls/block-support-slot-container.js index fb53e47afbd82..be2f01b5ed168 100644 --- a/packages/block-editor/src/components/inspector-controls/block-support-slot-container.js +++ b/packages/block-editor/src/components/inspector-controls/block-support-slot-container.js @@ -2,11 +2,27 @@ * WordPress dependencies */ import { __experimentalToolsPanelContext as ToolsPanelContext } from '@wordpress/components'; -import { useContext } from '@wordpress/element'; +import { useContext, useMemo } from '@wordpress/element'; -export default function BlockSupportSlotContainer( { Slot, ...props } ) { +export default function BlockSupportSlotContainer( { + Slot, + fillProps, + ...props +} ) { + // Add the toolspanel context provider and value to existing fill props const toolsPanelContext = useContext( ToolsPanelContext ); + const computedFillProps = useMemo( + () => ( { + ...( fillProps ?? {} ), + forwardedContext: [ + ...( fillProps?.forwardedContext ?? [] ), + [ ToolsPanelContext.Provider, { value: toolsPanelContext } ], + ], + } ), + [ toolsPanelContext, fillProps ] + ); + return ( - + ); } diff --git a/packages/block-editor/src/components/inspector-controls/fill.js b/packages/block-editor/src/components/inspector-controls/fill.js index 2db809a46b21e..f0640a9d31ddc 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.js +++ b/packages/block-editor/src/components/inspector-controls/fill.js @@ -7,7 +7,7 @@ import { } from '@wordpress/components'; import warning from '@wordpress/warning'; import deprecated from '@wordpress/deprecated'; -import { useEffect } from '@wordpress/element'; +import { useEffect, useContext } from '@wordpress/element'; /** * Internal dependencies @@ -60,28 +60,42 @@ export default function InspectorControlsFill( { ); } -function ToolsPanelInspectorControl( { children, resetAllFilter, fillProps } ) { - const { registerResetAllFilter, deregisterResetAllFilter } = fillProps; +function RegisterResetAll( { resetAllFilter, children } ) { + const { registerResetAllFilter, deregisterResetAllFilter } = + useContext( ToolsPanelContext ); useEffect( () => { - if ( resetAllFilter && registerResetAllFilter ) { + if ( + resetAllFilter && + registerResetAllFilter && + deregisterResetAllFilter + ) { registerResetAllFilter( resetAllFilter ); - } - return () => { - if ( resetAllFilter && deregisterResetAllFilter ) { + return () => { deregisterResetAllFilter( resetAllFilter ); - } - }; + }; + } }, [ resetAllFilter, registerResetAllFilter, deregisterResetAllFilter ] ); + return children; +} + +function ToolsPanelInspectorControl( { children, resetAllFilter, fillProps } ) { + // `fillProps.forwardedContext` is an array of context provider entries, provided by slot, + // that should wrap the fill markup. + const { forwardedContext = [] } = fillProps; // Children passed to InspectorControlsFill will not have // access to any React Context whose Provider is part of // the InspectorControlsSlot tree. So we re-create the // Provider in this subtree. - const value = - fillProps && Object.keys( fillProps ).length > 0 ? fillProps : null; - return ( - + const innerMarkup = ( + { children } - + + ); + return forwardedContext.reduce( + ( inner, [ Provider, props ] ) => ( + { inner } + ), + innerMarkup ); } diff --git a/packages/block-editor/src/components/inspector-controls/slot.js b/packages/block-editor/src/components/inspector-controls/slot.js index 3687644b21b4d..cc32b1c88480e 100644 --- a/packages/block-editor/src/components/inspector-controls/slot.js +++ b/packages/block-editor/src/components/inspector-controls/slot.js @@ -1,7 +1,11 @@ /** * WordPress dependencies */ -import { __experimentalUseSlotFills as useSlotFills } from '@wordpress/components'; +import { + __experimentalUseSlotFills as useSlotFills, + __unstableMotionContext as MotionContext, +} from '@wordpress/components'; +import { useContext, useMemo } from '@wordpress/element'; import warning from '@wordpress/warning'; import deprecated from '@wordpress/deprecated'; @@ -16,6 +20,7 @@ export default function InspectorControlsSlot( { __experimentalGroup, group = 'default', label, + fillProps, ...props } ) { if ( __experimentalGroup ) { @@ -31,6 +36,20 @@ export default function InspectorControlsSlot( { } const Slot = groups[ group ]?.Slot; const fills = useSlotFills( Slot?.__unstableName ); + + const motionContextValue = useContext( MotionContext ); + + const computedFillProps = useMemo( + () => ( { + ...( fillProps ?? {} ), + forwardedContext: [ + ...( fillProps?.forwardedContext ?? [] ), + [ MotionContext.Provider, { value: motionContextValue } ], + ], + } ), + [ motionContextValue, fillProps ] + ); + if ( ! Slot ) { warning( `Unknown InspectorControls group "${ group }" provided.` ); return null; @@ -43,10 +62,16 @@ export default function InspectorControlsSlot( { if ( label ) { return ( - + ); } - return ; + return ( + + ); } diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 9d9e6f4e188d4..4e0a1f2291b02 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -422,11 +422,7 @@ function LinkControl( { settings={ settings?.filter( ( { id } ) => id === 'opensInNewTab' ) } - onChange={ ( { opensInNewTab } ) => { - onChange( { - opensInNewTab, - } ); - } } + onChange={ onChange } /> ); } @@ -479,5 +475,6 @@ function LinkControl( { } LinkControl.ViewerFill = ViewerFill; +LinkControl.DEFAULT_LINK_SETTINGS = DEFAULT_LINK_SETTINGS; export default LinkControl; diff --git a/packages/block-editor/src/components/link-control/settings.js b/packages/block-editor/src/components/link-control/settings.js index 1d70cc97dff41..e63ef926358fe 100644 --- a/packages/block-editor/src/components/link-control/settings.js +++ b/packages/block-editor/src/components/link-control/settings.js @@ -26,6 +26,7 @@ const LinkControlSettings = ( { value, onChange = noop, settings } ) => { label={ setting.title } onChange={ handleSettingChange( setting ) } checked={ value ? !! value[ setting.id ] : false } + help={ setting?.help } /> ) ); diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index 0ca13fa916777..2b37d7fccdfe4 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -401,19 +401,19 @@ $preview-image-height: 140px; } .block-editor-link-control__setting { - margin-bottom: $grid-unit-20; + margin-bottom: 0; flex: 1; padding: $grid-unit-10 0 $grid-unit-10 $grid-unit-30; + .components-base-control__field { + display: flex; // don't allow label to wrap under checkbox. + } + // Cancel left margin inherited from WP Admin Forms CSS. input { margin-left: 0; } - &.block-editor-link-control__setting:last-child { - margin-bottom: 0; - } - .is-preview & { padding: 20px $grid-unit-10 $grid-unit-10 0; } diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index 4e3e9f45e4dea..f8e9aedcbd5bd 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -1789,6 +1789,7 @@ describe( 'Addition Settings UI', () => { expect( mockOnChange ).toHaveBeenCalledTimes( 1 ); expect( mockOnChange ).toHaveBeenCalledWith( { + ...selectedLink, opensInNewTab: true, } ); } ); diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index d4845dc769c7f..43fe4df4cb75a 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -23,6 +23,7 @@ import { import { useDispatch, useSelect } from '@wordpress/data'; import { sprintf, __ } from '@wordpress/i18n'; import { focus } from '@wordpress/dom'; +import { ESCAPE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -72,7 +73,9 @@ function ListViewBlock( { const { toggleBlockHighlight } = useDispatch( blockEditorStore ); const blockInformation = useBlockDisplayInformation( clientId ); - const blockTitle = blockInformation?.title || __( 'Untitled' ); + const blockTitle = + blockInformation?.name || blockInformation?.title || __( 'Untitled' ); + const block = useSelect( ( select ) => select( blockEditorStore ).getBlock( clientId ), [ clientId ] @@ -148,6 +151,20 @@ function ListViewBlock( { } }, [] ); + // If multiple blocks are selected, deselect all blocks when the user + // presses the escape key. + const onKeyDown = ( event ) => { + if ( + event.keyCode === ESCAPE && + ! event.defaultPrevented && + selectedClientIds.length > 0 + ) { + event.stopPropagation(); + event.preventDefault(); + selectBlock( event, undefined ); + } + }; + const onMouseEnter = useCallback( () => { setIsHovered( true ); toggleBlockHighlight( clientId, true ); @@ -255,6 +272,7 @@ function ListViewBlock( { return ( { - if ( ! event?.shiftKey ) { + if ( ! event?.shiftKey && event?.keyCode !== ESCAPE ) { selectBlock( clientId, focusPosition ); return; } @@ -39,6 +39,8 @@ export default function useBlockSelection() { // the browser default behavior of opening the link in a new window. event.preventDefault(); + const isOnlyDeselection = + event.type === 'keydown' && event.keyCode === ESCAPE; const isKeyPress = event.type === 'keydown' && ( event.keyCode === UP || @@ -63,10 +65,11 @@ export default function useBlockSelection() { ]; if ( - isKeyPress && - ! selectedBlocks.some( ( blockId ) => - clientIdWithParents.includes( blockId ) - ) + isOnlyDeselection || + ( isKeyPress && + ! selectedBlocks.some( ( blockId ) => + clientIdWithParents.includes( blockId ) + ) ) ) { // Ensure that shift-selecting blocks via the keyboard only // expands the current selection if focusing over already @@ -75,35 +78,38 @@ export default function useBlockSelection() { await clearSelectedBlock(); } - let startTarget = getBlockSelectionStart(); - let endTarget = clientId; - - // Handle keyboard behavior for selecting multiple blocks. - if ( isKeyPress ) { - if ( ! hasSelectedBlock() && ! hasMultiSelection() ) { - // Set the starting point of the selection to the currently - // focused block, if there are no blocks currently selected. - // This ensures that as the selection is expanded or contracted, - // the starting point of the selection is anchored to that block. - startTarget = clientId; + // Update selection, if not only clearing the selection. + if ( ! isOnlyDeselection ) { + let startTarget = getBlockSelectionStart(); + let endTarget = clientId; + + // Handle keyboard behavior for selecting multiple blocks. + if ( isKeyPress ) { + if ( ! hasSelectedBlock() && ! hasMultiSelection() ) { + // Set the starting point of the selection to the currently + // focused block, if there are no blocks currently selected. + // This ensures that as the selection is expanded or contracted, + // the starting point of the selection is anchored to that block. + startTarget = clientId; + } + if ( destinationClientId ) { + // If the user presses UP or DOWN, we want to ensure that the block they're + // moving to is the target for selection, and not the currently focused one. + endTarget = destinationClientId; + } } - if ( destinationClientId ) { - // If the user presses UP or DOWN, we want to ensure that the block they're - // moving to is the target for selection, and not the currently focused one. - endTarget = destinationClientId; - } - } - const startParents = getBlockParents( startTarget ); - const endParents = getBlockParents( endTarget ); + const startParents = getBlockParents( startTarget ); + const endParents = getBlockParents( endTarget ); - const { start, end } = getCommonDepthClientIds( - startTarget, - endTarget, - startParents, - endParents - ); - await multiSelect( start, end, null ); + const { start, end } = getCommonDepthClientIds( + startTarget, + endTarget, + startParents, + endParents + ); + await multiSelect( start, end, null ); + } // Announce deselected block, or number of deselected blocks if // the total number of blocks deselected is greater than one. diff --git a/packages/block-editor/src/components/media-placeholder/README.md b/packages/block-editor/src/components/media-placeholder/README.md index 8c26484ead115..001c18673f45c 100644 --- a/packages/block-editor/src/components/media-placeholder/README.md +++ b/packages/block-editor/src/components/media-placeholder/README.md @@ -133,9 +133,9 @@ An object that can contain a `title` and `instructions` properties. These proper ### multiple -Whether to allow multiple selection of files or not. +Whether to allow multiple selection of files or not. This property will also accept a string with the value `add` to allow multiple selection of files without the need to use the `Shift` or `Ctrl`/`Cmd` keys. -- Type: `Boolean` +- Type: `Boolean|String` - Required: No - Default: `false` - Platform: Web diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 4208e5665cfd4..41b1d3fe37c56 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -469,7 +469,7 @@ export function MediaPlaceholder( { { const content = ( <> @@ -508,7 +508,7 @@ export function MediaPlaceholder( { ) } onChange={ onUpload } accept={ accept } - multiple={ multiple } + multiple={ !! multiple } > { __( 'Upload' ) } diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index 9aa36b1b88c85..9010e0a1a9004 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -179,7 +179,7 @@ const MediaReplaceFlow = ( { uploadFiles( event, onClose ); } } accept={ accept } - multiple={ multiple } + multiple={ !! multiple } render={ ( { openFileDialog } ) => { return ( { it( 'renders successfully', () => { render( ); @@ -67,11 +57,7 @@ describe( 'General media replace flow', () => { ); const uploadMenu = screen.getByRole( 'menu' ); - await waitFor( () => - expect( - getWrappingPopoverElement( uploadMenu ) - ).toBePositionedPopover() - ); + await waitFor( () => expect( uploadMenu ).toBePositionedPopover() ); await waitFor( () => expect( uploadMenu ).toBeVisible() ); } ); @@ -92,9 +78,7 @@ describe( 'General media replace flow', () => { name: 'example.media (opens in a new tab)', } ); - await waitFor( () => - expect( getWrappingPopoverElement( link ) ).toBePositionedPopover() - ); + await waitFor( () => expect( link ).toBePositionedPopover() ); expect( link ).toHaveAttribute( 'href', 'https://example.media' ); } ); @@ -113,11 +97,9 @@ describe( 'General media replace flow', () => { await waitFor( () => expect( - getWrappingPopoverElement( - screen.getByRole( 'link', { - name: 'example.media (opens in a new tab)', - } ) - ) + screen.getByRole( 'link', { + name: 'example.media (opens in a new tab)', + } ) ).toBePositionedPopover() ); diff --git a/packages/block-editor/src/components/media-upload/README.md b/packages/block-editor/src/components/media-upload/README.md index cec211005e9cd..592d64e42f497 100644 --- a/packages/block-editor/src/components/media-upload/README.md +++ b/packages/block-editor/src/components/media-upload/README.md @@ -72,11 +72,12 @@ Value of Frame content default mode like 'browse', 'upload' etc. - Required: No - Default: false - Platform: Web + ### multiple -Whether to allow multiple selections or not. +Whether to allow multiple selection of files or not. This property will also accept a string with the value `add` to allow multiple selection of files without the need to use the `Shift` or `Ctrl`/`Cmd` keys. -- Type: `Boolean` +- Type: `Boolean|String` - Required: No - Default: false - Platform: Web diff --git a/packages/block-editor/src/components/observe-typing/README.md b/packages/block-editor/src/components/observe-typing/README.md index c589c6903416d..e44c612a14415 100644 --- a/packages/block-editor/src/components/observe-typing/README.md +++ b/packages/block-editor/src/components/observe-typing/README.md @@ -1,6 +1,6 @@ # Observe Typing -`` is a component used in managing the editor's internal typing flag. When used to wrap content — typically the top-level block list — it observes keyboard and mouse events to set and unset the typing flag. The typing flag is used in considering whether the block border and controls should be visible. While typing, these elements are hidden for a distraction-free experience. +`` is a component used in managing the editor's internal typing flag. When used to wrap content, it observes keyboard and mouse events to set and unset the typing flag. The typing flag is used in considering whether the block border and controls should be visible. While typing, these elements are hidden for a distraction-free experience. ## Usage @@ -10,7 +10,7 @@ Wrap the component where blocks are to be rendered with ``: function VisualEditor() { return ( - + ); } diff --git a/packages/block-editor/src/components/preview-options/index.js b/packages/block-editor/src/components/preview-options/index.js index c22109e7359d1..9f5f820c4edcb 100644 --- a/packages/block-editor/src/components/preview-options/index.js +++ b/packages/block-editor/src/components/preview-options/index.js @@ -33,6 +33,7 @@ export default function PreviewOptions( { const toggleProps = { className: 'block-editor-post-preview__button-toggle', disabled: ! isEnabled, + __experimentalIsFocusable: ! isEnabled, children: viewLabel, }; const menuProps = { @@ -53,6 +54,7 @@ export default function PreviewOptions( { menuProps={ menuProps } icon={ deviceIcons[ deviceType.toLowerCase() ] } label={ label || __( 'Preview' ) } + disableOpenOnArrowDown={ ! isEnabled } > { ( renderProps ) => ( <> diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 13d25aafd8c83..0fa3f042053d0 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -3,6 +3,7 @@ */ import { useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; +import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies @@ -12,6 +13,7 @@ import useBlockSync from './use-block-sync'; import { store as blockEditorStore } from '../../store'; import { BlockRefsProvider } from './block-refs-provider'; import { unlock } from '../../lock-unlock'; +import KeyboardShortcuts from '../keyboard-shortcuts'; /** @typedef {import('@wordpress/data').WPDataRegistry} WPDataRegistry */ @@ -42,7 +44,12 @@ export const ExperimentalBlockEditorProvider = withRegistryProvider( // Syncs the entity provider with changes in the block-editor store. useBlockSync( props ); - return { children }; + return ( + + + { children } + + ); } ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 0e5ff3847547a..a22b251dd607c 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -100,7 +100,6 @@ function RichTextWrapper( onMerge, onSplit, __unstableOnSplitAtEnd: onSplitAtEnd, - __unstableOnSplitMiddle: onSplitMiddle, identifier, preserveWhiteSpace, __unstablePastePlainText: pastePlainText, @@ -380,7 +379,6 @@ function RichTextWrapper( tagName, onReplace, onSplit, - onSplitMiddle, __unstableEmbedURLOnPaste, multilineTag, preserveWhiteSpace, @@ -396,7 +394,6 @@ function RichTextWrapper( value, onReplace, onSplit, - onSplitMiddle, multilineTag, onChange, disableLineBreaks, diff --git a/packages/block-editor/src/components/rich-text/split-value.js b/packages/block-editor/src/components/rich-text/split-value.js index e64fb603a05a1..0ec083c9fe1e5 100644 --- a/packages/block-editor/src/components/rich-text/split-value.js +++ b/packages/block-editor/src/components/rich-text/split-value.js @@ -13,7 +13,6 @@ export function splitValue( { pastedBlocks = [], onReplace, onSplit, - onSplitMiddle, multilineTag, } ) { if ( ! onReplace || ! onSplit ) { @@ -35,8 +34,8 @@ export function splitValue( { // Create a block with the content before the caret if there's no pasted // blocks, or if there are pasted blocks and the value is not empty. We do - // not want a leading empty block on paste, but we do if split with e.g. the - // enter key. + // not want a leading empty block on paste, but we do if we split with e.g. + // the enter key. if ( ! hasPastedBlocks || ! isEmpty( before ) ) { blocks.push( onSplit( @@ -53,19 +52,13 @@ export function splitValue( { if ( hasPastedBlocks ) { blocks.push( ...pastedBlocks ); lastPastedBlockIndex += pastedBlocks.length; - } else if ( onSplitMiddle ) { - blocks.push( onSplitMiddle() ); } - // If there's pasted blocks, append a block with non empty content / after - // the caret. Otherwise, do append an empty block if there is no - // `onSplitMiddle` prop, but if there is and the content is empty, the - // middle block is enough to set focus in. - if ( - hasPastedBlocks - ? ! isEmpty( after ) - : ! onSplitMiddle || ! isEmpty( after ) - ) { + // Create a block with the content after the caret if there's no pasted + // blocks, or if there are pasted blocks and the value is not empty. We do + // not want a trailing empty block on paste, but we do if we split with e.g. + // the enter key. + if ( ! hasPastedBlocks || ! isEmpty( after ) ) { blocks.push( onSplit( toHTMLString( { diff --git a/packages/block-editor/src/components/rich-text/use-enter.js b/packages/block-editor/src/components/rich-text/use-enter.js index ad51128172582..623203fb687df 100644 --- a/packages/block-editor/src/components/rich-text/use-enter.js +++ b/packages/block-editor/src/components/rich-text/use-enter.js @@ -37,7 +37,6 @@ export function useEnter( props ) { value, onReplace, onSplit, - onSplitMiddle, multilineTag, onChange, disableLineBreaks, @@ -78,7 +77,6 @@ export function useEnter( props ) { value: _value, onReplace, onSplit, - onSplitMiddle, multilineTag, } ); } else { @@ -100,7 +98,6 @@ export function useEnter( props ) { value: _value, onReplace, onSplit, - onSplitMiddle, multilineTag, } ); } diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js index d86691ced7097..d64d7ca6b15bb 100644 --- a/packages/block-editor/src/components/rich-text/use-paste-handler.js +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -58,7 +58,6 @@ export function usePasteHandler( props ) { tagName, onReplace, onSplit, - onSplitMiddle, __unstableEmbedURLOnPaste, multilineTag, preserveWhiteSpace, @@ -179,7 +178,6 @@ export function usePasteHandler( props ) { pastedBlocks: blocks, onReplace, onSplit, - onSplitMiddle, multilineTag, } ); } @@ -239,7 +237,6 @@ export function usePasteHandler( props ) { pastedBlocks: content, onReplace, onSplit, - onSplitMiddle, multilineTag, } ); } diff --git a/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap deleted file mode 100644 index c7ecbc209a241..0000000000000 --- a/packages/block-editor/src/components/url-popover/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`URLPopover matches the snapshot in its default state 1`] = ` -
- -
-
-
-
-
- Editor -
- -
-
-
-
-
-
-`; - -exports[`URLPopover matches the snapshot when the settings are toggled open 1`] = ` -
- -
-
-
-
-
- Editor -
- -
-
-
- Settings -
-
-
-
-
-
-
-`; - -exports[`URLPopover matches the snapshot when there are no settings 1`] = ` -
- -
-
-
-
-
- Editor -
-
-
-
-
-
-
-`; diff --git a/packages/block-editor/src/components/url-popover/test/index.js b/packages/block-editor/src/components/url-popover/test/index.js deleted file mode 100644 index 1a60d846b2e90..0000000000000 --- a/packages/block-editor/src/components/url-popover/test/index.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; - -/** - * Internal dependencies - */ -import URLPopover from '../'; - -/** - * Returns the first found popover element up the DOM tree. - * - * @param {HTMLElement} element Element to start with. - * @return {HTMLElement|null} Popover element, or `null` if not found. - */ -function getWrappingPopoverElement( element ) { - return element.closest( '.components-popover' ); -} - -describe( 'URLPopover', () => { - it( 'matches the snapshot in its default state', async () => { - const { container } = render( -
Settings
} - > -
Editor
-
- ); - - await waitFor( () => - expect( - getWrappingPopoverElement( screen.getByText( 'Editor' ) ) - ).toBePositionedPopover() - ); - - expect( container ).toMatchSnapshot(); - } ); - - it( 'matches the snapshot when the settings are toggled open', async () => { - const user = userEvent.setup(); - const { container } = render( -
Settings
} - > -
Editor
-
- ); - - await user.click( - screen.getByRole( 'button', { name: 'Link settings' } ) - ); - - expect( container ).toMatchSnapshot(); - } ); - - it( 'matches the snapshot when there are no settings', async () => { - const { container } = render( - -
Editor
-
- ); - - await waitFor( () => - expect( - getWrappingPopoverElement( screen.getByText( 'Editor' ) ) - ).toBePositionedPopover() - ); - - expect( container ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index bb7b7d97c3190..b44b19b25eadf 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -112,12 +112,18 @@ export const useTransformCommands = () => { }; const useActionsCommands = () => { - const { clientIds } = useSelect( ( select ) => { - const { getSelectedBlockClientIds } = select( blockEditorStore ); + const { clientIds, isUngroupable, isGroupable } = useSelect( ( select ) => { + const { + getSelectedBlockClientIds, + isUngroupable: _isUngroupable, + isGroupable: _isGroupable, + } = select( blockEditorStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); return { clientIds: selectedBlockClientIds, + isUngroupable: _isUngroupable(), + isGroupable: _isGroupable(), }; }, [] ); const { @@ -126,28 +132,12 @@ const useActionsCommands = () => { getBlocksByClientId, canMoveBlocks, canRemoveBlocks, + getBlockCount, } = useSelect( blockEditorStore ); const { getDefaultBlockName, getGroupingBlockName } = useSelect( blocksStore ); const blocks = getBlocksByClientId( clientIds ); - const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); - - const canDuplicate = blocks.every( ( block ) => { - return ( - !! block && - hasBlockSupport( block.name, 'multiple', true ) && - canInsertBlockType( block.name, rootClientId ) - ); - } ); - - const canInsertDefaultBlock = canInsertBlockType( - getDefaultBlockName(), - rootClientId - ); - - const canMove = canMoveBlocks( clientIds, rootClientId ); - const canRemove = canRemoveBlocks( clientIds, rootClientId ); const { removeBlocks, @@ -160,42 +150,6 @@ const useActionsCommands = () => { selectBlock, } = useDispatch( blockEditorStore ); - const onDuplicate = () => { - if ( ! canDuplicate ) { - return; - } - return duplicateBlocks( clientIds, true ); - }; - const onRemove = () => { - if ( ! canRemove ) { - return; - } - return removeBlocks( clientIds, true ); - }; - const onAddBefore = () => { - if ( ! canInsertDefaultBlock ) { - return; - } - const clientId = Array.isArray( clientIds ) ? clientIds[ 0 ] : clientId; - insertBeforeBlock( clientId ); - }; - const onAddAfter = () => { - if ( ! canInsertDefaultBlock ) { - return; - } - const clientId = Array.isArray( clientIds ) - ? clientIds[ clientIds.length - 1 ] - : clientId; - insertAfterBlock( clientId ); - }; - const onMoveTo = () => { - if ( ! canMove ) { - return; - } - setNavigationMode( true ); - selectBlock( clientIds[ 0 ] ); - setBlockMovingClientId( clientIds[ 0 ] ); - }; const onGroup = () => { if ( ! blocks.length ) { return; @@ -229,47 +183,105 @@ const useActionsCommands = () => { return { isLoading: false, commands: [] }; } - const icons = { - ungroup, - group, - move, - add, - remove, - duplicate: copy, - }; - - const commands = [ - onUngroup, - onGroup, - onMoveTo, - onAddAfter, - onAddBefore, - onRemove, - onDuplicate, - ].map( ( callback ) => { - const action = callback.name - .replace( 'on', '' ) - .replace( /([a-z])([A-Z])/g, '$1 $2' ); + const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); + const canInsertDefaultBlock = canInsertBlockType( + getDefaultBlockName(), + rootClientId + ); + const canDuplicate = blocks.every( ( block ) => { + return ( + !! block && + hasBlockSupport( block.name, 'multiple', true ) && + canInsertBlockType( block.name, rootClientId ) + ); + } ); + const canRemove = canRemoveBlocks( clientIds, rootClientId ); + const canMove = + canMoveBlocks( clientIds, rootClientId ) && + getBlockCount( rootClientId ) !== 1; - return { - name: 'core/block-editor/action-' + callback.name, - // translators: %s: type of the command. - label: action, - icon: icons[ - callback.name - .replace( 'on', '' ) - .match( /[A-Z]{1}[a-z]*/ ) - .toString() - .toLowerCase() - ], + const commands = []; + if ( canInsertDefaultBlock ) { + commands.push( + { + name: 'add-after', + label: __( 'Add after' ), + callback: () => { + const clientId = Array.isArray( clientIds ) + ? clientIds[ clientIds.length - 1 ] + : clientId; + insertAfterBlock( clientId ); + }, + icon: add, + }, + { + name: 'add-before', + label: __( 'Add before' ), + callback: () => { + const clientId = Array.isArray( clientIds ) + ? clientIds[ 0 ] + : clientId; + insertBeforeBlock( clientId ); + }, + icon: add, + } + ); + } + if ( canRemove ) { + commands.push( { + name: 'remove', + label: __( 'Remove' ), + callback: () => removeBlocks( clientIds, true ), + icon: remove, + } ); + } + if ( canDuplicate ) { + commands.push( { + name: 'duplicate', + label: __( 'Duplicate' ), + callback: () => duplicateBlocks( clientIds, true ), + icon: copy, + } ); + } + if ( canMove ) { + commands.push( { + name: 'move-to', + label: __( 'Move to' ), + callback: () => { + setNavigationMode( true ); + selectBlock( clientIds[ 0 ] ); + setBlockMovingClientId( clientIds[ 0 ] ); + }, + icon: move, + } ); + } + if ( isUngroupable ) { + commands.push( { + name: 'ungroup', + label: __( 'Ungroup' ), + callback: onUngroup, + icon: ungroup, + } ); + } + if ( isGroupable ) { + commands.push( { + name: 'Group', + label: __( 'Group' ), + callback: onGroup, + icon: group, + } ); + } + return { + isLoading: false, + commands: commands.map( ( command ) => ( { + ...command, + name: 'core/block-editor/action-' + command.name, callback: ( { close } ) => { - callback(); + command.callback(); close(); }, - }; - } ); - - return { isLoading: false, commands }; + } ) ), + }; }; export const useBlockCommands = () => { diff --git a/packages/block-editor/src/components/use-block-display-information/index.js b/packages/block-editor/src/components/use-block-display-information/index.js index 1cff9da4bc04a..68e9abf893674 100644 --- a/packages/block-editor/src/components/use-block-display-information/index.js +++ b/packages/block-editor/src/components/use-block-display-information/index.js @@ -26,6 +26,7 @@ import { store as blockEditorStore } from '../../store'; * @property {WPIcon} icon Block type icon. * @property {string} description A detailed block type description. * @property {string} anchor HTML anchor. + * @property {name} name A custom, human readable name for the block. */ /** @@ -94,6 +95,7 @@ export default function useBlockDisplayInformation( clientId ) { anchor: attributes?.anchor, positionLabel, positionType: attributes?.style?.position?.type, + name: attributes?.metadata?.name, }; if ( ! match ) return blockTypeInfo; @@ -105,6 +107,7 @@ export default function useBlockDisplayInformation( clientId ) { anchor: attributes?.anchor, positionLabel, positionType: attributes?.style?.position?.type, + name: attributes?.metadata?.name, }; }, [ clientId ] diff --git a/packages/block-editor/src/hooks/auto-inserting-blocks.js b/packages/block-editor/src/hooks/auto-inserting-blocks.js new file mode 100644 index 0000000000000..5b3adfbdde8b9 --- /dev/null +++ b/packages/block-editor/src/hooks/auto-inserting-blocks.js @@ -0,0 +1,271 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { addFilter } from '@wordpress/hooks'; +import { Fragment, useMemo } from '@wordpress/element'; +import { + __experimentalHStack as HStack, + PanelBody, + ToggleControl, +} from '@wordpress/components'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { createBlock, store as blocksStore } from '@wordpress/blocks'; +import { useDispatch, useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { BlockIcon, InspectorControls } from '../components'; +import { store as blockEditorStore } from '../store'; + +const EMPTY_OBJECT = {}; + +function AutoInsertingBlocksControl( props ) { + const blockTypes = useSelect( + ( select ) => select( blocksStore ).getBlockTypes(), + [] + ); + + const autoInsertedBlocksForCurrentBlock = useMemo( + () => + blockTypes?.filter( + ( { autoInsert } ) => + autoInsert && props.blockName in autoInsert + ), + [ blockTypes, props.blockName ] + ); + + const { blockIndex, rootClientId, innerBlocksLength } = useSelect( + ( select ) => { + const { getBlock, getBlockIndex, getBlockRootClientId } = + select( blockEditorStore ); + + return { + blockIndex: getBlockIndex( props.clientId ), + innerBlocksLength: getBlock( props.clientId )?.innerBlocks + ?.length, + rootClientId: getBlockRootClientId( props.clientId ), + }; + }, + [ props.clientId ] + ); + + const autoInsertedBlockClientIds = useSelect( + ( select ) => { + const { getBlock, getGlobalBlockCount } = + select( blockEditorStore ); + + const _autoInsertedBlockClientIds = + autoInsertedBlocksForCurrentBlock.reduce( + ( clientIds, block ) => { + // If the block doesn't exist anywhere in the block tree, + // we know that we have to display the toggle for it, and set + // it to disabled. + if ( getGlobalBlockCount( block.name ) === 0 ) { + return clientIds; + } + + const relativePosition = + block?.autoInsert?.[ props.blockName ]; + let candidates; + + switch ( relativePosition ) { + case 'before': + case 'after': + // Any of the current block's siblings (with the right block type) qualifies + // as an auto-inserted block (inserted `before` or `after` the current one), + // as the block might've been auto-inserted and then moved around a bit by the user. + candidates = + getBlock( rootClientId )?.innerBlocks; + break; + + case 'first_child': + case 'last_child': + // Any of the current block's child blocks (with the right block type) qualifies + // as an auto-inserted first or last child block, as the block might've been + // auto-inserted and then moved around a bit by the user. + candidates = getBlock( + props.clientId + ).innerBlocks; + break; + } + + const autoInsertedBlock = candidates?.find( + ( { name } ) => name === block.name + ); + + // If the block exists in the designated location, we consider it auto-inserted + // and show the toggle as enabled. + if ( autoInsertedBlock ) { + return { + ...clientIds, + [ block.name ]: autoInsertedBlock.clientId, + }; + } + + // If no auto-inserted block was found in any of its designated locations, + // but it exists elsewhere in the block tree, we consider it manually inserted. + // In this case, we take note and will remove the corresponding toggle from the + // block inspector panel. + return { + ...clientIds, + [ block.name ]: false, + }; + }, + {} + ); + + if ( Object.values( _autoInsertedBlockClientIds ).length > 0 ) { + return _autoInsertedBlockClientIds; + } + + return EMPTY_OBJECT; + }, + [ + autoInsertedBlocksForCurrentBlock, + props.blockName, + props.clientId, + rootClientId, + ] + ); + + const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); + + // Remove toggle if block isn't present in the designated location but elsewhere in the block tree. + const autoInsertedBlocksForCurrentBlockIfNotPresentElsewhere = + autoInsertedBlocksForCurrentBlock?.filter( + ( block ) => autoInsertedBlockClientIds?.[ block.name ] !== false + ); + + if ( ! autoInsertedBlocksForCurrentBlockIfNotPresentElsewhere.length ) { + return null; + } + + // Group by block namespace (i.e. prefix before the slash). + const groupedAutoInsertedBlocks = autoInsertedBlocksForCurrentBlock.reduce( + ( groups, block ) => { + const [ namespace ] = block.name.split( '/' ); + if ( ! groups[ namespace ] ) { + groups[ namespace ] = []; + } + groups[ namespace ].push( block ); + return groups; + }, + {} + ); + + const insertBlockIntoDesignatedLocation = ( block, relativePosition ) => { + switch ( relativePosition ) { + case 'before': + case 'after': + insertBlock( + block, + relativePosition === 'after' ? blockIndex + 1 : blockIndex, + rootClientId, // Insert as a child of the current block's parent + false + ); + break; + + case 'first_child': + case 'last_child': + insertBlock( + block, + // TODO: It'd be great if insertBlock() would accept negative indices for insertion. + relativePosition === 'first_child' ? 0 : innerBlocksLength, + props.clientId, // Insert as a child of the current block. + false + ); + break; + } + }; + + return ( + + + { Object.keys( groupedAutoInsertedBlocks ).map( ( vendor ) => { + return ( + +

{ vendor }

+ { groupedAutoInsertedBlocks[ vendor ].map( + ( block ) => { + const checked = + block.name in + autoInsertedBlockClientIds; + + return ( + + + { block.title } + + } + onChange={ () => { + if ( ! checked ) { + // Create and insert block. + const relativePosition = + block.autoInsert[ + props.blockName + ]; + insertBlockIntoDesignatedLocation( + createBlock( + block.name + ), + relativePosition + ); + return; + } + + // Remove block. + const clientId = + autoInsertedBlockClientIds[ + block.name + ]; + removeBlock( clientId, false ); + } } + /> + ); + } + ) } +
+ ); + } ) } +
+
+ ); +} + +export const withAutoInsertingBlocks = createHigherOrderComponent( + ( BlockEdit ) => { + return ( props ) => { + const blockEdit = ; + return ( + <> + { blockEdit } + + + ); + }; + }, + 'withAutoInsertingBlocks' +); + +if ( window?.__experimentalAutoInsertingBlocks ) { + addFilter( + 'editor.BlockEdit', + 'core/auto-inserting-blocks/with-inspector-control', + withAutoInsertingBlocks + ); +} diff --git a/packages/block-editor/src/hooks/auto-inserting-blocks.scss b/packages/block-editor/src/hooks/auto-inserting-blocks.scss new file mode 100644 index 0000000000000..8c43c3673053e --- /dev/null +++ b/packages/block-editor/src/hooks/auto-inserting-blocks.scss @@ -0,0 +1,16 @@ +.block-editor-hooks__auto-inserting-blocks { + /** + * Since we're displaying the block icon alongside the block name, + * we need to right-align the toggle. + */ + .components-toggle-control .components-h-stack { + flex-direction: row-reverse; + } + + /** + * Un-reverse the flex direction for the toggle's label. + */ + .components-toggle-control .components-h-stack .components-h-stack { + flex-direction: row; + } +} diff --git a/packages/block-editor/src/hooks/block-rename-ui.js b/packages/block-editor/src/hooks/block-rename-ui.js new file mode 100644 index 0000000000000..189090668cbb4 --- /dev/null +++ b/packages/block-editor/src/hooks/block-rename-ui.js @@ -0,0 +1,230 @@ +/** + * WordPress dependencies + */ +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; +import { __, sprintf } from '@wordpress/i18n'; +import { getBlockSupport } from '@wordpress/blocks'; +import { + MenuItem, + __experimentalHStack as HStack, + __experimentalVStack as VStack, + Button, + TextControl, + Modal, +} from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import { + BlockSettingsMenuControls, + useBlockDisplayInformation, + InspectorControls, +} from '../components'; + +const emptyString = ( testString ) => testString?.trim()?.length === 0; + +function RenameModal( { blockName, originalBlockName, onClose, onSave } ) { + const [ editedBlockName, setEditedBlockName ] = useState( blockName ); + + const nameHasChanged = editedBlockName !== blockName; + const nameIsOriginal = editedBlockName === originalBlockName; + const nameIsEmpty = emptyString( editedBlockName ); + + const isNameValid = nameHasChanged || nameIsOriginal; + + const autoSelectInputText = ( event ) => event.target.select(); + + const dialogDescription = useInstanceId( + RenameModal, + `block-editor-rename-modal__description` + ); + + const handleSubmit = () => { + // Must be assertive to immediately announce change. + speak( + sprintf( + /* translators: %1$s: type of update (either reset of changed). %2$s: new name/label for the block */ + __( 'Block name %1$s to: "%2$s".' ), + nameIsOriginal || nameIsEmpty ? __( 'reset' ) : __( 'changed' ), + editedBlockName + ), + 'assertive' + ); + + onSave( editedBlockName ); + + // Immediate close avoids ability to hit save multiple times. + onClose(); + }; + + return ( + +

+ { __( 'Choose a custom name for this block.' ) } +

+
{ + e.preventDefault(); + + if ( ! isNameValid ) { + return; + } + + handleSubmit(); + } } + > + + + + + + + + +
+
+ ); +} + +function BlockRenameControl( props ) { + const [ renamingBlock, setRenamingBlock ] = useState( false ); + + const { clientId, customName, onChange } = props; + + const blockInformation = useBlockDisplayInformation( clientId ); + + return ( + <> + + + + + { ( { selectedClientIds } ) => { + // Only enabled for single selections. + const canRename = + selectedClientIds.length === 1 && + clientId === selectedClientIds[ 0 ]; + + // This check ensures the `BlockSettingsMenuControls` fill + // doesn't render multiple times and also that it renders for + // the block from which the menu was triggered. + if ( ! canRename ) { + return null; + } + + return ( + { + setRenamingBlock( true ); + } } + aria-expanded={ renamingBlock } + aria-haspopup="dialog" + > + { __( 'Rename' ) } + + ); + } } + + + { renamingBlock && ( + setRenamingBlock( false ) } + onSave={ ( newName ) => { + // If the new value is the block's original name (e.g. `Group`) + // or it is an empty string then assume the intent is to reset + // the value. Therefore reset the metadata. + if ( + newName === blockInformation?.title || + emptyString( newName ) + ) { + newName = undefined; + } + + onChange( newName ); + } } + /> + ) } + + ); +} + +export const withBlockRenameControl = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { clientId, name, attributes, setAttributes } = props; + + const metaDataSupport = getBlockSupport( + name, + '__experimentalMetadata', + false + ); + + const supportsBlockNaming = !! ( + true === metaDataSupport || metaDataSupport?.name + ); + + return ( + <> + { supportsBlockNaming && ( + <> + { + setAttributes( { + metadata: { + ...( attributes?.metadata && + attributes?.metadata ), + name: newName, + }, + } ); + } } + /> + + ) } + + + + ); + }, + 'withToolbarControls' +); + +addFilter( + 'editor.BlockEdit', + 'core/block-rename-ui/with-block-rename-control', + withBlockRenameControl +); diff --git a/packages/block-editor/src/hooks/block-rename-ui.scss b/packages/block-editor/src/hooks/block-rename-ui.scss new file mode 100644 index 0000000000000..2b08e82662bc6 --- /dev/null +++ b/packages/block-editor/src/hooks/block-rename-ui.scss @@ -0,0 +1,3 @@ +.block-editor-block-rename-modal { + z-index: z-index(".block-editor-block-rename-modal"); +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 565fb6088682e..b7dac4cd758c6 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -21,6 +21,8 @@ import './content-lock-ui'; import './metadata'; import './metadata-name'; import './custom-fields'; +import './auto-inserting-blocks'; +import './block-rename-ui'; export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index b416c86405e51..e165a88882294 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -34,6 +34,7 @@ import { } from './dimensions'; import useDisplayBlockControls from '../components/use-display-block-controls'; import { shouldSkipSerialization } from './utils'; +import { scopeSelector } from '../components/global-styles/utils'; import { useBlockEditingMode } from '../components/block-editing-mode'; const styleSupportKeys = [ @@ -371,6 +372,18 @@ export const withBlockControls = createHigherOrderComponent( 'withToolbarControls' ); +// Defines which element types are supported, including their hover styles or +// any other elements that have been included under a single element type +// e.g. heading and h1-h6. +const elementTypes = [ + { elementType: 'button' }, + { elementType: 'link', pseudo: [ ':hover' ] }, + { + elementType: 'heading', + elements: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ], + }, +]; + /** * Override the default block element to include elements styles. * @@ -383,47 +396,84 @@ const withElementsStyles = createHigherOrderComponent( BlockListBlock ) }`; - const skipLinkColorSerialization = shouldSkipSerialization( - props.name, - COLOR_SUPPORT_KEY, - 'link' - ); + // The .editor-styles-wrapper selector is required on elements styles. As it is + // added to all other editor styles, not providing it causes reset and global + // styles to override element styles because of higher specificity. + const baseElementSelector = `.editor-styles-wrapper .${ blockElementsContainerIdentifier }`; + const blockElementStyles = props.attributes.style?.elements; const styles = useMemo( () => { - // The .editor-styles-wrapper selector is required on elements styles. As it is - // added to all other editor styles, not providing it causes reset and global - // styles to override element styles because of higher specificity. - const elements = [ - { - styles: ! skipLinkColorSerialization - ? props.attributes.style?.elements?.link - : undefined, - selector: `.editor-styles-wrapper .${ blockElementsContainerIdentifier } ${ ELEMENTS.link }`, - }, - { - styles: ! skipLinkColorSerialization - ? props.attributes.style?.elements?.link?.[ ':hover' ] - : undefined, - selector: `.editor-styles-wrapper .${ blockElementsContainerIdentifier } ${ ELEMENTS.link }:hover`, - }, - ]; - const elementCssRules = []; - for ( const { styles: elementStyles, selector } of elements ) { + if ( ! blockElementStyles ) { + return; + } + + const elementCSSRules = []; + + elementTypes.forEach( ( { elementType, pseudo, elements } ) => { + const skipSerialization = shouldSkipSerialization( + props.name, + COLOR_SUPPORT_KEY, + elementType + ); + + if ( skipSerialization ) { + return; + } + + const elementStyles = blockElementStyles?.[ elementType ]; + + // Process primary element type styles. if ( elementStyles ) { - const cssRule = compileCSS( elementStyles, { - selector, + const selector = scopeSelector( + baseElementSelector, + ELEMENTS[ elementType ] + ); + + elementCSSRules.push( + compileCSS( elementStyles, { selector } ) + ); + + // Process any interactive states for the element type. + if ( pseudo ) { + pseudo.forEach( ( pseudoSelector ) => { + if ( elementStyles[ pseudoSelector ] ) { + elementCSSRules.push( + compileCSS( + elementStyles[ pseudoSelector ], + { + selector: scopeSelector( + baseElementSelector, + `${ ELEMENTS[ elementType ] }${ pseudoSelector }` + ), + } + ) + ); + } + } ); + } + } + + // Process related elements e.g. h1-h6 for headings + if ( elements ) { + elements.forEach( ( element ) => { + if ( blockElementStyles[ element ] ) { + elementCSSRules.push( + compileCSS( blockElementStyles[ element ], { + selector: scopeSelector( + baseElementSelector, + ELEMENTS[ element ] + ), + } ) + ); + } } ); - elementCssRules.push( cssRule ); } - } - return elementCssRules.length > 0 - ? elementCssRules.join( '' ) + } ); + + return elementCSSRules.length > 0 + ? elementCSSRules.join( '' ) : undefined; - }, [ - props.attributes.style?.elements, - blockElementsContainerIdentifier, - skipLinkColorSerialization, - ] ); + }, [ baseElementSelector, blockElementStyles, props.name ] ); const element = useContext( BlockList.__unstableElementContext ); diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 9c6bf957d61c5..f81fc118ea84b 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -166,6 +166,8 @@ export function useBlockSettings( name, parentLayout ) { const isBackgroundEnabled = useSetting( 'color.background' ); const isLinkEnabled = useSetting( 'color.link' ); const isTextEnabled = useSetting( 'color.text' ); + const isHeadingEnabled = useSetting( 'color.heading' ); + const isButtonEnabled = useSetting( 'color.button' ); const rawSettings = useMemo( () => { return { @@ -193,6 +195,8 @@ export function useBlockSettings( name, parentLayout ) { customDuotone, background: isBackgroundEnabled, link: isLinkEnabled, + heading: isHeadingEnabled, + button: isButtonEnabled, text: isTextEnabled, }, typography: { diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index f0a5bfb3902aa..e6c80a731e180 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -23,6 +23,7 @@ import { useReusableBlocksRenameHint, } from './components/inserter/reusable-block-rename-hint'; import { usesContextKey } from './components/rich-text/format-edit'; +import { ExperimentalBlockCanvas } from './components/block-canvas'; /** * Private @wordpress/block-editor APIs. @@ -30,6 +31,7 @@ import { usesContextKey } from './components/rich-text/format-edit'; export const privateApis = {}; lock( privateApis, { ...globalStyles, + ExperimentalBlockCanvas, ExperimentalBlockEditorProvider, getRichTextValues, kebabCase, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 3c961c130b78a..2de9e3f00be75 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -2941,3 +2941,68 @@ export const getBlockEditingMode = createRegistrySelector( return parentMode === 'contentOnly' ? 'default' : parentMode; } ); + +/** + * Indicates if a block is ungroupable. + * A block is ungroupable if it is a single grouping block with inner blocks. + * If a block has an `ungroup` transform, it is also ungroupable, without the + * requirement of being the default grouping block. + * Additionally a block can only be ungrouped if it has inner blocks and can + * be removed. + * + * @param {Object} state Global application state. + * @param {string} clientId Client Id of the block. If not passed the selected block's client id will be used. + * @return {boolean} True if the block is ungroupable. + */ +export const isUngroupable = createRegistrySelector( + ( select ) => + ( state, clientId = '' ) => { + const _clientId = clientId || getSelectedBlockClientId( state ); + if ( ! _clientId ) { + return false; + } + const { getGroupingBlockName } = select( blocksStore ); + const block = getBlock( state, _clientId ); + const groupingBlockName = getGroupingBlockName(); + const _isUngroupable = + block && + ( block.name === groupingBlockName || + getBlockType( block.name )?.transforms?.ungroup ) && + !! block.innerBlocks.length; + + return _isUngroupable && canRemoveBlock( state, _clientId ); + } +); + +/** + * Indicates if the provided blocks(by client ids) are groupable. + * We need to have at least one block, have a grouping block name set and + * be able to remove these blocks. + * + * @param {Object} state Global application state. + * @param {string[]} clientIds Block client ids. If not passed the selected blocks client ids will be used. + * @return {boolean} True if the blocks are groupable. + */ +export const isGroupable = createRegistrySelector( + ( select ) => + ( state, clientIds = EMPTY_ARRAY ) => { + const { getGroupingBlockName } = select( blocksStore ); + const groupingBlockName = getGroupingBlockName(); + const _clientIds = clientIds?.length + ? clientIds + : getSelectedBlockClientIds( state ); + const rootClientId = _clientIds?.length + ? getBlockRootClientId( state, _clientIds[ 0 ] ) + : undefined; + const groupingBlockAvailable = canInsertBlockType( + state, + groupingBlockName, + rootClientId + ); + const _isGroupable = groupingBlockAvailable && _clientIds.length; + return ( + _isGroupable && + canRemoveBlocks( state, _clientIds, rootClientId ) + ); + } +); diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 5eafc0766ae22..e3e65469b4c54 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -46,6 +46,7 @@ @import "./components/url-input/style.scss"; @import "./components/url-popover/style.scss"; @import "./hooks/anchor.scss"; +@import "./hooks/auto-inserting-blocks.scss"; @import "./hooks/border.scss"; @import "./hooks/color.scss"; @import "./hooks/dimensions.scss"; @@ -53,6 +54,7 @@ @import "./hooks/padding.scss"; @import "./hooks/position.scss"; @import "./hooks/typography.scss"; +@import "./hooks/block-rename-ui.scss"; @import "./components/block-toolbar/style.scss"; @import "./components/inserter/style.scss"; diff --git a/packages/block-editor/src/utils/pre-parse-patterns.js b/packages/block-editor/src/utils/pre-parse-patterns.js deleted file mode 100644 index c18215ee8e63f..0000000000000 --- a/packages/block-editor/src/utils/pre-parse-patterns.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, select } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../store'; - -const requestIdleCallback = ( () => { - if ( typeof window === 'undefined' ) { - return ( callback ) => { - setTimeout( () => callback( Date.now() ), 0 ); - }; - } - - return window.requestIdleCallback || window.requestAnimationFrame; -} )(); - -const cancelIdleCallback = ( () => { - if ( typeof window === 'undefined' ) { - return clearTimeout; - } - - return window.cancelIdleCallback || window.cancelAnimationFrame; -} )(); - -export function usePreParsePatterns() { - const { patterns, isPreviewMode } = useSelect( ( _select ) => { - const { __experimentalBlockPatterns, __unstableIsPreviewMode } = - _select( blockEditorStore ).getSettings(); - return { - patterns: __experimentalBlockPatterns, - isPreviewMode: __unstableIsPreviewMode, - }; - }, [] ); - - useEffect( () => { - if ( isPreviewMode ) { - return; - } - if ( ! patterns?.length ) { - return; - } - - let handle; - let index = -1; - - const callback = () => { - index++; - if ( index >= patterns.length ) { - return; - } - - select( blockEditorStore ).__experimentalGetParsedPattern( - patterns[ index ].name - ); - - handle = requestIdleCallback( callback ); - }; - - handle = requestIdleCallback( callback ); - return () => cancelIdleCallback( handle ); - }, [ patterns, isPreviewMode ] ); - - return null; -} diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index a7fe0bd99bd4a..e31c8ba509518 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.18.0 (2023-08-31) + ## 8.17.0 (2023-08-16) ## 8.16.0 (2023-08-10) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 5fe213651f54a..443c48f8bf4ef 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.17.0", + "version": "8.18.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap index dca3f782efc67..e37c7fa107102 100644 --- a/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/audio/test/__snapshots__/edit.native.js.snap @@ -133,7 +133,7 @@ exports[`Audio block renders audio block error state without crashing 1`] = ` ] } > - + + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "", + } + } + textAlign="center" + triggerKeyCodes={[]} + /> + @@ -359,7 +373,7 @@ exports[`Audio block renders audio file without crashing 1`] = ` ] } > - + + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "", + } + } + textAlign="center" + triggerKeyCodes={[]} + /> + diff --git a/packages/block-library/src/column/block.json b/packages/block-library/src/column/block.json index dcd29cf100bda..7f61f307fab15 100644 --- a/packages/block-library/src/column/block.json +++ b/packages/block-library/src/column/block.json @@ -30,6 +30,7 @@ "color": { "gradients": true, "heading": true, + "button": true, "link": true, "__experimentalDefaultControls": { "background": true, diff --git a/packages/block-library/src/column/edit.native.js b/packages/block-library/src/column/edit.native.js index 46e5012f68a34..7e18e73a9b14a 100644 --- a/packages/block-library/src/column/edit.native.js +++ b/packages/block-library/src/column/edit.native.js @@ -111,10 +111,10 @@ function ColumnEdit( { }; const renderAppender = useCallback( () => { - const { width: columnWidth } = contentStyle[ clientId ]; - const isFullWidth = columnWidth === screenWidth; - if ( isSelected ) { + const { width: columnWidth } = contentStyle[ clientId ] || {}; + const isFullWidth = columnWidth === screenWidth; + return ( " `; +exports[`Columns block transforms a nested Columns block into a Group block 1`] = ` +" +
+
+
+
+

+
+
+
+
+" +`; + exports[`Columns block when using columns percentage mechanism sets custom values correctly 1`] = ` "
diff --git a/packages/block-library/src/columns/test/edit.native.js b/packages/block-library/src/columns/test/edit.native.js index bb7bfbdafea4b..20430704c8e1e 100644 --- a/packages/block-library/src/columns/test/edit.native.js +++ b/packages/block-library/src/columns/test/edit.native.js @@ -4,14 +4,16 @@ import { act, addBlock, + dismissModal, fireEvent, + getBlock, getEditorHtml, initializeEditor, + openBlockActionsMenu, openBlockSettings, - within, - getBlock, - dismissModal, + screen, waitForModalVisible, + within, } from 'test/helpers'; /** @@ -43,7 +45,7 @@ afterAll( () => { describe( 'Columns block', () => { it( 'inserts block', async () => { - const screen = await initializeEditor(); + await initializeEditor(); // Add block await addBlock( screen, 'Columns' ); @@ -55,7 +57,7 @@ describe( 'Columns block', () => { } ); it( 'adds a column block using the appender', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); @@ -73,7 +75,7 @@ describe( 'Columns block', () => { describe( 'when using the number of columns setting', () => { it( 'adds a column block when incrementing the value', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -95,7 +97,7 @@ describe( 'Columns block', () => { } ); it( 'adds at least 15 Column blocks without limitation', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -120,7 +122,7 @@ describe( 'Columns block', () => { } ); it( 'removes a column block when decrementing the value', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -142,7 +144,7 @@ describe( 'Columns block', () => { } ); it( 'reaches the minimum limit of number of column blocks', async () => { - const screen = await initializeEditor(); + await initializeEditor(); // Add block await addBlock( screen, 'Columns' ); @@ -185,7 +187,7 @@ describe( 'Columns block', () => { } ); it( 'removes column with the remove button', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -210,7 +212,7 @@ describe( 'Columns block', () => { } ); it( 'removes the only one left Column with the remove button', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -247,7 +249,7 @@ describe( 'Columns block', () => { } ); it( 'changes vertical alignment on Columns', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -270,7 +272,7 @@ describe( 'Columns block', () => { } ); it( 'changes the vertical alignment on individual Column', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); @@ -308,7 +310,7 @@ describe( 'Columns block', () => { } ); it( 'sets current vertical alignment on new Columns', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText } = screen; @@ -337,7 +339,7 @@ describe( 'Columns block', () => { describe( 'when using columns percentage mechanism', () => { it( "updates the slider's input value", async () => { - const screen = await initializeEditor(); + await initializeEditor(); // Add block await addBlock( screen, 'Columns' ); @@ -377,7 +379,7 @@ describe( 'Columns block', () => { } ); it( 'sets custom values correctly', async () => { - const screen = await initializeEditor( { + await initializeEditor( { initialHtml: TWO_COLUMNS_BLOCK_HTML, } ); const { getByLabelText, getByTestId } = screen; @@ -443,7 +445,7 @@ describe( 'Columns block', () => { test.each( testData )( 'sets the predefined percentages for %s', async ( layout ) => { - const screen = await initializeEditor(); + await initializeEditor(); // Add block await addBlock( screen, 'Columns' ); @@ -463,4 +465,32 @@ describe( 'Columns block', () => { } ); } ); + + it( 'transforms a nested Columns block into a Group block', async () => { + await initializeEditor( { + initialHtml: ` +
+
+
+

+
+
+
+ `, + } ); + + // Get Columns block + const columnsBlock = await getBlock( screen, 'Columns' ); + fireEvent.press( columnsBlock ); + + // Open block actions menu + await openBlockActionsMenu( screen ); + + // Tap on the Transform block button + fireEvent.press( screen.getByLabelText( /Transform block…/ ) ); + + // Tap on the Group transform button + fireEvent.press( screen.getByLabelText( 'Group' ) ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 8409d33a2e1ca..932281b1ab6f1 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -151,8 +151,10 @@ function CoverEdit( { const setMedia = attributesFromMedia( setAttributes, dimRatio ); const onSelectMedia = async ( newMedia ) => { + // Only pass the url to getCoverIsDark if the media is an image as video is not handled. + const newUrl = newMedia?.type === 'image' ? newMedia.url : undefined; const isDarkSetting = await getCoverIsDark( - newMedia.url, + newUrl, dimRatio, overlayColor.color ); diff --git a/packages/block-library/src/cover/shared.js b/packages/block-library/src/cover/shared.js index 1b9fdc0e56680..72a42c0d5c70b 100644 --- a/packages/block-library/src/cover/shared.js +++ b/packages/block-library/src/cover/shared.js @@ -167,26 +167,31 @@ export async function getCoverIsDark( url, dimRatio = 50, overlayColor ) { .toRgb(); if ( url ) { - const imgCrossOrigin = applyFilters( - 'media.crossOrigin', - undefined, - url - ); - const { - value: [ r, g, b, a ], - } = await retrieveFastAverageColor().getColorAsync( url, { - // Previously the default color was white, but that changed - // in v6.0.0 so it has to be manually set now. - defaultColor: [ 255, 255, 255, 255 ], - // Errors that come up don't reject the promise, so error - // logging has to be silenced with this option. - silent: process.env.NODE_ENV === 'production', - crossOrigin: imgCrossOrigin, - } ); - // FAC uses 0-255 for alpha, but colord expects 0-1. - const media = { r, g, b, a: a / 255 }; - const composite = compositeSourceOver( overlay, media ); - return colord( composite ).isDark(); + try { + const imgCrossOrigin = applyFilters( + 'media.crossOrigin', + undefined, + url + ); + const { + value: [ r, g, b, a ], + } = await retrieveFastAverageColor().getColorAsync( url, { + // Previously the default color was white, but that changed + // in v6.0.0 so it has to be manually set now. + defaultColor: [ 255, 255, 255, 255 ], + // Errors that come up don't reject the promise, so error + // logging has to be silenced with this option. + silent: process.env.NODE_ENV === 'production', + crossOrigin: imgCrossOrigin, + } ); + // FAC uses 0-255 for alpha, but colord expects 0-1. + const media = { r, g, b, a: a / 255 }; + const composite = compositeSourceOver( overlay, media ); + return colord( composite ).isDark(); + } catch ( error ) { + // If there's an error, just assume the image is dark. + return true; + } } // Assume a white background because it isn't easy to get the actual diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index 23005d1d0a8db..74db754ff1696 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -47,7 +47,7 @@ async function setup( attributes, useCoreBlocks, customSettings ) { async function createAndSelectBlock() { await userEvent.click( - screen.getByRole( 'button', { + screen.getByRole( 'option', { name: 'Color: Black', } ) ); @@ -72,7 +72,7 @@ describe( 'Cover block', () => { test( 'can set overlay color using color picker on block placeholder', async () => { const { container } = await setup(); - const colorPicker = screen.getByRole( 'button', { + const colorPicker = screen.getByRole( 'option', { name: 'Color: Black', } ); await userEvent.click( colorPicker ); @@ -96,7 +96,7 @@ describe( 'Cover block', () => { await setup(); await userEvent.click( - screen.getByRole( 'button', { + screen.getByRole( 'option', { name: 'Color: Black', } ) ); @@ -389,7 +389,7 @@ describe( 'Cover block', () => { describe( 'isDark settings', () => { test( 'should toggle is-light class if background changed from light to dark', async () => { await setup(); - const colorPicker = screen.getByRole( 'button', { + const colorPicker = screen.getByRole( 'option', { name: 'Color: White', } ); await userEvent.click( colorPicker ); @@ -405,7 +405,7 @@ describe( 'Cover block', () => { } ) ); await userEvent.click( screen.getByText( 'Overlay' ) ); - const popupColorPicker = screen.getByRole( 'button', { + const popupColorPicker = screen.getByRole( 'option', { name: 'Color: Black', } ); await userEvent.click( popupColorPicker ); @@ -413,7 +413,7 @@ describe( 'Cover block', () => { } ); test( 'should remove is-light class if overlay color is removed', async () => { await setup(); - const colorPicker = screen.getByRole( 'button', { + const colorPicker = screen.getByRole( 'option', { name: 'Color: White', } ); await userEvent.click( colorPicker ); @@ -428,7 +428,7 @@ describe( 'Cover block', () => { await userEvent.click( screen.getByText( 'Overlay' ) ); // The default color is black, so clicking the black color option will remove the background color, // which should remove the isDark setting and assign the is-light class. - const popupColorPicker = screen.getByRole( 'button', { + const popupColorPicker = screen.getByRole( 'option', { name: 'Color: White', } ); await userEvent.click( popupColorPicker ); diff --git a/packages/block-library/src/cover/test/edit.native.js b/packages/block-library/src/cover/test/edit.native.js index d5dffb3161a52..8ca3d40967a27 100644 --- a/packages/block-library/src/cover/test/edit.native.js +++ b/packages/block-library/src/cover/test/edit.native.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { Image, Pressable } from 'react-native'; +import { Image } from 'react-native'; import { getEditorHtml, initializeEditor, @@ -12,7 +12,6 @@ import { getBlock, openBlockSettings, } from 'test/helpers'; -import HsvColorPicker from 'react-native-hsv-color-picker'; /** * WordPress dependencies @@ -541,9 +540,6 @@ describe( 'color settings', () => { } ); it( 'displays the hex color value in the custom color picker', async () => { - HsvColorPicker.mockImplementation( ( props ) => { - return ; - } ); const screen = await initializeEditor( { initialHtml: COVER_BLOCK_PLACEHOLDER_HTML, } ); diff --git a/packages/block-library/src/embed/embed-link-settings.native.js b/packages/block-library/src/embed/embed-link-settings.native.js index ad4152c2fe19a..9c3fbffc8a196 100644 --- a/packages/block-library/src/embed/embed-link-settings.native.js +++ b/packages/block-library/src/embed/embed-link-settings.native.js @@ -90,7 +90,6 @@ const EmbedLinkSettings = ( { onDismiss={ onDismiss } setAttributes={ onSetAttributes } options={ linkSettingsOptions } - testID="embed-edit-url-modal" withBottomSheet={ withBottomSheet } showIcon /> diff --git a/packages/block-library/src/embed/test/index.native.js b/packages/block-library/src/embed/test/index.native.js index c2e65bcef35c4..b13b86b40d3fd 100644 --- a/packages/block-library/src/embed/test/index.native.js +++ b/packages/block-library/src/embed/test/index.native.js @@ -241,7 +241,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -259,7 +259,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -299,7 +299,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -338,7 +338,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -359,7 +359,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -400,7 +400,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -584,7 +584,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -623,7 +623,7 @@ describe( 'Embed block', () => { // Wait for edit URL modal to be visible. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); await waitForModalVisible( embedEditURLModal ); @@ -804,7 +804,7 @@ describe( 'Embed block', () => { // Dismiss the edit URL modal. const embedEditURLModal = editor.getByTestId( - 'embed-edit-url-modal' + 'link-settings-navigation' ); fireEvent( embedEditURLModal, 'backdropPress' ); fireEvent( embedEditURLModal, MODAL_DISMISS_EVENT ); diff --git a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap index 5ce876137ade0..a14a1de7b76bc 100644 --- a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap @@ -64,7 +64,7 @@ exports[`File block renders file error state without crashing 1`] = ` ] } > - + File name

", + deleteEnter={true} + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="File name" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } } - } - textAlign="left" - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

File name

", + } + } + textAlign="left" + triggerKeyCodes={[]} + /> + - + Download

", + color="white" + deleteEnter={true} + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + minWidth={40} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="" + placeholderTextColor="white" + selectionColor="white" + style={ + { + "backgroundColor": undefined, + "color": "white", + "maxWidth": 80, + "minHeight": 0, + } } - } - textAlign="center" - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Download

", + } + } + textAlign="center" + triggerKeyCodes={[]} + /> +
@@ -315,7 +343,7 @@ exports[`File block renders file without crashing 1`] = ` ] } > - + File name

", + deleteEnter={true} + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="File name" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } } - } - textAlign="left" - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

File name

", + } + } + textAlign="left" + triggerKeyCodes={[]} + /> + - + Download

", + color="white" + deleteEnter={true} + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + minWidth={40} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="" + placeholderTextColor="white" + selectionColor="white" + style={ + { + "backgroundColor": undefined, + "color": "white", + "maxWidth": 80, + "minHeight": 0, + } } - } - textAlign="center" - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Download

", + } + } + textAlign="center" + triggerKeyCodes={[]} + /> +
diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js index b8b92170fe217..111e0ba5d3a0e 100644 --- a/packages/block-library/src/footnotes/edit.js +++ b/packages/block-library/src/footnotes/edit.js @@ -23,7 +23,9 @@ export default function FootnotesEdit( { context: { postType, postId } } ) { } label={ __( 'Footnotes' ) } - // To do: add instructions. We can't add new string in RC. + instructions={ __( + 'Footnotes are not supported here. Add this block to post or page content.' + ) } />
); @@ -46,7 +48,19 @@ export default function FootnotesEdit( { context: { postType, postId } } ) { return (
    { footnotes.map( ( { id, content } ) => ( -
  1. + /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */ +
  2. { + // When clicking on the list item (not on descendants), + // focus the rich text element since it's only 1px wide when + // empty. + if ( event.target === event.currentTarget ) { + event.target.firstElementChild.focus(); + event.preventDefault(); + } + } } + > { innerBlocksProps.children } diff --git a/packages/block-library/src/heading/transforms.js b/packages/block-library/src/heading/transforms.js index 1076204f74dbf..a4db788462096 100644 --- a/packages/block-library/src/heading/transforms.js +++ b/packages/block-library/src/heading/transforms.js @@ -73,12 +73,7 @@ const transforms = { ...[ 1, 2, 3, 4, 5, 6 ].map( ( level ) => ( { type: 'enter', regExp: new RegExp( `^/(h|H)${ level }$` ), - transform( content ) { - return createBlock( 'core/heading', { - level, - content, - } ); - }, + transform: () => createBlock( 'core/heading', { level } ), } ) ), ], to: [ diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 38ccbaeb179a5..fceba38244033 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -47,6 +47,11 @@ function render_block_core_image( $attributes, $content, $block ) { $should_load_view_script = true; } + // If at least one block in the page has the lightbox, mark the block type as interactive. + if ( $should_load_view_script ) { + $block->block_type->supports['interactivity'] = true; + } + $view_js_file = 'wp-block-image-view'; if ( ! wp_script_is( $view_js_file ) ) { $script_handles = $block->block_type->view_script_handles; diff --git a/packages/block-library/src/paragraph/editor.scss b/packages/block-library/src/paragraph/editor.scss index 61dfe13ed9bba..369cc5cb1d63a 100644 --- a/packages/block-library/src/paragraph/editor.scss +++ b/packages/block-library/src/paragraph/editor.scss @@ -17,3 +17,8 @@ } } } + +.block-editor-block-list__block[data-type="core/paragraph"].has-text-align-right[style*="writing-mode: vertical-rl"], +.block-editor-block-list__block[data-type="core/paragraph"].has-text-align-left[style*="writing-mode: vertical-lr"] { + rotate: 180deg; +} diff --git a/packages/block-library/src/paragraph/style.scss b/packages/block-library/src/paragraph/style.scss index f3bb9f8c5aee8..34960bdb2fd58 100644 --- a/packages/block-library/src/paragraph/style.scss +++ b/packages/block-library/src/paragraph/style.scss @@ -49,3 +49,8 @@ p.has-background { :where(p.has-text-color:not(.has-link-color)) a { color: inherit; } + +p.has-text-align-right[style*="writing-mode:vertical-rl"], +p.has-text-align-left[style*="writing-mode:vertical-lr"] { + rotate: 180deg; +} diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js index 193c524dfe200..794c8eaa2fc86 100644 --- a/packages/block-library/src/post-content/edit.js +++ b/packages/block-library/src/post-content/edit.js @@ -103,7 +103,7 @@ function Placeholder( { layoutClassNames } ) {

    { __( - 'This is the Post Content block, it will display all the blocks in any single post or page.' + 'This is the Content block, it will display all the blocks in any single post or page.' ) }

    @@ -113,7 +113,7 @@ function Placeholder( { layoutClassNames } ) {

    { __( - 'If there are any Custom Post Types registered at your site, the Post Content block can display the contents of those entries as well.' + 'If there are any Custom Post Types registered at your site, the Content block can display the contents of those entries as well.' ) }

    diff --git a/packages/block-library/src/post-content/index.php b/packages/block-library/src/post-content/index.php index 2be1ef77b3b85..dd84574fdea65 100644 --- a/packages/block-library/src/post-content/index.php +++ b/packages/block-library/src/post-content/index.php @@ -35,12 +35,6 @@ function render_block_core_post_content( $attributes, $content, $block ) { $seen_ids[ $post_id ] = true; - // Check is needed for backward compatibility with third-party plugins - // that might rely on the `in_the_loop` check; calling `the_post` sets it to true. - if ( ! in_the_loop() && have_posts() ) { - the_post(); - } - // When inside the main loop, we want to use queried object // so that `the_preview` for the current post can apply. // We force this behavior by omitting the third argument (post ID) from the `get_the_content`. diff --git a/packages/block-library/src/post-featured-image/index.php b/packages/block-library/src/post-featured-image/index.php index 6cb4110ee000e..67c889b0befa5 100644 --- a/packages/block-library/src/post-featured-image/index.php +++ b/packages/block-library/src/post-featured-image/index.php @@ -19,12 +19,6 @@ function render_block_core_post_featured_image( $attributes, $content, $block ) } $post_ID = $block->context['postId']; - // Check is needed for backward compatibility with third-party plugins - // that might rely on the `in_the_loop` check; calling `the_post` sets it to true. - if ( ! in_the_loop() && have_posts() ) { - the_post(); - } - $is_link = isset( $attributes['isLink'] ) && $attributes['isLink']; $size_slug = isset( $attributes['sizeSlug'] ) ? $attributes['sizeSlug'] : 'post-thumbnail'; $attr = get_block_core_post_featured_image_border_attributes( $attributes ); diff --git a/packages/block-library/src/post-navigation-link/index.php b/packages/block-library/src/post-navigation-link/index.php index cb066ad69f2c5..c9e3bfa8aeff1 100644 --- a/packages/block-library/src/post-navigation-link/index.php +++ b/packages/block-library/src/post-navigation-link/index.php @@ -28,7 +28,16 @@ function render_block_core_post_navigation_link( $attributes, $content ) { if ( isset( $attributes['textAlign'] ) ) { $classes .= " has-text-align-{$attributes['textAlign']}"; } - $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classes ) ); + $styles = ''; + if ( isset( $attributes['style']['typography']['writingMode'] ) ) { + $styles = "writing-mode: {$attributes['style']['typography']['writingMode']};"; + } + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => $classes, + 'style' => $styles, + ) + ); // Set default values. $format = '%link'; $link = 'next' === $navigation_type ? _x( 'Next', 'label for next post link' ) : _x( 'Previous', 'label for previous post link' ); diff --git a/packages/block-library/src/post-navigation-link/style.scss b/packages/block-library/src/post-navigation-link/style.scss index 7af462c380819..0f6a9fd3062b8 100644 --- a/packages/block-library/src/post-navigation-link/style.scss +++ b/packages/block-library/src/post-navigation-link/style.scss @@ -20,4 +20,8 @@ } } + &.has-text-align-right[style*="writing-mode: vertical-rl"], + &.has-text-align-left[style*="writing-mode: vertical-lr"] { + rotate: 180deg; + } } diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index 1f0d4677e6727..48804de75d2ca 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -13,7 +13,8 @@ "queryContext", "displayLayout", "templateSlug", - "previewPostType" + "previewPostType", + "enhancedPagination" ], "supports": { "reusable": false, diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php index 88b7c27f1c66f..e616939514a68 100644 --- a/packages/block-library/src/post-template/index.php +++ b/packages/block-library/src/post-template/index.php @@ -43,14 +43,26 @@ function block_core_post_template_uses_featured_image( $inner_blocks ) { * @return string Returns the output of the query, structured using the layout defined by the block's inner blocks. */ function render_block_core_post_template( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // Use global query if needed. $use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ); if ( $use_global_query ) { global $wp_query; - $query = clone $wp_query; + + /* + * If already in the main query loop, duplicate the query instance to not tamper with the main instance. + * Since this is a nested query, it should start at the beginning, therefore rewind posts. + * Otherwise, the main query loop has not started yet and this block is responsible for doing so. + */ + if ( in_the_loop() ) { + $query = clone $wp_query; + $query->rewind_posts(); + } else { + $query = $wp_query; + } } else { $query_args = build_query_vars_from_query_block( $block, $page ); $query = new WP_Query( $query_args ); @@ -109,7 +121,10 @@ function render_block_core_post_template( $attributes, $content, $block ) { // Wrap the render inner blocks in a `li` element with the appropriate post classes. $post_classes = implode( ' ', get_post_class( 'wp-block-post' ) ); - $content .= '
  3. ' . $block_content . '
  4. '; + + $inner_block_directives = $enhanced_pagination ? ' data-wp-key="post-template-item-' . $post_id . '"' : ''; + + $content .= '' . $block_content . ''; } /* diff --git a/packages/block-library/src/post-terms/use-post-terms.js b/packages/block-library/src/post-terms/use-post-terms.js index 2d0aca51db162..ed22df9a9728b 100644 --- a/packages/block-library/src/post-terms/use-post-terms.js +++ b/packages/block-library/src/post-terms/use-post-terms.js @@ -4,6 +4,8 @@ import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +const EMPTY_ARRAY = []; + export default function usePostTerms( { postId, term } ) { const { slug } = term; @@ -12,8 +14,8 @@ export default function usePostTerms( { postId, term } ) { const visible = term?.visibility?.publicly_queryable; if ( ! visible ) { return { - postTerms: [], - _isLoading: false, + postTerms: EMPTY_ARRAY, + isLoading: false, hasPostTerms: false, }; } diff --git a/packages/block-library/src/query-no-results/index.php b/packages/block-library/src/query-no-results/index.php index 4342ba57cccbd..a6f4bd14d0197 100644 --- a/packages/block-library/src/query-no-results/index.php +++ b/packages/block-library/src/query-no-results/index.php @@ -32,14 +32,10 @@ function render_block_core_query_no_results( $attributes, $content, $block ) { $query = new WP_Query( $query_args ); } - if ( $query->have_posts() ) { + if ( $query->post_count > 0 ) { return ''; } - if ( ! $use_global_query ) { - wp_reset_postdata(); - } - $classes = ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) ? 'has-link-color' : ''; $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classes ) ); return sprintf( diff --git a/packages/block-library/src/query-pagination-next/block.json b/packages/block-library/src/query-pagination-next/block.json index 60d44d7ca17b5..95b1169dc992f 100644 --- a/packages/block-library/src/query-pagination-next/block.json +++ b/packages/block-library/src/query-pagination-next/block.json @@ -12,7 +12,13 @@ "type": "string" } }, - "usesContext": [ "queryId", "query", "paginationArrow", "showLabel" ], + "usesContext": [ + "queryId", + "query", + "paginationArrow", + "showLabel", + "enhancedPagination" + ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php index f0ded727ee8a9..83c177c6fb0a9 100644 --- a/packages/block-library/src/query-pagination-next/index.php +++ b/packages/block-library/src/query-pagination-next/index.php @@ -15,9 +15,10 @@ * @return string Returns the next posts link for the query pagination. */ function render_block_core_query_pagination_next( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; - $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; $wrapper_attributes = get_block_wrapper_attributes(); $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true; @@ -61,6 +62,22 @@ function render_block_core_query_pagination_next( $attributes, $content, $block } wp_reset_postdata(); // Restore original Post Data. } + + if ( $enhanced_pagination ) { + $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'wp-block-query-pagination-next', + ) + ) ) { + $p->set_attribute( 'data-wp-key', 'query-pagination-next' ); + $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + $p->set_attribute( 'data-wp-on--mouseenter', 'actions.core.query.prefetch' ); + $content = $p->get_updated_html(); + } + } + return $content; } diff --git a/packages/block-library/src/query-pagination-numbers/block.json b/packages/block-library/src/query-pagination-numbers/block.json index f00b2993a75f6..f05e269d2ece2 100644 --- a/packages/block-library/src/query-pagination-numbers/block.json +++ b/packages/block-library/src/query-pagination-numbers/block.json @@ -7,7 +7,13 @@ "parent": [ "core/query-pagination" ], "description": "Displays a list of page numbers for pagination", "textdomain": "default", - "usesContext": [ "queryId", "query" ], + "attributes": { + "midSize": { + "type": "number", + "default": 2 + } + }, + "usesContext": [ "queryId", "query", "enhancedPagination" ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-numbers/edit.js b/packages/block-library/src/query-pagination-numbers/edit.js index 3832f673ea124..eb83204b2cca2 100644 --- a/packages/block-library/src/query-pagination-numbers/edit.js +++ b/packages/block-library/src/query-pagination-numbers/edit.js @@ -1,25 +1,73 @@ /** * WordPress dependencies */ -import { useBlockProps } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { PanelBody, RangeControl } from '@wordpress/components'; const createPaginationItem = ( content, Tag = 'a', extraClass = '' ) => ( - { content } + + { content } + ); -const previewPaginationNumbers = () => ( - <> - { createPaginationItem( 1 ) } - { createPaginationItem( 2 ) } - { createPaginationItem( 3, 'span', 'current' ) } - { createPaginationItem( 4 ) } - { createPaginationItem( 5 ) } - { createPaginationItem( '...', 'span', 'dots' ) } - { createPaginationItem( 8 ) } - -); +const previewPaginationNumbers = ( midSize ) => { + const paginationItems = []; + + // First set of pagination items. + for ( let i = 1; i <= midSize; i++ ) { + paginationItems.push( createPaginationItem( i ) ); + } + + // Current pagination item. + paginationItems.push( + createPaginationItem( midSize + 1, 'span', 'current' ) + ); + + // Second set of pagination items. + for ( let i = 1; i <= midSize; i++ ) { + paginationItems.push( createPaginationItem( midSize + 1 + i ) ); + } + + // Dots. + paginationItems.push( createPaginationItem( '...', 'span', 'dots' ) ); + + // Last pagination item. + paginationItems.push( createPaginationItem( midSize * 2 + 3 ) ); + + return <>{ paginationItems }; +}; -export default function QueryPaginationNumbersEdit() { - const paginationNumbers = previewPaginationNumbers(); - return
    { paginationNumbers }
    ; +export default function QueryPaginationNumbersEdit( { + attributes, + setAttributes, +} ) { + const { midSize } = attributes; + const paginationNumbers = previewPaginationNumbers( + parseInt( midSize, 10 ) + ); + return ( + <> + + + { + setAttributes( { + midSize: parseInt( value, 10 ), + } ); + } } + min={ 0 } + max={ 5 } + withInputField={ false } + /> + + +
    { paginationNumbers }
    + + ); } diff --git a/packages/block-library/src/query-pagination-numbers/index.php b/packages/block-library/src/query-pagination-numbers/index.php index 60fe85efa1f8d..98098533adac7 100644 --- a/packages/block-library/src/query-pagination-numbers/index.php +++ b/packages/block-library/src/query-pagination-numbers/index.php @@ -15,13 +15,15 @@ * @return string Returns the pagination numbers for the Query. */ function render_block_core_query_pagination_numbers( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; - $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; $wrapper_attributes = get_block_wrapper_attributes(); $content = ''; global $wp_query; + $mid_size = isset( $block->attributes['midSize'] ) ? (int) $block->attributes['midSize'] : null; if ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ) { // Take into account if we have set a bigger `max page` // than what the query has. @@ -30,7 +32,10 @@ function render_block_core_query_pagination_numbers( $attributes, $content, $blo 'prev_next' => false, 'total' => $total, ); - $content = paginate_links( $paginate_args ); + if ( null !== $mid_size ) { + $paginate_args['mid_size'] = $mid_size; + } + $content = paginate_links( $paginate_args ); } else { $block_query = new WP_Query( build_query_vars_from_query_block( $block, $page ) ); // `paginate_links` works with the global $wp_query, so we have to @@ -45,6 +50,9 @@ function render_block_core_query_pagination_numbers( $attributes, $content, $blo 'total' => $total, 'prev_next' => false, ); + if ( null !== $mid_size ) { + $paginate_args['mid_size'] = $mid_size; + } if ( 1 !== $page ) { /** * `paginate_links` doesn't use the provided `format` when the page is `1`. @@ -77,9 +85,24 @@ function render_block_core_query_pagination_numbers( $attributes, $content, $blo wp_reset_postdata(); // Restore original Post Data. $wp_query = $prev_wp_query; } + if ( empty( $content ) ) { return ''; } + + if ( $enhanced_pagination ) { + $p = new WP_HTML_Tag_Processor( $content ); + while ( $p->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'page-numbers', + ) + ) ) { + $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + } + $content = $p->get_updated_html(); + } + return sprintf( '
    %2$s
    ', $wrapper_attributes, diff --git a/packages/block-library/src/query-pagination-previous/block.json b/packages/block-library/src/query-pagination-previous/block.json index d13442f831c97..fbaac543c1da3 100644 --- a/packages/block-library/src/query-pagination-previous/block.json +++ b/packages/block-library/src/query-pagination-previous/block.json @@ -12,7 +12,13 @@ "type": "string" } }, - "usesContext": [ "queryId", "query", "paginationArrow", "showLabel" ], + "usesContext": [ + "queryId", + "query", + "paginationArrow", + "showLabel", + "enhancedPagination" + ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index 5665506598f81..a580880f0f04c 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -15,8 +15,9 @@ * @return string Returns the previous posts link for the query. */ function render_block_core_query_pagination_previous( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; $wrapper_attributes = get_block_wrapper_attributes(); $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true; @@ -49,6 +50,22 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl $label ); } + + if ( $enhanced_pagination ) { + $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'wp-block-query-pagination-previous', + ) + ) ) { + $p->set_attribute( 'data-wp-key', 'query-pagination-previous' ); + $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + $p->set_attribute( 'data-wp-on--mouseenter', 'actions.core.query.prefetch' ); + $content = $p->get_updated_html(); + } + } + return $content; } diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index e4b78b585be0e..d30eccf376579 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -34,17 +34,24 @@ }, "namespace": { "type": "string" + }, + "enhancedPagination": { + "type": "boolean", + "default": false } }, "providesContext": { "queryId": "queryId", "query": "query", - "displayLayout": "displayLayout" + "displayLayout": "displayLayout", + "enhancedPagination": "enhancedPagination" }, "supports": { "align": [ "wide", "full" ], "html": false, "layout": true }, - "editorStyle": "wp-block-query-editor" + "editorStyle": "wp-block-query-editor", + "style": "wp-block-query", + "viewScript": "file:./view.min.js" } diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 5244a88831255..b0f38cf70b9e2 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -17,7 +17,8 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { debounce } from '@wordpress/compose'; -import { useEffect, useState, useCallback } from '@wordpress/element'; +import { useEffect, useState, useCallback, useRef } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -40,8 +41,8 @@ import { const { BlockInfo } = unlock( blockEditorPrivateApis ); export default function QueryInspectorControls( props ) { - const { attributes, setQuery, setDisplayLayout } = props; - const { query, displayLayout } = attributes; + const { attributes, setQuery, setDisplayLayout, setAttributes } = props; + const { query, displayLayout, enhancedPagination } = attributes; const { order, orderBy, @@ -123,6 +124,18 @@ export default function QueryInspectorControls( props ) { isControlAllowed( allowedControls, 'parents' ) && isPostTypeHierarchical; + const enhancedPaginationNotice = __( + 'Enhanced Pagination might cause interactive blocks within the Post Template to stop working. Disable it if you experience any issues.' + ); + + const isFirstRender = useRef( true ); // Don't speak on first render. + useEffect( () => { + if ( ! isFirstRender.current && enhancedPagination ) { + speak( enhancedPaginationNotice ); + } + isFirstRender.current = false; + }, [ enhancedPagination, enhancedPaginationNotice ] ); + const showFiltersPanel = showTaxControl || showAuthorControl || @@ -202,6 +215,29 @@ export default function QueryInspectorControls( props ) { } /> ) } + + setAttributes( { + enhancedPagination: !! value, + } ) + } + /> + { enhancedPagination && ( +
    + + { enhancedPaginationNotice } + +
    + ) } ) } diff --git a/packages/block-library/src/query/edit/query-content.js b/packages/block-library/src/query/edit/query-content.js index 1d795dd646d48..89c6efa280979 100644 --- a/packages/block-library/src/query/edit/query-content.js +++ b/packages/block-library/src/query/edit/query-content.js @@ -109,6 +109,7 @@ export default function QueryContent( { attributes={ attributes } setQuery={ updateQuery } setDisplayLayout={ updateDisplayLayout } + setAttributes={ setAttributes } /> next_tag() ) { + // Add the necessary directives. + $p->set_attribute( 'data-wp-interactive', true ); + $p->set_attribute( 'data-wp-navigation-id', 'query-' . $attributes['queryId'] ); + $p->set_attribute( + 'data-wp-context', + wp_json_encode( array( 'core' => array( 'query' => (object) array() ) ) ) + ); + $content = $p->get_updated_html(); + + // Mark the block as interactive. + $block->block_type->supports['interactivity'] = true; + + // Add a div to announce messages using `aria-live`. + $last_div_position = strripos( $content, '
' ); + $content = substr_replace( + $content, + '
+
', + $last_div_position, + 0 + ); + + // Use state to send translated strings. + wp_store( + array( + 'state' => array( + 'core' => array( + 'query' => array( + 'loadingText' => __( 'Loading page, please wait.' ), + 'loadedText' => __( 'Page Loaded.' ), + ), + ), + ), + ) + ); + } + } + + $view_asset = 'wp-block-query-view'; + if ( ! wp_script_is( $view_asset ) ) { + $script_handles = $block->block_type->view_script_handles; + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $attributes['enhancedPagination'] && in_array( $view_asset, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_asset ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $attributes['enhancedPagination'] && ! in_array( $view_asset, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_asset ) ); + } + } + + $style_asset = 'wp-block-query'; + if ( ! wp_style_is( $style_asset ) ) { + $style_handles = $block->block_type->style_handles; + // If the styles are not needed, and they are still in the `style_handles`, remove them. + if ( ! $attributes['enhancedPagination'] && in_array( $style_asset, $style_handles, true ) ) { + $block->block_type->style_handles = array_diff( $style_handles, array( $style_asset ) ); + } + // If the styles are needed, but they were previously removed, add them again. + if ( $attributes['enhancedPagination'] && ! in_array( $style_asset, $style_handles, true ) ) { + $block->block_type->style_handles = array_merge( $style_handles, array( $style_asset ) ); + } + } + + return $content; +} + /** * Registers the `core/query` block on the server. */ function register_block_core_query() { register_block_type_from_metadata( - __DIR__ . '/query' + __DIR__ . '/query', + array( + 'render_callback' => 'render_block_core_query', + ) ); } add_action( 'init', 'register_block_core_query' ); diff --git a/packages/block-library/src/query/style.scss b/packages/block-library/src/query/style.scss new file mode 100644 index 0000000000000..c560018056d7f --- /dev/null +++ b/packages/block-library/src/query/style.scss @@ -0,0 +1,63 @@ +.wp-block-query__enhanced-pagination-animation { + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + width: 100vw; + max-width: 100vw !important; + height: 4px; + background-color: var(--wp--preset--color--primary, #000); + opacity: 0; + + &.start-animation { + animation: + wp-block-query__enhanced-pagination-start-animation + 30s + cubic-bezier(0, 1, 0, 1) + infinite; + } + + &.finish-animation { + animation: + wp-block-query__enhanced-pagination-finish-animation + 300ms + ease-in; + } +} + +@keyframes wp-block-query__enhanced-pagination-start-animation { + 0% { + transform: scaleX(0); + transform-origin: 0% 0%; + opacity: 1; + } + 100% { + transform: scaleX(1); + transform-origin: 0% 0%; + opacity: 1; + } +} + +@keyframes wp-block-query__enhanced-pagination-finish-animation { + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.wp-block-query__enhanced-pagination-navigation-announce { + position: absolute; + clip: rect(0, 0, 0, 0); + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + border: 0; +} diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js new file mode 100644 index 0000000000000..cbd5573e05c6f --- /dev/null +++ b/packages/block-library/src/query/view.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { store, navigate, prefetch } from '@wordpress/interactivity'; + +const isValidLink = ( ref ) => + ref && + ref instanceof window.HTMLAnchorElement && + ref.href && + ( ! ref.target || ref.target === '_self' ) && + ref.origin === window.location.origin; + +const isValidEvent = ( event ) => + event.button === 0 && // left clicks only + ! event.metaKey && // open in new tab (mac) + ! event.ctrlKey && // open in new tab (windows) + ! event.altKey && // download + ! event.shiftKey && + ! event.defaultPrevented; + +store( { + selectors: { + core: { + query: { + startAnimation: ( { context } ) => + context.core.query.animation === 'start', + finishAnimation: ( { context } ) => + context.core.query.animation === 'finish', + }, + }, + }, + actions: { + core: { + query: { + navigate: async ( { event, ref, context, state } ) => { + if ( isValidLink( ref ) && isValidEvent( event ) ) { + event.preventDefault(); + + const id = ref.closest( '[data-wp-navigation-id]' ) + .dataset.wpNavigationId; + + // Don't announce the navigation immediately, wait 300 ms. + const timeout = setTimeout( () => { + context.core.query.message = + state.core.query.loadingText; + context.core.query.animation = 'start'; + }, 300 ); + + await navigate( ref.href ); + + // Dismiss loading message if it hasn't been added yet. + clearTimeout( timeout ); + + // Announce that the page has been loaded. If the message is the + // same, we use a no-break space similar to the @wordpress/a11y + // package: https://github.com/WordPress/gutenberg/blob/c395242b8e6ee20f8b06c199e4fc2920d7018af1/packages/a11y/src/filter-message.js#L20-L26 + context.core.query.message = + state.core.query.loadedText + + ( context.core.query.message === + state.core.query.loadedText + ? '\u00A0' + : '' ); + + context.core.query.animation = 'finish'; + + // Focus the first anchor of the Query block. + document + .querySelector( + `[data-wp-navigation-id=${ id }] a[href]` + ) + ?.focus(); + } + }, + prefetch: async ( { ref } ) => { + if ( isValidLink( ref ) ) { + await prefetch( ref.href ); + } + }, + }, + }, + }, +} ); diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 80238ab1741f7..ff957b575c7a4 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -52,7 +52,7 @@ import { PC_WIDTH_DEFAULT, PX_WIDTH_DEFAULT, MIN_WIDTH, - MIN_WIDTH_UNIT, + isPercentageUnit, } from './utils.js'; // Used to calculate border radius adjustment to avoid "fat" corners when @@ -405,7 +405,13 @@ export default function SearchEdit( { > { const filteredWidth = widthUnit === '%' && diff --git a/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap index e40797aea5775..dd0c7aa1694f4 100644 --- a/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap @@ -19,7 +19,7 @@ exports[`Search Block renders block with button inside option 1`] = ` ] } > - + Search

", + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="Add label…" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } } - } - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Search

", + } + } + triggerKeyCodes={[]} + /> + - + Search Button

", + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + minWidth={75} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": NaN, + "minHeight": 0, + } } - } - textAlign="center" - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Search Button

", + } + } + textAlign="center" + triggerKeyCodes={[]} + /> +
@@ -233,7 +261,7 @@ exports[`Search Block renders block with icon button option matches snapshot 1`] ] } > - + Search

", + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="Add label…" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } } - } - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Search

", + } + } + triggerKeyCodes={[]} + /> + @@ -517,7 +573,7 @@ exports[`Search Block renders with default configuration matches snapshot 1`] = ] } > - + Search

", + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="Add label…" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } } - } - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Search

", + } + } + triggerKeyCodes={[]} + /> + - + Search Button

", + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + minWidth={75} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": NaN, + "minHeight": 0, + } } - } - textAlign="center" - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Search Button

", + } + } + textAlign="center" + triggerKeyCodes={[]} + /> +
@@ -731,7 +815,7 @@ exports[`Search Block renders with no-button option matches snapshot 1`] = ` ] } > - + Search

", + disableEditingMenu={false} + fontFamily="serif" + fontSize={16} + isMultiline={false} + maxImagesWidth={200} + onBackspace={[Function]} + onBlur={[Function]} + onChange={[Function]} + onContentSizeChange={[Function]} + onEnter={[Function]} + onFocus={[Function]} + onHTMLContentWithCursor={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelectionChange={[Function]} + placeholder="Add label…" + placeholderTextColor="gray" + selectionColor="black" + style={ + { + "backgroundColor": undefined, + "maxWidth": undefined, + "minHeight": 0, + } } - } - triggerKeyCodes={[]} - /> + text={ + { + "eventCount": undefined, + "linkTextColor": undefined, + "selection": null, + "tag": "p", + "text": "

Search

", + } + } + triggerKeyCodes={[]} + /> + { - const blocks = getBlockTypes(); - - blocks.forEach( ( { name } ) => unregisterBlockType( name ) ); -}; - -describe( '', () => { - beforeAll( () => { - registerCoreBlocks(); - } ); - - afterAll( () => { - unregisterBlocks(); - } ); - - /** - * GIVEN an EDITOR is displayed; - * WHEN a SOCIAL ICONS BLOCK is selected from the BLOCK INSERTER; - */ - it( 'should display WORDPRESS, FACEBOOK, TWITTER, INSTAGRAM by default.', async () => { - // Arrange - const subject = await initializeEditor( {} ); - - // Act - fireEvent.press( - await waitFor( () => subject.getByLabelText( 'Add block' ) ) - ); - fireEvent.changeText( - await waitFor( () => - subject.getByPlaceholderText( 'Search blocks' ) - ), - 'social icons' - ); - fireEvent.press( - await subject.findByLabelText( 'Social Icons block' ) - ); - const [ socialIconsBlock ] = subject.getAllByLabelText( - /Social Icons Block. Row 1/ - ); - fireEvent( - within( socialIconsBlock ).getByTestId( 'block-list-wrapper' ), - 'layout', - { nativeEvent: { layout: { width: 100 } } } - ); - - // Assert - expect( - await waitFor( () => - subject.getByLabelText( /WordPress social icon/ ) - ) - ).toBeDefined(); - expect( - await waitFor( () => - subject.getByLabelText( /Facebook social icon/ ) - ) - ).toBeDefined(); - expect( - await waitFor( () => - subject.getByLabelText( /Twitter social icon/ ) - ) - ).toBeDefined(); - expect( - await waitFor( () => - subject.getByLabelText( /Instagram social icon/ ) - ) - ).toBeDefined(); - } ); - - /** - * GIVEN an EDITOR is displayed; - * WHEN a SOCIAL ICONS BLOCK is selected from the BLOCK INSERTER; - */ - it( `should display WORDPRESS with a URL set by default - AND should display FACEBOOK, TWITTER, INSTAGRAM with NO URL set by default.`, async () => { - // Arrange - const subject = await initializeEditor( {} ); - - // Act - fireEvent.press( - await waitFor( () => subject.getByLabelText( 'Add block' ) ) - ); - fireEvent.changeText( - await waitFor( () => - subject.getByPlaceholderText( 'Search blocks' ) - ), - 'social icons' - ); - fireEvent.press( - await subject.findByLabelText( 'Social Icons block' ) - ); - const [ socialIconsBlock ] = subject.getAllByLabelText( - /Social Icons Block. Row 1/ - ); - fireEvent( - within( socialIconsBlock ).getByTestId( 'block-list-wrapper' ), - 'layout', - { nativeEvent: { layout: { width: 100 } } } - ); - - // Assert - expect( - await waitFor( () => - subject.getByA11yHint( /WordPress has URL set/ ) - ) - ).toBeDefined(); - expect( - await waitFor( () => - subject.getByA11yHint( /Facebook has no URL set/ ) - ) - ).toBeDefined(); - expect( - await waitFor( () => - subject.getByA11yHint( /Twitter has no URL set/ ) - ) - ).toBeDefined(); - expect( - await waitFor( () => - subject.getByA11yHint( /Instagram has no URL set/ ) - ) - ).toBeDefined(); - } ); -} ); diff --git a/packages/block-library/src/social-links/test/edit.native.js b/packages/block-library/src/social-links/test/edit.native.js index 48fefaadaec8c..a4ac5246979aa 100644 --- a/packages/block-library/src/social-links/test/edit.native.js +++ b/packages/block-library/src/social-links/test/edit.native.js @@ -3,12 +3,14 @@ */ import { addBlock, + dismissModal, fireEvent, getEditorHtml, initializeEditor, within, getBlock, waitFor, + waitForModalVisible, } from 'test/helpers'; /** @@ -197,4 +199,32 @@ describe( 'Social links block', () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); + + it( "should set a icon's URL", async () => { + const screen = await initializeEditor(); + await addBlock( screen, 'Social Icons' ); + fireEvent.press( screen.getByLabelText( 'Facebook social icon' ) ); + fireEvent.press( screen.getByLabelText( 'Add link to Facebook' ) ); + + await waitForModalVisible( + screen.getByTestId( 'link-settings-navigation' ) + ); + fireEvent.changeText( + screen.getByPlaceholderText( 'Add URL' ), + 'https://facebook.com' + ); + dismissModal( screen.getByTestId( 'link-settings-navigation' ) ); + + expect( getEditorHtml() ).toMatchInlineSnapshot( ` + " + + " + ` ); + } ); } ); diff --git a/packages/block-library/src/table-of-contents/block.json b/packages/block-library/src/table-of-contents/block.json index 9623263166916..5fa7e37e6acbd 100644 --- a/packages/block-library/src/table-of-contents/block.json +++ b/packages/block-library/src/table-of-contents/block.json @@ -13,7 +13,8 @@ "type": "array", "items": { "type": "object" - } + }, + "default": [] }, "onlyIncludeCurrentPage": { "type": "boolean", diff --git a/packages/block-library/src/table-of-contents/edit.js b/packages/block-library/src/table-of-contents/edit.js index 7f3bb6529bf32..915375606b10c 100644 --- a/packages/block-library/src/table-of-contents/edit.js +++ b/packages/block-library/src/table-of-contents/edit.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import fastDeepEqual from 'fast-deep-equal/es6'; - /** * WordPress dependencies */ @@ -22,10 +17,8 @@ import { ToolbarGroup, } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; -import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; -import { renderToString, useEffect } from '@wordpress/element'; +import { renderToString } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -33,6 +26,7 @@ import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; import icon from './icon'; import TableOfContentsList from './list'; import { linearToNestedHeadingList } from './utils'; +import { useObserveHeadings } from './hooks'; /** @typedef {import('./utils').HeadingData} HeadingData */ @@ -53,6 +47,8 @@ export default function TableOfContentsEdit( { clientId, setAttributes, } ) { + useObserveHeadings( clientId ); + const blockProps = useBlockProps(); const canInsertList = useSelect( @@ -66,160 +62,7 @@ export default function TableOfContentsEdit( { [ clientId ] ); - const { __unstableMarkNextChangeAsNotPersistent, replaceBlocks } = - useDispatch( blockEditorStore ); - - /** - * The latest heading data, or null if the new data deeply equals the saved - * headings attribute. - * - * Since useSelect forces a re-render when its return value is shallowly - * inequal to its prior call, we would be re-rendering this block every time - * the stores change, even if the latest headings were deeply equal to the - * ones saved in the block attributes. - * - * By returning null when they're equal, we reduce that to 2 renders: one - * when there are new latest headings (and so it returns them), and one when - * they haven't changed (so it returns null). As long as the latest heading - * data remains the same, further calls of the useSelect callback will - * continue to return null, thus preventing any forced re-renders. - */ - const latestHeadings = useSelect( - ( select ) => { - const { - getBlockAttributes, - getBlockName, - getClientIdsWithDescendants, - __experimentalGetGlobalBlocksByName: getGlobalBlocksByName, - } = select( blockEditorStore ); - - // FIXME: @wordpress/block-library should not depend on @wordpress/editor. - // Blocks can be loaded into a *non-post* block editor, so to avoid - // declaring @wordpress/editor as a dependency, we must access its - // store by string. When the store is not available, editorSelectors - // will be null, and the block's saved markup will lack permalinks. - // eslint-disable-next-line @wordpress/data-no-store-string-literals - const editorSelectors = select( 'core/editor' ); - - const pageBreakClientIds = getGlobalBlocksByName( 'core/nextpage' ); - - const isPaginated = pageBreakClientIds.length !== 0; - - // Get the client ids of all blocks in the editor. - const allBlockClientIds = getClientIdsWithDescendants(); - - // If onlyIncludeCurrentPage is true, calculate the page (of a paginated post) this block is part of, so we know which headings to include; otherwise, skip the calculation. - let tocPage = 1; - - if ( isPaginated && onlyIncludeCurrentPage ) { - // We can't use getBlockIndex because it only returns the index - // relative to sibling blocks. - const tocIndex = allBlockClientIds.indexOf( clientId ); - - for ( const [ - blockIndex, - blockClientId, - ] of allBlockClientIds.entries() ) { - // If we've reached blocks after the Table of Contents, we've - // finished calculating which page the block is on. - if ( blockIndex >= tocIndex ) { - break; - } - if ( getBlockName( blockClientId ) === 'core/nextpage' ) { - tocPage++; - } - } - } - - const _latestHeadings = []; - - /** The page (of a paginated post) a heading will be part of. */ - let headingPage = 1; - - /** - * A permalink to the current post. If the core/editor store is - * unavailable, this variable will be null. - */ - const permalink = editorSelectors?.getPermalink() ?? null; - - let headingPageLink = null; - - // If the core/editor store is available, we can add permalinks to the - // generated table of contents. - if ( typeof permalink === 'string' ) { - headingPageLink = isPaginated - ? addQueryArgs( permalink, { page: headingPage } ) - : permalink; - } - - for ( const blockClientId of allBlockClientIds ) { - const blockName = getBlockName( blockClientId ); - if ( blockName === 'core/nextpage' ) { - headingPage++; - - // If we're only including headings from the current page (of - // a paginated post), then exit the loop if we've reached the - // pages after the one with the Table of Contents block. - if ( onlyIncludeCurrentPage && headingPage > tocPage ) { - break; - } - - if ( typeof permalink === 'string' ) { - headingPageLink = addQueryArgs( - removeQueryArgs( permalink, [ 'page' ] ), - { page: headingPage } - ); - } - } - // If we're including all headings or we've reached headings on - // the same page as the Table of Contents block, add them to the - // list. - else if ( - ! onlyIncludeCurrentPage || - headingPage === tocPage - ) { - if ( blockName === 'core/heading' ) { - const headingAttributes = - getBlockAttributes( blockClientId ); - - const canBeLinked = - typeof headingPageLink === 'string' && - typeof headingAttributes.anchor === 'string' && - headingAttributes.anchor !== ''; - - _latestHeadings.push( { - // Convert line breaks to spaces, and get rid of HTML tags in the headings. - content: stripHTML( - headingAttributes.content.replace( - /(
)+/g, - ' ' - ) - ), - level: headingAttributes.level, - link: canBeLinked - ? `${ headingPageLink }#${ headingAttributes.anchor }` - : null, - } ); - } - } - } - - if ( fastDeepEqual( headings, _latestHeadings ) ) { - return null; - } - return _latestHeadings; - }, - [ clientId, onlyIncludeCurrentPage, headings ] - ); - - useEffect( () => { - if ( latestHeadings !== null ) { - // This is required to keep undo working and not create 2 undo steps - // for each heading change. - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { headings: latestHeadings } ); - } - }, [ latestHeadings ] ); + const { replaceBlocks } = useDispatch( blockEditorStore ); const headingTree = linearToNestedHeadingList( headings ); diff --git a/packages/block-library/src/table-of-contents/hooks.js b/packages/block-library/src/table-of-contents/hooks.js new file mode 100644 index 0000000000000..af7b66568123f --- /dev/null +++ b/packages/block-library/src/table-of-contents/hooks.js @@ -0,0 +1,156 @@ +/** + * External dependencies + */ +import fastDeepEqual from 'fast-deep-equal/es6'; + +/** + * WordPress dependencies + */ +import { useRegistry } from '@wordpress/data'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { useEffect } from '@wordpress/element'; +import { addQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +function getLatestHeadings( select, clientId ) { + const { + getBlockAttributes, + getBlockName, + getClientIdsWithDescendants, + __experimentalGetGlobalBlocksByName: getGlobalBlocksByName, + } = select( blockEditorStore ); + + // FIXME: @wordpress/block-library should not depend on @wordpress/editor. + // Blocks can be loaded into a *non-post* block editor, so to avoid + // declaring @wordpress/editor as a dependency, we must access its + // store by string. When the store is not available, editorSelectors + // will be null, and the block's saved markup will lack permalinks. + // eslint-disable-next-line @wordpress/data-no-store-string-literals + const permalink = select( 'core/editor' ).getPermalink() ?? null; + + const isPaginated = getGlobalBlocksByName( 'core/nextpage' ).length !== 0; + const { onlyIncludeCurrentPage } = getBlockAttributes( clientId ) ?? {}; + + // Get the client ids of all blocks in the editor. + const allBlockClientIds = getClientIdsWithDescendants(); + + // If onlyIncludeCurrentPage is true, calculate the page (of a paginated post) this block is part of, so we know which headings to include; otherwise, skip the calculation. + let tocPage = 1; + + if ( isPaginated && onlyIncludeCurrentPage ) { + // We can't use getBlockIndex because it only returns the index + // relative to sibling blocks. + const tocIndex = allBlockClientIds.indexOf( clientId ); + + for ( const [ + blockIndex, + blockClientId, + ] of allBlockClientIds.entries() ) { + // If we've reached blocks after the Table of Contents, we've + // finished calculating which page the block is on. + if ( blockIndex >= tocIndex ) { + break; + } + if ( getBlockName( blockClientId ) === 'core/nextpage' ) { + tocPage++; + } + } + } + + const latestHeadings = []; + + /** The page (of a paginated post) a heading will be part of. */ + let headingPage = 1; + let headingPageLink = null; + + // If the core/editor store is available, we can add permalinks to the + // generated table of contents. + if ( typeof permalink === 'string' ) { + headingPageLink = isPaginated + ? addQueryArgs( permalink, { page: headingPage } ) + : permalink; + } + + for ( const blockClientId of allBlockClientIds ) { + const blockName = getBlockName( blockClientId ); + if ( blockName === 'core/nextpage' ) { + headingPage++; + + // If we're only including headings from the current page (of + // a paginated post), then exit the loop if we've reached the + // pages after the one with the Table of Contents block. + if ( onlyIncludeCurrentPage && headingPage > tocPage ) { + break; + } + + if ( typeof permalink === 'string' ) { + headingPageLink = addQueryArgs( + removeQueryArgs( permalink, [ 'page' ] ), + { page: headingPage } + ); + } + } + // If we're including all headings or we've reached headings on + // the same page as the Table of Contents block, add them to the + // list. + else if ( ! onlyIncludeCurrentPage || headingPage === tocPage ) { + if ( blockName === 'core/heading' ) { + const headingAttributes = getBlockAttributes( blockClientId ); + + const canBeLinked = + typeof headingPageLink === 'string' && + typeof headingAttributes.anchor === 'string' && + headingAttributes.anchor !== ''; + + latestHeadings.push( { + // Convert line breaks to spaces, and get rid of HTML tags in the headings. + content: stripHTML( + headingAttributes.content.replace( + /(
)+/g, + ' ' + ) + ), + level: headingAttributes.level, + link: canBeLinked + ? `${ headingPageLink }#${ headingAttributes.anchor }` + : null, + } ); + } + } + } + + return latestHeadings; +} + +function observeCallback( select, dispatch, clientId ) { + const { getBlockAttributes } = select( blockEditorStore ); + const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } = + dispatch( blockEditorStore ); + + /** + * If the block no longer exists in the store, skip the update. + * The "undo" action recreates the block and provides a new `clientId`. + * The hook still might be observing the changes while the old block unmounts. + */ + const attributes = getBlockAttributes( clientId ); + if ( attributes === null ) { + return; + } + + const headings = getLatestHeadings( select, clientId ); + if ( ! fastDeepEqual( headings, attributes.headings ) ) { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( clientId, { headings } ); + } +} + +export function useObserveHeadings( clientId ) { + const registry = useRegistry(); + useEffect( () => { + // Todo: Limit subscription to block editor store when data no longer depends on `getPermalink`. + // See: https://github.com/WordPress/gutenberg/pull/45513 + return registry.subscribe( () => + observeCallback( registry.select, registry.dispatch, clientId ) + ); + }, [ registry, clientId ] ); +} diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index c99b60550849e..3a235ffe77b2c 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.41.0 (2023-08-31) + ## 4.40.0 (2023-08-16) ## 4.39.0 (2023-08-10) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 0cda27b554e2e..61741c4ca0412 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "4.40.0", + "version": "4.41.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index 799f24ec66366..c760b228dc9f5 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.41.0 (2023-08-31) + ## 4.40.0 (2023-08-16) ## 4.39.0 (2023-08-10) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 7e39e035848be..ccdeee4fff5c3 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.40.0", + "version": "4.41.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 34c7f2c7ceb50..d843396d51504 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.18.0 (2023-08-31) + ## 12.17.0 (2023-08-16) ## 12.16.0 (2023-08-10) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 1dee1f1d4ae00..71b560154223e 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.17.0", + "version": "12.18.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index bf866b7a2143b..72c0a30db0205 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -1,10 +1,5 @@ /* eslint no-console: [ 'error', { allow: [ 'error', 'warn' ] } ] */ -/** - * External dependencies - */ -import { camelCase } from 'change-case'; - /** * WordPress dependencies */ @@ -15,8 +10,8 @@ import { _x } from '@wordpress/i18n'; * Internal dependencies */ import i18nBlockSchema from './i18n-block.json'; -import { BLOCK_ICON_DEFAULT } from './constants'; import { store as blocksStore } from '../store'; +import { unlock } from '../lock-unlock'; /** * An icon type definition. One of a Dashicon slug, an element, @@ -129,8 +124,6 @@ import { store as blocksStore } from '../store'; * then no preview is shown. */ -const serverSideBlockDefinitions = {}; - function isObject( object ) { return object !== null && typeof object === 'object'; } @@ -142,54 +135,9 @@ function isObject( object ) { */ // eslint-disable-next-line camelcase export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { - for ( const blockName of Object.keys( definitions ) ) { - // Don't overwrite if already set. It covers the case when metadata - // was initialized from the server. - if ( serverSideBlockDefinitions[ blockName ] ) { - // We still need to polyfill `apiVersion` for WordPress version - // lower than 5.7. If it isn't present in the definition shared - // from the server, we try to fallback to the definition passed. - // @see https://github.com/WordPress/gutenberg/pull/29279 - if ( - serverSideBlockDefinitions[ blockName ].apiVersion === - undefined && - definitions[ blockName ].apiVersion - ) { - serverSideBlockDefinitions[ blockName ].apiVersion = - definitions[ blockName ].apiVersion; - } - // The `ancestor` prop is not included in the definitions shared - // from the server yet, so it needs to be polyfilled as well. - // @see https://github.com/WordPress/gutenberg/pull/39894 - if ( - serverSideBlockDefinitions[ blockName ].ancestor === - undefined && - definitions[ blockName ].ancestor - ) { - serverSideBlockDefinitions[ blockName ].ancestor = - definitions[ blockName ].ancestor; - } - // The `selectors` prop is not yet included in the server provided - // definitions. Polyfill it as well. This can be removed when the - // minimum supported WordPress is >= 6.3. - if ( - serverSideBlockDefinitions[ blockName ].selectors === - undefined && - definitions[ blockName ].selectors - ) { - serverSideBlockDefinitions[ blockName ].selectors = - definitions[ blockName ].selectors; - } - continue; - } - - serverSideBlockDefinitions[ blockName ] = Object.fromEntries( - Object.entries( definitions[ blockName ] ) - .filter( - ( [ , value ] ) => value !== null && value !== undefined - ) - .map( ( [ key, value ] ) => [ camelCase( key ), value ] ) - ); + const { addBootstrappedBlockType } = unlock( dispatch( blocksStore ) ); + for ( const [ name, blockType ] of Object.entries( definitions ) ) { + addBootstrappedBlockType( name, blockType ); } } @@ -219,6 +167,7 @@ function getBlockSettingsFromMetadata( { textdomain, ...metadata } ) { 'styles', 'example', 'variations', + '__experimentalAutoInsert', ]; const settings = Object.fromEntries( @@ -290,29 +239,16 @@ export function registerBlockType( blockNameOrMetadata, settings ) { return; } + const { addBootstrappedBlockType, addUnprocessedBlockType } = unlock( + dispatch( blocksStore ) + ); + if ( isObject( blockNameOrMetadata ) ) { - unstable__bootstrapServerSideBlockDefinitions( { - [ name ]: getBlockSettingsFromMetadata( blockNameOrMetadata ), - } ); + const metadata = getBlockSettingsFromMetadata( blockNameOrMetadata ); + addBootstrappedBlockType( name, metadata ); } - const blockType = { - name, - icon: BLOCK_ICON_DEFAULT, - keywords: [], - attributes: {}, - providesContext: {}, - usesContext: [], - selectors: {}, - supports: {}, - styles: [], - variations: [], - save: () => null, - ...serverSideBlockDefinitions?.[ name ], - ...settings, - }; - - dispatch( blocksStore ).__experimentalRegisterBlockType( blockType ); + addUnprocessedBlockType( name, settings ); return select( blocksStore ).getBlockType( name ); } diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 42f4dcfbf0e48..877c9fdc4a038 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -5,7 +5,7 @@ */ import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks'; import { logged } from '@wordpress/deprecated'; -import { select } from '@wordpress/data'; +import { select, dispatch } from '@wordpress/data'; /** * Internal dependencies @@ -33,6 +33,7 @@ import { import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../constants'; import { omit } from '../utils'; import { store as blocksStore } from '../../store'; +import { unlock } from '../../lock-unlock'; const noop = () => {}; @@ -48,19 +49,14 @@ describe( 'blocks', () => { title: 'block title', }; - beforeAll( () => { - // Initialize the block store. - require( '../../store' ); - } ); - afterEach( () => { - getBlockTypes().forEach( ( block ) => { - unregisterBlockType( block.name ); - } ); + const registeredNames = Object.keys( + unlock( select( blocksStore ) ).getUnprocessedBlockTypes() + ); + dispatch( blocksStore ).removeBlockTypes( registeredNames ); setFreeformContentHandlerName( undefined ); setUnregisteredTypeHandlerName( undefined ); setDefaultBlockName( undefined ); - unstable__bootstrapServerSideBlockDefinitions( {} ); // Reset deprecation logging to ensure we properly track warnings. for ( const key in logged ) { @@ -392,80 +388,6 @@ describe( 'blocks', () => { } ); } ); - // This test can be removed once the polyfill for apiVersion gets removed. - it( 'should apply apiVersion on the client when not set on the server', () => { - const blockName = 'core/test-block-back-compat'; - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - category: 'widgets', - }, - } ); - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - apiVersion: 3, - category: 'ignored', - }, - } ); - - const blockType = { - title: 'block title', - }; - registerBlockType( blockName, blockType ); - expect( getBlockType( blockName ) ).toEqual( { - apiVersion: 3, - name: blockName, - save: expect.any( Function ), - title: 'block title', - category: 'widgets', - icon: { src: BLOCK_ICON_DEFAULT }, - attributes: {}, - providesContext: {}, - usesContext: [], - keywords: [], - selectors: {}, - supports: {}, - styles: [], - variations: [], - } ); - } ); - - // This test can be removed once the polyfill for ancestor gets removed. - it( 'should apply ancestor on the client when not set on the server', () => { - const blockName = 'core/test-block-with-ancestor'; - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - category: 'widgets', - }, - } ); - unstable__bootstrapServerSideBlockDefinitions( { - [ blockName ]: { - ancestor: 'core/test-block-ancestor', - category: 'ignored', - }, - } ); - - const blockType = { - title: 'block title', - }; - registerBlockType( blockName, blockType ); - expect( getBlockType( blockName ) ).toEqual( { - ancestor: 'core/test-block-ancestor', - name: blockName, - save: expect.any( Function ), - title: 'block title', - category: 'widgets', - icon: { src: BLOCK_ICON_DEFAULT }, - attributes: {}, - providesContext: {}, - usesContext: [], - keywords: [], - selectors: {}, - supports: {}, - styles: [], - variations: [], - } ); - } ); - // This can be removed once polyfill adding selectors has been removed. it( 'should apply selectors on the client when not set on the server', () => { const blockName = 'core/test-block-with-selectors'; @@ -920,6 +842,34 @@ describe( 'blocks', () => { 'Declaring non-string block descriptions is deprecated since version 6.2.' ); } ); + + it( 're-applies block filters', () => { + // register block + registerBlockType( 'test/block', defaultBlockSettings ); + + // register a filter after registering a block + addFilter( + 'blocks.registerBlockType', + 'core/blocks/reapply', + ( settings ) => ( { + ...settings, + title: settings.title + ' filtered', + } ) + ); + + // check that block type has unfiltered values + expect( getBlockType( 'test/block' ).title ).toBe( + 'block title' + ); + + // reapply the block filters + dispatch( blocksStore ).reapplyBlockTypeFilters(); + + // check that block type has filtered values + expect( getBlockType( 'test/block' ).title ).toBe( + 'block title filtered' + ); + } ); } ); test( 'registers block from metadata', () => { diff --git a/packages/blocks/src/store/actions.js b/packages/blocks/src/store/actions.js index 2c02fb73b0352..d3bd71c067ebe 100644 --- a/packages/blocks/src/store/actions.js +++ b/packages/blocks/src/store/actions.js @@ -1,154 +1,17 @@ -/** - * External dependencies - */ -import { isPlainObject } from 'is-plain-object'; - /** * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; -import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import { isValidIcon, normalizeIconObject, omit } from '../api/utils'; -import { DEPRECATED_ENTRY_KEYS } from '../api/constants'; +import { processBlockType } from './process-block-type'; /** @typedef {import('../api/registration').WPBlockVariation} WPBlockVariation */ /** @typedef {import('../api/registration').WPBlockType} WPBlockType */ /** @typedef {import('./reducer').WPBlockCategory} WPBlockCategory */ -const { error, warn } = window.console; - -/** - * Mapping of legacy category slugs to their latest normal values, used to - * accommodate updates of the default set of block categories. - * - * @type {Record} - */ -const LEGACY_CATEGORY_MAPPING = { - common: 'text', - formatting: 'text', - layout: 'design', -}; - -/** - * Whether the argument is a function. - * - * @param {*} maybeFunc The argument to check. - * @return {boolean} True if the argument is a function, false otherwise. - */ -function isFunction( maybeFunc ) { - return typeof maybeFunc === 'function'; -} - -/** - * Takes the unprocessed block type data and applies all the existing filters for the registered block type. - * Next, it validates all the settings and performs additional processing to the block type definition. - * - * @param {WPBlockType} blockType Unprocessed block type settings. - * @param {Object} thunkArgs Argument object for the thunk middleware. - * @param {Function} thunkArgs.select Function to select from the store. - * - * @return {WPBlockType | undefined} The block, if it has been successfully registered; otherwise `undefined`. - */ -const processBlockType = ( blockType, { select } ) => { - const { name } = blockType; - - const settings = applyFilters( - 'blocks.registerBlockType', - { ...blockType }, - name, - null - ); - - if ( settings.description && typeof settings.description !== 'string' ) { - deprecated( 'Declaring non-string block descriptions', { - since: '6.2', - } ); - } - - if ( settings.deprecated ) { - settings.deprecated = settings.deprecated.map( ( deprecation ) => - Object.fromEntries( - Object.entries( - // Only keep valid deprecation keys. - applyFilters( - 'blocks.registerBlockType', - // Merge deprecation keys with pre-filter settings - // so that filters that depend on specific keys being - // present don't fail. - { - // Omit deprecation keys here so that deprecations - // can opt out of specific keys like "supports". - ...omit( blockType, DEPRECATED_ENTRY_KEYS ), - ...deprecation, - }, - name, - deprecation - ) - ).filter( ( [ key ] ) => DEPRECATED_ENTRY_KEYS.includes( key ) ) - ) - ); - } - - if ( ! isPlainObject( settings ) ) { - error( 'Block settings must be a valid object.' ); - return; - } - - if ( ! isFunction( settings.save ) ) { - error( 'The "save" property must be a valid function.' ); - return; - } - if ( 'edit' in settings && ! isFunction( settings.edit ) ) { - error( 'The "edit" property must be a valid function.' ); - return; - } - - // Canonicalize legacy categories to equivalent fallback. - if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) { - settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ]; - } - - if ( - 'category' in settings && - ! select - .getCategories() - .some( ( { slug } ) => slug === settings.category ) - ) { - warn( - 'The block "' + - name + - '" is registered with an invalid category "' + - settings.category + - '".' - ); - delete settings.category; - } - - if ( ! ( 'title' in settings ) || settings.title === '' ) { - error( 'The block "' + name + '" must have a title.' ); - return; - } - if ( typeof settings.title !== 'string' ) { - error( 'Block titles must be strings.' ); - return; - } - - settings.icon = normalizeIconObject( settings.icon ); - if ( ! isValidIcon( settings.icon.src ) ) { - error( - 'The icon passed is invalid. ' + - 'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional' - ); - return; - } - - return settings; -}; - /** * Returns an action object used in signalling that block types have been added. * Ignored from documentation as the recommended usage for this action through registerBlockType from @wordpress/blocks. @@ -167,26 +30,6 @@ export function addBlockTypes( blockTypes ) { }; } -/** - * Signals that the passed block type's settings should be stored in the state. - * - * @param {WPBlockType} blockType Unprocessed block type settings. - */ -export const __experimentalRegisterBlockType = - ( blockType ) => - ( { dispatch, select } ) => { - dispatch( { - type: 'ADD_UNPROCESSED_BLOCK_TYPE', - blockType, - } ); - - const processedBlockType = processBlockType( blockType, { select } ); - if ( ! processedBlockType ) { - return; - } - dispatch.addBlockTypes( processedBlockType ); - }; - /** * Signals that all block types should be computed again. * It uses stored unprocessed block types and all the most recent list of registered filters. @@ -201,25 +44,17 @@ export const __experimentalRegisterBlockType = * 7. Filter G. * In this scenario some filters would not get applied for all blocks because they are registered too late. */ -export const __experimentalReapplyBlockTypeFilters = - () => - ( { dispatch, select } ) => { - const unprocessedBlockTypes = - select.__experimentalGetUnprocessedBlockTypes(); - - const processedBlockTypes = Object.keys( unprocessedBlockTypes ).reduce( - ( accumulator, blockName ) => { - const result = processBlockType( - unprocessedBlockTypes[ blockName ], - { select } - ); - if ( result ) { - accumulator.push( result ); - } - return accumulator; - }, - [] - ); +export function reapplyBlockTypeFilters() { + return ( { dispatch, select } ) => { + const processedBlockTypes = []; + for ( const [ name, settings ] of Object.entries( + select.getUnprocessedBlockTypes() + ) ) { + const result = dispatch( processBlockType( name, settings ) ); + if ( result ) { + processedBlockTypes.push( result ); + } + } if ( ! processedBlockTypes.length ) { return; @@ -227,6 +62,19 @@ export const __experimentalReapplyBlockTypeFilters = dispatch.addBlockTypes( processedBlockTypes ); }; +} + +export function __experimentalReapplyBlockFilters() { + deprecated( + 'wp.data.dispatch( "core/blocks" ).__experimentalReapplyBlockFilters', + { + since: '6.4', + alternative: 'reapplyBlockFilters', + } + ); + + return reapplyBlockTypeFilters(); +} /** * Returns an action object used to remove a registered block type. diff --git a/packages/blocks/src/store/index.js b/packages/blocks/src/store/index.js index ce69fd83d4e6c..ffda3ffe00026 100644 --- a/packages/blocks/src/store/index.js +++ b/packages/blocks/src/store/index.js @@ -10,6 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as privateSelectors from './private-selectors'; import * as actions from './actions'; +import * as privateActions from './private-actions'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -28,3 +29,4 @@ export const store = createReduxStore( STORE_NAME, { register( store ); unlock( store ).registerPrivateSelectors( privateSelectors ); +unlock( store ).registerPrivateActions( privateActions ); diff --git a/packages/blocks/src/store/private-actions.js b/packages/blocks/src/store/private-actions.js new file mode 100644 index 0000000000000..bc06e231b1722 --- /dev/null +++ b/packages/blocks/src/store/private-actions.js @@ -0,0 +1,42 @@ +/** + * Internal dependencies + */ +import { processBlockType } from './process-block-type'; + +/** @typedef {import('../api/registration').WPBlockType} WPBlockType */ + +/** + * Add bootstrapped block type metadata to the store. These metadata usually come from + * the `block.json` file and are either statically boostrapped from the server, or + * passed as the `metadata` parameter to the `registerBlockType` function. + * + * @param {string} name Block name. + * @param {WPBlockType} blockType Block type metadata. + */ +export function addBootstrappedBlockType( name, blockType ) { + return { + type: 'ADD_BOOTSTRAPPED_BLOCK_TYPE', + name, + blockType, + }; +} + +/** + * Add unprocessed block type settings to the store. These data are passed as the + * `settings` parameter to the client-side `registerBlockType` function. + * + * @param {string} name Block name. + * @param {WPBlockType} blockType Unprocessed block type settings. + */ +export function addUnprocessedBlockType( name, blockType ) { + return ( { dispatch } ) => { + dispatch( { type: 'ADD_UNPROCESSED_BLOCK_TYPE', name, blockType } ); + const processedBlockType = dispatch( + processBlockType( name, blockType ) + ); + if ( ! processedBlockType ) { + return; + } + dispatch.addBlockTypes( processedBlockType ); + }; +} diff --git a/packages/blocks/src/store/private-selectors.js b/packages/blocks/src/store/private-selectors.js index 4df947ea72270..7e4311658c869 100644 --- a/packages/blocks/src/store/private-selectors.js +++ b/packages/blocks/src/store/private-selectors.js @@ -106,15 +106,7 @@ export const getSupportedStyles = createSelector( // 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' - ) - ) { + if ( blockType?.supports?.spacing?.blockGap ) { supportKeys.push( 'blockGap' ); } @@ -160,3 +152,27 @@ export const getSupportedStyles = createSelector( }, ( state, name ) => [ state.blockTypes[ name ] ] ); + +/** + * Returns the bootstrapped block type metadata for a give block name. + * + * @param {Object} state Data state. + * @param {string} name Block name. + * + * @return {Object} Bootstrapped block type metadata for a block. + */ +export function getBootstrappedBlockType( state, name ) { + return state.bootstrappedBlockTypes[ name ]; +} + +/** + * Returns all the unprocessed (before applying the `registerBlockType` filter) + * block type settings as passed during block registration. + * + * @param {Object} state Data state. + * + * @return {Array} Unprocessed block type settings for all blocks. + */ +export function getUnprocessedBlockTypes( state ) { + return state.unprocessedBlockTypes; +} diff --git a/packages/blocks/src/store/process-block-type.js b/packages/blocks/src/store/process-block-type.js new file mode 100644 index 0000000000000..aab198af6c66f --- /dev/null +++ b/packages/blocks/src/store/process-block-type.js @@ -0,0 +1,159 @@ +/** + * External dependencies + */ +import { isPlainObject } from 'is-plain-object'; + +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; +import { applyFilters } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import { isValidIcon, normalizeIconObject, omit } from '../api/utils'; +import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../api/constants'; + +/** @typedef {import('../api/registration').WPBlockType} WPBlockType */ + +const { error, warn } = window.console; + +/** + * Mapping of legacy category slugs to their latest normal values, used to + * accommodate updates of the default set of block categories. + * + * @type {Record} + */ +const LEGACY_CATEGORY_MAPPING = { + common: 'text', + formatting: 'text', + layout: 'design', +}; + +/** + * Takes the unprocessed block type settings, merges them with block type metadata + * and applies all the existing filters for the registered block type. + * Next, it validates all the settings and performs additional processing to the block type definition. + * + * @param {string} name Block name. + * @param {WPBlockType} blockSettings Unprocessed block type settings. + * + * @return {WPBlockType | undefined} The block, if it has been processed and can be registered; otherwise `undefined`. + */ +export const processBlockType = + ( name, blockSettings ) => + ( { select } ) => { + const blockType = { + name, + icon: BLOCK_ICON_DEFAULT, + keywords: [], + attributes: {}, + providesContext: {}, + usesContext: [], + selectors: {}, + supports: {}, + styles: [], + variations: [], + save: () => null, + ...select.getBootstrappedBlockType( name ), + ...blockSettings, + }; + + const settings = applyFilters( + 'blocks.registerBlockType', + blockType, + name, + null + ); + + if ( + settings.description && + typeof settings.description !== 'string' + ) { + deprecated( 'Declaring non-string block descriptions', { + since: '6.2', + } ); + } + + if ( settings.deprecated ) { + settings.deprecated = settings.deprecated.map( ( deprecation ) => + Object.fromEntries( + Object.entries( + // Only keep valid deprecation keys. + applyFilters( + 'blocks.registerBlockType', + // Merge deprecation keys with pre-filter settings + // so that filters that depend on specific keys being + // present don't fail. + { + // Omit deprecation keys here so that deprecations + // can opt out of specific keys like "supports". + ...omit( blockType, DEPRECATED_ENTRY_KEYS ), + ...deprecation, + }, + blockType.name, + deprecation + ) + ).filter( ( [ key ] ) => + DEPRECATED_ENTRY_KEYS.includes( key ) + ) + ) + ); + } + + if ( ! isPlainObject( settings ) ) { + error( 'Block settings must be a valid object.' ); + return; + } + + if ( typeof settings.save !== 'function' ) { + error( 'The "save" property must be a valid function.' ); + return; + } + if ( 'edit' in settings && typeof settings.edit !== 'function' ) { + error( 'The "edit" property must be a valid function.' ); + return; + } + + // Canonicalize legacy categories to equivalent fallback. + if ( LEGACY_CATEGORY_MAPPING.hasOwnProperty( settings.category ) ) { + settings.category = LEGACY_CATEGORY_MAPPING[ settings.category ]; + } + + if ( + 'category' in settings && + ! select + .getCategories() + .some( ( { slug } ) => slug === settings.category ) + ) { + warn( + 'The block "' + + name + + '" is registered with an invalid category "' + + settings.category + + '".' + ); + delete settings.category; + } + + if ( ! ( 'title' in settings ) || settings.title === '' ) { + error( 'The block "' + name + '" must have a title.' ); + return; + } + if ( typeof settings.title !== 'string' ) { + error( 'Block titles must be strings.' ); + return; + } + + settings.icon = normalizeIconObject( settings.icon ); + if ( ! isValidIcon( settings.icon.src ) ) { + error( + 'The icon passed is invalid. ' + + 'The icon should be a string, an element, a function, or an object following the specifications documented in https://developer.wordpress.org/block-editor/developers/block-api/block-registration/#icon-optional' + ); + return; + } + + return settings; + }; diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index d8f76e00fc71d..a8f114fea79c7 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { camelCase } from 'change-case'; + /** * WordPress dependencies */ @@ -52,6 +57,72 @@ function getUniqueItemsByName( items ) { }, [] ); } +function bootstrappedBlockTypes( state = {}, action ) { + switch ( action.type ) { + case 'ADD_BOOTSTRAPPED_BLOCK_TYPE': + const { name, blockType } = action; + const serverDefinition = state[ name ]; + let newDefinition; + // Don't overwrite if already set. It covers the case when metadata + // was initialized from the server. + if ( serverDefinition ) { + // The `selectors` prop is not yet included in the server provided + // definitions and needs to be polyfilled. This can be removed when the + // minimum supported WordPress is >= 6.3. + if ( + serverDefinition.selectors === undefined && + blockType.selectors + ) { + newDefinition = { + ...serverDefinition, + selectors: blockType.selectors, + }; + } + + // The `autoInsert` prop is not yet included in the server provided + // definitions and needs to be polyfilled. This can be removed when the + // minimum supported WordPress is >= 6.4. + if ( + serverDefinition.__experimentalAutoInsert === undefined && + blockType.__experimentalAutoInsert + ) { + newDefinition = { + ...serverDefinition, + ...newDefinition, + __experimentalAutoInsert: + blockType.__experimentalAutoInsert, + }; + } + } else { + newDefinition = Object.fromEntries( + Object.entries( blockType ) + .filter( + ( [ , value ] ) => + value !== null && value !== undefined + ) + .map( ( [ key, value ] ) => [ + camelCase( key ), + value, + ] ) + ); + newDefinition.name = name; + } + + if ( newDefinition ) { + return { + ...state, + [ name ]: newDefinition, + }; + } + + return state; + case 'REMOVE_BLOCK_TYPES': + return omit( state, action.names ); + } + + return state; +} + /** * Reducer managing the unprocessed block types in a form passed when registering the by block. * It's for internal use only. It allows recomputing the processed block types on-demand after block type filters @@ -67,7 +138,7 @@ export function unprocessedBlockTypes( state = {}, action ) { case 'ADD_UNPROCESSED_BLOCK_TYPE': return { ...state, - [ action.blockType.name ]: action.blockType, + [ action.name ]: action.blockType, }; case 'REMOVE_BLOCK_TYPES': return omit( state, action.names ); @@ -300,6 +371,7 @@ export function collections( state = {}, action ) { } export default combineReducers( { + bootstrappedBlockTypes, unprocessedBlockTypes, blockTypes, blockStyles, diff --git a/packages/blocks/src/store/selectors.js b/packages/blocks/src/store/selectors.js index cf577c695c9c5..b2b8ab8106f09 100644 --- a/packages/blocks/src/store/selectors.js +++ b/packages/blocks/src/store/selectors.js @@ -32,17 +32,6 @@ const getNormalizedBlockType = ( state, nameOrType ) => ? getBlockType( state, nameOrType ) : nameOrType; -/** - * Returns all the unprocessed block types as passed during the registration. - * - * @param {Object} state Data state. - * - * @return {Array} Unprocessed block types. - */ -export function __experimentalGetUnprocessedBlockTypes( state ) { - return state.unprocessedBlockTypes; -} - /** * Returns all the available block types. * diff --git a/packages/blocks/src/store/test/reducer.js b/packages/blocks/src/store/test/reducer.js index b4312d0fd7df2..5664f9d876cb6 100644 --- a/packages/blocks/src/store/test/reducer.js +++ b/packages/blocks/src/store/test/reducer.js @@ -31,24 +31,25 @@ describe( 'unprocessedBlockTypes', () => { it( 'should add a new block type', () => { const original = deepFreeze( { - 'core/paragraph': { name: 'core/paragraph' }, + 'core/paragraph': { title: 'Paragraph' }, } ); const state = unprocessedBlockTypes( original, { type: 'ADD_UNPROCESSED_BLOCK_TYPE', - blockType: { name: 'core/code' }, + name: 'core/code', + blockType: { title: 'Code' }, } ); expect( state ).toEqual( { - 'core/paragraph': { name: 'core/paragraph' }, - 'core/code': { name: 'core/code' }, + 'core/paragraph': { title: 'Paragraph' }, + 'core/code': { title: 'Code' }, } ); } ); it( 'should remove unprocessed block types', () => { const original = deepFreeze( { - 'core/paragraph': { name: 'core/paragraph' }, - 'core/code': { name: 'core/code' }, + 'core/paragraph': { title: 'Paragraph' }, + 'core/code': { title: 'Code' }, } ); const state = blockTypes( original, { @@ -57,7 +58,7 @@ describe( 'unprocessedBlockTypes', () => { } ); expect( state ).toEqual( { - 'core/paragraph': { name: 'core/paragraph' }, + 'core/paragraph': { title: 'Paragraph' }, } ); } ); } ); diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index cd3e675cb577d..a78c54cf9a662 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.24.0 (2023-08-31) + ## 5.23.0 (2023-08-16) ## 5.22.0 (2023-08-10) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index 9136613a01dc2..4490a5a949526 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.23.0", + "version": "5.24.0", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index 7eaf493e7ce98..e3af451de196a 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.12.0 (2023-08-31) + ## 0.11.0 (2023-08-16) ## 0.10.0 (2023-08-10) diff --git a/packages/commands/package.json b/packages/commands/package.json index ea89a529dc4c4..5c33f43deb2e9 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.11.0", + "version": "0.12.0", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3b9682c4adf4c..6ec68347af259 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,9 +2,50 @@ ## Unreleased +### Breaking changes + +- Make the `Popover.Slot` optional and render popovers at the bottom of the document's body by default. ([#53889](https://github.com/WordPress/gutenberg/pull/53889), [#53982](https://github.com/WordPress/gutenberg/pull/53982)). + +### Enhancements + +- Making Circular Option Picker a `listbox`. Note that while this changes some public API, new props are optional, and currently have default values; this will change in another patch ([#52255](https://github.com/WordPress/gutenberg/pull/52255)). +- `ToggleGroupControl`: Rewrite backdrop animation using framer motion shared layout animations, add better support for controlled and uncontrolled modes ([#50278](https://github.com/WordPress/gutenberg/pull/50278)). +- `Popover`: Add the `is-positioned` CSS class only after the popover has finished animating ([#54178](https://github.com/WordPress/gutenberg/pull/54178)). + +### Bug Fix + +- `PaletteEdit`: Fix padding in RTL languages ([#54034](https://github.com/WordPress/gutenberg/pull/54034)). +- `CircularOptionPicker`: make focus styles resilient to button size changes ([#54196](https://github.com/WordPress/gutenberg/pull/54196)). + + +### Internal + +- `Composite`: Convert to TypeScript ([#54028](https://github.com/WordPress/gutenberg/pull/54028)). +- `BorderControl`: Refactor unit tests to use `userEvent` ([#54155](https://github.com/WordPress/gutenberg/pull/54155)) +- `FocusableIframe`: Convert to TypeScript ([#53979](https://github.com/WordPress/gutenberg/pull/53979)). +- `Popover`: Remove unused `overlay` type from `positionToPlacement` utility function ([#54101](https://github.com/WordPress/gutenberg/pull/54101)). + +### Experimental + +- `DropdownMenu` v2: Fix submenu chevron direction in RTL languages ([#54036](https://github.com/WordPress/gutenberg/pull/54036). + +## 25.7.0 (2023-08-31) + +### Enhancements + +- `ProgressBar`: Add transition to determinate indicator ([#53877](https://github.com/WordPress/gutenberg/pull/53877)). +- Prevent nested `SlotFillProvider` from rendering ([#53940](https://github.com/WordPress/gutenberg/pull/53940)). + ### Bug Fix - `SandBox`: Fix the cleanup method in useEffect ([#53796](https://github.com/WordPress/gutenberg/pull/53796)). +- `PaletteEdit`: Fix the height of the `PaletteItems`. Don't rely on styles only present in the block editor ([#54000](https://github.com/WordPress/gutenberg/pull/54000)). + +### Internal + +- `Shortcut`: Add Storybook stories ([#53627](https://github.com/WordPress/gutenberg/pull/53627)). +- `SlotFill`: Do not render children when using ``. ([#53272](https://github.com/WordPress/gutenberg/pull/53272)) +- Update `@floating-ui/react-dom` to the latest version ([#46845](https://github.com/WordPress/gutenberg/pull/46845)). ## 25.6.0 (2023-08-16) @@ -29,6 +70,7 @@ - `ControlGroup`, `FormGroup`, `ControlLabel`, `Spinner`: Remove unused `ui/` components from the codebase ([#52953](https://github.com/WordPress/gutenberg/pull/52953)). - `MenuItem`: Convert to TypeScript ([#53132](https://github.com/WordPress/gutenberg/pull/53132)). +- `MenuItem`: Add Storybook stories ([#53613](https://github.com/WordPress/gutenberg/pull/53613)). - `MenuGroup`: Add Storybook stories ([#53090](https://github.com/WordPress/gutenberg/pull/53090)). - Components: Remove unnecessary utils ([#53679](https://github.com/WordPress/gutenberg/pull/53679)). @@ -42,7 +84,6 @@ - `ColorPalette`, `BorderControl`: Don't hyphenate hex value in `aria-label` ([#52932](https://github.com/WordPress/gutenberg/pull/52932)). - `MenuItemsChoice`, `MenuItem`: Support a `disabled` prop on a menu item ([#52737](https://github.com/WordPress/gutenberg/pull/52737)). -- `TabPanel`: Introduce a new version of `TabPanel` with updated internals and improved adherence to ARIA guidance on `tabpanel` focus behavior while maintaining the same functionality and API surface.([#52133](https://github.com/WordPress/gutenberg/pull/52133)). ### Bug Fix diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index aa3dee7ecff55..5a94194fa6358 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -619,7 +619,7 @@ Given a component folder (e.g. `packages/components/src/unit-control`): 3. Rewrite the `meta` story object, and export it as default. In particular, make sure you add the following settings under the `parameters` key: ```tsx - const meta: ComponentMeta< typeof MyComponent > = { + const meta: Meta< typeof MyComponent > = { parameters: { controls: { expanded: true }, docs: { canvas: { sourceState: 'shown' } }, diff --git a/packages/components/README.md b/packages/components/README.md index 9274139f85121..f324cb48c66d7 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -33,14 +33,9 @@ In non-WordPress projects, link to the `build-style/style.css` file directly, it _If you're using [`Popover`](/packages/components/src/popover/README.md) or [`Tooltip`](/packages/components/src/tooltip/README.md) components outside of the editor, make sure they are rendered within a `SlotFillProvider` and with a `Popover.Slot` somewhere up the element tree._ -By default, the `Popover` component will render inline i.e. within its -parent to which it should anchor. Depending upon the context in which the -`Popover` is being consumed, this might lead to incorrect positioning. For -example, when being nested within another popover. - -This issue can be solved by rendering popovers to a specific location in the DOM via the -`Popover.Slot`. For this to work, you will need your use of the `Popover` -component and its `Slot` to be wrapped in a [`SlotFill`](/packages/components/src/slot-fill/README.md) provider. +By default, the `Popover` component will render within an extra element appended to the body of the document. + +If you want to precisely contol where the popovers render, you will need to use the `Popover.Slot` component. A `Popover` is also used as the underlying mechanism to display `Tooltip` components. So the same considerations should be applied to them. diff --git a/packages/components/package.json b/packages/components/package.json index a245281fc5ec9..cc4db850438b8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.6.0", + "version": "25.7.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -38,7 +38,7 @@ "@emotion/serialize": "^1.0.2", "@emotion/styled": "^11.6.0", "@emotion/utils": "^1.0.0", - "@floating-ui/react-dom": "1.0.0", + "@floating-ui/react-dom": "^2.0.1", "@radix-ui/react-dropdown-menu": "2.0.4", "@use-gesture/react": "^10.2.24", "@wordpress/a11y": "file:../a11y", diff --git a/packages/components/src/alignment-matrix-control/stories/index.story.tsx b/packages/components/src/alignment-matrix-control/stories/index.story.tsx index 1ed93cb647d73..24b496d1f2432 100644 --- a/packages/components/src/alignment-matrix-control/stories/index.story.tsx +++ b/packages/components/src/alignment-matrix-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -16,9 +16,13 @@ import AlignmentMatrixControl from '..'; import { HStack } from '../../h-stack'; import type { AlignmentMatrixControlProps } from '../types'; -const meta: ComponentMeta< typeof AlignmentMatrixControl > = { +const meta: Meta< typeof AlignmentMatrixControl > = { title: 'Components (Experimental)/AlignmentMatrixControl', component: AlignmentMatrixControl, + subcomponents: { + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + 'AlignmentMatrixControl.Icon': AlignmentMatrixControl.Icon, + }, argTypes: { onChange: { action: 'onChange', control: { type: null } }, value: { control: { type: null } }, @@ -30,7 +34,7 @@ const meta: ComponentMeta< typeof AlignmentMatrixControl > = { }; export default meta; -const Template: ComponentStory< typeof AlignmentMatrixControl > = ( { +const Template: StoryFn< typeof AlignmentMatrixControl > = ( { defaultValue, onChange, ...props diff --git a/packages/components/src/angle-picker-control/stories/index.story.tsx b/packages/components/src/angle-picker-control/stories/index.story.tsx index 1550e158b6c8a..d10403a436bfc 100644 --- a/packages/components/src/angle-picker-control/stories/index.story.tsx +++ b/packages/components/src/angle-picker-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import { AnglePickerControl } from '..'; -const meta: ComponentMeta< typeof AnglePickerControl > = { +const meta: Meta< typeof AnglePickerControl > = { title: 'Components/AnglePickerControl', component: AnglePickerControl, argTypes: { @@ -31,7 +31,7 @@ const meta: ComponentMeta< typeof AnglePickerControl > = { export default meta; -const AnglePickerWithState: ComponentStory< typeof AnglePickerControl > = ( { +const AnglePickerWithState: StoryFn< typeof AnglePickerControl > = ( { onChange, ...args } ) => { diff --git a/packages/components/src/animate/stories/index.story.tsx b/packages/components/src/animate/stories/index.story.tsx index 267f34ed7b15f..be076b4e6976c 100644 --- a/packages/components/src/animate/stories/index.story.tsx +++ b/packages/components/src/animate/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -9,7 +9,7 @@ import type { ComponentMeta, Story } from '@storybook/react'; import { Animate } from '..'; import Notice from '../../notice'; -const meta: ComponentMeta< typeof Animate > = { +const meta: Meta< typeof Animate > = { title: 'Components/Animate', component: Animate, parameters: { @@ -19,9 +19,11 @@ const meta: ComponentMeta< typeof Animate > = { }; export default meta; -const Template: Story< typeof Animate > = ( props ) => ; +const Template: StoryFn< typeof Animate > = ( props ) => ( + +); -export const Default: Story< typeof Animate > = Template.bind( {} ); +export const Default = Template.bind( {} ); Default.args = { children: ( { className } ) => ( @@ -30,7 +32,7 @@ Default.args = { ), }; -export const AppearTopLeft: Story< typeof Animate > = Template.bind( {} ); +export const AppearTopLeft = Template.bind( {} ); AppearTopLeft.args = { type: 'appear', options: { origin: 'top left' }, @@ -40,7 +42,7 @@ AppearTopLeft.args = { ), }; -export const AppearTopRight: Story< typeof Animate > = Template.bind( {} ); +export const AppearTopRight = Template.bind( {} ); AppearTopRight.args = { type: 'appear', options: { origin: 'top right' }, @@ -50,7 +52,7 @@ AppearTopRight.args = { ), }; -export const AppearBottomLeft: Story< typeof Animate > = Template.bind( {} ); +export const AppearBottomLeft = Template.bind( {} ); AppearBottomLeft.args = { type: 'appear', options: { origin: 'bottom left' }, @@ -60,7 +62,7 @@ AppearBottomLeft.args = { ), }; -export const AppearBottomRight: Story< typeof Animate > = Template.bind( {} ); +export const AppearBottomRight = Template.bind( {} ); AppearBottomRight.args = { type: 'appear', options: { origin: 'bottom right' }, @@ -71,7 +73,7 @@ AppearBottomRight.args = { ), }; -export const Loading: Story< typeof Animate > = Template.bind( {} ); +export const Loading = Template.bind( {} ); Loading.args = { type: 'loading', children: ( { className } ) => ( @@ -81,7 +83,7 @@ Loading.args = { ), }; -export const SlideIn: Story< typeof Animate > = Template.bind( {} ); +export const SlideIn = Template.bind( {} ); SlideIn.args = { type: 'slide-in', options: { origin: 'left' }, diff --git a/packages/components/src/animation/index.tsx b/packages/components/src/animation/index.tsx index ceeb89da7f329..39507803d2f40 100644 --- a/packages/components/src/animation/index.tsx +++ b/packages/components/src/animation/index.tsx @@ -10,4 +10,5 @@ export { motion as __unstableMotion, AnimatePresence as __unstableAnimatePresence, + MotionContext as __unstableMotionContext, } from 'framer-motion'; diff --git a/packages/components/src/base-control/stories/index.story.tsx b/packages/components/src/base-control/stories/index.story.tsx index f3ad5bf8d6722..3b6e228bd4fc8 100644 --- a/packages/components/src/base-control/stories/index.story.tsx +++ b/packages/components/src/base-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -9,7 +9,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import BaseControl, { useBaseControlProps } from '..'; import Button from '../../button'; -const meta: ComponentMeta< typeof BaseControl > = { +const meta: Meta< typeof BaseControl > = { title: 'Components/BaseControl', component: BaseControl, argTypes: { @@ -24,9 +24,7 @@ const meta: ComponentMeta< typeof BaseControl > = { }; export default meta; -const BaseControlWithTextarea: ComponentStory< typeof BaseControl > = ( - props -) => { +const BaseControlWithTextarea: StoryFn< typeof BaseControl > = ( props ) => { const { baseControlProps, controlProps } = useBaseControlProps( props ); return ( @@ -36,7 +34,7 @@ const BaseControlWithTextarea: ComponentStory< typeof BaseControl > = ( ); }; -export const Default: ComponentStory< typeof BaseControl > = +export const Default: StoryFn< typeof BaseControl > = BaseControlWithTextarea.bind( {} ); Default.args = { __nextHasNoMarginBottom: true, @@ -56,9 +54,7 @@ WithHelpText.args = { * e.g., a button, but we want an additional visual label for that section equivalent to the labels `BaseControl` would * otherwise use if the `label` prop was passed. */ -export const WithVisualLabel: ComponentStory< typeof BaseControl > = ( - props -) => { +export const WithVisualLabel: StoryFn< typeof BaseControl > = ( props ) => { // @ts-expect-error - Unclear how to fix, see also https://github.com/WordPress/gutenberg/pull/39468#discussion_r827150516 BaseControl.VisualLabel.displayName = 'BaseControl.VisualLabel'; diff --git a/packages/components/src/border-box-control/stories/index.story.tsx b/packages/components/src/border-box-control/stories/index.story.tsx index b76a86e455700..5b5d7f311208c 100644 --- a/packages/components/src/border-box-control/stories/index.story.tsx +++ b/packages/components/src/border-box-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; import type { ComponentProps } from 'react'; /** @@ -13,11 +13,9 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import Button from '../../button'; -import Popover from '../../popover'; import { BorderBoxControl } from '../'; -import { Provider as SlotFillProvider } from '../../slot-fill'; -const meta: ComponentMeta< typeof BorderBoxControl > = { +const meta: Meta< typeof BorderBoxControl > = { title: 'Components (Experimental)/BorderBoxControl', component: BorderBoxControl, argTypes: { @@ -41,7 +39,7 @@ const colors = [ { name: 'Yellow 40', color: '#bd8600' }, ]; -const Template: ComponentStory< typeof BorderBoxControl > = ( props ) => { +const Template: StoryFn< typeof BorderBoxControl > = ( props ) => { const { onChange, ...otherProps } = props; const [ borders, setBorders ] = useState< ( typeof props )[ 'value' ] >(); @@ -53,7 +51,7 @@ const Template: ComponentStory< typeof BorderBoxControl > = ( props ) => { }; return ( - + <> = ( props ) => { > Reset - { /* @ts-expect-error Ignore until Popover.Slot is converted to TS */ } - - + ); }; export const Default = Template.bind( {} ); diff --git a/packages/components/src/border-control/stories/index.story.tsx b/packages/components/src/border-control/stories/index.story.tsx index 6dbee9f98299c..9a5349d302c27 100644 --- a/packages/components/src/border-control/stories/index.story.tsx +++ b/packages/components/src/border-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; import type { ComponentProps } from 'react'; /** @@ -13,11 +13,9 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import { BorderControl } from '..'; -import { Provider as SlotFillProvider } from '../../slot-fill'; -import Popover from '../../popover'; import type { Border } from '../types'; -const meta: ComponentMeta< typeof BorderControl > = { +const meta: Meta< typeof BorderControl > = { title: 'Components (Experimental)/BorderControl', component: BorderControl, argTypes: { @@ -70,7 +68,7 @@ const multipleOriginColors = [ }, ]; -const Template: ComponentStory< typeof BorderControl > = ( { +const Template: StoryFn< typeof BorderControl > = ( { onChange, ...props } ) => { @@ -83,15 +81,11 @@ const Template: ComponentStory< typeof BorderControl > = ( { }; return ( - - - { /* @ts-expect-error Ignore until Popover.Slot is converted to TS */ } - - + ); }; diff --git a/packages/components/src/border-control/test/index.js b/packages/components/src/border-control/test/index.js index 4e971e59e87b2..4ca8c1fb8bb67 100644 --- a/packages/components/src/border-control/test/index.js +++ b/packages/components/src/border-control/test/index.js @@ -1,13 +1,8 @@ /** * External dependencies */ -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -43,36 +38,28 @@ function createProps( customProps ) { const toggleLabelRegex = /Border color( and style)* picker/; -function getWrappingPopoverElement( element ) { - return element.closest( '.components-popover' ); -} - -const openPopover = async () => { +const openPopover = async ( user ) => { const toggleButton = screen.getByLabelText( toggleLabelRegex ); - fireEvent.click( toggleButton ); + await user.click( toggleButton ); // Wait for color picker popover to fully appear const pickerButton = screen.getByRole( 'button', { name: /^Custom color picker/, } ); - await waitFor( () => - expect( - getWrappingPopoverElement( pickerButton ) - ).toBePositionedPopover() - ); + await waitFor( () => expect( pickerButton ).toBePositionedPopover() ); }; const getButton = ( name ) => { return screen.getByRole( 'button', { name } ); }; -const queryButton = ( name ) => { - return screen.queryByRole( 'button', { name } ); +const getColorOption = ( color ) => { + return screen.getByRole( 'option', { name: `Color: ${ color }` } ); }; -const clickButton = ( name ) => { - fireEvent.click( getButton( name ) ); +const queryButton = ( name ) => { + return screen.queryByRole( 'button', { name } ); }; const getSliderInput = () => { @@ -82,15 +69,6 @@ const getSliderInput = () => { const getWidthInput = () => { return screen.getByRole( 'spinbutton', { name: 'Border width' } ); }; -const setWidthInput = ( value ) => { - const widthInput = getWidthInput(); - act( () => { - widthInput.focus(); - } ); - fireEvent.change( widthInput, { target: { value } } ); -}; - -const clearWidthInput = () => setWidthInput( '' ); describe( 'BorderControl', () => { describe( 'basic rendering', () => { @@ -146,12 +124,13 @@ describe( 'BorderControl', () => { } ); it( 'should render color and style popover', async () => { + const user = userEvent.setup(); const props = createProps(); render( ); - await openPopover(); + await openPopover( user ); const customColorPicker = getButton( /Custom color picker/ ); - const colorSwatchButtons = screen.getAllByRole( 'button', { + const colorSwatchButtons = screen.getAllByRole( 'option', { name: /^Color:/, } ); const styleLabel = screen.getByText( 'Style' ); @@ -170,9 +149,10 @@ describe( 'BorderControl', () => { } ); it( 'should render color and style popover header', async () => { + const user = userEvent.setup(); const props = createProps( { showDropdownHeader: true } ); render( ); - await openPopover(); + await openPopover( user ); const headerLabel = screen.getByText( 'Border color' ); const closeButton = getButton( 'Close border color' ); @@ -182,9 +162,10 @@ describe( 'BorderControl', () => { } ); it( 'should not render style options when opted out of', async () => { + const user = userEvent.setup(); const props = createProps( { enableStyle: false } ); render( ); - await openPopover(); + await openPopover( user ); const styleLabel = screen.queryByText( 'Style' ); const solidButton = queryButton( 'Solid' ); @@ -307,6 +288,10 @@ describe( 'BorderControl', () => { const { rerender } = render( ); const slider = getSliderInput(); + // As per [1], it is not currently possible to reasonably + // replicate this interaction using `userEvent`, so leaving + // `fireEvent` in place to cover it. + // [1]: https://github.com/testing-library/user-event/issues/871 fireEvent.change( slider, { target: { value: '5' } } ); expect( props.onChange ).toHaveBeenNthCalledWith( 1, { @@ -321,10 +306,11 @@ describe( 'BorderControl', () => { } ); it( 'should update color selection', async () => { + const user = userEvent.setup(); const props = createProps(); render( ); - await openPopover(); - clickButton( 'Color: Green' ); + await openPopover( user ); + await user.click( getColorOption( 'Green' ) ); expect( props.onChange ).toHaveBeenNthCalledWith( 1, { ...defaultBorder, @@ -333,10 +319,11 @@ describe( 'BorderControl', () => { } ); it( 'should clear color selection when toggling swatch off', async () => { + const user = userEvent.setup(); const props = createProps(); render( ); - await openPopover(); - clickButton( 'Color: Blue' ); + await openPopover( user ); + await user.click( getColorOption( 'Blue' ) ); expect( props.onChange ).toHaveBeenNthCalledWith( 1, { ...defaultBorder, @@ -345,10 +332,11 @@ describe( 'BorderControl', () => { } ); it( 'should update style selection', async () => { + const user = userEvent.setup(); const props = createProps(); render( ); - await openPopover(); - clickButton( 'Dashed' ); + await openPopover( user ); + await user.click( getButton( 'Dashed' ) ); expect( props.onChange ).toHaveBeenNthCalledWith( 1, { ...defaultBorder, @@ -357,19 +345,21 @@ describe( 'BorderControl', () => { } ); it( 'should take no action when color and style popover is closed', async () => { + const user = userEvent.setup(); const props = createProps( { showDropdownHeader: true } ); render( ); - await openPopover(); - clickButton( 'Close border color' ); + await openPopover( user ); + await user.click( getButton( 'Close border color' ) ); expect( props.onChange ).not.toHaveBeenCalled(); } ); it( 'should reset color and style only when popover reset button clicked', async () => { + const user = userEvent.setup(); const props = createProps(); render( ); - await openPopover(); - clickButton( 'Reset to default' ); + await openPopover( user ); + await user.click( getButton( 'Reset to default' ) ); expect( props.onChange ).toHaveBeenNthCalledWith( 1, { color: undefined, @@ -379,25 +369,27 @@ describe( 'BorderControl', () => { } ); it( 'should sanitize border when width and color are undefined', async () => { + const user = userEvent.setup(); const props = createProps(); const { rerender } = render( ); - clearWidthInput(); + await user.clear( getWidthInput() ); rerender( ); - await openPopover(); - clickButton( 'Color: Blue' ); + await openPopover( user ); + await user.click( getColorOption( 'Blue' ) ); expect( props.onChange ).toHaveBeenCalledWith( undefined ); } ); it( 'should not sanitize border when requested', async () => { + const user = userEvent.setup(); const props = createProps( { shouldSanitizeBorder: false, } ); const { rerender } = render( ); - clearWidthInput(); + await user.clear( getWidthInput() ); rerender( ); - await openPopover(); - clickButton( 'Color: Blue' ); + await openPopover( user ); + await user.click( getColorOption( 'Blue' ) ); expect( props.onChange ).toHaveBeenNthCalledWith( 2, { color: undefined, @@ -407,12 +399,16 @@ describe( 'BorderControl', () => { } ); it( 'should clear color and set style to `none` when setting zero width', async () => { + const user = userEvent.setup(); const props = createProps(); render( ); - await openPopover(); - clickButton( 'Color: Green' ); - clickButton( 'Dotted' ); - setWidthInput( '0' ); + await openPopover( user ); + await user.click( getColorOption( 'Green' ) ); + await user.click( getButton( 'Dotted' ) ); + await user.type( getWidthInput(), '0', { + initialSelectionStart: 0, + initialSelectionEnd: 1, + } ); expect( props.onChange ).toHaveBeenNthCalledWith( 3, { color: undefined, @@ -422,15 +418,23 @@ describe( 'BorderControl', () => { } ); it( 'should reselect color and style selections when changing to non-zero width', async () => { + const user = userEvent.setup(); const props = createProps(); const { rerender } = render( ); - await openPopover(); - clickButton( 'Color: Green' ); + await openPopover( user ); + await user.click( getColorOption( 'Green' ) ); rerender( ); - clickButton( 'Dotted' ); + await user.click( getButton( 'Dotted' ) ); rerender( ); - setWidthInput( '0' ); - setWidthInput( '5' ); + const widthInput = getWidthInput(); + await user.type( widthInput, '0', { + initialSelectionStart: 0, + initialSelectionEnd: 1, + } ); + await user.type( widthInput, '5', { + initialSelectionStart: 0, + initialSelectionEnd: 1, + } ); expect( props.onChange ).toHaveBeenNthCalledWith( 4, { color: '#00a32a', @@ -440,10 +444,11 @@ describe( 'BorderControl', () => { } ); it( 'should set a non-zero width when applying color to zero width border', async () => { + const user = userEvent.setup(); const props = createProps( { value: undefined } ); const { rerender } = render( ); - await openPopover(); - clickButton( 'Color: Yellow' ); + await openPopover( user ); + await user.click( getColorOption( 'Yellow' ) ); expect( props.onChange ).toHaveBeenCalledWith( { color: '#bd8600', @@ -451,9 +456,11 @@ describe( 'BorderControl', () => { width: undefined, } ); - setWidthInput( '0' ); + await user.type( getWidthInput(), '0' ); + rerender( ); - clickButton( 'Color: Green' ); + await openPopover( user ); + await user.click( getColorOption( 'Green' ) ); expect( props.onChange ).toHaveBeenCalledWith( { color: '#00a32a', @@ -463,13 +470,14 @@ describe( 'BorderControl', () => { } ); it( 'should set a non-zero width when applying style to zero width border', async () => { + const user = userEvent.setup(); const props = createProps( { value: undefined, shouldSanitizeBorder: false, } ); const { rerender } = render( ); - await openPopover(); - clickButton( 'Dashed' ); + await openPopover( user ); + await user.click( getButton( 'Dashed' ) ); expect( props.onChange ).toHaveBeenCalledWith( { color: undefined, @@ -477,9 +485,11 @@ describe( 'BorderControl', () => { width: undefined, } ); - setWidthInput( '0' ); + await user.type( getWidthInput(), '0' ); + rerender( ); - clickButton( 'Dotted' ); + await openPopover( user ); + await user.click( getButton( 'Dotted' ) ); expect( props.onChange ).toHaveBeenCalledWith( { color: undefined, diff --git a/packages/components/src/button-group/stories/index.story.tsx b/packages/components/src/button-group/stories/index.story.tsx index 3ad0a34582545..958a0d137763e 100644 --- a/packages/components/src/button-group/stories/index.story.tsx +++ b/packages/components/src/button-group/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -9,7 +9,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import ButtonGroup from '..'; import Button from '../../button'; -const meta: ComponentMeta< typeof ButtonGroup > = { +const meta: Meta< typeof ButtonGroup > = { title: 'Components/ButtonGroup', component: ButtonGroup, argTypes: { @@ -22,7 +22,7 @@ const meta: ComponentMeta< typeof ButtonGroup > = { }; export default meta; -const Template: ComponentStory< typeof ButtonGroup > = ( args ) => { +const Template: StoryFn< typeof ButtonGroup > = ( args ) => { const style = { margin: '0 4px' }; return ( @@ -36,6 +36,4 @@ const Template: ComponentStory< typeof ButtonGroup > = ( args ) => { ); }; -export const Default: ComponentStory< typeof ButtonGroup > = Template.bind( - {} -); +export const Default: StoryFn< typeof ButtonGroup > = Template.bind( {} ); diff --git a/packages/components/src/button/stories/e2e/index.story.tsx b/packages/components/src/button/stories/e2e/index.story.tsx index 24418fb12e5eb..c2ec8e237d3b2 100644 --- a/packages/components/src/button/stories/e2e/index.story.tsx +++ b/packages/components/src/button/stories/e2e/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { Story, Meta } from '@storybook/react'; +import type { StoryFn, Meta } from '@storybook/react'; /** * WordPress dependencies @@ -20,7 +20,7 @@ const meta: Meta< typeof Button > = { }; export default meta; -export const VariantStates: Story< typeof Button > = ( +export const VariantStates: StoryFn< typeof Button > = ( props: ButtonAsButtonProps ) => { const variants: ( typeof props.variant )[] = [ @@ -57,7 +57,7 @@ Icon.args = { icon: wordpress, }; -export const Dashicons: Story< typeof Button > = ( props ) => { +export const Dashicons: StoryFn< typeof Button > = ( props ) => { return (
; }; -export const Default: Story< typeof Button > = Template.bind( {} ); +export const Default = Template.bind( {} ); Default.args = { children: 'Code is poetry', }; -export const Primary: Story< typeof Button > = Template.bind( {} ); +export const Primary = Template.bind( {} ); Primary.args = { ...Default.args, variant: 'primary', }; -export const Secondary: Story< typeof Button > = Template.bind( {} ); +export const Secondary = Template.bind( {} ); Secondary.args = { ...Default.args, variant: 'secondary', }; -export const Tertiary: Story< typeof Button > = Template.bind( {} ); +export const Tertiary = Template.bind( {} ); Tertiary.args = { ...Default.args, variant: 'tertiary', }; -export const Link: Story< typeof Button > = Template.bind( {} ); +export const Link = Template.bind( {} ); Link.args = { ...Default.args, variant: 'link', }; -export const IsDestructive: Story< typeof Button > = Template.bind( {} ); +export const IsDestructive = Template.bind( {} ); IsDestructive.args = { ...Default.args, isDestructive: true, }; -export const Icon: Story< typeof Button > = Template.bind( {} ); +export const Icon = Template.bind( {} ); Icon.args = { label: 'Code is poetry', icon: 'wordpress', }; -export const GroupedIcons: Story< typeof Button > = () => { +export const GroupedIcons = () => { const GroupContainer = ( { children }: { children: ReactNode } ) => (
{ children }
); diff --git a/packages/components/src/card/stories/index.story.tsx b/packages/components/src/card/stories/index.story.tsx index 43f00143eccbe..8a20d82b318e3 100644 --- a/packages/components/src/card/stories/index.story.tsx +++ b/packages/components/src/card/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -18,8 +18,10 @@ import { Text } from '../../text'; import { Heading } from '../../heading'; import Button from '../../button'; -const meta: ComponentMeta< typeof Card > = { +const meta: Meta< typeof Card > = { component: Card, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { CardHeader, CardBody, CardDivider, CardMedia, CardFooter }, title: 'Components/Card', argTypes: { as: { @@ -39,9 +41,7 @@ const meta: ComponentMeta< typeof Card > = { export default meta; -const Template: ComponentStory< typeof Card > = ( args ) => ( - -); +const Template: StoryFn< typeof Card > = ( args ) => ; export const Default = Template.bind( {} ); Default.args = { diff --git a/packages/components/src/checkbox-control/stories/index.story.tsx b/packages/components/src/checkbox-control/stories/index.story.tsx index 2afcb967c67b4..ce55cfb655a17 100644 --- a/packages/components/src/checkbox-control/stories/index.story.tsx +++ b/packages/components/src/checkbox-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { useState } from '@wordpress/element'; import CheckboxControl from '..'; import { VStack } from '../../v-stack'; -const meta: ComponentMeta< typeof CheckboxControl > = { +const meta: Meta< typeof CheckboxControl > = { component: CheckboxControl, title: 'Components/CheckboxControl', argTypes: { @@ -36,7 +36,7 @@ const meta: ComponentMeta< typeof CheckboxControl > = { }; export default meta; -const DefaultTemplate: ComponentStory< typeof CheckboxControl > = ( { +const DefaultTemplate: StoryFn< typeof CheckboxControl > = ( { onChange, ...args } ) => { @@ -54,14 +54,15 @@ const DefaultTemplate: ComponentStory< typeof CheckboxControl > = ( { ); }; -export const Default: ComponentStory< typeof CheckboxControl > = - DefaultTemplate.bind( {} ); +export const Default: StoryFn< typeof CheckboxControl > = DefaultTemplate.bind( + {} +); Default.args = { label: 'Is author', help: 'Is the user an author or not?', }; -export const Indeterminate: ComponentStory< typeof CheckboxControl > = ( { +export const Indeterminate: StoryFn< typeof CheckboxControl > = ( { onChange, ...args } ) => { diff --git a/packages/components/src/circular-option-picker/index.tsx b/packages/components/src/circular-option-picker/index.tsx index 1736d2227ee77..e32cfb822808c 100644 --- a/packages/components/src/circular-option-picker/index.tsx +++ b/packages/components/src/circular-option-picker/index.tsx @@ -6,22 +6,31 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { useInstanceId } from '@wordpress/compose'; +import { createContext, useContext, useEffect } from '@wordpress/element'; import { Icon, check } from '@wordpress/icons'; +import { isRTL } from '@wordpress/i18n'; /** * Internal dependencies */ import Button from '../button'; +import { Composite, CompositeItem, useCompositeState } from '../composite'; import Dropdown from '../dropdown'; import Tooltip from '../tooltip'; import type { CircularOptionPickerProps, DropdownLinkActionProps, + OptionGroupProps, OptionProps, } from './types'; import type { WordPressComponentProps } from '../ui/context'; import type { ButtonAsButtonProps } from '../button/types'; +const CircularOptionPickerContext = createContext( {} ); + +const hasSelectedOption = new Map(); + export function Option( { className, isSelected, @@ -29,13 +38,44 @@ export function Option( { tooltipText, ...additionalProps }: OptionProps ) { - const optionButton = ( -
-
-
-
-
- -
- -
- - - -`; - -exports[`ColorPalette should render a dynamic toolbar of colors 1`] = ` -.emotion-0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - gap: calc(4px * 3); - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; -} - -.emotion-0>* { - min-height: 0; -} - -.emotion-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - gap: 0; - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; -} - -.emotion-2>* { - min-height: 0; -} - -.emotion-4 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - gap: calc(4px * 0.5); - -webkit-box-pack: center; - -ms-flex-pack: center; - -webkit-justify-content: center; - justify-content: center; -} - -.emotion-4>* { - min-height: 0; -} - -.emotion-6 { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-`; diff --git a/packages/components/src/color-palette/test/index.tsx b/packages/components/src/color-palette/test/index.tsx index 62b9cdf51a59d..e302fb476db3c 100644 --- a/packages/components/src/color-palette/test/index.tsx +++ b/packages/components/src/color-palette/test/index.tsx @@ -19,10 +19,6 @@ const EXAMPLE_COLORS = [ ]; const INITIAL_COLOR = EXAMPLE_COLORS[ 0 ].color; -function getWrappingPopoverElement( element: HTMLElement ) { - return element.closest( '.components-popover' ); -} - const ControlledColorPalette = ( { onChange, }: { @@ -43,20 +39,6 @@ const ControlledColorPalette = ( { }; describe( 'ColorPalette', () => { - it( 'should render a dynamic toolbar of colors', () => { - const onChange = jest.fn(); - - const { container } = render( - - ); - - expect( container ).toMatchSnapshot(); - } ); - it( 'should render three color button options', () => { const onChange = jest.fn(); @@ -69,7 +51,7 @@ describe( 'ColorPalette', () => { ); expect( - screen.getAllByRole( 'button', { name: /^Color:/ } ) + screen.getAllByRole( 'option', { name: /^Color:/ } ) ).toHaveLength( 3 ); } ); @@ -86,7 +68,7 @@ describe( 'ColorPalette', () => { ); await user.click( - screen.getByRole( 'button', { name: /^Color:/, pressed: true } ) + screen.getByRole( 'option', { name: /^Color:/, selected: true } ) ); expect( onChange ).toHaveBeenCalledTimes( 1 ); @@ -108,9 +90,9 @@ describe( 'ColorPalette', () => { // Click the first unpressed button // (i.e. a button representing a color that is not the current color) await user.click( - screen.getAllByRole( 'button', { + screen.getAllByRole( 'option', { name: /^Color:/, - pressed: false, + selected: false, } )[ 0 ] ); @@ -137,10 +119,26 @@ describe( 'ColorPalette', () => { expect( onChange ).toHaveBeenCalledWith( undefined ); } ); + it( 'should render custom color picker', () => { + const onChange = jest.fn(); + + render( + + ); + + expect( + screen.getByRole( 'button', { name: /^Custom color picker\./ } ) + ).toBeInTheDocument(); + } ); + it( 'should allow disabling custom color picker', () => { const onChange = jest.fn(); - const { container } = render( + render( { /> ); - expect( container ).toMatchSnapshot(); + expect( + screen.queryByRole( 'button', { name: /^Custom color picker\./ } ) + ).not.toBeInTheDocument(); } ); it( 'should render dropdown and its content', async () => { @@ -188,9 +188,7 @@ describe( 'ColorPalette', () => { const dropdownColorInput = screen.getByLabelText( 'Hex color' ); await waitFor( () => - expect( - getWrappingPopoverElement( dropdownColorInput ) - ).toBePositionedPopover() + expect( dropdownColorInput ).toBePositionedPopover() ); } ); @@ -231,9 +229,9 @@ describe( 'ColorPalette', () => { // Click the first unpressed button await user.click( - screen.getAllByRole( 'button', { + screen.getAllByRole( 'option', { name: /^Color:/, - pressed: false, + selected: false, } )[ 0 ] ); diff --git a/packages/components/src/color-palette/types.ts b/packages/components/src/color-palette/types.ts index a5136341aa68a..7025ef156127b 100644 --- a/packages/components/src/color-palette/types.ts +++ b/packages/components/src/color-palette/types.ts @@ -88,4 +88,23 @@ export type ColorPaletteProps = Pick< PaletteProps, 'onChange' > & { * @default false */ __experimentalIsRenderedInSidebar?: boolean; -}; +} & ( + | { + /** + * A label to identify the purpose of the control. + * + * @todo [#54055] Either this or `aria-labelledby` should be required + */ + 'aria-label'?: string; + 'aria-labelledby'?: never; + } + | { + /** + * An ID of an element to provide a label for the control. + * + * @todo [#54055] Either this or `aria-label` should be required + */ + 'aria-labelledby'?: string; + 'aria-label'?: never; + } + ); diff --git a/packages/components/src/color-picker/hsv-color-picker.native.js b/packages/components/src/color-picker/hsv-color-picker.native.js new file mode 100644 index 0000000000000..46499c94df5b3 --- /dev/null +++ b/packages/components/src/color-picker/hsv-color-picker.native.js @@ -0,0 +1,88 @@ +/** + * External dependencies + */ +import { View, Dimensions } from 'react-native'; + +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import HuePicker from './hue-picker'; +import SaturationValuePicker from './saturation-picker'; +import styles from './style.native.scss'; + +const HsvColorPicker = ( props ) => { + const maxWidth = Dimensions.get( 'window' ).width - 32; + const satValPickerRef = useRef( null ); + + const { + containerStyle = {}, + currentColor, + huePickerContainerStyle = {}, + huePickerBorderRadius = 0, + huePickerHue = 0, + huePickerBarWidth = maxWidth, + huePickerBarHeight = 12, + huePickerSliderSize = 24, + onHuePickerDragStart, + onHuePickerDragMove, + onHuePickerDragEnd, + onHuePickerDragTerminate, + onHuePickerPress, + satValPickerContainerStyle = {}, + satValPickerBorderRadius = 0, + satValPickerSize = { width: maxWidth, height: 200 }, + satValPickerSliderSize = 24, + satValPickerHue = 0, + satValPickerSaturation = 1, + satValPickerValue = 1, + onSatValPickerDragStart, + onSatValPickerDragMove, + onSatValPickerDragEnd, + onSatValPickerDragTerminate, + onSatValPickerPress, + } = props; + + return ( + + + + + ); +}; + +export default HsvColorPicker; diff --git a/packages/components/src/color-picker/hue-picker.native.js b/packages/components/src/color-picker/hue-picker.native.js new file mode 100644 index 0000000000000..d7d391e183765 --- /dev/null +++ b/packages/components/src/color-picker/hue-picker.native.js @@ -0,0 +1,194 @@ +/** + * External dependencies + */ +import { Animated, View, PanResponder } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; + +/** + * WordPress dependencies + */ +import React, { Component } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +export default class HuePicker extends Component { + constructor( props ) { + super( props ); + this.hueColors = [ + '#ff0000', + '#ffff00', + '#00ff00', + '#00ffff', + '#0000ff', + '#ff00ff', + '#ff0000', + ]; + this.sliderX = new Animated.Value( + ( props.barHeight * props.hue ) / 360 + ); + this.panResponder = PanResponder.create( { + onStartShouldSetPanResponder: () => true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderGrant: ( evt, gestureState ) => { + const { onPress } = this.props; + this.dragStartValue = this.computeHueValuePress( evt ); + + if ( onPress ) { + onPress( { + hue: this.computeHueValuePress( evt ), + nativeEvent: evt.nativeEvent, + } ); + } + + this.fireDragEvent( 'onDragStart', gestureState ); + }, + onPanResponderMove: ( evt, gestureState ) => { + this.fireDragEvent( 'onDragMove', gestureState ); + }, + onPanResponderTerminationRequest: () => true, + onPanResponderRelease: ( evt, gestureState ) => { + this.fireDragEvent( 'onDragEnd', gestureState ); + }, + onPanResponderTerminate: ( evt, gestureState ) => { + this.fireDragEvent( 'onDragTerminate', gestureState ); + }, + onShouldBlockNativeResponder: () => true, + } ); + } + + componentDidUpdate( prevProps ) { + const { hue = 0, barWidth = 200, sliderSize = 24 } = this.props; + const borderWidth = sliderSize / 10; + if ( prevProps.hue !== hue || prevProps.barWidth !== barWidth ) { + this.sliderX.setValue( + ( ( barWidth - sliderSize + borderWidth ) * hue ) / 360 + ); + } + } + + normalizeValue( value ) { + if ( value < 0 ) return 0; + if ( value > 1 ) return 1; + return value; + } + + getContainerStyle() { + const { + sliderSize = 24, + barHeight = 12, + containerStyle = {}, + } = this.props; + const paddingLeft = sliderSize / 2; + const paddingTop = + sliderSize - barHeight > 0 ? ( sliderSize - barHeight ) / 2 : 0; + return [ + styles[ 'hsv-container' ], + containerStyle, + { + paddingTop, + paddingBottom: paddingTop, + paddingLeft, + paddingRight: paddingLeft, + }, + ]; + } + + computeHueValueDrag( gestureState ) { + const { dx } = gestureState; + const { barWidth = 200 } = this.props; + const { dragStartValue } = this; + const diff = dx / barWidth; + const updatedHue = + this.normalizeValue( dragStartValue / 360 + diff ) * 360; + return updatedHue; + } + + computeHueValuePress( event ) { + const { nativeEvent } = event; + const { locationX } = nativeEvent; + const { barWidth = 200 } = this.props; + const updatedHue = this.normalizeValue( locationX / barWidth ) * 360; + return updatedHue; + } + + fireDragEvent( eventName, gestureState ) { + const { [ eventName ]: event } = this.props; + if ( event ) { + event( { + hue: this.computeHueValueDrag( gestureState ), + gestureState, + } ); + } + } + + firePressEvent( event ) { + const { onPress } = this.props; + if ( onPress ) { + onPress( { + hue: this.computeHueValuePress( event ), + nativeEvent: event.nativeEvent, + } ); + } + } + + render() { + const { hueColors } = this; + const { + sliderSize = 24, + barWidth = 200, + barHeight = 12, + borderRadius = 0, + } = this.props; + const borderWidth = sliderSize / 10; + return ( + + + + + + + ); + } +} diff --git a/packages/components/src/color-picker/index.native.js b/packages/components/src/color-picker/index.native.js index 1dfd2353cad1c..a2fee512ce26f 100644 --- a/packages/components/src/color-picker/index.native.js +++ b/packages/components/src/color-picker/index.native.js @@ -2,7 +2,6 @@ * External dependencies */ import { View, Text, TouchableWithoutFeedback, Platform } from 'react-native'; -import HsvColorPicker from 'react-native-hsv-color-picker'; import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; /** @@ -17,6 +16,7 @@ import { Icon, check, close } from '@wordpress/icons'; * Internal dependencies */ import styles from './style.scss'; +import HsvColorPicker from './hsv-color-picker.native.js'; extend( [ namesPlugin ] ); @@ -122,6 +122,7 @@ function ColorPicker( { <> true, + onStartShouldSetPanResponderCapture: () => true, + onMoveShouldSetPanResponder: () => true, + onMoveShouldSetPanResponderCapture: () => true, + onPanResponderGrant: ( evt, gestureState ) => { + const { onPress } = this.props; + const { saturation, value } = this.computeSatValPress( evt ); + this.dragStartValue = { + saturation, + value, + }; + + if ( onPress ) { + onPress( { + ...this.computeSatValPress( evt ), + nativeEvent: evt.nativeEvent, + } ); + } + + this.fireDragEvent( 'onDragStart', gestureState ); + }, + onPanResponderMove: ( evt, gestureState ) => { + this.fireDragEvent( 'onDragMove', gestureState ); + }, + onPanResponderTerminationRequest: () => true, + onPanResponderRelease: ( evt, gestureState ) => { + this.fireDragEvent( 'onDragEnd', gestureState ); + }, + onPanResponderTerminate: ( evt, gestureState ) => { + this.fireDragEvent( 'onDragTerminate', gestureState ); + }, + onShouldBlockNativeResponder: () => true, + } ); + } + + normalizeValue( value ) { + if ( value < 0 ) return 0; + if ( value > 1 ) return 1; + return value; + } + + computeSatValDrag( gestureState ) { + const { dx, dy } = gestureState; + const { size } = this.props; + const { saturation, value } = this.dragStartValue; + const diffx = dx / size.width; + const diffy = dy / size.height; + return { + saturation: this.normalizeValue( saturation + diffx ), + value: this.normalizeValue( value - diffy ), + }; + } + + computeSatValPress( event ) { + const { nativeEvent } = event; + const { locationX, locationY } = nativeEvent; + const { size } = this.props; + return { + saturation: this.normalizeValue( locationX / size.width ), + value: 1 - this.normalizeValue( locationY / size.height ), + }; + } + + fireDragEvent( eventName, gestureState ) { + const { [ eventName ]: event } = this.props; + if ( event ) { + event( { + ...this.computeSatValDrag( gestureState ), + gestureState, + } ); + } + } + + render() { + const { + size, + sliderSize = 24, + hue = 0, + value = 1, + saturation = 1, + containerStyle = {}, + borderRadius = 0, + currentColor, + } = this.props; + + return ( + + + + + + + + + ); + } +} diff --git a/packages/components/src/color-picker/stories/index.story.tsx b/packages/components/src/color-picker/stories/index.story.tsx index 0cc0181b8f5de..81500c1558822 100644 --- a/packages/components/src/color-picker/stories/index.story.tsx +++ b/packages/components/src/color-picker/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import { ColorPicker } from '../component'; -const meta: ComponentMeta< typeof ColorPicker > = { +const meta: Meta< typeof ColorPicker > = { component: ColorPicker, title: 'Components/ColorPicker', argTypes: { @@ -30,7 +30,7 @@ const meta: ComponentMeta< typeof ColorPicker > = { }; export default meta; -const Template: Story< typeof ColorPicker > = ( { onChange, ...props } ) => { +const Template: StoryFn< typeof ColorPicker > = ( { onChange, ...props } ) => { const [ color, setColor ] = useState< string | undefined >(); return ( diff --git a/packages/components/src/color-picker/style.native.scss b/packages/components/src/color-picker/style.native.scss index 9932830e4e313..248a464ae4ad8 100644 --- a/packages/components/src/color-picker/style.native.scss +++ b/packages/components/src/color-picker/style.native.scss @@ -62,3 +62,26 @@ .pickerPointer { height: 16px; } + +.hsv-container { + justify-content: center; + align-items: center; +} + +.gradient-gontainer { + overflow: hidden; +} + +.saturation-slider { + top: 0; + left: 0; + position: absolute; + border-color: $white; +} + +.hue-slider { + position: absolute; + background-color: #fff; + box-shadow: 0 7px 10px rgba(0, 0, 0, 0.5); + z-index: 5; +} diff --git a/packages/components/src/combobox-control/stories/index.story.tsx b/packages/components/src/combobox-control/stories/index.story.tsx index 5b88624209531..9c0e5455ebc06 100644 --- a/packages/components/src/combobox-control/stories/index.story.tsx +++ b/packages/components/src/combobox-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -22,7 +22,7 @@ const countries = [ { name: 'American Samoa', code: 'AS' }, ]; -const meta: ComponentMeta< typeof ComboboxControl > = { +const meta: Meta< typeof ComboboxControl > = { title: 'Components/ComboboxControl', component: ComboboxControl, argTypes: { @@ -43,7 +43,7 @@ const mapCountryOption = ( country: ( typeof countries )[ number ] ) => ( { const countryOptions = countries.map( mapCountryOption ); -const Template: ComponentStory< typeof ComboboxControl > = ( { +const Template: StoryFn< typeof ComboboxControl > = ( { onChange, ...args } ) => { diff --git a/packages/components/src/composite/index.js b/packages/components/src/composite/index.ts similarity index 100% rename from packages/components/src/composite/index.js rename to packages/components/src/composite/index.ts diff --git a/packages/components/src/confirm-dialog/stories/index.story.js b/packages/components/src/confirm-dialog/stories/index.story.js index f308206ad1df8..ea561ff297c43 100644 --- a/packages/components/src/confirm-dialog/stories/index.story.js +++ b/packages/components/src/confirm-dialog/stories/index.story.js @@ -87,22 +87,22 @@ const _defaultSnippet = `() => { return ( <> - - Would you like to privately publish the post now? - + + Would you like to privately publish the post now? + - { confirmVal } + { confirmVal } - + - ); - };`; + ); +};`; _default.args = {}; _default.parameters = { docs: { @@ -110,7 +110,6 @@ _default.parameters = { code: _defaultSnippet, language: 'jsx', type: 'auto', - format: 'true', }, }, }; diff --git a/packages/components/src/custom-gradient-picker/stories/index.story.tsx b/packages/components/src/custom-gradient-picker/stories/index.story.tsx index f902d34c0bd92..6d19157238d28 100644 --- a/packages/components/src/custom-gradient-picker/stories/index.story.tsx +++ b/packages/components/src/custom-gradient-picker/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies */ @@ -12,7 +12,7 @@ import { useState } from '@wordpress/element'; */ import CustomGradientPicker from '../'; -const meta: ComponentMeta< typeof CustomGradientPicker > = { +const meta: Meta< typeof CustomGradientPicker > = { title: 'Components/CustomGradientPicker', component: CustomGradientPicker, parameters: { @@ -23,7 +23,7 @@ const meta: ComponentMeta< typeof CustomGradientPicker > = { }; export default meta; -const CustomGradientPickerWithState: ComponentStory< +const CustomGradientPickerWithState: StoryFn< typeof CustomGradientPicker > = ( { onChange, ...props } ) => { const [ gradient, setGradient ] = useState< string >(); diff --git a/packages/components/src/date-time/stories/date-time.story.tsx b/packages/components/src/date-time/stories/date-time.story.tsx index 600a419e9d6a1..86a627bbec35e 100644 --- a/packages/components/src/date-time/stories/date-time.story.tsx +++ b/packages/components/src/date-time/stories/date-time.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { useState, useEffect } from '@wordpress/element'; import DateTimePicker from '../date-time'; import { daysFromNow, isWeekend } from './utils'; -const meta: ComponentMeta< typeof DateTimePicker > = { +const meta: Meta< typeof DateTimePicker > = { title: 'Components/DateTimePicker', component: DateTimePicker, argTypes: { @@ -28,7 +28,7 @@ const meta: ComponentMeta< typeof DateTimePicker > = { }; export default meta; -const Template: ComponentStory< typeof DateTimePicker > = ( { +const Template: StoryFn< typeof DateTimePicker > = ( { currentDate, onChange, ...args @@ -49,12 +49,9 @@ const Template: ComponentStory< typeof DateTimePicker > = ( { ); }; -export const Default: ComponentStory< typeof DateTimePicker > = Template.bind( - {} -); +export const Default: StoryFn< typeof DateTimePicker > = Template.bind( {} ); -export const WithEvents: ComponentStory< typeof DateTimePicker > = - Template.bind( {} ); +export const WithEvents: StoryFn< typeof DateTimePicker > = Template.bind( {} ); WithEvents.args = { currentDate: new Date(), events: [ @@ -65,8 +62,9 @@ WithEvents.args = { ], }; -export const WithInvalidDates: ComponentStory< typeof DateTimePicker > = - Template.bind( {} ); +export const WithInvalidDates: StoryFn< typeof DateTimePicker > = Template.bind( + {} +); WithInvalidDates.args = { currentDate: new Date(), isInvalidDate: isWeekend, diff --git a/packages/components/src/date-time/stories/date.story.tsx b/packages/components/src/date-time/stories/date.story.tsx index 8bde0bda4859c..8d1513d014c8c 100644 --- a/packages/components/src/date-time/stories/date.story.tsx +++ b/packages/components/src/date-time/stories/date.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { useState, useEffect } from '@wordpress/element'; import DatePicker from '../date'; import { daysFromNow, isWeekend } from './utils'; -const meta: ComponentMeta< typeof DatePicker > = { +const meta: Meta< typeof DatePicker > = { title: 'Components/DatePicker', component: DatePicker, argTypes: { @@ -28,7 +28,7 @@ const meta: ComponentMeta< typeof DatePicker > = { }; export default meta; -const Template: ComponentStory< typeof DatePicker > = ( { +const Template: StoryFn< typeof DatePicker > = ( { currentDate, onChange, ...args @@ -49,11 +49,9 @@ const Template: ComponentStory< typeof DatePicker > = ( { ); }; -export const Default: ComponentStory< typeof DatePicker > = Template.bind( {} ); +export const Default: StoryFn< typeof DatePicker > = Template.bind( {} ); -export const WithEvents: ComponentStory< typeof DatePicker > = Template.bind( - {} -); +export const WithEvents: StoryFn< typeof DatePicker > = Template.bind( {} ); WithEvents.args = { currentDate: new Date(), events: [ @@ -64,8 +62,9 @@ WithEvents.args = { ], }; -export const WithInvalidDates: ComponentStory< typeof DatePicker > = - Template.bind( {} ); +export const WithInvalidDates: StoryFn< typeof DatePicker > = Template.bind( + {} +); WithInvalidDates.args = { currentDate: new Date(), isInvalidDate: isWeekend, diff --git a/packages/components/src/date-time/stories/time.story.tsx b/packages/components/src/date-time/stories/time.story.tsx index 4ad40626b73af..c48b8fb1d1592 100644 --- a/packages/components/src/date-time/stories/time.story.tsx +++ b/packages/components/src/date-time/stories/time.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState, useEffect } from '@wordpress/element'; */ import TimePicker from '../time'; -const meta: ComponentMeta< typeof TimePicker > = { +const meta: Meta< typeof TimePicker > = { title: 'Components/TimePicker', component: TimePicker, argTypes: { @@ -27,7 +27,7 @@ const meta: ComponentMeta< typeof TimePicker > = { }; export default meta; -const Template: ComponentStory< typeof TimePicker > = ( { +const Template: StoryFn< typeof TimePicker > = ( { currentTime, onChange, ...args @@ -48,4 +48,4 @@ const Template: ComponentStory< typeof TimePicker > = ( { ); }; -export const Default: ComponentStory< typeof TimePicker > = Template.bind( {} ); +export const Default: StoryFn< typeof TimePicker > = Template.bind( {} ); diff --git a/packages/components/src/dimension-control/stories/index.story.tsx b/packages/components/src/dimension-control/stories/index.story.tsx index 0abb5b815429d..0698125c446ca 100644 --- a/packages/components/src/dimension-control/stories/index.story.tsx +++ b/packages/components/src/dimension-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ @@ -34,9 +34,9 @@ export default { docs: { canvas: { sourceState: 'shown' } }, }, }, -} as ComponentMeta< typeof DimensionControl >; +} as Meta< typeof DimensionControl >; -const Template: ComponentStory< typeof DimensionControl > = ( args ) => ( +const Template: StoryFn< typeof DimensionControl > = ( args ) => ( ); diff --git a/packages/components/src/disabled/stories/index.story.tsx b/packages/components/src/disabled/stories/index.story.tsx index 40260fe106643..b5da6ccedddc0 100644 --- a/packages/components/src/disabled/stories/index.story.tsx +++ b/packages/components/src/disabled/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -17,7 +17,7 @@ import TextControl from '../../text-control/'; import TextareaControl from '../../textarea-control/'; import { VStack } from '../../v-stack/'; -const meta: ComponentMeta< typeof Disabled > = { +const meta: Meta< typeof Disabled > = { title: 'Components/Disabled', component: Disabled, argTypes: { @@ -66,7 +66,7 @@ const Form = () => { ); }; -export const Default: ComponentStory< typeof Disabled > = ( args ) => { +export const Default: StoryFn< typeof Disabled > = ( args ) => { return (
@@ -77,7 +77,7 @@ Default.args = { isDisabled: true, }; -export const ContentEditable: ComponentStory< typeof Disabled > = ( args ) => { +export const ContentEditable: StoryFn< typeof Disabled > = ( args ) => { return (
diff --git a/packages/components/src/divider/stories/index.story.tsx b/packages/components/src/divider/stories/index.story.tsx index b1776e1f67fc2..d60a43164506b 100644 --- a/packages/components/src/divider/stories/index.story.tsx +++ b/packages/components/src/divider/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -10,7 +10,7 @@ import { Text } from '../../text'; import { Divider } from '..'; import { Flex } from '../../flex'; -const meta: ComponentMeta< typeof Divider > = { +const meta: Meta< typeof Divider > = { component: Divider, title: 'Components (Experimental)/Divider', argTypes: { @@ -31,7 +31,7 @@ const meta: ComponentMeta< typeof Divider > = { }; export default meta; -const Template: ComponentStory< typeof Divider > = ( args ) => ( +const Template: StoryFn< typeof Divider > = ( args ) => (
Some text before the divider @@ -39,19 +39,19 @@ const Template: ComponentStory< typeof Divider > = ( args ) => (
); -export const Horizontal: ComponentStory< typeof Divider > = Template.bind( {} ); +export const Horizontal: StoryFn< typeof Divider > = Template.bind( {} ); Horizontal.args = { margin: '2', }; -export const Vertical: ComponentStory< typeof Divider > = Template.bind( {} ); +export const Vertical: StoryFn< typeof Divider > = Template.bind( {} ); Vertical.args = { ...Horizontal.args, orientation: 'vertical', }; // Inside a `flex` container, the divider will need to be `stretch` aligned in order to be visible. -export const InFlexContainer: ComponentStory< typeof Divider > = ( args ) => { +export const InFlexContainer: StoryFn< typeof Divider > = ( args ) => { return ( diff --git a/packages/components/src/draggable/stories/index.story.tsx b/packages/components/src/draggable/stories/index.story.tsx index ad94802feb93a..fc48618b1083a 100644 --- a/packages/components/src/draggable/stories/index.story.tsx +++ b/packages/components/src/draggable/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; import type { DragEvent } from 'react'; /** @@ -16,7 +16,7 @@ import { Icon, more } from '@wordpress/icons'; */ import Draggable from '..'; -const meta: ComponentMeta< typeof Draggable > = { +const meta: Meta< typeof Draggable > = { component: Draggable, title: 'Components/Draggable', argTypes: { @@ -31,7 +31,7 @@ const meta: ComponentMeta< typeof Draggable > = { }; export default meta; -const DefaultTemplate: ComponentStory< typeof Draggable > = ( args ) => { +const DefaultTemplate: StoryFn< typeof Draggable > = ( args ) => { const [ isDragging, setDragging ] = useState( false ); const instanceId = useInstanceId( DefaultTemplate ); @@ -100,9 +100,7 @@ const DefaultTemplate: ComponentStory< typeof Draggable > = ( args ) => { ); }; -export const Default: ComponentStory< typeof Draggable > = DefaultTemplate.bind( - {} -); +export const Default: StoryFn< typeof Draggable > = DefaultTemplate.bind( {} ); Default.args = {}; /** @@ -112,7 +110,7 @@ Default.args = {}; * For example, when the element's parent sets a `z-index` value that would cause the dragged * element to be rendered behind other elements. */ -export const AppendElementToOwnerDocument: ComponentStory< typeof Draggable > = +export const AppendElementToOwnerDocument: StoryFn< typeof Draggable > = DefaultTemplate.bind( {} ); AppendElementToOwnerDocument.args = { appendToOwnerDocument: true, diff --git a/packages/components/src/drop-zone/stories/index.story.tsx b/packages/components/src/drop-zone/stories/index.story.tsx index 6470b77e41c9f..1ee9af5b851eb 100644 --- a/packages/components/src/drop-zone/stories/index.story.tsx +++ b/packages/components/src/drop-zone/stories/index.story.tsx @@ -1,13 +1,13 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import DropZone from '..'; -const meta: ComponentMeta< typeof DropZone > = { +const meta: Meta< typeof DropZone > = { component: DropZone, title: 'Components/DropZone', parameters: { @@ -18,7 +18,7 @@ const meta: ComponentMeta< typeof DropZone > = { }; export default meta; -const Template: ComponentStory< typeof DropZone > = ( props ) => { +const Template: StoryFn< typeof DropZone > = ( props ) => { return (
Drop something here diff --git a/packages/components/src/dropdown-menu-v2/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2/stories/index.story.tsx index 4182285d60054..78aee12bf1f93 100644 --- a/packages/components/src/dropdown-menu-v2/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { Meta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; import styled from '@emotion/styled'; /** @@ -21,8 +21,6 @@ import { DropdownSubMenuTrigger, } from '..'; import Button from '../../button'; -import Popover from '../../popover'; -import { Provider as SlotFillProvider } from '../../slot-fill'; /** * WordPress dependencies @@ -39,6 +37,26 @@ import { ContextSystemProvider } from '../../ui/context'; const meta: Meta< typeof DropdownMenu > = { title: 'Components (Experimental)/DropdownMenu v2', component: DropdownMenu, + subcomponents: { + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownSubMenu, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownSubMenuTrigger, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuSeparator, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuCheckboxItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuGroup, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuLabel, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuRadioGroup, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + DropdownMenuRadioItem, + }, argTypes: { children: { control: { type: null } }, trigger: { control: { type: null } }, @@ -122,12 +140,8 @@ const RadioItemsGroup = () => { ); }; -const Template: Story< typeof DropdownMenu > = ( props ) => ( - - - { /* @ts-expect-error Slot is not currently typed on Popover */ } - - +const Template: StoryFn< typeof DropdownMenu > = ( props ) => ( + ); export const Default = Template.bind( {} ); Default.args = { @@ -197,7 +211,7 @@ const toolbarVariantContextValue = { variant: 'toolbar', }, }; -export const ToolbarVariant: Story< typeof DropdownMenu > = ( props ) => ( +export const ToolbarVariant: StoryFn< typeof DropdownMenu > = ( props ) => ( diff --git a/packages/components/src/dropdown-menu-v2/styles.ts b/packages/components/src/dropdown-menu-v2/styles.ts index eb1aec2d8a2d7..f1ba603797a7f 100644 --- a/packages/components/src/dropdown-menu-v2/styles.ts +++ b/packages/components/src/dropdown-menu-v2/styles.ts @@ -274,5 +274,5 @@ export const SubmenuRtlChevronIcon = styled( Icon )` { transform: `scaleX(-1) translateX(${ space( 2 ) })`, } - )() } + ) } `; diff --git a/packages/components/src/dropdown-menu/stories/index.story.tsx b/packages/components/src/dropdown-menu/stories/index.story.tsx index 5b9bb9bcfa0c8..0490636cfa206 100644 --- a/packages/components/src/dropdown-menu/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ @@ -21,7 +21,7 @@ import { trash, } from '@wordpress/icons'; -const meta: ComponentMeta< typeof DropdownMenu > = { +const meta: Meta< typeof DropdownMenu > = { title: 'Components/DropdownMenu', component: DropdownMenu, parameters: { @@ -38,7 +38,7 @@ const meta: ComponentMeta< typeof DropdownMenu > = { }; export default meta; -const Template: Story< typeof DropdownMenu > = ( props ) => ( +const Template: StoryFn< typeof DropdownMenu > = ( props ) => (
diff --git a/packages/components/src/dropdown/stories/index.story.tsx b/packages/components/src/dropdown/stories/index.story.tsx index 3ef9465d606bc..0b29da916b8d8 100644 --- a/packages/components/src/dropdown/stories/index.story.tsx +++ b/packages/components/src/dropdown/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -10,9 +10,11 @@ import Dropdown from '..'; import Button from '../../button'; import { DropdownContentWrapper } from '../dropdown-content-wrapper'; -const meta: ComponentMeta< typeof Dropdown > = { +const meta: Meta< typeof Dropdown > = { title: 'Components/Dropdown', component: Dropdown, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { DropdownContentWrapper }, argTypes: { focusOnMount: { options: [ 'firstElement', true, false ], @@ -32,7 +34,7 @@ const meta: ComponentMeta< typeof Dropdown > = { }; export default meta; -const Template: Story< typeof Dropdown > = ( args ) => { +const Template: StoryFn< typeof Dropdown > = ( args ) => { return (
@@ -40,7 +42,7 @@ const Template: Story< typeof Dropdown > = ( args ) => { ); }; -export const Default: Story< typeof Dropdown > = Template.bind( {} ); +export const Default = Template.bind( {} ); Default.args = { renderToggle: ( { isOpen, onToggle } ) => ( diff --git a/packages/components/src/navigable-container/stories/tabbable-container.story.tsx b/packages/components/src/navigable-container/stories/tabbable-container.story.tsx index 95b97340bdf8d..3dd090f0b0585 100644 --- a/packages/components/src/navigable-container/stories/tabbable-container.story.tsx +++ b/packages/components/src/navigable-container/stories/tabbable-container.story.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import { TabbableContainer } from '..'; -const meta: ComponentMeta< typeof TabbableContainer > = { +const meta: Meta< typeof TabbableContainer > = { title: 'Components/TabbableContainer', component: TabbableContainer, argTypes: { @@ -24,7 +24,7 @@ const meta: ComponentMeta< typeof TabbableContainer > = { }; export default meta; -export const Default: ComponentStory< typeof TabbableContainer > = ( args ) => { +export const Default: StoryFn< typeof TabbableContainer > = ( args ) => { return ( <> diff --git a/packages/components/src/navigation/index.tsx b/packages/components/src/navigation/index.tsx index e3d309783e1ee..dfc1b26cb33ad 100644 --- a/packages/components/src/navigation/index.tsx +++ b/packages/components/src/navigation/index.tsx @@ -28,7 +28,6 @@ const noop = () => {}; /** * Render a navigation list with optional groupings and hierarchy. * - * @example * ```jsx * import { * __experimentalNavigation as Navigation, diff --git a/packages/components/src/navigation/stories/index.story.tsx b/packages/components/src/navigation/stories/index.story.tsx index d335ec1ef6b8a..e0a3f1e139757 100644 --- a/packages/components/src/navigation/stories/index.story.tsx +++ b/packages/components/src/navigation/stories/index.story.tsx @@ -1,12 +1,16 @@ /** * External dependencies */ -import type { ComponentMeta } from '@storybook/react'; +import type { Meta } from '@storybook/react'; /** * Internal dependencies */ import { Navigation } from '..'; +import { NavigationBackButton } from '../back-button'; +import { NavigationGroup } from '../group'; +import { NavigationItem } from '../item'; +import { NavigationMenu } from '../menu'; import { DefaultStory } from './utils/default'; import { GroupStory } from './utils/group'; import { ControlledStateStory } from './utils/controlled-state'; @@ -15,9 +19,19 @@ import { MoreExamplesStory } from './utils/more-examples'; import { HideIfEmptyStory } from './utils/hide-if-empty'; import './style.css'; -const meta: ComponentMeta< typeof Navigation > = { +const meta: Meta< typeof Navigation > = { title: 'Components (Experimental)/Navigation', component: Navigation, + subcomponents: { + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + NavigationBackButton, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + NavigationGroup, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + NavigationItem, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + NavigationMenu, + }, argTypes: { activeItem: { control: { type: null } }, activeMenu: { control: { type: null } }, diff --git a/packages/components/src/navigation/stories/utils/controlled-state.tsx b/packages/components/src/navigation/stories/utils/controlled-state.tsx index fa9687c981f2e..b5d842b67e78c 100644 --- a/packages/components/src/navigation/stories/utils/controlled-state.tsx +++ b/packages/components/src/navigation/stories/utils/controlled-state.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory } from '@storybook/react'; +import type { StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -16,7 +16,7 @@ import { Navigation } from '../..'; import { NavigationItem } from '../../item'; import { NavigationMenu } from '../../menu'; -export const ControlledStateStory: ComponentStory< typeof Navigation > = ( { +export const ControlledStateStory: StoryFn< typeof Navigation > = ( { className, ...props } ) => { diff --git a/packages/components/src/navigation/stories/utils/default.tsx b/packages/components/src/navigation/stories/utils/default.tsx index 78188802861d3..d2948494adfb6 100644 --- a/packages/components/src/navigation/stories/utils/default.tsx +++ b/packages/components/src/navigation/stories/utils/default.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory } from '@storybook/react'; +import type { StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -15,7 +15,7 @@ import { Navigation } from '../..'; import { NavigationItem } from '../../item'; import { NavigationMenu } from '../../menu'; -export const DefaultStory: ComponentStory< typeof Navigation > = ( { +export const DefaultStory: StoryFn< typeof Navigation > = ( { className, ...props } ) => { diff --git a/packages/components/src/navigation/stories/utils/group.tsx b/packages/components/src/navigation/stories/utils/group.tsx index 007a71e3f40d1..bdf4f7011a171 100644 --- a/packages/components/src/navigation/stories/utils/group.tsx +++ b/packages/components/src/navigation/stories/utils/group.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory } from '@storybook/react'; +import type { StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -16,7 +16,7 @@ import { NavigationItem } from '../../item'; import { NavigationMenu } from '../../menu'; import { NavigationGroup } from '../../group'; -export const GroupStory: ComponentStory< typeof Navigation > = ( { +export const GroupStory: StoryFn< typeof Navigation > = ( { className, ...props } ) => { diff --git a/packages/components/src/navigation/stories/utils/hide-if-empty.tsx b/packages/components/src/navigation/stories/utils/hide-if-empty.tsx index 2595aba6b26a7..9b1414db861c0 100644 --- a/packages/components/src/navigation/stories/utils/hide-if-empty.tsx +++ b/packages/components/src/navigation/stories/utils/hide-if-empty.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory } from '@storybook/react'; +import type { StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -10,7 +10,7 @@ import { Navigation } from '../..'; import { NavigationItem } from '../../item'; import { NavigationMenu } from '../../menu'; -export const HideIfEmptyStory: ComponentStory< typeof Navigation > = ( { +export const HideIfEmptyStory: StoryFn< typeof Navigation > = ( { className, ...props } ) => { diff --git a/packages/components/src/navigation/stories/utils/more-examples.tsx b/packages/components/src/navigation/stories/utils/more-examples.tsx index c2a181dce58f6..e0ce566a5ab66 100644 --- a/packages/components/src/navigation/stories/utils/more-examples.tsx +++ b/packages/components/src/navigation/stories/utils/more-examples.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory } from '@storybook/react'; +import type { StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -17,7 +17,7 @@ import { NavigationGroup } from '../../group'; import { NavigationItem } from '../../item'; import { NavigationMenu } from '../../menu'; -export const MoreExamplesStory: ComponentStory< typeof Navigation > = ( { +export const MoreExamplesStory: StoryFn< typeof Navigation > = ( { className, ...props } ) => { diff --git a/packages/components/src/navigation/stories/utils/search.tsx b/packages/components/src/navigation/stories/utils/search.tsx index 74f1457adf0b7..44fb352bbdbf1 100644 --- a/packages/components/src/navigation/stories/utils/search.tsx +++ b/packages/components/src/navigation/stories/utils/search.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory } from '@storybook/react'; +import type { StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -29,7 +29,7 @@ const searchItems = [ { item: 'waldo', title: 'Waldo' }, ]; -export const SearchStory: ComponentStory< typeof Navigation > = ( { +export const SearchStory: StoryFn< typeof Navigation > = ( { className, ...props } ) => { diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index c86067f7f72ce..2d27605eab15a 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -268,7 +268,6 @@ function UnconnectedNavigatorProvider( * view (via the `NavigatorButton` and `NavigatorBackButton` components or the * `useNavigator` hook). * - * @example * ```jsx * import { * __experimentalNavigatorProvider as NavigatorProvider, diff --git a/packages/components/src/navigator/stories/index.story.tsx b/packages/components/src/navigator/stories/index.story.tsx index 1cd6d8be9eabb..5adeadcf7ac1d 100644 --- a/packages/components/src/navigator/stories/index.story.tsx +++ b/packages/components/src/navigator/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -19,8 +19,10 @@ import { useNavigator, } from '..'; -const meta: ComponentMeta< typeof NavigatorProvider > = { +const meta: Meta< typeof NavigatorProvider > = { component: NavigatorProvider, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { NavigatorScreen, NavigatorButton, NavigatorBackButton }, title: 'Components (Experimental)/Navigator', argTypes: { as: { control: { type: null } }, @@ -34,7 +36,7 @@ const meta: ComponentMeta< typeof NavigatorProvider > = { }; export default meta; -const Template: ComponentStory< typeof NavigatorProvider > = ( { +const Template: StoryFn< typeof NavigatorProvider > = ( { style, ...props } ) => ( @@ -178,8 +180,7 @@ const Template: ComponentStory< typeof NavigatorProvider > = ( { ); -export const Default: ComponentStory< typeof NavigatorProvider > = - Template.bind( {} ); +export const Default: StoryFn< typeof NavigatorProvider > = Template.bind( {} ); Default.args = { initialPath: '/', }; @@ -233,7 +234,7 @@ function ProductDetails() { ); } -const NestedNavigatorTemplate: ComponentStory< typeof NavigatorProvider > = ( { +const NestedNavigatorTemplate: StoryFn< typeof NavigatorProvider > = ( { style, ...props } ) => ( @@ -292,7 +293,7 @@ const NestedNavigatorTemplate: ComponentStory< typeof NavigatorProvider > = ( { ); -export const NestedNavigator: ComponentStory< typeof NavigatorProvider > = +export const NestedNavigator: StoryFn< typeof NavigatorProvider > = NestedNavigatorTemplate.bind( {} ); NestedNavigator.args = { initialPath: '/child2/grandchild', @@ -316,9 +317,7 @@ const NavigatorButtonWithSkipFocus = ( { ); }; -export const SkipFocus: ComponentStory< typeof NavigatorProvider > = ( - args -) => { +export const SkipFocus: StoryFn< typeof NavigatorProvider > = ( args ) => { return ; }; SkipFocus.args = { diff --git a/packages/components/src/notice/stories/index.story.tsx b/packages/components/src/notice/stories/index.story.tsx index 957d3cca3ef93..16a68ab293f55 100644 --- a/packages/components/src/notice/stories/index.story.tsx +++ b/packages/components/src/notice/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -16,9 +16,11 @@ import Button from '../../button'; import NoticeList from '../list'; import type { NoticeListProps } from '../types'; -const meta: ComponentMeta< typeof Notice > = { +const meta: Meta< typeof Notice > = { title: 'Components/Notice', component: Notice, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { NoticeList }, parameters: { actions: { argTypesRegex: '^on.*' }, controls: { expanded: true }, @@ -27,7 +29,7 @@ const meta: ComponentMeta< typeof Notice > = { }; export default meta; -const Template: ComponentStory< typeof Notice > = ( props ) => { +const Template: StoryFn< typeof Notice > = ( props ) => { return ; }; @@ -81,9 +83,7 @@ WithActions.args = { ], }; -export const NoticeListSubcomponent: ComponentStory< - typeof NoticeList -> = () => { +export const NoticeListSubcomponent: StoryFn< typeof NoticeList > = () => { const exampleNotices = [ { id: 'second-notice', diff --git a/packages/components/src/number-control/stories/index.story.tsx b/packages/components/src/number-control/stories/index.story.tsx index fedbbb41e0dd4..3588063f0f4bb 100644 --- a/packages/components/src/number-control/stories/index.story.tsx +++ b/packages/components/src/number-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import NumberControl from '..'; -const meta: ComponentMeta< typeof NumberControl > = { +const meta: Meta< typeof NumberControl > = { title: 'Components (Experimental)/NumberControl', component: NumberControl, argTypes: { @@ -32,7 +32,7 @@ const meta: ComponentMeta< typeof NumberControl > = { export default meta; -const Template: ComponentStory< typeof NumberControl > = ( { +const Template: StoryFn< typeof NumberControl > = ( { onChange, ...props } ) => { diff --git a/packages/components/src/palette-edit/stories/index.story.tsx b/packages/components/src/palette-edit/stories/index.story.tsx index 45500cf41e072..dd2ab92978c92 100644 --- a/packages/components/src/palette-edit/stories/index.story.tsx +++ b/packages/components/src/palette-edit/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { useState } from '@wordpress/element'; import PaletteEdit from '..'; import type { Color, Gradient } from '../types'; -const meta: ComponentMeta< typeof PaletteEdit > = { +const meta: Meta< typeof PaletteEdit > = { title: 'Components/PaletteEdit', component: PaletteEdit, parameters: { @@ -25,7 +25,7 @@ const meta: ComponentMeta< typeof PaletteEdit > = { }; export default meta; -const Template: Story< typeof PaletteEdit > = ( args ) => { +const Template: StoryFn< typeof PaletteEdit > = ( args ) => { const { colors, gradients, onChange, ...props } = args; const [ value, setValue ] = useState( gradients || colors ); diff --git a/packages/components/src/palette-edit/styles.js b/packages/components/src/palette-edit/styles.js index e04aa1840028f..0c72493f46564 100644 --- a/packages/components/src/palette-edit/styles.js +++ b/packages/components/src/palette-edit/styles.js @@ -41,8 +41,8 @@ export const NameInputControl = styled( InputControl )` `; export const PaletteItem = styled( View )` - padding: 3px 0 3px ${ space( 3 ) }; - height: calc( 40px - ${ CONFIG.borderWidth } ); + padding-block: 3px; + padding-inline-start: ${ space( 3 ) }; border: 1px solid ${ CONFIG.surfaceBorderColor }; border-bottom-color: transparent; &:first-of-type { diff --git a/packages/components/src/panel/stories/index.story.tsx b/packages/components/src/panel/stories/index.story.tsx index af3bdbee46b88..7f69f766603eb 100644 --- a/packages/components/src/panel/stories/index.story.tsx +++ b/packages/components/src/panel/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -16,9 +16,11 @@ import InputControl from '../../input-control'; */ import { wordpress } from '@wordpress/icons'; -const meta: ComponentMeta< typeof Panel > = { +const meta: Meta< typeof Panel > = { title: 'Components/Panel', component: Panel, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { PanelRow, PanelBody }, argTypes: { children: { control: { type: null } }, }, @@ -29,11 +31,9 @@ const meta: ComponentMeta< typeof Panel > = { }; export default meta; -const Template: ComponentStory< typeof Panel > = ( props ) => ( - -); +const Template: StoryFn< typeof Panel > = ( props ) => ; -export const Default: ComponentStory< typeof Panel > = Template.bind( {} ); +export const Default: StoryFn< typeof Panel > = Template.bind( {} ); Default.args = { header: 'My panel', children: ( @@ -68,7 +68,7 @@ Default.args = { * `PanelRow` is a generic container for rows within a `PanelBody`. * It is a flex container with a top margin for spacing. */ -export const _PanelRow: ComponentStory< typeof Panel > = Template.bind( {} ); +export const _PanelRow: StoryFn< typeof Panel > = Template.bind( {} ); _PanelRow.args = { children: ( @@ -85,9 +85,7 @@ _PanelRow.args = { ), }; -export const DisabledSection: ComponentStory< typeof Panel > = Template.bind( - {} -); +export const DisabledSection: StoryFn< typeof Panel > = Template.bind( {} ); DisabledSection.args = { ...Default.args, children: ( @@ -99,7 +97,7 @@ DisabledSection.args = { ), }; -export const WithIcon: ComponentStory< typeof Panel > = Template.bind( {} ); +export const WithIcon: StoryFn< typeof Panel > = Template.bind( {} ); WithIcon.args = { ...Default.args, children: ( diff --git a/packages/components/src/placeholder/stories/index.story.tsx b/packages/components/src/placeholder/stories/index.story.tsx index 7c414e4a1cca3..541eeceedc27d 100644 --- a/packages/components/src/placeholder/stories/index.story.tsx +++ b/packages/components/src/placeholder/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -17,7 +17,7 @@ import TextControl from '../../text-control'; const ICONS = { starEmpty, starFilled, styles, wordpress }; -const meta: ComponentMeta< typeof Placeholder > = { +const meta: Meta< typeof Placeholder > = { component: Placeholder, title: 'Components/Placeholder', argTypes: { @@ -37,7 +37,7 @@ const meta: ComponentMeta< typeof Placeholder > = { }; export default meta; -const Template: ComponentStory< typeof Placeholder > = ( args ) => { +const Template: StoryFn< typeof Placeholder > = ( args ) => { const [ value, setValue ] = useState( '' ); return ( @@ -55,9 +55,7 @@ const Template: ComponentStory< typeof Placeholder > = ( args ) => { ); }; -export const Default: ComponentStory< typeof Placeholder > = Template.bind( - {} -); +export const Default: StoryFn< typeof Placeholder > = Template.bind( {} ); Default.args = { icon: 'wordpress', label: 'My Placeholder Label', diff --git a/packages/components/src/popover/README.md b/packages/components/src/popover/README.md index ad04de226e90d..709254672be8b 100644 --- a/packages/components/src/popover/README.md +++ b/packages/components/src/popover/README.md @@ -6,7 +6,7 @@ The behavior of the popover when it exceeds the viewport's edges can be controll ## Usage -Render a Popover within the parent to which it should anchor. +Render a Popover adjacent to its container. If a Popover is returned by your component, it will be shown. To hide the popover, simply omit it from your component's render value. @@ -60,7 +60,7 @@ const MyPopover = () => { }; ``` -If you want Popover elements to render to a specific location on the page to allow style cascade to take effect, you must render a `Popover.Slot` further up the element tree: +By default Popovers render at the end of the body of your document. If you want Popover elements to render to a specific location on the page, you must render a `Popover.Slot` further up the element tree: ```jsx import { render } from '@wordpress/element'; diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 593d51f27e828..403f10de57a42 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -3,11 +3,11 @@ */ import type { ForwardedRef, SyntheticEvent, RefCallback } from 'react'; import classnames from 'classnames'; -import type { Middleware, MiddlewareArguments } from '@floating-ui/react-dom'; import { useFloating, flip as flipMiddleware, shift as shiftMiddleware, + limitShift, autoUpdate, arrow, offset as offsetMiddleware, @@ -30,6 +30,7 @@ import { useMemo, useState, useCallback, + createPortal, } from '@wordpress/element'; import { useViewportMatch, @@ -49,8 +50,6 @@ import ScrollLock from '../scroll-lock'; import { Slot, Fill, useSlot } from '../slot-fill'; import { computePopoverPosition, - getFrameOffset, - getFrameScale, positionToPlacement, placementToMotionAnimationProps, getReferenceOwnerDocument, @@ -59,11 +58,9 @@ import { import type { WordPressComponentProps } from '../ui/context'; import type { PopoverProps, - AnimatedWrapperProps, PopoverAnchorRefReference, PopoverAnchorRefTopBottom, } from './types'; -import { limitShift as customLimitShift } from './limit-shift'; import { overlayMiddlewares } from './overlay-middlewares'; /** @@ -96,48 +93,21 @@ const ArrowTriangle = () => ( ); -const AnimatedWrapper = forwardRef( - ( - { - style: receivedInlineStyles, - placement, - shouldAnimate = false, - ...props - }: HTMLMotionProps< 'div' > & AnimatedWrapperProps, - forwardedRef: ForwardedRef< any > - ) => { - const shouldReduceMotion = useReducedMotion(); - - const { style: motionInlineStyles, ...otherMotionProps } = useMemo( - () => placementToMotionAnimationProps( placement ), - [ placement ] - ); - - const computedAnimationProps: HTMLMotionProps< 'div' > = - shouldAnimate && ! shouldReduceMotion - ? { - style: { - ...motionInlineStyles, - ...receivedInlineStyles, - }, - ...otherMotionProps, - } - : { - animate: false, - style: receivedInlineStyles, - }; - - return ( - - ); +const slotNameContext = createContext< string | undefined >( undefined ); + +const fallbackContainerClassname = 'components-popover__fallback-container'; +const getPopoverFallbackContainer = () => { + let container = document.body.querySelector( + '.' + fallbackContainerClassname + ); + if ( ! container ) { + container = document.createElement( 'div' ); + container.className = fallbackContainerClassname; + document.body.append( container ); } -); -const slotNameContext = createContext< string | undefined >( undefined ); + return container; +}; const UnforwardedPopover = ( props: Omit< @@ -167,6 +137,7 @@ const UnforwardedPopover = ( flip = true, resize = true, shift = false, + inline = false, variant, // Deprecated props @@ -246,69 +217,34 @@ const UnforwardedPopover = ( ? positionToPlacement( position ) : placementProp; - /** - * Offsets the position of the popover when the anchor is inside an iframe. - * - * Store the offset in a ref, due to constraints with floating-ui: - * https://floating-ui.com/docs/react-dom#variables-inside-middleware-functions. - */ - const frameOffsetRef = useRef( getFrameOffset( referenceOwnerDocument ) ); - const middleware = [ ...( placementProp === 'overlay' ? overlayMiddlewares() : [] ), - // Custom middleware which adjusts the popover's position by taking into - // account the offset of the anchor's iframe (if any) compared to the page. - { - name: 'frameOffset', - fn( { x, y }: MiddlewareArguments ) { - if ( ! frameOffsetRef.current ) { - return { - x, - y, - }; - } - - return { - x: x + frameOffsetRef.current.x, - y: y + frameOffsetRef.current.y, - data: { - // This will be used in the customLimitShift() function. - amount: frameOffsetRef.current, - }, - }; - }, - }, offsetMiddleware( offsetProp ), - computedFlipProp ? flipMiddleware() : undefined, - computedResizeProp - ? size( { - apply( sizeProps ) { - const { firstElementChild } = - refs.floating.current ?? {}; - - // Only HTMLElement instances have the `style` property. - if ( ! ( firstElementChild instanceof HTMLElement ) ) - return; - - // Reduce the height of the popover to the available space. - Object.assign( firstElementChild.style, { - maxHeight: `${ sizeProps.availableHeight }px`, - overflow: 'auto', - } ); - }, - } ) - : undefined, - shift - ? shiftMiddleware( { - crossAxis: true, - limiter: customLimitShift(), - padding: 1, // Necessary to avoid flickering at the edge of the viewport. - } ) - : undefined, + computedFlipProp && flipMiddleware(), + computedResizeProp && + size( { + apply( sizeProps ) { + const { firstElementChild } = refs.floating.current ?? {}; + + // Only HTMLElement instances have the `style` property. + if ( ! ( firstElementChild instanceof HTMLElement ) ) + return; + + // Reduce the height of the popover to the available space. + Object.assign( firstElementChild.style, { + maxHeight: `${ sizeProps.availableHeight }px`, + overflow: 'auto', + } ); + }, + } ), + shift && + shiftMiddleware( { + crossAxis: true, + limiter: limitShift(), + padding: 1, // Necessary to avoid flickering at the edge of the viewport. + } ), arrow( { element: arrowRef } ), - ].filter( - ( m: Middleware | undefined ): m is Middleware => m !== undefined - ); + ]; const slotName = useContext( slotNameContext ) || __unstableSlotName; const slot = useSlot( slotName ); @@ -337,10 +273,6 @@ const UnforwardedPopover = ( // Positioning coordinates x, y, - // Callback refs (not regular refs). This allows the position to be updated. - // when either elements change. - reference: referenceCallbackRef, - floating, // Object with "regular" refs to both "reference" and "floating" refs, // Type of CSS position property to use (absolute or fixed) @@ -356,6 +288,7 @@ const UnforwardedPopover = ( middleware, whileElementsMounted: ( referenceParam, floatingParam, updateParam ) => autoUpdate( referenceParam, floatingParam, updateParam, { + layoutShift: false, animationFrame: true, } ), } ); @@ -390,17 +323,16 @@ const UnforwardedPopover = ( fallbackReferenceElement, fallbackDocument: document, } ); - const scale = getFrameScale( resultingReferenceOwnerDoc ); + const resultingReferenceElement = getReferenceElement( { anchor, anchorRef, anchorRect, getAnchorRect, fallbackReferenceElement, - scale, } ); - referenceCallbackRef( resultingReferenceElement ); + refs.setReference( resultingReferenceElement ); setReferenceOwnerDocument( resultingReferenceOwnerDoc ); }, [ @@ -413,23 +345,17 @@ const UnforwardedPopover = ( anchorRect, getAnchorRect, fallbackReferenceElement, - referenceCallbackRef, + refs, ] ); // If the reference element is in a different ownerDocument (e.g. iFrame), // we need to manually update the floating's position as the reference's owner - // document scrolls. Also update the frame offset if the view resizes. + // document scrolls. useLayoutEffect( () => { if ( - // Reference and root documents are the same. - referenceOwnerDocument === document || - // Reference and floating are in the same document. - referenceOwnerDocument === refs.floating.current?.ownerDocument || - // The reference's document has no view (i.e. window) - // or frame element (ie. it's not an iframe). - ! referenceOwnerDocument?.defaultView?.frameElement + ! referenceOwnerDocument || + ! referenceOwnerDocument.defaultView ) { - frameOffsetRef.current = undefined; return; } @@ -440,39 +366,70 @@ const UnforwardedPopover = ( ? getScrollContainer( frameElement ) : null; - const updateFrameOffset = () => { - frameOffsetRef.current = getFrameOffset( referenceOwnerDocument ); - update(); - }; - defaultView.addEventListener( 'resize', updateFrameOffset ); - scrollContainer?.addEventListener( 'scroll', updateFrameOffset ); - - updateFrameOffset(); + defaultView.addEventListener( 'resize', update ); + scrollContainer?.addEventListener( 'scroll', update ); return () => { - defaultView.removeEventListener( 'resize', updateFrameOffset ); - scrollContainer?.removeEventListener( 'scroll', updateFrameOffset ); + defaultView.removeEventListener( 'resize', update ); + scrollContainer?.removeEventListener( 'scroll', update ); }; - }, [ referenceOwnerDocument, update, refs.floating ] ); + }, [ referenceOwnerDocument, update ] ); const mergedFloatingRef = useMergeRefs( [ - floating, + refs.setFloating, dialogRef, forwardedRef, ] ); - // Disable reason: We care to capture the _bubbled_ events from inputs - // within popover as inferring close intent. + const style = isExpanded + ? undefined + : { + position: strategy, + top: 0, + left: 0, + // `x` and `y` are framer-motion specific props and are shorthands + // for `translateX` and `translateY`. Currently it is not possible + // to use `translateX` and `translateY` because those values would + // be overridden by the return value of the + // `placementToMotionAnimationProps` function. + x: computePopoverPosition( x ), + y: computePopoverPosition( y ), + }; + + const shouldReduceMotion = useReducedMotion(); + const shouldAnimate = animate && ! isExpanded && ! shouldReduceMotion; + + const [ animationFinished, setAnimationFinished ] = useState( false ); + + const { style: motionInlineStyles, ...otherMotionProps } = useMemo( + () => placementToMotionAnimationProps( computedPlacement ), + [ computedPlacement ] + ); + + const animationProps: HTMLMotionProps< 'div' > = shouldAnimate + ? { + style: { + ...motionInlineStyles, + ...style, + }, + onAnimationComplete: () => setAnimationFinished( true ), + ...otherMotionProps, + } + : { + animate: false, + style, + }; + + // When Floating UI has finished positioning and Framer Motion has finished animating + // the popover, add the `is-positioned` class to signal that all transitions have finished. + const isPositioned = + ( ! shouldAnimate || animationFinished ) && x !== null && y !== null; let content = ( - // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions - // eslint-disable-next-line jsx-a11y/no-static-element-interactions - { /* Prevents scroll on the document */ } { isExpanded && } @@ -527,36 +469,40 @@ const UnforwardedPopover = ( left: typeof arrowData?.x !== 'undefined' && Number.isFinite( arrowData.x ) - ? `${ - arrowData.x + - ( frameOffsetRef.current?.x ?? 0 ) - }px` + ? `${ arrowData.x }px` : '', top: typeof arrowData?.y !== 'undefined' && Number.isFinite( arrowData.y ) - ? `${ - arrowData.y + - ( frameOffsetRef.current?.y ?? 0 ) - }px` + ? `${ arrowData.y }px` : '', } } >
) } - + ); - if ( slot.ref ) { + const shouldRenderWithinSlot = slot.ref && ! inline; + const hasAnchor = anchorRef || anchorRect || anchor; + + if ( shouldRenderWithinSlot ) { content = { content }; + } else if ( ! inline ) { + content = createPortal( content, getPopoverFallbackContainer() ); } - if ( anchorRef || anchorRect || anchor ) { + if ( hasAnchor ) { return content; } - return { content }; + return ( + <> + + { content } + + ); }; /** diff --git a/packages/components/src/popover/limit-shift.ts b/packages/components/src/popover/limit-shift.ts deleted file mode 100644 index 45e65a0b61909..0000000000000 --- a/packages/components/src/popover/limit-shift.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * External dependencies - */ -import type { - Axis, - Coords, - Placement, - Side, - MiddlewareArguments, -} from '@floating-ui/react-dom'; - -/** - * Parts of this source were derived and modified from `floating-ui`, - * released under the MIT license. - * - * https://github.com/floating-ui/floating-ui - * - * Copyright (c) 2021 Floating UI contributors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/** - * Custom limiter function for the `shift` middleware. - * This function is mostly identical default `limitShift` from ``@floating-ui`; - * the only difference is that, when computing the min/max shift limits, it - * also takes into account the iframe offset that is added by the - * custom "frameOffset" middleware. - * - * All unexported types and functions are also from the `@floating-ui` library, - * and have been copied to this file for convenience. - */ - -type LimitShiftOffset = - | ( ( args: MiddlewareArguments ) => - | number - | { - /** - * Offset the limiting of the axis that runs along the alignment of the - * floating element. - */ - mainAxis?: number; - /** - * Offset the limiting of the axis that runs along the side of the - * floating element. - */ - crossAxis?: number; - } ) - | number - | { - /** - * Offset the limiting of the axis that runs along the alignment of the - * floating element. - */ - mainAxis?: number; - /** - * Offset the limiting of the axis that runs along the side of the - * floating element. - */ - crossAxis?: number; - }; - -type LimitShiftOptions = { - /** - * Offset when limiting starts. `0` will limit when the opposite edges of the - * reference and floating elements are aligned. - * - positive = start limiting earlier - * - negative = start limiting later - */ - offset: LimitShiftOffset; - /** - * Whether to limit the axis that runs along the alignment of the floating - * element. - */ - mainAxis: boolean; - /** - * Whether to limit the axis that runs along the side of the floating element. - */ - crossAxis: boolean; -}; - -function getSide( placement: Placement ): Side { - return placement.split( '-' )[ 0 ] as Side; -} - -function getMainAxisFromPlacement( placement: Placement ): Axis { - return [ 'top', 'bottom' ].includes( getSide( placement ) ) ? 'x' : 'y'; -} - -function getCrossAxis( axis: Axis ): Axis { - return axis === 'x' ? 'y' : 'x'; -} - -export const limitShift = ( - options: Partial< LimitShiftOptions > = {} -): { - options: Partial< LimitShiftOffset >; - fn: ( middlewareArguments: MiddlewareArguments ) => Coords; -} => ( { - options, - fn( middlewareArguments ) { - const { x, y, placement, rects, middlewareData } = middlewareArguments; - const { - offset = 0, - mainAxis: checkMainAxis = true, - crossAxis: checkCrossAxis = true, - } = options; - - const coords = { x, y }; - const mainAxis = getMainAxisFromPlacement( placement ); - const crossAxis = getCrossAxis( mainAxis ); - - let mainAxisCoord = coords[ mainAxis ]; - let crossAxisCoord = coords[ crossAxis ]; - - const rawOffset = - typeof offset === 'function' - ? offset( middlewareArguments ) - : offset; - const computedOffset = - typeof rawOffset === 'number' - ? { mainAxis: rawOffset, crossAxis: 0 } - : { mainAxis: 0, crossAxis: 0, ...rawOffset }; - - // At the moment of writing, this is the only difference - // with the `limitShift` function from `@floating-ui`. - // This offset needs to be added to all min/max limits - // in order to make the shift-limiting work as expected. - const additionalFrameOffset = { - x: 0, - y: 0, - ...middlewareData.frameOffset?.amount, - }; - - if ( checkMainAxis ) { - const len = mainAxis === 'y' ? 'height' : 'width'; - const limitMin = - rects.reference[ mainAxis ] - - rects.floating[ len ] + - computedOffset.mainAxis + - additionalFrameOffset[ mainAxis ]; - const limitMax = - rects.reference[ mainAxis ] + - rects.reference[ len ] - - computedOffset.mainAxis + - additionalFrameOffset[ mainAxis ]; - - if ( mainAxisCoord < limitMin ) { - mainAxisCoord = limitMin; - } else if ( mainAxisCoord > limitMax ) { - mainAxisCoord = limitMax; - } - } - - if ( checkCrossAxis ) { - const len = mainAxis === 'y' ? 'width' : 'height'; - const isOriginSide = [ 'top', 'left' ].includes( - getSide( placement ) - ); - const limitMin = - rects.reference[ crossAxis ] - - rects.floating[ len ] + - ( isOriginSide - ? middlewareData.offset?.[ crossAxis ] ?? 0 - : 0 ) + - ( isOriginSide ? 0 : computedOffset.crossAxis ) + - additionalFrameOffset[ crossAxis ]; - const limitMax = - rects.reference[ crossAxis ] + - rects.reference[ len ] + - ( isOriginSide - ? 0 - : middlewareData.offset?.[ crossAxis ] ?? 0 ) - - ( isOriginSide ? computedOffset.crossAxis : 0 ) + - additionalFrameOffset[ crossAxis ]; - - if ( crossAxisCoord < limitMin ) { - crossAxisCoord = limitMin; - } else if ( crossAxisCoord > limitMax ) { - crossAxisCoord = limitMax; - } - } - - return { - [ mainAxis ]: mainAxisCoord, - [ crossAxis ]: crossAxisCoord, - } as Coords; - }, -} ); diff --git a/packages/components/src/popover/overlay-middlewares.tsx b/packages/components/src/popover/overlay-middlewares.tsx index 83cc1cd0d21a9..fb64d739dce3b 100644 --- a/packages/components/src/popover/overlay-middlewares.tsx +++ b/packages/components/src/popover/overlay-middlewares.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { MiddlewareArguments } from '@floating-ui/react-dom'; +import type { MiddlewareState } from '@floating-ui/react-dom'; import { size } from '@floating-ui/react-dom'; export function overlayMiddlewares() { return [ { name: 'overlay', - fn( { rects }: MiddlewareArguments ) { + fn( { rects }: MiddlewareState ) { return rects.reference; }, }, diff --git a/packages/components/src/popover/stories/index.story.tsx b/packages/components/src/popover/stories/index.story.tsx index c5636941037c1..7f06045ac9b05 100644 --- a/packages/components/src/popover/stories/index.story.tsx +++ b/packages/components/src/popover/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory, ComponentMeta } from '@storybook/react'; +import type { StoryFn, Meta } from '@storybook/react'; /** * WordPress dependencies @@ -34,7 +34,7 @@ const AVAILABLE_PLACEMENTS: PopoverProps[ 'placement' ][] = [ 'overlay', ]; -const meta: ComponentMeta< typeof Popover > = { +const meta: Meta< typeof Popover > = { title: 'Components/Popover', component: Popover, argTypes: { @@ -81,7 +81,7 @@ const PopoverWithAnchor = ( args: PopoverProps ) => { ); }; -const Template: ComponentStory< typeof Popover > = ( args ) => { +const Template: StoryFn< typeof Popover > = ( args ) => { const [ isVisible, setIsVisible ] = useState( false ); const toggleVisible = () => { setIsVisible( ( state ) => ! state ); @@ -116,7 +116,7 @@ const Template: ComponentStory< typeof Popover > = ( args ) => { ); }; -export const Default: ComponentStory< typeof Popover > = Template.bind( {} ); +export const Default: StoryFn< typeof Popover > = Template.bind( {} ); Default.args = { children: (
@@ -128,7 +128,7 @@ Default.args = { ), }; -export const Unstyled: ComponentStory< typeof Popover > = Template.bind( {} ); +export const Unstyled: StoryFn< typeof Popover > = Template.bind( {} ); Unstyled.args = { children: (
@@ -141,7 +141,7 @@ Unstyled.args = { variant: 'unstyled', }; -export const AllPlacements: ComponentStory< typeof Popover > = ( { +export const AllPlacements: StoryFn< typeof Popover > = ( { children, ...args } ) => ( @@ -194,7 +194,7 @@ AllPlacements.args = { flip: false, }; -export const DynamicHeight: ComponentStory< typeof Popover > = ( { +export const DynamicHeight: StoryFn< typeof Popover > = ( { children, ...args } ) => { @@ -246,9 +246,7 @@ DynamicHeight.args = { children: 'Content with dynamic height', }; -export const WithSlotOutsideIframe: ComponentStory< typeof Popover > = ( - args -) => { +export const WithSlotOutsideIframe: StoryFn< typeof Popover > = ( args ) => { return ; }; WithSlotOutsideIframe.args = { diff --git a/packages/components/src/popover/test/index.tsx b/packages/components/src/popover/test/index.tsx index f98c4e27d3700..fa96c74cffbf3 100644 --- a/packages/components/src/popover/test/index.tsx +++ b/packages/components/src/popover/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, getByText } from '@testing-library/react'; import type { CSSProperties } from 'react'; /** @@ -112,6 +112,20 @@ describe( 'Popover', () => { expect( screen.getByRole( 'tooltip' ) ).toBeVisible() ); } ); + + it( 'should render inline regardless of slot name', async () => { + const { container } = render( + + Hello + + ); + + await waitFor( () => + // We want to explicitly check if it's within the container. + // eslint-disable-next-line testing-library/prefer-screen-queries + expect( getByText( container, 'Hello' ) ).toBeVisible() + ); + } ); } ); describe( 'anchor', () => { diff --git a/packages/components/src/popover/types.ts b/packages/components/src/popover/types.ts index 6dc3a4ae7d53f..c4250b22ba834 100644 --- a/packages/components/src/popover/types.ts +++ b/packages/components/src/popover/types.ts @@ -14,11 +14,6 @@ type DomRectWithOwnerDocument = DOMRect & { type PopoverPlacement = Placement | 'overlay'; -export type AnimatedWrapperProps = { - placement: PopoverPlacement; - shouldAnimate?: boolean; -}; - export type PopoverAnchorRefReference = MutableRefObject< Element | null | undefined >; @@ -150,6 +145,12 @@ export type PopoverProps = { * @default undefined */ variant?: 'unstyled' | 'toolbar'; + /** + * Whether to render the popover inline or within the slot. + * + * @default false + */ + inline?: boolean; // Deprecated props /** * Prevent the popover from flipping and resizing when meeting the viewport diff --git a/packages/components/src/popover/utils.ts b/packages/components/src/popover/utils.ts index 2112b28596982..e03ef03680330 100644 --- a/packages/components/src/popover/utils.ts +++ b/packages/components/src/popover/utils.ts @@ -3,7 +3,11 @@ */ // eslint-disable-next-line no-restricted-imports import type { MotionProps } from 'framer-motion'; -import type { ReferenceType } from '@floating-ui/react-dom'; +import type { + Placement, + ReferenceType, + VirtualElement, +} from '@floating-ui/react-dom'; /** * Internal dependencies @@ -16,7 +20,7 @@ import type { const POSITION_TO_PLACEMENT: Record< NonNullable< PopoverProps[ 'position' ] >, - NonNullable< PopoverProps[ 'placement' ] > + Placement > = { bottom: 'bottom', top: 'top', @@ -79,8 +83,7 @@ const POSITION_TO_PLACEMENT: Record< */ export const positionToPlacement = ( position: NonNullable< PopoverProps[ 'position' ] > -): NonNullable< PopoverProps[ 'placement' ] > => - POSITION_TO_PLACEMENT[ position ] ?? 'bottom'; +) => POSITION_TO_PLACEMENT[ position ] ?? 'bottom'; /** * @typedef AnimationOrigin @@ -139,42 +142,6 @@ export const placementToMotionAnimationProps = ( }; }; -/** - * Returns the offset of a document's frame element. - * - * @param document The iframe's owner document. - * - * @return The offset of the document's frame element, or undefined if the - * document has no frame element. - */ -export const getFrameOffset = ( - document?: Document -): { x: number; y: number } | undefined => { - const frameElement = document?.defaultView?.frameElement; - if ( ! frameElement ) { - return; - } - const iframeRect = frameElement.getBoundingClientRect(); - return { x: iframeRect.left, y: iframeRect.top }; -}; - -export const getFrameScale = ( - document?: Document -): { - x: number; - y: number; -} => { - const frameElement = document?.defaultView?.frameElement as HTMLElement; - if ( ! frameElement ) { - return { x: 1, y: 1 }; - } - const rect = frameElement.getBoundingClientRect(); - return { - x: rect.width / frameElement.offsetWidth, - y: rect.height / frameElement.offsetHeight, - }; -}; - export const getReferenceOwnerDocument = ( { anchor, anchorRef, @@ -197,7 +164,10 @@ export const getReferenceOwnerDocument = ( { // with the `getBoundingClientRect()` function (like real elements). // See https://floating-ui.com/docs/virtual-elements for more info. let resultingReferenceOwnerDoc; - if ( anchor ) { + if ( ( anchor as VirtualElement )?.contextElement ) { + resultingReferenceOwnerDoc = ( anchor as VirtualElement ).contextElement + ?.ownerDocument; + } else if ( anchor ) { resultingReferenceOwnerDoc = anchor.ownerDocument; } else if ( ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top ) { resultingReferenceOwnerDoc = ( anchorRef as PopoverAnchorRefTopBottom ) @@ -231,13 +201,11 @@ export const getReferenceElement = ( { anchorRect, getAnchorRect, fallbackReferenceElement, - scale, }: Pick< PopoverProps, 'anchorRef' | 'anchorRect' | 'getAnchorRect' | 'anchor' > & { fallbackReferenceElement: Element | null; - scale: { x: number; y: number }; } ): ReferenceType | null => { let referenceElement = null; @@ -299,22 +267,6 @@ export const getReferenceElement = ( { referenceElement = fallbackReferenceElement.parentElement; } - if ( referenceElement && ( scale.x !== 1 || scale.y !== 1 ) ) { - // If the popover is inside an iframe, the coordinates of the - // reference element need to be scaled to match the iframe's scale. - const rect = referenceElement.getBoundingClientRect(); - referenceElement = { - getBoundingClientRect() { - return new window.DOMRect( - rect.x * scale.x, - rect.y * scale.y, - rect.width * scale.x, - rect.height * scale.y - ); - }, - }; - } - // Convert any `undefined` value to `null`. return referenceElement ?? null; }; diff --git a/packages/components/src/progress-bar/stories/index.story.tsx b/packages/components/src/progress-bar/stories/index.story.tsx index a9cce7c2fe272..e3d217ffd3f41 100644 --- a/packages/components/src/progress-bar/stories/index.story.tsx +++ b/packages/components/src/progress-bar/stories/index.story.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import { ProgressBar } from '..'; -const meta: ComponentMeta< typeof ProgressBar > = { +const meta: Meta< typeof ProgressBar > = { component: ProgressBar, title: 'Components (Experimental)/ProgressBar', argTypes: { @@ -23,11 +23,9 @@ const meta: ComponentMeta< typeof ProgressBar > = { }; export default meta; -const Template: ComponentStory< typeof ProgressBar > = ( { ...args } ) => { +const Template: StoryFn< typeof ProgressBar > = ( { ...args } ) => { return ; }; -export const Default: ComponentStory< typeof ProgressBar > = Template.bind( - {} -); +export const Default: StoryFn< typeof ProgressBar > = Template.bind( {} ); Default.args = {}; diff --git a/packages/components/src/progress-bar/styles.ts b/packages/components/src/progress-bar/styles.ts index 293fb5e14c073..e983797d3d92b 100644 --- a/packages/components/src/progress-bar/styles.ts +++ b/packages/components/src/progress-bar/styles.ts @@ -54,7 +54,10 @@ export const Indicator = styled.div< { animationName: animateProgressBar, width: `${ INDETERMINATE_TRACK_WIDTH }%`, } ) - : css( { width: `${ value }%` } ) }; + : css( { + width: `${ value }%`, + transition: 'width 0.4s ease-in-out', + } ) }; `; export const ProgressElement = styled.progress` diff --git a/packages/components/src/query-controls/stories/index.story.tsx b/packages/components/src/query-controls/stories/index.story.tsx index 293da6c500b45..04fe185a59eac 100644 --- a/packages/components/src/query-controls/stories/index.story.tsx +++ b/packages/components/src/query-controls/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -18,7 +18,7 @@ import type { QueryControlsWithMultipleCategorySelectionProps, } from '../types'; -const meta: ComponentMeta< typeof QueryControls > = { +const meta: Meta< typeof QueryControls > = { title: 'Components/QueryControls', component: QueryControls, argTypes: { @@ -37,7 +37,7 @@ const meta: ComponentMeta< typeof QueryControls > = { }; export default meta; -export const Default: Story< typeof QueryControls > = ( args ) => { +export const Default: StoryFn< typeof QueryControls > = ( args ) => { const { onAuthorChange, onCategoryChange, @@ -146,7 +146,7 @@ Default.args = { selectedAuthorId: 1, }; -const SingleCategoryTemplate: Story< typeof QueryControls > = ( args ) => { +const SingleCategoryTemplate: StoryFn< typeof QueryControls > = ( args ) => { const { onAuthorChange, onCategoryChange, @@ -184,8 +184,7 @@ const SingleCategoryTemplate: Story< typeof QueryControls > = ( args ) => { /> ); }; -export const SelectSingleCategory: Story< typeof QueryControls > = - SingleCategoryTemplate.bind( {} ); +export const SelectSingleCategory = SingleCategoryTemplate.bind( {} ); SelectSingleCategory.args = { categoriesList: [ { diff --git a/packages/components/src/radio-control/stories/index.story.tsx b/packages/components/src/radio-control/stories/index.story.tsx index c6729804bb595..7be398e77e17c 100644 --- a/packages/components/src/radio-control/stories/index.story.tsx +++ b/packages/components/src/radio-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import RadioControl from '..'; -const meta: ComponentMeta< typeof RadioControl > = { +const meta: Meta< typeof RadioControl > = { component: RadioControl, title: 'Components/RadioControl', argTypes: { @@ -39,7 +39,7 @@ const meta: ComponentMeta< typeof RadioControl > = { }; export default meta; -const Template: ComponentStory< typeof RadioControl > = ( { +const Template: StoryFn< typeof RadioControl > = ( { onChange, options, ...args @@ -59,9 +59,7 @@ const Template: ComponentStory< typeof RadioControl > = ( { ); }; -export const Default: ComponentStory< typeof RadioControl > = Template.bind( - {} -); +export const Default: StoryFn< typeof RadioControl > = Template.bind( {} ); Default.args = { label: 'Post visibility', options: [ diff --git a/packages/components/src/radio-group/stories/index.story.js b/packages/components/src/radio-group/stories/index.story.js index 2099c88d866af..58125bf808be2 100644 --- a/packages/components/src/radio-group/stories/index.story.js +++ b/packages/components/src/radio-group/stories/index.story.js @@ -12,6 +12,7 @@ import RadioGroup from '../'; export default { title: 'Components (Deprecated)/RadioGroup', component: RadioGroup, + subcomponents: { Radio }, parameters: { docs: { description: { diff --git a/packages/components/src/range-control/stories/index.story.tsx b/packages/components/src/range-control/stories/index.story.tsx index 3c07d85b8088a..4e0c1f19e078b 100644 --- a/packages/components/src/range-control/stories/index.story.tsx +++ b/packages/components/src/range-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -16,7 +16,7 @@ import RangeControl from '..'; const ICONS = { starEmpty, starFilled, styles, wordpress }; -const meta: ComponentMeta< typeof RangeControl > = { +const meta: Meta< typeof RangeControl > = { component: RangeControl, title: 'Components/RangeControl', argTypes: { @@ -53,10 +53,7 @@ const meta: ComponentMeta< typeof RangeControl > = { }; export default meta; -const Template: ComponentStory< typeof RangeControl > = ( { - onChange, - ...args -} ) => { +const Template: StoryFn< typeof RangeControl > = ( { onChange, ...args } ) => { const [ value, setValue ] = useState< number >(); return ( @@ -71,9 +68,7 @@ const Template: ComponentStory< typeof RangeControl > = ( { ); }; -export const Default: ComponentStory< typeof RangeControl > = Template.bind( - {} -); +export const Default: StoryFn< typeof RangeControl > = Template.bind( {} ); Default.args = { help: 'Please select how transparent you would like this.', initialPosition: 50, @@ -87,7 +82,7 @@ Default.args = { * values. This also overrides both `withInputField` and `showTooltip` props to * `false`. */ -export const WithAnyStep: ComponentStory< typeof RangeControl > = ( { +export const WithAnyStep: StoryFn< typeof RangeControl > = ( { onChange, ...args } ) => { @@ -113,7 +108,7 @@ WithAnyStep.args = { step: 'any', }; -const MarkTemplate: ComponentStory< typeof RangeControl > = ( { +const MarkTemplate: StoryFn< typeof RangeControl > = ( { label, onChange, ...args @@ -168,7 +163,7 @@ const marksWithNegatives = [ * automatically generated or custom mark indicators can be provided by an * `Array`. */ -export const WithIntegerStepAndMarks: ComponentStory< typeof RangeControl > = +export const WithIntegerStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithIntegerStepAndMarks.args = { @@ -184,7 +179,7 @@ WithIntegerStepAndMarks.args = { * `step` ticks. Marks may be automatically generated or custom mark indicators * can be provided by an `Array`. */ -export const WithDecimalStepAndMarks: ComponentStory< typeof RangeControl > = +export const WithDecimalStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithDecimalStepAndMarks.args = { @@ -203,9 +198,8 @@ WithDecimalStepAndMarks.args = { * indicators can represent negative values as well. Marks may be automatically * generated or custom mark indicators can be provided by an `Array`. */ -export const WithNegativeMinimumAndMarks: ComponentStory< - typeof RangeControl -> = MarkTemplate.bind( {} ); +export const WithNegativeMinimumAndMarks: StoryFn< typeof RangeControl > = + MarkTemplate.bind( {} ); WithNegativeMinimumAndMarks.args = { marks: marksWithNegatives, @@ -219,7 +213,7 @@ WithNegativeMinimumAndMarks.args = { * indicators can represent negative values as well. Marks may be automatically * generated or custom mark indicators can be provided by an `Array`. */ -export const WithNegativeRangeAndMarks: ComponentStory< typeof RangeControl > = +export const WithNegativeRangeAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithNegativeRangeAndMarks.args = { @@ -234,7 +228,7 @@ WithNegativeRangeAndMarks.args = { * non-integer values. This may still be used in conjunction with `marks` * rendering a visual representation of `step` ticks. */ -export const WithAnyStepAndMarks: ComponentStory< typeof RangeControl > = +export const WithAnyStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithAnyStepAndMarks.args = { diff --git a/packages/components/src/resizable-box/stories/index.story.tsx b/packages/components/src/resizable-box/stories/index.story.tsx index 2e05877853b51..81852f9cb4ea4 100644 --- a/packages/components/src/resizable-box/stories/index.story.tsx +++ b/packages/components/src/resizable-box/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -13,7 +13,7 @@ import ResizableBox from '..'; */ import { useState } from '@wordpress/element'; -const meta: ComponentMeta< typeof ResizableBox > = { +const meta: Meta< typeof ResizableBox > = { title: 'Components/ResizableBox', component: ResizableBox, argTypes: { @@ -28,7 +28,7 @@ const meta: ComponentMeta< typeof ResizableBox > = { }; export default meta; -const Template: ComponentStory< typeof ResizableBox > = ( { +const Template: StoryFn< typeof ResizableBox > = ( { onResizeStop, ...props } ) => { diff --git a/packages/components/src/responsive-wrapper/stories/index.story.tsx b/packages/components/src/responsive-wrapper/stories/index.story.tsx index 409a266e81da2..cf676c3bcec80 100644 --- a/packages/components/src/responsive-wrapper/stories/index.story.tsx +++ b/packages/components/src/responsive-wrapper/stories/index.story.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import ResponsiveWrapper from '..'; -const meta: ComponentMeta< typeof ResponsiveWrapper > = { +const meta: Meta< typeof ResponsiveWrapper > = { component: ResponsiveWrapper, title: 'Components/ResponsiveWrapper', argTypes: { @@ -21,7 +21,7 @@ const meta: ComponentMeta< typeof ResponsiveWrapper > = { }; export default meta; -const Template: ComponentStory< typeof ResponsiveWrapper > = ( args ) => ( +const Template: StoryFn< typeof ResponsiveWrapper > = ( args ) => ( ); @@ -46,8 +46,7 @@ Default.args = { * ``. In this case, the SVG simply keeps scaling up to fill * its container, unless the `height` and `width` attributes are specified. */ -export const WithSVG: ComponentStory< typeof ResponsiveWrapper > = - Template.bind( {} ); +export const WithSVG: StoryFn< typeof ResponsiveWrapper > = Template.bind( {} ); WithSVG.args = { children: ( = { +const meta: Meta< typeof SandBox > = { component: SandBox, title: 'Components/SandBox', argTypes: { @@ -22,9 +22,7 @@ const meta: ComponentMeta< typeof SandBox > = { }; export default meta; -const Template: ComponentStory< typeof SandBox > = ( args ) => ( - -); +const Template: StoryFn< typeof SandBox > = ( args ) => ; export const Default = Template.bind( {} ); Default.args = { diff --git a/packages/components/src/scroll-lock/stories/index.story.tsx b/packages/components/src/scroll-lock/stories/index.story.tsx index 113a7d0c7f7bc..8518c0c520a48 100644 --- a/packages/components/src/scroll-lock/stories/index.story.tsx +++ b/packages/components/src/scroll-lock/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; import type { ReactNode } from 'react'; /** @@ -15,7 +15,7 @@ import { useState } from '@wordpress/element'; import Button from '../../button'; import ScrollLock from '..'; -const meta: ComponentMeta< typeof ScrollLock > = { +const meta: Meta< typeof ScrollLock > = { component: ScrollLock, title: 'Components/ScrollLock', parameters: { @@ -59,7 +59,7 @@ function ToggleContainer( props: { children: ReactNode } ) { ); } -export const Default: ComponentStory< typeof ScrollLock > = () => { +export const Default: StoryFn< typeof ScrollLock > = () => { const [ isScrollLocked, setScrollLocked ] = useState( false ); const toggleLock = () => setScrollLocked( ! isScrollLocked ); diff --git a/packages/components/src/scrollable/stories/index.story.tsx b/packages/components/src/scrollable/stories/index.story.tsx index 0048beb400501..53d4919de3aab 100644 --- a/packages/components/src/scrollable/stories/index.story.tsx +++ b/packages/components/src/scrollable/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { useRef } from '@wordpress/element'; import { View } from '../../view'; import { Scrollable } from '..'; -const meta: ComponentMeta< typeof Scrollable > = { +const meta: Meta< typeof Scrollable > = { component: Scrollable, title: 'Components (Experimental)/Scrollable', argTypes: { @@ -34,7 +34,7 @@ const meta: ComponentMeta< typeof Scrollable > = { }; export default meta; -const Template: ComponentStory< typeof Scrollable > = ( { ...args } ) => { +const Template: StoryFn< typeof Scrollable > = ( { ...args } ) => { const targetRef = useRef< HTMLInputElement >( null ); const onButtonClick = () => { @@ -76,7 +76,7 @@ const Template: ComponentStory< typeof Scrollable > = ( { ...args } ) => { ); }; -export const Default: ComponentStory< typeof Scrollable > = Template.bind( {} ); +export const Default: StoryFn< typeof Scrollable > = Template.bind( {} ); Default.args = { smoothScroll: false, scrollDirection: 'y', diff --git a/packages/components/src/search-control/index.native.js b/packages/components/src/search-control/index.native.js index 6f640f9960a3a..6cdabf966899f 100644 --- a/packages/components/src/search-control/index.native.js +++ b/packages/components/src/search-control/index.native.js @@ -14,7 +14,13 @@ import { /** * WordPress dependencies */ -import { useState, useRef, useMemo, useEffect } from '@wordpress/element'; +import { + useState, + useRef, + useMemo, + useEffect, + useCallback, +} from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Button, Gridicons } from '@wordpress/components'; import { @@ -120,23 +126,44 @@ function SearchControl( { // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isActive, isDark ] ); + const clearInput = useCallback( () => { + onChange( '' ); + }, [ onChange ] ); + + const onPress = useCallback( () => { + setIsActive( true ); + inputRef.current?.focus(); + }, [] ); + + const onFocus = useCallback( () => { + setIsActive( true ); + }, [] ); + + const onCancel = useCallback( () => { + clearTimeout( onCancelTimer.current ); + onCancelTimer.current = setTimeout( () => { + inputRef.current?.blur(); + clearInput(); + setIsActive( false ); + }, 0 ); + }, [ clearInput ] ); + + const onKeyboardDidHide = useCallback( () => { + if ( ! isIOS ) { + onCancel(); + } + }, [ isIOS, onCancel ] ); + useEffect( () => { const keyboardHideSubscription = Keyboard.addListener( 'keyboardDidHide', - () => { - if ( ! isIOS ) { - onCancel(); - } - } + onKeyboardDidHide ); return () => { clearTimeout( onCancelTimer.current ); keyboardHideSubscription.remove(); }; - // Disable reason: deferring this refactor to the native team. - // see https://github.com/WordPress/gutenberg/pull/41166 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [] ); + }, [ onKeyboardDidHide ] ); const { 'search-control__container': containerStyle, @@ -153,18 +180,6 @@ function SearchControl( { 'search-control__right-icon': rightIconStyle, } = currentStyles; - function clearInput() { - onChange( '' ); - } - - function onCancel() { - onCancelTimer.current = setTimeout( () => { - inputRef.current.blur(); - clearInput(); - setIsActive( false ); - }, 0 ); - } - function renderLeftButton() { const button = ! isIOS && isActive ? ( @@ -234,10 +249,7 @@ function SearchControl( { return ( { - setIsActive( true ); - inputRef.current.focus(); - } } + onPress={ onPress } activeOpacity={ 1 } > @@ -248,7 +260,7 @@ function SearchControl( { style={ formInputStyle } placeholderTextColor={ placeholderStyle?.color } onChangeText={ onChange } - onFocus={ () => setIsActive( true ) } + onFocus={ onFocus } value={ value } placeholder={ placeholder } /> diff --git a/packages/components/src/search-control/stories/index.story.tsx b/packages/components/src/search-control/stories/index.story.tsx index b6235b4bad63e..1a6e58724ccb2 100644 --- a/packages/components/src/search-control/stories/index.story.tsx +++ b/packages/components/src/search-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import SearchControl from '..'; -const meta: ComponentMeta< typeof SearchControl > = { +const meta: Meta< typeof SearchControl > = { title: 'Components/SearchControl', component: SearchControl, argTypes: { @@ -26,7 +26,7 @@ const meta: ComponentMeta< typeof SearchControl > = { }; export default meta; -const Template: ComponentStory< typeof SearchControl > = ( { +const Template: StoryFn< typeof SearchControl > = ( { onChange, ...props } ) => { diff --git a/packages/components/src/select-control/stories/index.story.tsx b/packages/components/src/select-control/stories/index.story.tsx index eea1561cf2c2b..42966ef4f05f4 100644 --- a/packages/components/src/select-control/stories/index.story.tsx +++ b/packages/components/src/select-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { Meta, Story } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -31,7 +31,7 @@ const meta: Meta< typeof SelectControl > = { }; export default meta; -const SelectControlWithState: Story< typeof SelectControl > = ( props ) => { +const SelectControlWithState: StoryFn< typeof SelectControl > = ( props ) => { const [ selection, setSelection ] = useState< string[] >(); if ( props.multiple ) { @@ -82,7 +82,7 @@ WithLabelAndHelpText.args = { * As an alternative to the `options` prop, `optgroup`s and `options` can be * passed in as `children` for more customizeability. */ -export const WithCustomChildren: Story< typeof SelectControl > = ( args ) => { +export const WithCustomChildren: StoryFn< typeof SelectControl > = ( args ) => { return ( diff --git a/packages/components/src/shortcut/index.tsx b/packages/components/src/shortcut/index.tsx index dbdaa955384f5..5ba7c7efdfff0 100644 --- a/packages/components/src/shortcut/index.tsx +++ b/packages/components/src/shortcut/index.tsx @@ -3,6 +3,19 @@ */ import type { ShortcutProps } from './types'; +/** + * Shortcut component is used to display keyboard shortcuts, and it can be customized with a custom display and aria label if needed. + * + * ```jsx + * import { Shortcut } from '@wordpress/components'; + * + * const MyShortcut = () => { + * return ( + * + * ); + * }; + * ``` + */ function Shortcut( props: ShortcutProps ) { const { shortcut, className } = props; diff --git a/packages/components/src/shortcut/stories/index.story.tsx b/packages/components/src/shortcut/stories/index.story.tsx new file mode 100644 index 0000000000000..27e96e3f6d198 --- /dev/null +++ b/packages/components/src/shortcut/stories/index.story.tsx @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + +/** + * Internal dependencies + */ +import Shortcut from '../'; + +const meta: Meta< typeof Shortcut > = { + component: Shortcut, + title: 'Components/Shortcut', + parameters: { + controls: { + expanded: true, + }, + docs: { canvas: { sourceState: 'shown' } }, + }, +}; +export default meta; + +const Template: StoryFn< typeof Shortcut > = ( props ) => { + return ; +}; + +export const Default: StoryFn< typeof Shortcut > = Template.bind( {} ); + +export const WithAriaLabel = Template.bind( {} ); +WithAriaLabel.args = { + ...Default.args, + shortcut: { display: 'Ctrl + L', ariaLabel: 'Load' }, +}; diff --git a/packages/components/src/slot-fill/README.md b/packages/components/src/slot-fill/README.md index ccd4675588c23..a04416bdee50d 100644 --- a/packages/components/src/slot-fill/README.md +++ b/packages/components/src/slot-fill/README.md @@ -70,7 +70,7 @@ Both `Slot` and `Fill` accept a `name` string prop, where a `Slot` with a given `Slot` with `bubblesVirtually` set to true also accept an optional `className` to add to the slot container. -`Slot` accepts an optional `children` function prop, which takes `fills` as a param. It allows you to perform additional processing and wrap `fills` conditionally. +`Slot` **without** `bubblesVirtually` accepts an optional `children` function prop, which takes `fills` as a param. It allows you to perform additional processing and wrap `fills` conditionally. _Example_: @@ -103,14 +103,14 @@ const ToolbarItem = () => ( ); -const Toolbar = () => - const hideToolbar() => { +const Toolbar = () => { + const hideToolbar = () => { console.log( 'Hide toolbar' ); - } + }; return (
); -); +}; ``` diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js index cd1576fd19c3a..e3c6652f22e94 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.js @@ -22,6 +22,9 @@ const SlotFillContext = createContext( { unregisterSlot: () => {}, registerFill: () => {}, unregisterFill: () => {}, + + // This helps the provider know if it's using the default context value or not. + isDefault: true, } ); export default SlotFillContext; diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot.js b/packages/components/src/slot-fill/bubbles-virtually/slot.js index ef7ad56cc68ba..be6fde0c8e6b7 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot.js +++ b/packages/components/src/slot-fill/bubbles-virtually/slot.js @@ -13,12 +13,20 @@ import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies */ +import { View } from '../../view'; import SlotFillContext from './slot-fill-context'; -function Slot( - { name, fillProps = {}, as: Component = 'div', ...props }, - forwardedRef -) { +function Slot( props, forwardedRef ) { + const { + name, + fillProps = {}, + as, + // `children` is not allowed. However, if it is passed, + // it will be displayed as is, so remove `children`. + children, + ...restProps + } = props; + const { registerSlot, unregisterSlot, ...registry } = useContext( SlotFillContext ); const ref = useRef(); @@ -41,7 +49,11 @@ function Slot( } ); return ( - + ); } diff --git a/packages/components/src/slot-fill/index.js b/packages/components/src/slot-fill/index.js index 8deaa180492a7..34216fd347c05 100644 --- a/packages/components/src/slot-fill/index.js +++ b/packages/components/src/slot-fill/index.js @@ -2,7 +2,7 @@ /** * WordPress dependencies */ -import { forwardRef } from '@wordpress/element'; +import { forwardRef, useContext } from '@wordpress/element'; /** * Internal dependencies @@ -13,6 +13,7 @@ import BubblesVirtuallyFill from './bubbles-virtually/fill'; import BubblesVirtuallySlot from './bubbles-virtually/slot'; import BubblesVirtuallySlotFillProvider from './bubbles-virtually/slot-fill-provider'; import SlotFillProvider from './provider'; +import SlotFillContext from './bubbles-virtually/slot-fill-context'; export { default as useSlot } from './bubbles-virtually/use-slot'; export { default as useSlotFills } from './bubbles-virtually/use-slot-fills'; @@ -35,6 +36,10 @@ export const Slot = forwardRef( ( { bubblesVirtually, ...props }, ref ) => { } ); export function Provider( { children, ...props } ) { + const parent = useContext( SlotFillContext ); + if ( ! parent.isDefault ) { + return children; + } return ( diff --git a/packages/components/src/slot-fill/stories/index.story.js b/packages/components/src/slot-fill/stories/index.story.js index 09de9dd67573c..c1ab26162497e 100644 --- a/packages/components/src/slot-fill/stories/index.story.js +++ b/packages/components/src/slot-fill/stories/index.story.js @@ -11,6 +11,8 @@ import { Slot, Fill, Provider as SlotFillProvider } from '../'; export default { title: 'Components/SlotFill', component: Slot, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { Fill, SlotFillProvider }, parameters: { controls: { expanded: true }, docs: { canvas: { sourceState: 'shown' } }, diff --git a/packages/components/src/slot-fill/test/__snapshots__/slot.js.snap b/packages/components/src/slot-fill/test/__snapshots__/slot.js.snap index d57954b0444f9..b9379eda7171a 100644 --- a/packages/components/src/slot-fill/test/__snapshots__/slot.js.snap +++ b/packages/components/src/slot-fill/test/__snapshots__/slot.js.snap @@ -42,12 +42,16 @@ exports[`Slot bubblesVirtually true should subsume another slot by the same name
-
+
-
+
Content
@@ -62,7 +66,9 @@ exports[`Slot bubblesVirtually true should subsume another slot by the same name
-
+
Content
@@ -187,7 +193,9 @@ exports[`Slot should render in expected order when fills unmounted 1`] = ` exports[`Slot should warn without a Provider 1`] = `
-
+
`; diff --git a/packages/components/src/snackbar/stories/index.story.tsx b/packages/components/src/snackbar/stories/index.story.tsx index 1ac577e222035..953b33d273b3f 100644 --- a/packages/components/src/snackbar/stories/index.story.tsx +++ b/packages/components/src/snackbar/stories/index.story.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import Snackbar from '..'; -const meta: ComponentMeta< typeof Snackbar > = { +const meta: Meta< typeof Snackbar > = { title: 'Components/Snackbar', component: Snackbar, argTypes: { @@ -34,23 +34,22 @@ const meta: ComponentMeta< typeof Snackbar > = { }; export default meta; -const DefaultTemplate: ComponentStory< typeof Snackbar > = ( { +const DefaultTemplate: StoryFn< typeof Snackbar > = ( { children, ...props } ) => { return { children }; }; -export const Default: ComponentStory< typeof Snackbar > = DefaultTemplate.bind( - {} -); +export const Default: StoryFn< typeof Snackbar > = DefaultTemplate.bind( {} ); Default.args = { children: 'Use Snackbars to communicate low priority, non-interruptive messages to the user.', }; -export const WithActions: ComponentStory< typeof Snackbar > = - DefaultTemplate.bind( {} ); +export const WithActions: StoryFn< typeof Snackbar > = DefaultTemplate.bind( + {} +); WithActions.args = { actions: [ { @@ -61,9 +60,7 @@ WithActions.args = { children: 'Use Snackbars with an action link to an external page.', }; -export const WithIcon: ComponentStory< typeof Snackbar > = DefaultTemplate.bind( - {} -); +export const WithIcon: StoryFn< typeof Snackbar > = DefaultTemplate.bind( {} ); WithIcon.args = { children: 'Add an icon to make your snackbar stand out', icon: ( @@ -73,7 +70,7 @@ WithIcon.args = { ), }; -export const WithExplicitDismiss: ComponentStory< typeof Snackbar > = +export const WithExplicitDismiss: StoryFn< typeof Snackbar > = DefaultTemplate.bind( {} ); WithExplicitDismiss.args = { children: @@ -81,7 +78,7 @@ WithExplicitDismiss.args = { explicitDismiss: true, }; -export const WithActionAndExplicitDismiss: ComponentStory< typeof Snackbar > = +export const WithActionAndExplicitDismiss: StoryFn< typeof Snackbar > = DefaultTemplate.bind( {} ); WithActionAndExplicitDismiss.args = { actions: [ diff --git a/packages/components/src/snackbar/stories/list.story.tsx b/packages/components/src/snackbar/stories/list.story.tsx index f30e01aa48ec3..5a759ddc661bf 100644 --- a/packages/components/src/snackbar/stories/list.story.tsx +++ b/packages/components/src/snackbar/stories/list.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import SnackbarList from '../list'; -const meta: ComponentMeta< typeof SnackbarList > = { +const meta: Meta< typeof SnackbarList > = { title: 'Components/SnackbarList', component: SnackbarList, argTypes: { @@ -32,7 +32,7 @@ const meta: ComponentMeta< typeof SnackbarList > = { }; export default meta; -export const Default: ComponentStory< typeof SnackbarList > = ( { +export const Default: StoryFn< typeof SnackbarList > = ( { children, notices: noticesProp, ...props diff --git a/packages/components/src/spacer/stories/index.story.tsx b/packages/components/src/spacer/stories/index.story.tsx index c73f2e1ee5e02..586658ac0f01f 100644 --- a/packages/components/src/spacer/stories/index.story.tsx +++ b/packages/components/src/spacer/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -29,7 +29,7 @@ const controls = [ {} ); -const meta: ComponentMeta< typeof Spacer > = { +const meta: Meta< typeof Spacer > = { component: Spacer, title: 'Components (Experimental)/Spacer', argTypes: { @@ -54,7 +54,7 @@ const BlackBox = () => ( /> ); -const Template: ComponentStory< typeof Spacer > = ( { onChange, ...args } ) => { +const Template: StoryFn< typeof Spacer > = ( { onChange, ...args } ) => { return ( <> @@ -64,7 +64,7 @@ const Template: ComponentStory< typeof Spacer > = ( { onChange, ...args } ) => { ); }; -export const Default: ComponentStory< typeof Spacer > = Template.bind( {} ); +export const Default: StoryFn< typeof Spacer > = Template.bind( {} ); Default.args = { children: 'This is the spacer', }; diff --git a/packages/components/src/spinner/index.tsx b/packages/components/src/spinner/index.tsx index 8fa34b9b230d3..9eee9dde18ef2 100644 --- a/packages/components/src/spinner/index.tsx +++ b/packages/components/src/spinner/index.tsx @@ -50,7 +50,6 @@ export function UnforwardedSpinner( /** * `Spinner` is a component used to notify users that their action is being processed. * - * @example * ```js * import { Spinner } from '@wordpress/components'; * diff --git a/packages/components/src/spinner/stories/index.story.tsx b/packages/components/src/spinner/stories/index.story.tsx index 63b38548bc582..dfd6fb26f25ae 100644 --- a/packages/components/src/spinner/stories/index.story.tsx +++ b/packages/components/src/spinner/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentStory, ComponentMeta } from '@storybook/react'; +import type { StoryFn, Meta } from '@storybook/react'; /** * Internal dependencies @@ -9,7 +9,7 @@ import type { ComponentStory, ComponentMeta } from '@storybook/react'; import Spinner from '../'; import { space } from '../../ui/utils/space'; -const meta: ComponentMeta< typeof Spinner > = { +const meta: Meta< typeof Spinner > = { title: 'Components/Spinner', component: Spinner, parameters: { @@ -21,12 +21,12 @@ const meta: ComponentMeta< typeof Spinner > = { }; export default meta; -const Template: ComponentStory< typeof Spinner > = ( args ) => { +const Template: StoryFn< typeof Spinner > = ( args ) => { return ; }; -export const Default: ComponentStory< typeof Spinner > = Template.bind( {} ); +export const Default: StoryFn< typeof Spinner > = Template.bind( {} ); // The Spinner can be resized to any size, but the stroke width will remain unchanged. -export const CustomSize: ComponentStory< typeof Spinner > = Template.bind( {} ); +export const CustomSize: StoryFn< typeof Spinner > = Template.bind( {} ); CustomSize.args = { style: { width: space( 20 ), height: space( 20 ) } }; diff --git a/packages/components/src/surface/stories/index.story.tsx b/packages/components/src/surface/stories/index.story.tsx index 7689f66eabdc5..7f6790d09c848 100644 --- a/packages/components/src/surface/stories/index.story.tsx +++ b/packages/components/src/surface/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -9,7 +9,7 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Surface } from '..'; import { Text } from '../../text'; -const meta: ComponentMeta< typeof Surface > = { +const meta: Meta< typeof Surface > = { component: Surface, title: 'Components (Experimental)/Surface', argTypes: { @@ -25,7 +25,7 @@ const meta: ComponentMeta< typeof Surface > = { }; export default meta; -const Template: ComponentStory< typeof Surface > = ( args ) => { +const Template: StoryFn< typeof Surface > = ( args ) => { return ( = ( args ) => { ); }; -export const Default: ComponentStory< typeof Surface > = Template.bind( {} ); +export const Default: StoryFn< typeof Surface > = Template.bind( {} ); Default.args = {}; diff --git a/packages/components/src/tab-panel/stories/index.story.tsx b/packages/components/src/tab-panel/stories/index.story.tsx index e5f68932cd497..e2f146d55865c 100644 --- a/packages/components/src/tab-panel/stories/index.story.tsx +++ b/packages/components/src/tab-panel/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -12,10 +12,8 @@ import { link, more, wordpress } from '@wordpress/icons'; * Internal dependencies */ import TabPanel from '..'; -import Popover from '../../popover'; -import { Provider as SlotFillProvider } from '../../slot-fill'; -const meta: ComponentMeta< typeof TabPanel > = { +const meta: Meta< typeof TabPanel > = { title: 'Components/TabPanel', component: TabPanel, parameters: { @@ -26,7 +24,7 @@ const meta: ComponentMeta< typeof TabPanel > = { }; export default meta; -const Template: ComponentStory< typeof TabPanel > = ( props ) => { +const Template: StoryFn< typeof TabPanel > = ( props ) => { return ; }; @@ -65,14 +63,8 @@ DisabledTab.args = { ], }; -const SlotFillTemplate: ComponentStory< typeof TabPanel > = ( props ) => { - return ( - - - { /* @ts-expect-error The 'Slot' component hasn't been typed yet. */ } - - - ); +const SlotFillTemplate: StoryFn< typeof TabPanel > = ( props ) => { + return ; }; export const WithTabIconsAndTooltips = SlotFillTemplate.bind( {} ); diff --git a/packages/components/src/tab-panel/test/index.tsx b/packages/components/src/tab-panel/test/index.tsx index c3102fe26833b..723ed4d17ff2d 100644 --- a/packages/components/src/tab-panel/test/index.tsx +++ b/packages/components/src/tab-panel/test/index.tsx @@ -13,8 +13,6 @@ import { wordpress, category, media } from '@wordpress/icons'; * Internal dependencies */ import TabPanel from '..'; -import Popover from '../../popover'; -import { Provider as SlotFillProvider } from '../../slot-fill'; const TABS = [ { @@ -107,17 +105,10 @@ describe.each( [ ]; render( - // In order for the tooltip to display properly, there needs to be - // `Popover.Slot` in which the `Popover` renders outside of the - // `TabPanel` component, otherwise the tooltip renders inline. - - - { /* @ts-expect-error The 'Slot' component hasn't been typed yet. */ } - - + ); const allTabs = screen.getAllByRole( 'tab' ); @@ -152,18 +143,11 @@ describe.each( [ ]; render( - // In order for the tooltip to display properly, there needs to be - // `Popover.Slot` in which the `Popover` renders outside of the - // `TabPanel` component, otherwise the tooltip renders inline. - - - { /* @ts-expect-error The 'Slot' component hasn't been typed yet. */ } - - + ); expect( await getSelectedTab() ).not.toHaveTextContent( 'Alpha' ); diff --git a/packages/components/src/text-control/stories/index.story.tsx b/packages/components/src/text-control/stories/index.story.tsx index b5be8f88dc64f..ddc8af7a9f2b3 100644 --- a/packages/components/src/text-control/stories/index.story.tsx +++ b/packages/components/src/text-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import TextControl from '..'; -const meta: ComponentMeta< typeof TextControl > = { +const meta: Meta< typeof TextControl > = { component: TextControl, title: 'Components/TextControl', argTypes: { @@ -31,7 +31,7 @@ const meta: ComponentMeta< typeof TextControl > = { }; export default meta; -const DefaultTemplate: ComponentStory< typeof TextControl > = ( { +const DefaultTemplate: StoryFn< typeof TextControl > = ( { onChange, ...args } ) => { @@ -49,11 +49,12 @@ const DefaultTemplate: ComponentStory< typeof TextControl > = ( { ); }; -export const Default: ComponentStory< typeof TextControl > = - DefaultTemplate.bind( {} ); +export const Default: StoryFn< typeof TextControl > = DefaultTemplate.bind( + {} +); Default.args = {}; -export const WithLabelAndHelpText: ComponentStory< typeof TextControl > = +export const WithLabelAndHelpText: StoryFn< typeof TextControl > = DefaultTemplate.bind( {} ); WithLabelAndHelpText.args = { ...Default.args, diff --git a/packages/components/src/text-highlight/stories/index.story.tsx b/packages/components/src/text-highlight/stories/index.story.tsx index bf86311414b1a..d54149d8e19d3 100644 --- a/packages/components/src/text-highlight/stories/index.story.tsx +++ b/packages/components/src/text-highlight/stories/index.story.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import TextHighlight from '..'; -const meta: ComponentMeta< typeof TextHighlight > = { +const meta: Meta< typeof TextHighlight > = { component: TextHighlight, title: 'Components/TextHighlight', parameters: { @@ -20,13 +20,11 @@ const meta: ComponentMeta< typeof TextHighlight > = { }; export default meta; -const Template: ComponentStory< typeof TextHighlight > = ( args ) => { +const Template: StoryFn< typeof TextHighlight > = ( args ) => { return ; }; -export const Default: ComponentStory< typeof TextHighlight > = Template.bind( - {} -); +export const Default: StoryFn< typeof TextHighlight > = Template.bind( {} ); Default.args = { text: 'We call the new editor Gutenberg. The entire editing experience has been rebuilt for media rich pages and posts.', highlight: 'Gutenberg', diff --git a/packages/components/src/textarea-control/stories/index.story.tsx b/packages/components/src/textarea-control/stories/index.story.tsx index f227a0c0c31cb..e227519a06938 100644 --- a/packages/components/src/textarea-control/stories/index.story.tsx +++ b/packages/components/src/textarea-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import TextareaControl from '..'; -const meta: ComponentMeta< typeof TextareaControl > = { +const meta: Meta< typeof TextareaControl > = { component: TextareaControl, title: 'Components/TextareaControl', argTypes: { @@ -31,7 +31,7 @@ const meta: ComponentMeta< typeof TextareaControl > = { }; export default meta; -const Template: ComponentStory< typeof TextareaControl > = ( { +const Template: StoryFn< typeof TextareaControl > = ( { onChange, ...args } ) => { @@ -49,9 +49,7 @@ const Template: ComponentStory< typeof TextareaControl > = ( { ); }; -export const Default: ComponentStory< typeof TextareaControl > = Template.bind( - {} -); +export const Default: StoryFn< typeof TextareaControl > = Template.bind( {} ); Default.args = { label: 'Text', help: 'Enter some text', diff --git a/packages/components/src/theme/README.md b/packages/components/src/theme/README.md index d1bfe237c9608..ee0513988b9c1 100644 --- a/packages/components/src/theme/README.md +++ b/packages/components/src/theme/README.md @@ -40,8 +40,8 @@ If you would like your custom component to be themeable as a child of the `Theme - Grayscale: - `--wp-components-color-gray-100`: Used for light gray backgrounds. - `--wp-components-color-gray-200`: Used sparingly for light borders. - - `--wp-components-color-gray-300`: Used for most borders. - - `--wp-components-color-gray-400` - - `--wp-components-color-gray-600`: Meets 3:1 UI or large text contrast against white. - - `--wp-components-color-gray-700`: Meets 4.6:1 text contrast against white. - - `--wp-components-color-gray-800` + - `--wp-components-color-gray-300`: Used for most borders. + - `--wp-components-color-gray-400` + - `--wp-components-color-gray-600`: Meets 3:1 UI or large text contrast against white. + - `--wp-components-color-gray-700`: Meets 4.6:1 text contrast against white. + - `--wp-components-color-gray-800` diff --git a/packages/components/src/theme/index.tsx b/packages/components/src/theme/index.tsx index 984ec32a07ce3..ce1e11246e0d3 100644 --- a/packages/components/src/theme/index.tsx +++ b/packages/components/src/theme/index.tsx @@ -18,7 +18,6 @@ import { useCx } from '../utils'; * Multiple `Theme` components can be nested in order to override specific theme variables. * * - * @example * ```jsx * const Example = () => { * return ( diff --git a/packages/components/src/theme/stories/index.story.tsx b/packages/components/src/theme/stories/index.story.tsx index 235c9fe2c8d54..3d52dea7fba57 100644 --- a/packages/components/src/theme/stories/index.story.tsx +++ b/packages/components/src/theme/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies @@ -11,7 +11,7 @@ import Button from '../../button'; import { generateThemeVariables, checkContrasts } from '../color-algorithms'; import { HStack } from '../../h-stack'; -const meta: ComponentMeta< typeof Theme > = { +const meta: Meta< typeof Theme > = { component: Theme, title: 'Components (Experimental)/Theme', argTypes: { @@ -25,7 +25,7 @@ const meta: ComponentMeta< typeof Theme > = { }; export default meta; -const Template: ComponentStory< typeof Theme > = ( args ) => ( +const Template: StoryFn< typeof Theme > = ( args ) => ( @@ -34,7 +34,7 @@ const Template: ComponentStory< typeof Theme > = ( args ) => ( export const Default = Template.bind( {} ); Default.args = {}; -export const Nested: ComponentStory< typeof Theme > = ( args ) => ( +export const Nested: StoryFn< typeof Theme > = ( args ) => ( @@ -52,7 +52,7 @@ Nested.args = { /** * The rest of the required colors are generated based on the given accent and background colors. */ -export const ColorScheme: ComponentStory< typeof Theme > = ( { +export const ColorScheme: StoryFn< typeof Theme > = ( { accent, background, } ) => { diff --git a/packages/components/src/tip/stories/index.story.tsx b/packages/components/src/tip/stories/index.story.tsx index dcdee273147a0..3999c6b9be45f 100644 --- a/packages/components/src/tip/stories/index.story.tsx +++ b/packages/components/src/tip/stories/index.story.tsx @@ -1,14 +1,14 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * Internal dependencies */ import Tip from '..'; -const meta: ComponentMeta< typeof Tip > = { +const meta: Meta< typeof Tip > = { component: Tip, title: 'Components/Tip', argTypes: { @@ -23,11 +23,11 @@ const meta: ComponentMeta< typeof Tip > = { }; export default meta; -const Template: ComponentStory< typeof Tip > = ( args ) => { +const Template: StoryFn< typeof Tip > = ( args ) => { return ; }; -export const Default: ComponentStory< typeof Tip > = Template.bind( {} ); +export const Default: StoryFn< typeof Tip > = Template.bind( {} ); Default.args = { children: 'An example tip', }; diff --git a/packages/components/src/toggle-control/stories/index.story.tsx b/packages/components/src/toggle-control/stories/index.story.tsx index 1279801ac7164..b8043b8f48e52 100644 --- a/packages/components/src/toggle-control/stories/index.story.tsx +++ b/packages/components/src/toggle-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -13,7 +13,7 @@ import { useState } from '@wordpress/element'; */ import ToggleControl from '..'; -const meta: ComponentMeta< typeof ToggleControl > = { +const meta: Meta< typeof ToggleControl > = { title: 'Components/ToggleControl', component: ToggleControl, argTypes: { @@ -29,7 +29,7 @@ const meta: ComponentMeta< typeof ToggleControl > = { }; export default meta; -const Template: ComponentStory< typeof ToggleControl > = ( { +const Template: StoryFn< typeof ToggleControl > = ( { onChange, ...props } ) => { diff --git a/packages/components/src/toggle-group-control/stories/index.story.tsx b/packages/components/src/toggle-group-control/stories/index.story.tsx index d818859083e97..92f1e6076248b 100644 --- a/packages/components/src/toggle-group-control/stories/index.story.tsx +++ b/packages/components/src/toggle-group-control/stories/index.story.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies @@ -23,8 +23,10 @@ import type { ToggleGroupControlProps, } from '../types'; -const meta: ComponentMeta< typeof ToggleGroupControl > = { +const meta: Meta< typeof ToggleGroupControl > = { component: ToggleGroupControl, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + subcomponents: { ToggleGroupControlOption, ToggleGroupControlOptionIcon }, title: 'Components (Experimental)/ToggleGroupControl', argTypes: { help: { control: { type: 'text' } }, @@ -38,7 +40,7 @@ const meta: ComponentMeta< typeof ToggleGroupControl > = { }; export default meta; -const Template: ComponentStory< typeof ToggleGroupControl > = ( { +const Template: StoryFn< typeof ToggleGroupControl > = ( { onChange, ...props } ) => { @@ -72,8 +74,9 @@ const mapPropsToOptionIconComponent = ( { ); -export const Default: ComponentStory< typeof ToggleGroupControl > = - Template.bind( {} ); +export const Default: StoryFn< typeof ToggleGroupControl > = Template.bind( + {} +); Default.args = { children: [ { value: 'left', label: 'Left' }, @@ -90,8 +93,9 @@ Default.args = { * The `aria-label` will be used in the tooltip if provided. Otherwise, the * `label` will be used. */ -export const WithTooltip: ComponentStory< typeof ToggleGroupControl > = - Template.bind( {} ); +export const WithTooltip: StoryFn< typeof ToggleGroupControl > = Template.bind( + {} +); WithTooltip.args = { ...Default.args, children: [ @@ -114,8 +118,9 @@ WithTooltip.args = { * The `ToggleGroupControlOptionIcon` component can be used for icon options. A `label` is required * on each option for accessibility, which will be shown in a tooltip. */ -export const WithIcons: ComponentStory< typeof ToggleGroupControl > = - Template.bind( {} ); +export const WithIcons: StoryFn< typeof ToggleGroupControl > = Template.bind( + {} +); WithIcons.args = { ...Default.args, children: [ @@ -128,8 +133,9 @@ WithIcons.args = { /** * When the `isDeselectable` prop is true, the option can be deselected by clicking on it again. */ -export const Deselectable: ComponentStory< typeof ToggleGroupControl > = - Template.bind( {} ); +export const Deselectable: StoryFn< typeof ToggleGroupControl > = Template.bind( + {} +); Deselectable.args = { ...WithIcons.args, isDeselectable: true, diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index e5ea6c14f5ef6..296e9483b9f70 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ToggleGroupControl should render correctly with icons 1`] = ` +exports[`ToggleGroupControl controlled should render correctly with icons 1`] = ` .emotion-0 { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif; font-size: 13px; @@ -49,17 +49,332 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` min-width: 0; padding: 2px; position: relative; - -webkit-transition: -webkit-transform 100ms linear; - transition: transform 100ms linear; min-height: 36px; } +.emotion-8:hover { + border-color: #757575; +} + +.emotion-8:focus-within { + border-color: var(--wp-components-color-accent-darker-10, var(--wp-admin-theme-color-darker-10, #2145e6)); + box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + z-index: 1; + outline: 2px solid transparent; + outline-offset: -2px; +} + +.emotion-10 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + max-width: 100%; + min-width: 0; + position: relative; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.emotion-12 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; + background: transparent; + border: none; + border-radius: 2px; + color: #757575; + fill: currentColor; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: inherit; + height: 100%; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + line-height: 100%; + outline: none; + padding: 0 12px; + position: relative; + text-align: center; + -webkit-transition: background 160ms linear,color 160ms linear,font-weight 60ms linear; + transition: background 160ms linear,color 160ms linear,font-weight 60ms linear; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100%; + z-index: 2; + color: #1e1e1e; + width: 30px; + padding-left: 0; + padding-right: 0; + color: #fff; +} + +@media ( prefers-reduced-motion: reduce ) { + .emotion-12 { + transition-duration: 0ms; + } +} + +.emotion-12::-moz-focus-inner { + border: 0; +} + +.emotion-12:active { + background: #fff; +} + +.emotion-12:active { + background: transparent; +} + +.emotion-13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-size: 13px; + line-height: 1; +} + +.emotion-15 { + background: #1e1e1e; + border-radius: 2px; + position: absolute; + inset: 0; + z-index: 1; + outline: 2px solid transparent; + outline-offset: -3px; +} + +.emotion-18 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; + background: transparent; + border: none; + border-radius: 2px; + color: #757575; + fill: currentColor; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: inherit; + height: 100%; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + line-height: 100%; + outline: none; + padding: 0 12px; + position: relative; + text-align: center; + -webkit-transition: background 160ms linear,color 160ms linear,font-weight 60ms linear; + transition: background 160ms linear,color 160ms linear,font-weight 60ms linear; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100%; + z-index: 2; + color: #1e1e1e; + width: 30px; + padding-left: 0; + padding-right: 0; +} + @media ( prefers-reduced-motion: reduce ) { - .emotion-8 { + .emotion-18 { transition-duration: 0ms; } } +.emotion-18::-moz-focus-inner { + border: 0; +} + +.emotion-18:active { + background: #fff; +} + +
+
+
+
+ + Test Toggle Group Control + +
+
+
+ + +
+ +
+
+
+
+ +
+`; + +exports[`ToggleGroupControl controlled should render correctly with text options 1`] = ` +.emotion-0 { + font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif; + font-size: 13px; + box-sizing: border-box; +} + +.emotion-0 *, +.emotion-0 *::before, +.emotion-0 *::after { + box-sizing: inherit; +} + +.emotion-2 { + margin-bottom: calc(4px * 2); +} + +.components-panel__row .emotion-2 { + margin-bottom: inherit; +} + +.emotion-4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.emotion-6 { + font-size: 11px; + font-weight: 500; + line-height: 1.4; + text-transform: uppercase; + display: inline-block; + margin-bottom: calc(4px * 2); + padding: 0; +} + +.emotion-8 { + background: #fff; + border: 1px solid transparent; + border-radius: 2px; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + min-width: 0; + padding: 2px; + position: relative; + min-height: 36px; +} + .emotion-8:hover { border-color: #757575; } @@ -73,26 +388,224 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` } .emotion-10 { - background: #1e1e1e; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + max-width: 100%; + min-width: 0; + position: relative; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.emotion-12 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + appearance: none; + background: transparent; + border: none; border-radius: 2px; - left: 0; - position: absolute; - top: 2px; - bottom: 2px; - -webkit-transition: -webkit-transform 160ms ease; - transition: transform 160ms ease; - z-index: 1; - outline: 2px solid transparent; - outline-offset: -3px; + color: #757575; + fill: currentColor; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: inherit; + height: 100%; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + line-height: 100%; + outline: none; + padding: 0 12px; + position: relative; + text-align: center; + -webkit-transition: background 160ms linear,color 160ms linear,font-weight 60ms linear; + transition: background 160ms linear,color 160ms linear,font-weight 60ms linear; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100%; + z-index: 2; } @media ( prefers-reduced-motion: reduce ) { - .emotion-10 { + .emotion-12 { transition-duration: 0ms; } } -.emotion-12 { +.emotion-12::-moz-focus-inner { + border: 0; +} + +.emotion-12:active { + background: #fff; +} + +.emotion-13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-size: 13px; + line-height: 1; +} + +
+
+
+
+ + Test Toggle Group Control + +
+
+
+ +
+
+ +
+
+
+
+ +
+`; + +exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] = ` +.emotion-0 { + font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif; + font-size: 13px; + box-sizing: border-box; +} + +.emotion-0 *, +.emotion-0 *::before, +.emotion-0 *::after { + box-sizing: inherit; +} + +.emotion-2 { + margin-bottom: calc(4px * 2); +} + +.components-panel__row .emotion-2 { + margin-bottom: inherit; +} + +.emotion-4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.emotion-6 { + font-size: 11px; + font-weight: 500; + line-height: 1.4; + text-transform: uppercase; + display: inline-block; + margin-bottom: calc(4px * 2); + padding: 0; +} + +.emotion-8 { + background: #fff; + border: 1px solid transparent; + border-radius: 2px; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + min-width: 0; + padding: 2px; + position: relative; + min-height: 36px; +} + +.emotion-8:hover { + border-color: #757575; +} + +.emotion-8:focus-within { + border-color: var(--wp-components-color-accent-darker-10, var(--wp-admin-theme-color-darker-10, #2145e6)); + box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); + z-index: 1; + outline: 2px solid transparent; + outline-offset: -2px; +} + +.emotion-10 { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; @@ -105,7 +618,7 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` flex: 1; } -.emotion-14 { +.emotion-12 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -151,24 +664,24 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` } @media ( prefers-reduced-motion: reduce ) { - .emotion-14 { + .emotion-12 { transition-duration: 0ms; } } -.emotion-14::-moz-focus-inner { +.emotion-12::-moz-focus-inner { border: 0; } -.emotion-14:active { +.emotion-12:active { background: #fff; } -.emotion-14:active { +.emotion-12:active { background: transparent; } -.emotion-15 { +.emotion-13 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -177,7 +690,17 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` line-height: 1; } -.emotion-19 { +.emotion-15 { + background: #1e1e1e; + border-radius: 2px; + position: absolute; + inset: 0; + z-index: 1; + outline: 2px solid transparent; + outline-offset: -3px; +} + +.emotion-18 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -222,16 +745,16 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` } @media ( prefers-reduced-motion: reduce ) { - .emotion-19 { + .emotion-18 { transition-duration: 0ms; } } -.emotion-19::-moz-focus-inner { +.emotion-18::-moz-focus-inner { border: 0; } -.emotion-19:active { +.emotion-18:active { background: #fff; } @@ -259,31 +782,23 @@ exports[`ToggleGroupControl should render correctly with icons 1`] = ` id="toggle-group-control-as-radio-group-1" role="radiogroup" > - + +
+
+
+ + + + +
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js index 46483aaa2ea53..1bab3946a3d4b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-context/view.js @@ -1,5 +1,19 @@ ( ( { wp } ) => { - const { store } = wp.interactivity; + const { store, navigate } = wp.interactivity; + + const html = ` +
+
+
+ + + + +
`; store( { derived: { @@ -17,6 +31,25 @@ toggleContextText: ( { context } ) => { context.text = context.text === 'Text 1' ? 'Text 2' : 'Text 1'; }, + toggleText: ( { context } ) => { + context.text = "changed dynamically"; + }, + addNewText: ( { context } ) => { + context.newText = 'some new text'; + }, + navigate: () => { + navigate( window.location, { + force: true, + html, + } ); + }, + asyncNavigate: async ({ context }) => { + await navigate( window.location, { + force: true, + html, + } ); + context.newText = 'changed from async action'; + } }, } ); } )( window ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json new file mode 100644 index 0000000000000..0cbdd065e63a1 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json @@ -0,0 +1,14 @@ +{ + "apiVersion": 2, + "name": "test/directive-key", + "title": "E2E Interactivity tests - directive key", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "directive-key-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-key/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-key/render.php new file mode 100644 index 0000000000000..07c6e4e3de161 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-key/render.php @@ -0,0 +1,18 @@ + + +
+
    +
  • 2
  • +
  • 3
  • +
+ +
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-key/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-key/view.js new file mode 100644 index 0000000000000..a155dec99e0aa --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-key/view.js @@ -0,0 +1,23 @@ +( ( { wp } ) => { + const { store, navigate } = wp.interactivity; + + const html = ` +
+
    +
  • 1
  • +
  • 2
  • +
  • 3
  • +
+
`; + + store( { + actions: { + navigate: () => { + navigate( window.location, { + force: true, + html, + } ); + }, + }, + } ); +} )( window ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json new file mode 100644 index 0000000000000..f79f89a6e81b8 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json @@ -0,0 +1,14 @@ +{ + "apiVersion": 2, + "name": "test/directive-slots", + "title": "E2E Interactivity tests - directive slots", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "directive-slots-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/render.php b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/render.php new file mode 100644 index 0000000000000..5c1558d35403d --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/render.php @@ -0,0 +1,67 @@ + +
+
+
[1]
+
[2]
+
[3]
+
[4]
+
[5]
+
+ +
+ initial +
+ +
+ + + + + + +
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/view.js b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/view.js new file mode 100644 index 0000000000000..ab5b39379f3a8 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/view.js @@ -0,0 +1,18 @@ +( ( { wp } ) => { + const { store } = wp.interactivity; + + store( { + state: { + slot: '' + }, + actions: { + changeSlot: ( { state, event } ) => { + state.slot = event.target.dataset.slot; + }, + updateSlotText: ( { context } ) => { + const n = context.text[1]; + context.text = `[${n} updated]`; + }, + }, + } ); +} )( window ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json new file mode 100644 index 0000000000000..44cc260d87d3f --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json @@ -0,0 +1,14 @@ +{ + "apiVersion": 2, + "name": "test/router-regions", + "title": "E2E Interactivity tests - router regions", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "router-regions-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php new file mode 100644 index 0000000000000..db6e75709f979 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php @@ -0,0 +1,89 @@ + + +
+

Region 1

+
+

not hydrated

+

content from page

+ + + + + Next + + Back + +
+
+ +
+

not hydrated

+
+ + +
+

Region 2

+
+

not hydrated

+

content from page

+ + + +
+
+

not hydrated

+
+ +
+

Nested region

+
+

content from page

+
+
+
+
+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js new file mode 100644 index 0000000000000..296c77d3ee7b3 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/view.js @@ -0,0 +1,43 @@ +( ( { wp } ) => { + /** + * WordPress dependencies + */ + const { store, navigate } = wp.interactivity; + + store( { + state: { + region1: { + text: 'hydrated' + }, + region2: { + text: 'hydrated' + }, + counter: { + value: 0, + }, + }, + actions: { + router: { + navigate: async ( { event: e } ) => { + e.preventDefault(); + await navigate( e.target.href ); + }, + back: () => history.back(), + }, + counter: { + increment: ( { state, context } ) => { + if ( context.counter ) { + context.counter.value += 1; + } else { + state.counter.value += 1; + } + }, + init: ( { context } ) => { + if ( context.counter ) { + context.counter.value = context.counter.initialValue; + } + } + }, + }, + } ); +} )( window ); diff --git a/packages/e2e-tests/specs/editor/plugins/annotations.test.js b/packages/e2e-tests/specs/editor/plugins/annotations.test.js index f0134812d4a7e..24eaebb765966 100644 --- a/packages/e2e-tests/specs/editor/plugins/annotations.test.js +++ b/packages/e2e-tests/specs/editor/plugins/annotations.test.js @@ -29,13 +29,6 @@ describe( 'Annotations', () => { beforeEach( async () => { await createNewPost(); - // To do: run with iframe. - await page.evaluate( () => { - window.wp.blocks.registerBlockType( 'test/v2', { - apiVersion: '2', - title: 'test', - } ); - } ); } ); /** @@ -100,7 +93,7 @@ describe( 'Annotations', () => { */ async function getRichTextInnerHTML() { const htmlContent = await canvas().$$( '.wp-block-paragraph' ); - return await page.evaluate( ( el ) => { + return await canvas().evaluate( ( el ) => { return el.innerHTML; }, htmlContent[ 0 ] ); } @@ -126,7 +119,7 @@ describe( 'Annotations', () => { const htmlContent = await canvas().$$( '.block-editor-block-list__block-html-textarea' ); - const html = await page.evaluate( ( el ) => { + const html = await canvas().evaluate( ( el ) => { return el.innerHTML; }, htmlContent[ 0 ] ); @@ -145,7 +138,7 @@ describe( 'Annotations', () => { await removeAnnotations(); const htmlContent = await canvas().$$( '.wp-block-paragraph' ); - const html = await page.evaluate( ( el ) => { + const html = await canvas().evaluate( ( el ) => { return el.innerHTML; }, htmlContent[ 0 ] ); diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap deleted file mode 100644 index 7705ff11cbff9..0000000000000 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/rich-text.test.js.snap +++ /dev/null @@ -1,231 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`RichText should apply active formatting for inline paste 1`] = ` -" -

1323

-" -`; - -exports[`RichText should apply formatting when selection is collapsed 1`] = ` -" -

Some bold.

-" -`; - -exports[`RichText should apply formatting with primary shortcut 1`] = ` -" -

test

-" -`; - -exports[`RichText should apply multiple formats when selection is collapsed 1`] = ` -" -

1.

-" -`; - -exports[`RichText should copy/paste heading 1`] = ` -" -

Heading

- - - -

Heading

-" -`; - -exports[`RichText should handle Home and End keys 1`] = ` -" -

-12+

-" -`; - -exports[`RichText should handle change in tag name gracefully 1`] = ` -" -

-" -`; - -exports[`RichText should keep internal selection after blur 1`] = ` -" -

12

-" -`; - -exports[`RichText should make bold after split and merge 1`] = ` -" -

12

-" -`; - -exports[`RichText should navigate arround emoji 1`] = ` -" -

1🍓

-" -`; - -exports[`RichText should navigate consecutive format boundaries 1`] = ` -" -

12

-" -`; - -exports[`RichText should navigate consecutive format boundaries 2`] = ` -" -

1-2

-" -`; - -exports[`RichText should not format text after code backtick 1`] = ` -" -

A backtick and more.

-" -`; - -exports[`RichText should not lose selection direction 1`] = ` -" -

12-3

-" -`; - -exports[`RichText should not split rich text on inline paste 1`] = ` -" -

123

-" -`; - -exports[`RichText should not split rich text on inline paste with formatting 1`] = ` -" -

a123b

-" -`; - -exports[`RichText should not undo backtick transform with backspace after selection change 1`] = `""`; - -exports[`RichText should not undo backtick transform with backspace after typing 1`] = `""`; - -exports[`RichText should only mutate text data on input 1`] = ` -" -

1234

-" -`; - -exports[`RichText should paste list contents into paragraph 1`] = ` -" -
    -
  • 1 -
      -
    • 2
    • -
    -
  • -
- - - -
    -
  • 1 -
      -
    • 2
    • -
    -
  • -
-" -`; - -exports[`RichText should paste paragraph contents into list 1`] = ` -" -

1
2

- - - -
    -
  • 1
  • - - - -
  • 2
  • -
-" -`; - -exports[`RichText should preserve internal formatting 1`] = ` -" -

1

-" -`; - -exports[`RichText should preserve internal formatting 2`] = ` -" -

1

- - - -

1

-" -`; - -exports[`RichText should return focus when pressing formatting button 1`] = ` -" -

Some bold.

-" -`; - -exports[`RichText should run input rules after composition end 1`] = ` -" -

a

-" -`; - -exports[`RichText should split rich text on paste 1`] = ` -" -

a

- - - -

1

- - - -

2

- - - -

b

-" -`; - -exports[`RichText should transform backtick to code 1`] = ` -" -

A backtick

-" -`; - -exports[`RichText should transform backtick to code 2`] = ` -" -

A \`backtick\`

-" -`; - -exports[`RichText should transform when typing backtick over selection 1`] = ` -" -

A selection test.

-" -`; - -exports[`RichText should transform when typing backtick over selection 2`] = ` -" -

A \`selection\` test.

-" -`; - -exports[`RichText should undo backtick transform with backspace 1`] = ` -" -

\`a\`

-" -`; - -exports[`RichText should update internal selection after fresh focus 1`] = ` -" -

12

-" -`; diff --git a/packages/e2e-tests/specs/editor/various/rich-text.test.js b/packages/e2e-tests/specs/editor/various/rich-text.test.js deleted file mode 100644 index ff651e61d52ea..0000000000000 --- a/packages/e2e-tests/specs/editor/various/rich-text.test.js +++ /dev/null @@ -1,570 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - getEditedPostContent, - insertBlock, - clickBlockAppender, - pressKeyWithModifier, - showBlockToolbar, - clickBlockToolbarButton, - canvas, -} from '@wordpress/e2e-test-utils'; - -describe( 'RichText', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'should handle change in tag name gracefully', async () => { - // Regression test: The heading block changes the tag name of its - // RichText element. Historically this has been prone to breakage, - // because the Editable component prevents rerenders, so React cannot - // update the element by itself. - // - // See: https://github.com/WordPress/gutenberg/issues/3091 - await insertBlock( 'Heading' ); - await page.waitForSelector( '[aria-label="Change level"]' ); - await page.click( '[aria-label="Change level"]' ); - await page.click( '[aria-label="Heading 3"]' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply formatting with primary shortcut', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'test' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'b' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply formatting when selection is collapsed', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'Some ' ); - // All following characters should now be bold. - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( 'bold' ); - // All following characters should no longer be bold. - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply multiple formats when selection is collapsed', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await pressKeyWithModifier( 'primary', 'i' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'i' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not highlight more than one format', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( ' 2' ); - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'b' ); - - const count = await canvas().evaluate( - () => - document.querySelectorAll( '*[data-rich-text-format-boundary]' ) - .length - ); - - expect( count ).toBe( 1 ); - } ); - - it( 'should return focus when pressing formatting button', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'Some ' ); - await showBlockToolbar(); - await page.click( '[aria-label="Bold"]' ); - await page.keyboard.type( 'bold' ); - await showBlockToolbar(); - await page.click( '[aria-label="Bold"]' ); - await page.keyboard.type( '.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should transform backtick to code', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'A `backtick`' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should undo backtick transform with backspace', async () => { - await clickBlockAppender(); - await page.keyboard.type( '`a`' ); - await page.keyboard.press( 'Backspace' ); - - // Expect "`a`" to be restored. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not undo backtick transform with backspace after typing', async () => { - await clickBlockAppender(); - await page.keyboard.type( '`a`' ); - await page.keyboard.type( 'b' ); - await page.keyboard.press( 'Backspace' ); - await page.keyboard.press( 'Backspace' ); - - // Expect "a" to be deleted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not undo backtick transform with backspace after selection change', async () => { - await clickBlockAppender(); - await page.keyboard.type( '`a`' ); - await page.evaluate( () => new Promise( window.requestIdleCallback ) ); - // Move inside format boundary. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'Backspace' ); - - // Expect "a" to be deleted. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not format text after code backtick', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'A `backtick` and more.' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should transform when typing backtick over selection', async () => { - await clickBlockAppender(); - await page.keyboard.type( 'A selection test.' ); - await page.keyboard.press( 'Home' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'ArrowRight' ); - await pressKeyWithModifier( 'shiftAlt', 'ArrowRight' ); - await page.keyboard.type( '`' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Should undo the transform. - await pressKeyWithModifier( 'primary', 'z' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should only mutate text data on input', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '3' ); - - await canvas().evaluate( () => { - let called; - const { body } = document; - const config = { - attributes: true, - childList: true, - characterData: true, - subtree: true, - }; - - const mutationObserver = new MutationObserver( ( records ) => { - if ( called || records.length > 1 ) { - throw new Error( 'Typing should only mutate once.' ); - } - - records.forEach( ( record ) => { - if ( record.type !== 'characterData' ) { - throw new Error( - `Typing mutated more than character data: ${ record.type }` - ); - } - } ); - - called = true; - } ); - - mutationObserver.observe( body, config ); - - window.unsubscribes = [ () => mutationObserver.disconnect() ]; - - document.addEventListener( - 'selectionchange', - () => { - function throwMultipleSelectionChange() { - throw new Error( - 'Typing should only emit one selection change event.' - ); - } - - document.addEventListener( - 'selectionchange', - throwMultipleSelectionChange, - { - once: true, - } - ); - - window.unsubscribes.push( () => { - document.removeEventListener( - 'selectionchange', - throwMultipleSelectionChange - ); - } ); - }, - { once: true } - ); - } ); - - await page.keyboard.type( '4' ); - - await canvas().evaluate( () => { - // The selection change event should be called once. If there's only - // one item in `window.unsubscribes`, it means that only one - // function is present to disconnect the `mutationObserver`. - if ( window.unsubscribes.length === 1 ) { - throw new Error( - 'The selection change event listener was never called.' - ); - } - - window.unsubscribes.forEach( ( unsubscribe ) => unsubscribe() ); - } ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not lose selection direction', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '23' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.down( 'Shift' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.up( 'Shift' ); - - // There should be no selection. The following should insert "-" without - // deleting the numbers. - await page.keyboard.type( '-' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should handle Home and End keys', async () => { - await page.keyboard.press( 'Enter' ); - - // Wait for rich text editor to load. - await canvas().waitForSelector( '.block-editor-rich-text__editable' ); - - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '12' ); - await pressKeyWithModifier( 'primary', 'b' ); - - await page.keyboard.press( 'Home' ); - await page.keyboard.type( '-' ); - await page.keyboard.press( 'End' ); - await page.keyboard.type( '+' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should update internal selection after fresh focus', async () => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Tab' ); - await pressKeyWithModifier( 'shift', 'Tab' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should keep internal selection after blur', async () => { - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '1' ); - // Simulate moving focus to a different app, then moving focus back, - // without selection being changed. - await canvas().evaluate( () => { - const activeElement = document.activeElement; - activeElement.blur(); - activeElement.focus(); - } ); - // Wait for the next animation frame, see the focus event listener in - // RichText. - await page.evaluate( - () => new Promise( window.requestAnimationFrame ) - ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should split rich text on paste', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'x' ); - await page.keyboard.type( 'ab' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not split rich text on inline paste', async () => { - await clickBlockAppender(); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'x' ); - await page.keyboard.type( '13' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should not split rich text on inline paste with formatting', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '3' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'x' ); - await page.keyboard.type( 'ab' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should make bold after split and merge', async () => { - await clickBlockAppender(); - await page.keyboard.type( '1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.press( 'Backspace' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '2' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should apply active formatting for inline paste', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '3' ); - await pressKeyWithModifier( 'shift', 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'c' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.press( 'ArrowLeft' ); - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should preserve internal formatting', async () => { - await clickBlockAppender(); - - // Add text and select to color. - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'a' ); - await clickBlockToolbarButton( 'More' ); - - const button = await page.waitForXPath( - `//button[text()='Highlight']` - ); - // Clicks may fail if the button is out of view. Assure it is before click. - await button.evaluate( ( element ) => element.scrollIntoView() ); - await button.click(); - - // Wait for the popover with "Text" tab to appear. - await page.waitForXPath( - '//button[@role="tab"][@aria-selected="true"][text()="Text"]' - ); - // Initial focus is on the "Text" tab. - // Tab to the "Custom color picker". - await page.keyboard.press( 'Tab' ); - // Tab to black. - await page.keyboard.press( 'Tab' ); - // Select color other than black. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Enter' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Dismiss color picker popover. - await page.keyboard.press( 'Escape' ); - - // Navigate to the block. - await page.keyboard.press( 'Tab' ); - - // Copy the colored text. - await pressKeyWithModifier( 'primary', 'c' ); - - // Collapse the selection to the end. - await page.keyboard.press( 'ArrowRight' ); - - // Create a new paragraph. - await page.keyboard.press( 'Enter' ); - - // Paste the colored text. - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should paste paragraph contents into list', async () => { - await clickBlockAppender(); - - // Create two lines of text in a paragraph. - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'shift', 'Enter' ); - await page.keyboard.type( '2' ); - - // Select all and copy. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'c' ); - - // Collapse the selection to the end. - await page.keyboard.press( 'ArrowRight' ); - - // Create a list. - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( '* ' ); - - // Paste paragraph contents. - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should paste list contents into paragraph', async () => { - await clickBlockAppender(); - - // Create an indented list of two lines. - await page.keyboard.type( '* 1' ); - await page.keyboard.press( 'Enter' ); - await page.keyboard.type( ' 2' ); - - // Select all text. - await pressKeyWithModifier( 'primary', 'a' ); - // Select the nested list. - await pressKeyWithModifier( 'primary', 'a' ); - // Select the parent list item. - await pressKeyWithModifier( 'primary', 'a' ); - // Select all the parent list item text. - await pressKeyWithModifier( 'primary', 'a' ); - // Select the entire list. - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'c' ); - - await page.keyboard.press( 'Enter' ); - - // Paste paragraph contents. - await pressKeyWithModifier( 'primary', 'v' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should navigate arround emoji', async () => { - await clickBlockAppender(); - await page.keyboard.type( '🍓' ); - // Only one press on arrow left should be required to move in front of - // the emoji. - await page.keyboard.press( 'ArrowLeft' ); - await page.keyboard.type( '1' ); - - // Expect '1🍓'. - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should run input rules after composition end', async () => { - await clickBlockAppender(); - // Puppeteer doesn't support composition, so emulate it by inserting - // text in the DOM directly, setting selection in the right place, and - // firing `compositionend`. - // See https://github.com/puppeteer/puppeteer/issues/4981. - await canvas().evaluate( async () => { - document.activeElement.textContent = '`a`'; - const selection = window.getSelection(); - // The `selectionchange` and `compositionend` events should run in separate event - // loop ticks to process all data store updates in time. Native events would be - // scheduled the same way. - selection.selectAllChildren( document.activeElement ); - selection.collapseToEnd(); - await new Promise( ( r ) => setTimeout( r, 0 ) ); - document.activeElement.dispatchEvent( - new CompositionEvent( 'compositionend' ) - ); - } ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - it( 'should navigate consecutive format boundaries', async () => { - await clickBlockAppender(); - await pressKeyWithModifier( 'primary', 'b' ); - await page.keyboard.type( '1' ); - await pressKeyWithModifier( 'primary', 'b' ); - await pressKeyWithModifier( 'primary', 'i' ); - await page.keyboard.type( '2' ); - await pressKeyWithModifier( 'primary', 'i' ); - - expect( await getEditedPostContent() ).toMatchSnapshot(); - - // Should move into the second format. - await page.keyboard.press( 'ArrowLeft' ); - // Should move to the start of the second format. - await page.keyboard.press( 'ArrowLeft' ); - // Should move between the first and second format. - await page.keyboard.press( 'ArrowLeft' ); - - await page.keyboard.type( '-' ); - - // Expect: 1-2 - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); - - test( 'should copy/paste heading', async () => { - await insertBlock( 'Heading' ); - await page.keyboard.type( 'Heading' ); - await pressKeyWithModifier( 'primary', 'a' ); - await pressKeyWithModifier( 'primary', 'c' ); - await page.keyboard.press( 'ArrowRight' ); - await page.keyboard.press( 'Enter' ); - await pressKeyWithModifier( 'primary', 'v' ); - expect( await getEditedPostContent() ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/shortcut-help.test.js b/packages/e2e-tests/specs/editor/various/shortcut-help.test.js deleted file mode 100644 index 838a3edac2a2a..0000000000000 --- a/packages/e2e-tests/specs/editor/various/shortcut-help.test.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - clickOnMoreMenuItem, - clickOnCloseModalButton, - pressKeyWithModifier, -} from '@wordpress/e2e-test-utils'; - -describe( 'keyboard shortcut help modal', () => { - beforeAll( async () => { - await createNewPost(); - } ); - - it( 'displays the shortcut help modal when opened using the menu item in the more menu', async () => { - await clickOnMoreMenuItem( 'Keyboard shortcuts' ); - const shortcutHelpModalElements = await page.$$( - '.edit-post-keyboard-shortcut-help-modal' - ); - expect( shortcutHelpModalElements ).toHaveLength( 1 ); - } ); - - it( 'closes the shortcut help modal when the close icon is clicked', async () => { - await clickOnCloseModalButton(); - const shortcutHelpModalElements = await page.$$( - '.edit-post-keyboard-shortcut-help-modal' - ); - expect( shortcutHelpModalElements ).toHaveLength( 0 ); - } ); - - it( 'displays the shortcut help modal when opened using the shortcut key (access+h)', async () => { - await pressKeyWithModifier( 'access', 'h' ); - const shortcutHelpModalElements = await page.$$( - '.edit-post-keyboard-shortcut-help-modal' - ); - expect( shortcutHelpModalElements ).toHaveLength( 1 ); - } ); - - it( 'closes the shortcut help modal when the shortcut key (access+h) is pressed again', async () => { - await pressKeyWithModifier( 'access', 'h' ); - const shortcutHelpModalElements = await page.$$( - '.edit-post-keyboard-shortcut-help-modal' - ); - expect( shortcutHelpModalElements ).toHaveLength( 0 ); - } ); -} ); diff --git a/packages/e2e-tests/specs/performance/post-editor.test.results.json b/packages/e2e-tests/specs/performance/post-editor.test.results.json new file mode 100644 index 0000000000000..4317d3ef68807 --- /dev/null +++ b/packages/e2e-tests/specs/performance/post-editor.test.results.json @@ -0,0 +1,19 @@ +{ + "serverResponse": [], + "firstPaint": [], + "domContentLoaded": [], + "loaded": [], + "firstContentfulPaint": [], + "firstBlock": [], + "type": [ + 75.483, 96.376, 82.063, 101.192, 87.573, 56.599000000000004, + 63.778999999999996, 93.079, 98.277, 95.365, 94.48599999999999, 55.739, + 81.715, 66.875, 60.897, 54.249, 52.537, 58.745, 49.615 + ], + "typeContainer": [], + "focus": [], + "listViewOpen": [], + "inserterOpen": [], + "inserterHover": [], + "inserterSearch": [] +} diff --git a/packages/e2e-tests/specs/performance/site-editor.test.results.json b/packages/e2e-tests/specs/performance/site-editor.test.results.json new file mode 100644 index 0000000000000..a043c552f12d3 --- /dev/null +++ b/packages/e2e-tests/specs/performance/site-editor.test.results.json @@ -0,0 +1,60 @@ +{ + "serverResponse": [ + 409.40000009536743, 405.59999990463257, 410.09999990463257 + ], + "firstPaint": [ 438.59999990463257, 447.3999996185303, 449.59999990463257 ], + "domContentLoaded": [ 676, 690.0999999046326, 693 ], + "loaded": [ 1405.6999998092651, 1400.3999996185303, 1425.9000000953674 ], + "firstContentfulPaint": [ + 903.2999997138977, 921.0999999046326, 925.2999997138977 + ], + "firstBlock": [ + 3166.7999997138977, 3206.5999999046326, 3238.4000000953674 + ], + "type": [ + 81.45900000000002, 37.088, 36.051, 38.596000000000004, 49.931, 40.322, + 38.99999999999999, 34.235, 33.608999999999995, 32.88399999999999, 30.44, + 37.113, 31.534999999999997, 33.792, 36.942, 35.251000000000005, 33.722, + 33.471999999999994, 35.26499999999999, 29.682, 30.173, + 30.674999999999997, 35.668000000000006, 38.278, 37.62, 37.562, 38.091, + 32.237, 28.119999999999997, 31.342, 39.89, 37.443, 37.761, 40.262, + 37.922, 30.727, 30.955000000000002, 36.53000000000001, 32.293, 37.299, + 38.55800000000001, 39.85699999999999, 33.721999999999994, 30.139, + 29.294, 31.016, 35.7, 36.839, 31.061000000000003, 29.540000000000003, + 48.998999999999995, 35.423, 33.650000000000006, 29.404999999999998, + 32.744, 30.584999999999997, 30.705, 31.873, 28.907, 30.516, 30.882, + 29.257, 29.794, 31.150000000000002, 32.095, 31.066000000000003, 32.872, + 31.894, 31.331, 31.796, 31.675, 30.427999999999997, 30.872, 30.974, + 32.707, 31.849999999999998, 28.935, 28.441000000000003, + 30.566000000000003, 29.014, 33.158, 32.272, 28.990000000000002, 28.76, + 28.967000000000002, 29.418, 28.503, 31.255000000000003, 28.703, + 30.369000000000003, 34.910000000000004, 31.03, 28.523, + 32.361999999999995, 33.870000000000005, 30.11, 30.944000000000003, + 28.601, 30.572999999999997, 33.216, 30.822, 28.892000000000003, + 32.95099999999999, 31.228, 28.251, 34.89, 30.131000000000004, 29.395, + 31.557000000000002, 28.137, 32.051, 38.242, 36.382999999999996, 35.037, + 36.2, 31.717999999999996, 28.927999999999997, 32.540000000000006, + 35.448, 28.292, 35.059999999999995, 31.345000000000002, 36.122, 31.69, + 28.492, 29.308, 30.793000000000003, 28.784000000000002, + 28.275999999999996, 36.577999999999996, 30.220000000000002, 35.832, + 31.192, 36.102999999999994, 30.733999999999998, 30.574, + 35.455999999999996, 29.963, 37.967, 29.323999999999998, 36.643, + 31.200000000000003, 36.864999999999995, 32.344, 30.321, 29.214, 28.627, + 29.71, 29.006, 36.067, 29.583, 29.562, 37.795, 30.166999999999998, + 30.811999999999998, 33.319, 32.939, 39.233999999999995, 28.856, + 34.81700000000001, 30.324, 33.611000000000004, 33.707, + 30.191000000000003, 29.191, 29.23, 30.715, 29.281, 28.168, + 33.449000000000005, 36.36600000000001, 29.086, 30.589, 29.13, 28.789, + 29.156000000000002, 43.327, 34.439, 28.777, 30.586, 28.973000000000003, + 30.026, 40.023, 30.203, 28.328000000000003, 30.825000000000003, 29.739, + 31.504, 43.708000000000006, 29.296999999999997, 32.294, 31.733, 30.44, + 28.879, 30.349999999999998, 29.466, 29.302999999999997, 30, + 29.468999999999998, 28.740000000000002 + ], + "typeContainer": [], + "focus": [], + "inserterOpen": [], + "inserterHover": [], + "inserterSearch": [], + "listViewOpen": [] +} diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index dc5f6267945b7..c56c3b968764a 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.18.0 (2023-08-31) + ## 7.17.0 (2023-08-16) ## 7.16.0 (2023-08-10) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index d3ac35d5012d9..3f1625891960a 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.17.0", + "version": "7.18.0", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index bcb8180427291..840067e9fb9b3 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -107,6 +107,7 @@ function HeaderToolbar() { shortcut={ listViewShortcut } showTooltip={ ! showIconLabels } variant={ showIconLabels ? 'tertiary' : undefined } + aria-expanded={ isListViewOpen } /> ); @@ -148,6 +149,7 @@ function HeaderToolbar() { icon={ plus } label={ showIconLabels ? shortLabel : longLabel } showTooltip={ ! showIconLabels } + aria-expanded={ isInserterOpened } /> { ( isWideViewport || ! showIconLabels ) && ( <> diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js index 6d8982501d137..0380e648a1733 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js @@ -7,7 +7,6 @@ import { render, screen } from '@testing-library/react'; * WordPress dependencies */ import { EditorKeyboardShortcutsRegister } from '@wordpress/editor'; -import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; /** * Internal dependencies @@ -19,10 +18,10 @@ const noop = () => {}; describe( 'KeyboardShortcutHelpModal', () => { it( 'should match snapshot when the modal is active', () => { render( - + <> - + ); expect( @@ -34,13 +33,13 @@ describe( 'KeyboardShortcutHelpModal', () => { it( 'should not render the modal when inactive', () => { render( - + <> - + ); expect( diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index 1760edd8d3fc0..18f6acc9de89f 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -10,8 +10,6 @@ import { import { __ } from '@wordpress/i18n'; import { store as editorStore } from '@wordpress/editor'; import { store as blockEditorStore } from '@wordpress/block-editor'; -import { store as noticesStore } from '@wordpress/notices'; -import { store as preferencesStore } from '@wordpress/preferences'; import { createBlock } from '@wordpress/blocks'; /** @@ -21,39 +19,24 @@ import { store as editPostStore } from '../../store'; function KeyboardShortcuts() { const { getBlockSelectionStart } = useSelect( blockEditorStore ); - const { - getEditorMode, - isEditorSidebarOpened, - isListViewOpened, - isFeatureActive, - } = useSelect( editPostStore ); + const { getEditorMode, isEditorSidebarOpened, isListViewOpened } = + useSelect( editPostStore ); const isModeToggleDisabled = useSelect( ( select ) => { const { richEditingEnabled, codeEditingEnabled } = select( editorStore ).getEditorSettings(); return ! richEditingEnabled || ! codeEditingEnabled; }, [] ); - const { createInfoNotice } = useDispatch( noticesStore ); - const { switchEditorMode, openGeneralSidebar, closeGeneralSidebar, toggleFeature, setIsListViewOpened, - setIsInserterOpened, + toggleDistractionFree, } = useDispatch( editPostStore ); const { registerShortcut } = useDispatch( keyboardShortcutsStore ); - const { set: setPreference } = useDispatch( preferencesStore ); - - const toggleDistractionFree = () => { - setPreference( 'core/edit-post', 'fixedToolbar', false ); - setIsInserterOpened( false ); - setIsListViewOpened( false ); - closeGeneralSidebar(); - }; - const { replaceBlocks } = useDispatch( blockEditorStore ); const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = useSelect( blockEditorStore ); @@ -224,16 +207,6 @@ function KeyboardShortcuts() { useShortcut( 'core/edit-post/toggle-distraction-free', () => { toggleDistractionFree(); - toggleFeature( 'distractionFree' ); - createInfoNotice( - isFeatureActive( 'distractionFree' ) - ? __( 'Distraction free on.' ) - : __( 'Distraction free off.' ), - { - id: 'core/edit-post/distraction-free-mode/notice', - type: 'snackbar', - } - ); } ); useShortcut( 'core/edit-post/toggle-sidebar', ( event ) => { @@ -252,8 +225,9 @@ function KeyboardShortcuts() { } ); // Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar. - useShortcut( 'core/edit-post/toggle-list-view', () => { + useShortcut( 'core/edit-post/toggle-list-view', ( event ) => { if ( ! isListViewOpened() ) { + event.preventDefault(); setIsListViewOpened( true ); } } ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 35969ca1eb47d..c0018d40d6ef8 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -23,7 +23,7 @@ import { BlockBreadcrumb, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { Button, ScrollLock, Popover } from '@wordpress/components'; +import { Button, ScrollLock } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { PluginArea } from '@wordpress/plugins'; import { __, _x, sprintf } from '@wordpress/i18n'; @@ -357,7 +357,6 @@ function Layout() { - ); diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap deleted file mode 100644 index bcaed355d48ad..0000000000000 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,942 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EditPostPreferencesModal should match snapshot when the modal is active large viewports 1`] = ` -.emotion-0 { - font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',sans-serif; - font-size: 13px; - box-sizing: border-box; -} - -.emotion-0 *, -.emotion-0 *::before, -.emotion-0 *::after { - box-sizing: inherit; -} - -.components-panel__row .emotion-2 { - margin-bottom: inherit; -} - -.emotion-4 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: calc(4px * 3); - -webkit-box-pack: start; - -ms-flex-pack: start; - -webkit-justify-content: flex-start; - justify-content: flex-start; - width: 100%; -} - -.emotion-4>* { - min-width: 0; -} - -.emotion-6 { - display: block; - max-height: 100%; - max-width: 100%; - min-height: 0; - min-width: 0; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.emotion-8 { - margin-top: calc(4px * 2); - margin-bottom: 0; - font-size: 12px; - font-style: normal; - color: #757575; -} - - -`; - -exports[`EditPostPreferencesModal should match snapshot when the modal is active small viewports 1`] = ` -.emotion-0 { - overflow-x: hidden; -} - -.emotion-2 { - overflow-x: auto; - max-height: 100%; -} - -.emotion-3 { - background-color: #fff; - color: #1e1e1e; - position: relative; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); - outline: none; - box-shadow: none; - border-radius: calc(2px - 1px); -} - -.emotion-5 { - height: 100%; -} - -.emotion-7 { - box-sizing: border-box; - height: auto; - max-height: 100%; - padding: calc(4px * 4); -} - -.emotion-7:first-of-type { - border-top-left-radius: calc(2px - 1px); - border-top-right-radius: calc(2px - 1px); -} - -.emotion-7:last-of-type { - border-bottom-left-radius: calc(2px - 1px); - border-bottom-right-radius: calc(2px - 1px); -} - -.emotion-9 { - border-radius: 2px; -} - -.emotion-9>*:first-of-type>* { - border-top-left-radius: 2px; - border-top-right-radius: 2px; -} - -.emotion-9>*:last-of-type>* { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; -} - -.emotion-11 { - width: 100%; - display: block; -} - -.emotion-13 { - font-size: 13px; - font-family: inherit; - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - appearance: none; - border: 1px solid transparent; - cursor: pointer; - background: none; - text-align: start; - padding: calc((36px - calc(13px * 1.2) - 2px) / 2) 12px; - box-sizing: border-box; - width: 100%; - display: block; - margin: 0; - color: inherit; - border-radius: 2px; -} - -.emotion-13 svg, -.emotion-13 path { - fill: currentColor; -} - -.emotion-13:hover { - color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); -} - -.emotion-13:focus { - box-shadow: none; - outline: none; -} - -.emotion-13:focus-visible { - box-shadow: 0 0 0 var( --wp-admin-border-width-focus ) var( - --wp-components-color-accent, - var( --wp-admin-theme-color, var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)) ) - ); - outline: 2px solid transparent; - outline-offset: 0; -} - -.emotion-15 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - gap: calc(4px * 2); - -webkit-box-pack: justify; - -webkit-justify-content: space-between; - justify-content: space-between; - width: 100%; -} - -.emotion-15>* { - min-width: 0; -} - -.emotion-17 { - display: block; - max-height: 100%; - max-width: 100%; - min-height: 0; - min-width: 0; -} - -.emotion-19 { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.emotion-47 { - background: transparent; - display: block; - margin: 0!important; - pointer-events: none; - position: absolute; - will-change: box-shadow; - border-radius: inherit; - bottom: 0; - box-shadow: 0 0px 0px 0 rgba(0, 0, 0, 0); - opacity: 1; - left: 0; - right: 0; - top: 0; - -webkit-transition: box-shadow 200ms cubic-bezier(0.08, 0.52, 0.52, 1); - transition: box-shadow 200ms cubic-bezier(0.08, 0.52, 0.52, 1); - border-radius: 2px; -} - -@media ( prefers-reduced-motion: reduce ) { - .emotion-47 { - transition-duration: 0ms; - } -} - - 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 90b7537bf48ab..f3a7b84d5ded6 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 @@ -11,12 +11,10 @@ import { } from '@wordpress/core-data'; import { useMemo } from '@wordpress/element'; import { - BlockEditorKeyboardShortcuts, CopyHandler, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; -import { ShortcutProvider } from '@wordpress/keyboard-shortcuts'; import { store as preferencesStore } from '@wordpress/preferences'; /** @@ -99,22 +97,19 @@ export default function WidgetAreasBlockEditorProvider( { ); return ( - - + - - - { children } - - - - + + { children } + + + ); } diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 56421ebe357ba..eb87d22fefef9 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -70,7 +70,7 @@ export function initializeEditor( id, settings ) { themeStyles: true, } ); - dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters(); + dispatch( blocksStore ).reapplyBlockTypeFilters(); registerCoreBlocks( coreBlocks ); registerLegacyWidgetBlock(); if ( process.env.IS_GUTENBERG_PLUGIN ) { diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 2a7d933ad5c66..30c51d93f23b3 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 13.18.0 (2023-08-31) + ## 13.17.0 (2023-08-16) ## 13.16.0 (2023-08-10) diff --git a/packages/editor/package.json b/packages/editor/package.json index ba7cba3fa03e5..e0ab76755e547 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.17.0", + "version": "13.18.0", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/src/components/post-last-revision/index.js b/packages/editor/src/components/post-last-revision/index.js index 17df8e8c38d3b..f4d896555fbf6 100644 --- a/packages/editor/src/components/post-last-revision/index.js +++ b/packages/editor/src/components/post-last-revision/index.js @@ -28,7 +28,6 @@ function LastRevision() {