diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 9bd6ba212f0d80..777549e334aa96 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -17,14 +17,11 @@ concurrency: jobs: e2e-puppeteer: - name: Puppeteer - ${{ matrix.part }} + name: Puppeteer runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false - matrix: - part: [1, 2, 3] - totalParts: [3] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -43,8 +40,7 @@ jobs: - name: Running the tests run: | - npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % ${{ matrix.totalParts }} == ${{ matrix.part }} - 1' < ~/.jest-e2e-tests ) + npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 @@ -69,8 +65,8 @@ jobs: strategy: fail-fast: false matrix: - part: [1, 2, 3, 4] - totalParts: [4] + part: [1, 2, 3, 4, 5, 6, 7, 8] + totalParts: [8] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 485668a755b8c2..517febe9774a99 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -68,13 +68,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 bd2a881101727b03b0be09382b34841af5a3c03e and it needs to be updated every WP major release. + # The current one is b61dde2e5ec29d98801e623de968bfb286c5be3f 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 bd2a881101727b03b0be09382b34841af5a3c03e --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" + ./bin/plugin/cli.js perf $GITHUB_SHA b61dde2e5ec29d98801e623de968bfb286c5be3f --tests-branch $GITHUB_SHA --wp-version "$WP_MAJOR" - name: Compare performance with custom branches if: github.event_name == 'workflow_dispatch' @@ -97,7 +97,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 bd2a881101727b03b0be09382b34841af5a3c03e $COMMITTED_AT + ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA b61dde2e5ec29d98801e623de968bfb286c5be3f $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 diff --git a/.github/workflows/stale-issue-gardening.yml b/.github/workflows/stale-issue-gardening.yml index 0bdb1cfbf0cefd..cbeb04ead53214 100644 --- a/.github/workflows/stale-issue-gardening.yml +++ b/.github/workflows/stale-issue-gardening.yml @@ -27,8 +27,8 @@ jobs: remove-stale-when-updated: true stale-issue-label: '[Status] Stale' - name: 'Flaky test issues without activity' - message: 'This issue has gone 30 days without any activity.' - days-before-stale: 30 + message: 'This issue has gone 15 days without any activity.' + days-before-stale: 15 days-before-close: 1 only-labels: '[Type] Flaky Test' remove-stale-when-updated: true diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 65ba01d0b70e89..78f70cc4ed9f74 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -126,7 +126,7 @@ jobs: # dependency versions are installed and cached. ## - name: Set up PHP - uses: shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0 # v2.27.1 + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.28.0 with: php-version: '${{ matrix.php }}' ini-file: development @@ -226,7 +226,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Set up PHP - uses: shivammathur/setup-php@a36e1e52ff4a1c9e9c9be31551ee4712a6cb6bd0 # v2.27.1 + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.28.0 with: php-version: '7.4' coverage: none diff --git a/changelog.txt b/changelog.txt index 8159928bd84fb4..2356249f0b9f5e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,386 @@ == Changelog == += 17.1.3 = + + +## Changelog + +### Bug fixes + +#### Components +- https://github.com/WordPress/gutenberg/pull/56570 + + + += 17.1.2 = + +## Changelog + +### Bug Fixes + +#### Block Editor + +- PostCSS style transformation: fail gracefully instead of throwing an error (https://github.com/WordPress/gutenberg/pull/56093) + +## Contributors + +The following contributors merged PRs in this release: + +@zaguiini + + += 17.1.1 = + +# Changelog + +## Bug Fixes + +### Block Library + +Fix fatal error when calling undefined block library function. #56459 + + += 17.0.3 = + +## Changelog + +### Bug Fixes + +#### Block Editor + +- PostCSS style transformation: fail gracefully instead of throwing an error (https://github.com/WordPress/gutenberg/pull/56093) + + + += 17.1.0 = + + +## Changelog + +### Enhancements + +#### Block Library +- Navigation block: Fix Inaccurate description of the Show icon button setting. ([55429](https://github.com/WordPress/gutenberg/pull/55429)) +- Query Loop: Add accesibility markup at the end of the loop in all cases. ([55890](https://github.com/WordPress/gutenberg/pull/55890)) +- Template Part: Add fallback to the current theme when not provided. ([55965](https://github.com/WordPress/gutenberg/pull/55965)) +- Update components to use __next40pxDefaultSize. ([56022](https://github.com/WordPress/gutenberg/pull/56022)) + +#### Components +- Tabs: Improve focus behavior. ([55287](https://github.com/WordPress/gutenberg/pull/55287)) +- Tabs: Update subcomponents to accept full HTML element props. ([55860](https://github.com/WordPress/gutenberg/pull/55860)) +- TextControl: Add opt-in prop for 40px default size. ([55471](https://github.com/WordPress/gutenberg/pull/55471)) +- ToggleGroupControl: Add opt-in prop for 40px default size. ([55789](https://github.com/WordPress/gutenberg/pull/55789)) + +#### Patterns +- Move "Manage patterns" below "Detach pattern". ([56018](https://github.com/WordPress/gutenberg/pull/56018)) +- Show theme patterns from directory in site editor. ([55877](https://github.com/WordPress/gutenberg/pull/55877)) + +#### Global Styles +- Global Style Revisions: Ensure consistent back button behaviour. ([55881](https://github.com/WordPress/gutenberg/pull/55881)) +- Global Styles Revisions: More descriptive text timeline. ([55868](https://github.com/WordPress/gutenberg/pull/55868)) +- Global Styles Revisions: Add route for single styles revisions. ([55827](https://github.com/WordPress/gutenberg/pull/55827)) + +#### Block Locking +- Block Quick Navigation: Truncate text. ([56142](https://github.com/WordPress/gutenberg/pull/56142)) + +#### Block Editor +- Button block: Support double enter to skip to default block. ([56134](https://github.com/WordPress/gutenberg/pull/56134)) + +#### Design Tools +- Add block gap support to Quote block. ([56064](https://github.com/WordPress/gutenberg/pull/56064)) + +#### Post Editor +- "Detach" text change in template options. ([55870](https://github.com/WordPress/gutenberg/pull/55870)) + +#### Site Editor +- Site editor: Add edit page slug field. ([55767](https://github.com/WordPress/gutenberg/pull/55767)) + +#### Interactivity API +- Server directive processing: Process only root blocks. ([55739](https://github.com/WordPress/gutenberg/pull/55739)) + +#### Block settings menu +- Remove the extraneous template part title in replace control. ([55603](https://github.com/WordPress/gutenberg/pull/55603)) + +#### List View +- Add keyboard shortcut to select all blocks. ([54899](https://github.com/WordPress/gutenberg/pull/54899)) + + +### New APIs + +- Download blob: Remove downloadjs dependency. ([56024](https://github.com/WordPress/gutenberg/pull/56024)) + + +### Bug Fixes + +#### Block Library +- Background Image Support: Hide the background image reset button when there's no image. ([55973](https://github.com/WordPress/gutenberg/pull/55973)) +- Background image support: Fix focus loss when resetting background image. ([55984](https://github.com/WordPress/gutenberg/pull/55984)) +- Custom Link: Decode value in URL input field. ([55549](https://github.com/WordPress/gutenberg/pull/55549)) +- Fix lightbox trigger styles. ([55859](https://github.com/WordPress/gutenberg/pull/55859)) +- Form block: Use `type="submit"` for buttons. ([55690](https://github.com/WordPress/gutenberg/pull/55690)) +- Image block: Add check for lightbox values during image block migration. ([56057](https://github.com/WordPress/gutenberg/pull/56057)) +- Image block: Don't show pointer cursor on linked image in the editor. ([55882](https://github.com/WordPress/gutenberg/pull/55882)) +- Lightbox: Fix button misalignment in gallery image. ([56060](https://github.com/WordPress/gutenberg/pull/56060)) +- Lightbox: Fix close button position. ([56125](https://github.com/WordPress/gutenberg/pull/56125)) +- Missing block: Use raw source for originalContent. ([56014](https://github.com/WordPress/gutenberg/pull/56014)) +- Navigation Link block: Register variations on post type / taxonomy registration. ([54801](https://github.com/WordPress/gutenberg/pull/54801)) +- Pattern: Fix regression error in post type templates. ([55858](https://github.com/WordPress/gutenberg/pull/55858)) +- Pattern: Process embeds. ([55979](https://github.com/WordPress/gutenberg/pull/55979)) +- Post feature image block: Wrap images with hrefs in an A tag. ([55498](https://github.com/WordPress/gutenberg/pull/55498)) +- Quote Block: Fix the Quote block layout supports. ([55240](https://github.com/WordPress/gutenberg/pull/55240)) +- Read More block: Reduce text decoration specificity. ([56038](https://github.com/WordPress/gutenberg/pull/56038)) + +#### Data Views +- DataViews: Add missing key to `ResetFilters` component. ([56189](https://github.com/WordPress/gutenberg/pull/56189)) +- DataViews: Fix issue with irrelevant statuses. ([55967](https://github.com/WordPress/gutenberg/pull/55967)) +- DataViews: Fix nested button tags on sidebar. ([56089](https://github.com/WordPress/gutenberg/pull/56089)) +- DataViews: Fix pagination on manual input. ([55940](https://github.com/WordPress/gutenberg/pull/55940)) +- DataViews: Fix spacing issue in top-level bar. ([56151](https://github.com/WordPress/gutenberg/pull/56151)) +- DataViews: Fix status filter upon switching the default views from the sidebar. ([55856](https://github.com/WordPress/gutenberg/pull/55856)) +- DataViews: Make items per page an even number. ([55906](https://github.com/WordPress/gutenberg/pull/55906)) +- DataViews: Make used taxonomy private. ([55918](https://github.com/WordPress/gutenberg/pull/55918)) +- DataViews: Reset pagination upon filter change. ([55797](https://github.com/WordPress/gutenberg/pull/55797)) +- DataViews: Add a missing icon for the side by side view. ([55925](https://github.com/WordPress/gutenberg/pull/55925)) + +#### Components +- DropdownMenu: Remove extra vertical space around the toggle button. ([56136](https://github.com/WordPress/gutenberg/pull/56136)) +- DropdownMenuV2: Prevent default on Escape key presses. ([55962](https://github.com/WordPress/gutenberg/pull/55962)) +- DropdownMenuV2: Use the `Icon` component to render radio checks. ([55964](https://github.com/WordPress/gutenberg/pull/55964)) + +#### Typography +- Fix fatal error in WP_Fonts_Resolver::Get_settings(). ([55981](https://github.com/WordPress/gutenberg/pull/55981)) +- Font Library: Create fonts dir if a font face needs to use the filesystem. ([56120](https://github.com/WordPress/gutenberg/pull/56120)) +- Font Library: Fix font installation failure. ([55893](https://github.com/WordPress/gutenberg/pull/55893)) + +#### Block Editor +- Iframe: Bubble events from html element instead of body element to fix drag chip positioning. ([56099](https://github.com/WordPress/gutenberg/pull/56099)) +- Post Featured Image: Handling correctly when uploading a file without mime type. ([56133](https://github.com/WordPress/gutenberg/pull/56133)) +- Block Editor: Fix Block editor crash. ([56051](https://github.com/WordPress/gutenberg/pull/56051)) +- Move clientId key to BlockContextualToolbar. ([56008](https://github.com/WordPress/gutenberg/pull/56008)) + +#### Patterns +- Add context for translators to any unclear usage of "synced". ([55935](https://github.com/WordPress/gutenberg/pull/55935)) +- Use existing download function for JSON downloads to fix non-ASCII encoding. ([55912](https://github.com/WordPress/gutenberg/pull/55912)) + +#### Inspector Controls +- Global Styles: Don't show "Apply Styles Globally" button in non-block based themes. ([56033](https://github.com/WordPress/gutenberg/pull/56033)) + +#### Template Editor +- Templates: Update filter to call all of the individual methods. ([55980](https://github.com/WordPress/gutenberg/pull/55980)) + +#### Global Styles +- Global styles revisions: Load unsaved revision item into the revisions preview. ([55880](https://github.com/WordPress/gutenberg/pull/55880)) + +#### Post Editor +- Edit Post: Fix pattern modal reopening when making the title empty again. ([55873](https://github.com/WordPress/gutenberg/pull/55873)) + +#### Data Layer +- Core data: Fix wrong store results when page receives less items that what is stored. ([55832](https://github.com/WordPress/gutenberg/pull/55832)) + + +### Accessibility + +#### Data Views +- DataViews: Add labels to "in-filters". ([56001](https://github.com/WordPress/gutenberg/pull/56001)) +- DataViews: Show actions label. ([56027](https://github.com/WordPress/gutenberg/pull/56027)) + +#### Components +- Fix the image link button pressed state. ([56123](https://github.com/WordPress/gutenberg/pull/56123)) + +#### Block Editor +- Fix mismatching link control action buttons visual order and DOM order. ([56042](https://github.com/WordPress/gutenberg/pull/56042)) +- Escape on Block Toolbar returns focus to Editor Canvas. ([55712](https://github.com/WordPress/gutenberg/pull/55712)) + +#### Site Editor +- Prevent sidebar focus in site editor on small screens. ([55934](https://github.com/WordPress/gutenberg/pull/55934)) + +#### Block Library +- Heading level dropdown: Remove obtrusive tooltips in favor of visible text. ([56035](https://github.com/WordPress/gutenberg/pull/56035)) + + +### Performance + +#### Tooling +- Add a metric to trace template navigation in the site editor. ([55796](https://github.com/WordPress/gutenberg/pull/55796)) + +#### List View +- ListViewBlock: Combine 'useSelect' hooks. ([55889](https://github.com/WordPress/gutenberg/pull/55889)) + +#### Block Editor +- Block Editor: Optimize 'Block Hooks' inspector controls. ([56101](https://github.com/WordPress/gutenberg/pull/56101)) +- Block Editor: Optimize BlockListAppender. ([56116](https://github.com/WordPress/gutenberg/pull/56116)) + +#### Site Editor +- Avoid rerendering the sitehub unnecessarily. ([55818](https://github.com/WordPress/gutenberg/pull/55818)) + +#### Layout +- Block Editor: Optimize layout style renderer subscription. ([55762](https://github.com/WordPress/gutenberg/pull/55762)) + + +### Experiments + +#### Data Views +- DataViews: Add ability to create custom views. ([55773](https://github.com/WordPress/gutenberg/pull/55773)) +- DataViews: Add control to reset all filters at once. ([55955](https://github.com/WordPress/gutenberg/pull/55955)) +- DataViews: Add delete and restore actions. ([55781](https://github.com/WordPress/gutenberg/pull/55781)) +- DataViews: Add initial "Side by side" prototype. ([55343](https://github.com/WordPress/gutenberg/pull/55343)) +- DataViews: Add new page size option. ([56112](https://github.com/WordPress/gutenberg/pull/56112)) +- DataViews: Add rename functionality to custom views. ([55997](https://github.com/WordPress/gutenberg/pull/55997)) +- DataViews: Allow users to add filters dynamically. ([55992](https://github.com/WordPress/gutenberg/pull/55992)) +- DataViews: Update 'All pages' sidebar heading. ([56148](https://github.com/WordPress/gutenberg/pull/56148)) +- DataViews: Update 'View' button. ([56144](https://github.com/WordPress/gutenberg/pull/56144)) +- DataViews: Update `all templates` page. ([55848](https://github.com/WordPress/gutenberg/pull/55848)) +- DataViews: Update author and title fields in template's list. ([56029](https://github.com/WordPress/gutenberg/pull/56029)) +- DataViews: Update filters in view configuration. ([55735](https://github.com/WordPress/gutenberg/pull/55735)) +- DataViews: Add filters to table columns. ([55508](https://github.com/WordPress/gutenberg/pull/55508)) +- DataViews: Add: Ability to delete custom views. ([55924](https://github.com/WordPress/gutenberg/pull/55924)) +- DataViews: Add: Custom views header indication. ([55926](https://github.com/WordPress/gutenberg/pull/55926)) +- DataViews: Remove unnecessary label when no visible filters exist. ([55838](https://github.com/WordPress/gutenberg/pull/55838)) + + +### Documentation + +- Add a first block type page to the platform documentation. ([56109](https://github.com/WordPress/gutenberg/pull/56109)) +- Add new block development "Quick Start Guide" and update the `create-block-tutorial-template`. ([56056](https://github.com/WordPress/gutenberg/pull/56056)) +- Clean up DataViews docs: `filter.id` is not used. ([55833](https://github.com/WordPress/gutenberg/pull/55833)) +- DataViews: Document `enableSorting` and `enableHiding`. ([55988](https://github.com/WordPress/gutenberg/pull/55988)) +- DataViews: Document actions. ([55959](https://github.com/WordPress/gutenberg/pull/55959)) +- Doc: Corrected + updated links. ([56084](https://github.com/WordPress/gutenberg/pull/56084)) +- Doc: Fixes wrong link in #56084. ([56106](https://github.com/WordPress/gutenberg/pull/56106)) +- Docs: Changes imports from `wp.editor` to `wp.blockEditor` for PlainText and RichText. ([55841](https://github.com/WordPress/gutenberg/pull/55841)) +- Fix formatting issue in the "Get started with create-block" doc. ([55872](https://github.com/WordPress/gutenberg/pull/55872)) +- Fix: 404 Link on git workflow docs. ([55897](https://github.com/WordPress/gutenberg/pull/55897)) +- Fix: 404 link in get-started-with-create-block docs. ([55932](https://github.com/WordPress/gutenberg/pull/55932)) +- Fix: Create meta block link in block attributes documentation. ([55804](https://github.com/WordPress/gutenberg/pull/55804)) +- Fix: Filter duotone link on block-supports documentation. ([55896](https://github.com/WordPress/gutenberg/pull/55896)) +- Fix: Two invalid links on docs/contributors/documentation/README.md. ([55843](https://github.com/WordPress/gutenberg/pull/55843)) +- New additional resource for wp-env. ([55987](https://github.com/WordPress/gutenberg/pull/55987)) +- Update documentation to clarify workflow branch for release package publishing. ([56183](https://github.com/WordPress/gutenberg/pull/56183)) +- Update jest links to the new site. ([55802](https://github.com/WordPress/gutenberg/pull/55802)) + + +### Code Quality + +- Block lib: Remove multiline=false (deprecated). ([56113](https://github.com/WordPress/gutenberg/pull/56113)) +- Delete unused `SelectedBlockPopover` component. ([55821](https://github.com/WordPress/gutenberg/pull/55821)) +- Fix: Remove unrequired nullish coalescing. ([55854](https://github.com/WordPress/gutenberg/pull/55854)) +- Fix: Use of integer value in a conditional rendering condition on Gradients. ([55855](https://github.com/WordPress/gutenberg/pull/55855)) +- Give nice unique names to block controls HOCs. ([55795](https://github.com/WordPress/gutenberg/pull/55795)) +- Migrating `PatternTransformationsMenu`. ([56122](https://github.com/WordPress/gutenberg/pull/56122)) +- Migrating block inserter media tab components. ([56195](https://github.com/WordPress/gutenberg/pull/56195)) +- Move document tools motion to header-edit-mode layout level. ([55904](https://github.com/WordPress/gutenberg/pull/55904)) +- Only render block toolbar if blockType has value. ([55861](https://github.com/WordPress/gutenberg/pull/55861)) +- Refactor Edit Widgets Document Tools Navigation to own component. ([55778](https://github.com/WordPress/gutenberg/pull/55778)) +- Refactor Selected Block Tools. ([55737](https://github.com/WordPress/gutenberg/pull/55737)) +- Refactor Site Editor Document Tools Navigation to own component. ([55770](https://github.com/WordPress/gutenberg/pull/55770)) +- Remove BlockStyles.Slot empty component. ([55991](https://github.com/WordPress/gutenberg/pull/55991)) +- Remove obsolete `queryContext`. ([56034](https://github.com/WordPress/gutenberg/pull/56034)) +- Remove unnecessary empty className. ([55998](https://github.com/WordPress/gutenberg/pull/55998)) +- Rename Unforward to Unforwarded and export the named const. ([55820](https://github.com/WordPress/gutenberg/pull/55820)) +- Render Selected Block Tools in Header when using Top Toolbar. ([55787](https://github.com/WordPress/gutenberg/pull/55787)) +- Reusable Blocks: Unlock a private hook and a component at the file level. ([55809](https://github.com/WordPress/gutenberg/pull/55809)) +- Server directive processing: Improve how block references are saved. ([56107](https://github.com/WordPress/gutenberg/pull/56107)) +- Share the editor settings between the post and site editors. ([55970](https://github.com/WordPress/gutenberg/pull/55970)) +- Site Editor: Fix deprecation console error in top toolbar. ([55678](https://github.com/WordPress/gutenberg/pull/55678)) +- Site Editor: Unlock global styles' private hooks at the file level. ([55800](https://github.com/WordPress/gutenberg/pull/55800)) +- Site Editor: Update edited entity sync logic. ([55928](https://github.com/WordPress/gutenberg/pull/55928)) +- Site Editor: Use EditorProvider instead of custom logic. ([56000](https://github.com/WordPress/gutenberg/pull/56000)) +- SiteEditor: Optimize BackToPageNotification component. ([56102](https://github.com/WordPress/gutenberg/pull/56102)) +- SiteEditor: Refactor disable non page content blocks. ([56103](https://github.com/WordPress/gutenberg/pull/56103)) +- Unify the PageUrl and PageSlug components between site and post editors. ([56203](https://github.com/WordPress/gutenberg/pull/56203)) + +#### Data Views +- DataViews: Fix translatable string. ([56075](https://github.com/WordPress/gutenberg/pull/56075)) +- DataViews: Remove `filter.name`. ([55834](https://github.com/WordPress/gutenberg/pull/55834)) +- DataViews: Remove reset values from filters. ([55839](https://github.com/WordPress/gutenberg/pull/55839)) +- DataViews: Remove unnecessary `sortingFn` prop from field description. ([55989](https://github.com/WordPress/gutenberg/pull/55989)) +- DataViews: Simplify filters API. ([55917](https://github.com/WordPress/gutenberg/pull/55917)) +- DataViews: Update actions API. ([56026](https://github.com/WordPress/gutenberg/pull/56026)) + +#### Block Editor +- Rich text: Remove preserveWhiteSpace serialisation differences. ([55999](https://github.com/WordPress/gutenberg/pull/55999)) +- Rich text: highlight format: Gracefully handle old span format. ([56071](https://github.com/WordPress/gutenberg/pull/56071)) +- Update Link Control labels to use gray-900. ([55867](https://github.com/WordPress/gutenberg/pull/55867)) + +#### Components +- `DisclosureContent`: Migrate from `reakit` to `@ariakit/react`. ([55639](https://github.com/WordPress/gutenberg/pull/55639)) +- `Divider`: Migrate from `reakit` to `@ariakit/react`. ([55622](https://github.com/WordPress/gutenberg/pull/55622)) +- `RadioGroup`: Migrate from `reakit` to `ariakit`. ([55580](https://github.com/WordPress/gutenberg/pull/55580)) + +#### Site Editor +- Core Data: Move the template lookup to core-data selectors/resolvers. ([55883](https://github.com/WordPress/gutenberg/pull/55883)) +- Don't use 'useEntityRecord' to only dispatch actions. ([56076](https://github.com/WordPress/gutenberg/pull/56076)) + +#### Block Library +- Navigation: Refactor the PHP render function to make it easier to make changes in the future. ([55605](https://github.com/WordPress/gutenberg/pull/55605)) +- Update `blockEditor.__unstableCanInsertBlockType` hook namespace. ([55845](https://github.com/WordPress/gutenberg/pull/55845)) + +#### Data Layer +- Data: Fix ESLint warnings for the 'useSelect' hook. ([55916](https://github.com/WordPress/gutenberg/pull/55916)) + +#### Post Editor +- Edit Post: Use a single 'useSelect' hook for getting selectors. ([55902](https://github.com/WordPress/gutenberg/pull/55902)) + +#### Colors +- Add Unit testing for duotone enhanced pagination. ([55542](https://github.com/WordPress/gutenberg/pull/55542)) + +#### Patterns +- Split up the block editor inserter patterns tab into separate component files. ([55315](https://github.com/WordPress/gutenberg/pull/55315)) + +#### Design Tools +- Block styles: Remove __unstableElementContext in favour of useStyleOverride. ([54493](https://github.com/WordPress/gutenberg/pull/54493)) + + +### Tools + +- Issue Templates: Add default type labels to issue templates. ([55826](https://github.com/WordPress/gutenberg/pull/55826)) +- Label enforcer: Make the warning message less scary for new contributors. ([55900](https://github.com/WordPress/gutenberg/pull/55900)) +- Quote feature request label. ([55862](https://github.com/WordPress/gutenberg/pull/55862)) + +#### Testing +- Disable 'no-conditional-in-test' ESLint rule for Playwright. ([56088](https://github.com/WordPress/gutenberg/pull/56088)) +- Fix 'Block Switcher' test file name for Playwright end-to-end tests. ([55840](https://github.com/WordPress/gutenberg/pull/55840)) +- Fix flaky 'Meta boxes' end-to-end tests. ([56083](https://github.com/WordPress/gutenberg/pull/56083)) +- Migrate 'CPT locking' end-to-end tests to Playwright. ([55929](https://github.com/WordPress/gutenberg/pull/55929)) +- Migrate 'Meta boxes' end-to-end tests to Playwright. ([55915](https://github.com/WordPress/gutenberg/pull/55915)) +- Migrate 'Plugins API' end-to-end tests to Playwright. ([55958](https://github.com/WordPress/gutenberg/pull/55958)) +- Migrate 'annotations' end-to-end tests to Playwright. ([55966](https://github.com/WordPress/gutenberg/pull/55966)) +- Migrate 'container blocks' end-to-end tests to Playwright. ([56141](https://github.com/WordPress/gutenberg/pull/56141)) +- Migrate 'inner-blocks-prioritized-inserter-blocks' end-to-end tests to Playwright. ([55828](https://github.com/WordPress/gutenberg/pull/55828)) +- Migrate 'inner-blocks-render-appender' end-to-end tests to Playwright. ([55814](https://github.com/WordPress/gutenberg/pull/55814)) +- Migrate 'meta-attribute-block' end-to-end tests to Playwright. ([55830](https://github.com/WordPress/gutenberg/pull/55830)) +- Migrate Child Block Test to Playwright. ([55199](https://github.com/WordPress/gutenberg/pull/55199)) +- Migrate flaky PostPublishButton end-to-end tests to Playwright. ([52285](https://github.com/WordPress/gutenberg/pull/52285)) +- Perf Tests: Stabilise the Site Editor metrics. ([55922](https://github.com/WordPress/gutenberg/pull/55922)) +- Playwright Utils: Fix 'clickBlockOptionsMenuItem' helper. ([55923](https://github.com/WordPress/gutenberg/pull/55923)) +- Query block enhanced pagination: Simplify test setup. ([55805](https://github.com/WordPress/gutenberg/pull/55805)) +- Site editor template preview: Add end-to-end test and aria-pressed attribute to template preview toggle. ([56096](https://github.com/WordPress/gutenberg/pull/56096)) +- Upgrade Playwright to 1.39.0. ([54051](https://github.com/WordPress/gutenberg/pull/54051)) +- end-to-end Utils: Add setPreferences and editPost utils. ([55099](https://github.com/WordPress/gutenberg/pull/55099)) +- end-to-end Utils: Add support for web-vitals.js. ([55660](https://github.com/WordPress/gutenberg/pull/55660)) + +#### Build Tooling +- Package `@ariakit/test` should be a dev dependency. ([56091](https://github.com/WordPress/gutenberg/pull/56091)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @joanrodas: Update Link Control labels to use gray-900. ([55867](https://github.com/WordPress/gutenberg/pull/55867)) +- @JorgeVilchez95: "Detach" text change in template options. ([55870](https://github.com/WordPress/gutenberg/pull/55870)) +- @sacerro: Styles: More descriptive text for revisions timeline. ([55868](https://github.com/WordPress/gutenberg/pull/55868)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @andrewhayward @andrewserong @anomiex @anton-vlasenko @aristath @artemiomorales @bph @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @DAreRodz @dcalhoun @dsas @ellatrix @flootr @fluiddot @gaambo @glendaviesnz @gziolo @jameskoster @jeryj @jhnstn @joanrodas @jorgefilipecosta @JorgeVilchez95 @jsnajdr @juanmaguitar @kevin940726 @Mamaduka @masteradhoc @matiasbenedetto @ndiego @ntsekouras @oandregal @peterwilsoncc @pooja-muchandikar @priethor @ramonjd @renatho @richtabor @sacerro @scruffian @shimotmk @SiobhyB @Soean @swissspidy @t-hamano @talldan @tellthemachines @torounit @tyxla @WunderBart @youknowriad + + + + = 17.0.2 = diff --git a/docs/README.md b/docs/README.md index d04df59e957529..b94a8d78d41a75 100644 --- a/docs/README.md +++ b/docs/README.md @@ -59,7 +59,7 @@ This handbook should be considered the canonical resource for all things related - [**Learn WordPress**](https://learn.wordpress.org/) - The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) - [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [block-editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. - [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository is another useful reference._ - +- [**End User Documentation**](https://wordpress.org/documentation/) - Documentation site targeted to the end user (not developers) where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). ## Are you in the right place? @@ -70,4 +70,4 @@ This handbook should be considered the canonical resource for all things related - [/apis](https://developer.wordpress.org/apis) - Common APIs Handbook - [/advanced-administration](https://developer.wordpress.org/advanced-administration) - WP Advanced Administration Handbook - [/rest-api](https://developer.wordpress.org/rest-api/) - REST API Handbook -- [/coding-standards](https://developer.wordpress.org/coding-standards) - Best practices for WordPress developers \ No newline at end of file +- [/coding-standards](https://developer.wordpress.org/coding-standards) - Best practices for WordPress developers diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index 83868f95ce184d..d19be240f4870f 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -25,6 +25,7 @@ Similar requirements apply to releasing WordPress's [npm packages](https://devel - [Automated cherry-picking](#automated-cherry-picking) - [Manual cherry-picking](#manual-cherry-picking) - [Publishing the release](#publishing-the-release) + - [Troubleshooting the release](#troubleshooting-the-release) - [Documenting the release](#documenting-the-release) - [Selecting the release highlights](#selecting-the-release-highlights) - [Requesting release assets](#requesting-release-assets) @@ -253,6 +254,41 @@ Once approved, the new Gutenberg version will be available to WordPress users al The final step is to write a release post on [make.wordpress.org/core](https://make.wordpress.org/core/). You can find some tips on that below. +#### Troubleshooting the release + +> The plugin was published to the WordPress.org plugin directory but the workflow failed. + +This has happened ocassionally, see [this one](https://github.com/WordPress/gutenberg/actions/runs/6955409957/job/18924124118) for example. + +It's important to check that: + +- the plugin from the directory works as expected +- the ZIP contents (see [Downloads](https://plugins.trac.wordpress.org/browser/gutenberg/)) looks correct (doesn't have anything obvious missing) +- the [Gutenberg SVN repo](https://plugins.trac.wordpress.org/browser/gutenberg/) has two new commits (see [the log](https://plugins.trac.wordpress.org/browser/gutenberg/)): + - the `trunk` folder should have "Commiting version X.Y.Z" + - there is a new `tags/X.Y.Z` folder with the same contents as `trunk` whose latest commit is "Tagging version X.Y.Z" + +Most likely, the tag folder couldn't be created. This is a [known issue](https://plugins.trac.wordpress.org/browser/gutenberg/) that [can be fixed manually](https://github.com/WordPress/gutenberg/issues/55295#issuecomment-1759292978). + +Either substitute SVN_USERNAME, SVN_PASSWORD, and VERSION for the proper values or set them as global environment variables first: + +```sh +# CHECKOUT THE REPOSITORY +svn checkout https://plugins.svn.wordpress.org/gutenberg/trunk --username "$SVN_USERNAME" --password "$SVN_PASSWORD" gutenberg-svn + +# MOVE TO THE LOCAL FOLDER +cd gutenberg-svn + +# IF YOU HAPPEN TO HAVE ALREADY THE REPO LOCALLY +# AND DIDN'T CHECKOUT, MAKE SURE IT IS UPDATED +# svn up . + +# COPY CURRENT TRUNK INTO THE NEW TAGS FOLDER +svn copy https://plugins.svn.wordpress.org/gutenberg/trunk https://plugins.svn.wordpress.org/gutenberg/tags/$VERSION -m 'Tagging version $VERSION' --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" +``` + +Ask around if you need help with any of this. + ### Documenting the release Documenting the release is led by the release manager with the help of [Gutenberg development team](https://developer.wordpress.org/block-editor/block-editor/contributors/repository-management/#teams) members. This process is comprised of a series of sequential steps that, because of the number of people involved, and the coordination required, need to adhere to a timeline between the RC and stable releases. Stable Gutenberg releases happen on Wednesdays, one week after the initial RC. @@ -422,7 +458,7 @@ Now, the `wp/X.Y` branch is ready for publishing npm packages. In order to start ![Run workflow dropdown for npm publishing](https://developer.wordpress.org/files/2023/07/image-2.png) -To publish packages to npm for the WordPress major release, select `wp` from the "Release type" dropdown and enter `X.Y` (example `5.2`) in the "WordPress major release" input field. Finally, press the green "Run workflow" button. It triggers the npm publishing job, and this needs to be approved by a Gutenberg Core team member. Locate the ["Publish npm packages" action](https://github.com/WordPress/gutenberg/actions/workflows/publish-npm-packages.yml) for the current publishing, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). +To publish packages to npm for the WordPress major release, select `trunk` as the branch to run the workflow from (this means that the script used to run the workflow comes from the trunk branch, though the packages themselves will published from the release branch as long as the correct "Release type" is selected below), then select `wp` from the "Release type" dropdown and enter `X.Y` (example `5.2`) in the "WordPress major release" input field. Finally, press the green "Run workflow" button. It triggers the npm publishing job, and this needs to be approved by a Gutenberg Core team member. Locate the ["Publish npm packages" action](https://github.com/WordPress/gutenberg/actions/workflows/publish-npm-packages.yml) for the current publishing, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). For the record, the manual process would look like the following: diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index 649fe10d439aa6..4449f13996c629 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 16.2-16.7 | 6.4.1 | | 16.2-16.7 | 6.4 | | 15.2-16.1 | 6.3.1 | | 15.2-16.1 | 6.3 | diff --git a/docs/getting-started/devenv/get-started-with-wp-scripts.md b/docs/getting-started/devenv/get-started-with-wp-scripts.md index 8a7d100c8921fa..6416adc081e70a 100644 --- a/docs/getting-started/devenv/get-started-with-wp-scripts.md +++ b/docs/getting-started/devenv/get-started-with-wp-scripts.md @@ -20,7 +20,7 @@ The package abstracts away much of the initial setup, configuration, and boilerp ## Quick start
- If you want to build a custom block, the @wordpress/create-block package allows you to scaffold the structure of files needed to create and register a block. It generates all the necessary code to start a project and integrates a modern JavaScript build setup (using wp-scripts) with no configuration required. Refer to Get started with create-block for more details. + If you use @wordpress/create-block package to scaffold the structure of files needed to create and register a block, you'll also get a modern JavaScript build setup (using wp-scripts) with no configuration required, so you don't need to worry about installing wp-scripts or enqueuing assets. Refer to Get started with create-block for more details.
### Installation @@ -64,15 +64,25 @@ Once installed, you can run the predefined scripts provided with `wp-scripts` by } ``` -These scripts can then be run using the command `npm run {script name}`. The two scripts you will use most often are `start` and `build` since they handle the build step. See the [package documentation](https://developer.wordpress.org/block-editor/packages/packages-scripts/) for all options. +These scripts can then be run using the command `npm run {script name}`. + +### The build process with `wp-scripts` + +The two scripts you will use most often are `start` and `build` since they handle the build step. See the [package documentation](https://developer.wordpress.org/block-editor/packages/packages-scripts/) for all options. When working on your project, use the `npm run start` command. This will start a development server and automatically rebuild the project whenever any change is detected. Note that the compiled code in `build/index.js` will not be optimized. When you are ready to deploy your project, use the `npm run build` command. This optimizes your code and makes it production-ready. -After the build finishes, you will see the compiled JavaScript file created at `build/index.js`. A `build/index.asset.php` file will also be created, which contains an array of dependencies and a version number (for cache busting). +After the build finishes, you will see the compiled JavaScript file created at `build/index.js`. + +A `build/index.asset.php` file will also be created in the build process, which contains an array of dependencies and a version number (for cache busting). Please, note that to register a block without this `wp-scripts` build process you'll need to manually create `*.asset.php` dependencies files (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-no-build-e621a6)). -Enqueue the file in the Editor using PHP as you would any other JavaScript file. You can refer to the [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) guide for more information, but here's a typical implementation. +### Enqueuing assets + +If you register a block via `register_block_type` the scripts defined in `block.json` will be automatically enqueued (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda)) + +To manually enqueue files in the editor, in any other context, you can refer to the [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) guide for more information, but here's a typical implementation. ```php /** @@ -91,6 +101,8 @@ function example_project_enqueue_editor_assets() { add_action( 'enqueue_block_editor_assets', 'example_project_enqueue_editor_assets' ); ``` +Here's [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) of manually enqueuing files in the editor. + ## Next steps While `start` and `build` will be the two most used scripts, several other useful tools come with `wp-scripts` that are worth exploring. Here's a look at a few. diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md new file mode 100644 index 00000000000000..6367603351c82b --- /dev/null +++ b/docs/getting-started/fundamentals/README.md @@ -0,0 +1,11 @@ +# Fundamentals of Block Development + +This section provides an introduction to the most important concepts in Block Development. + +In this section, you will learn: + +1. [**File structure of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block) - The purpose of each one of the types of files available for a block, the relationships between them, and their role in the output of the block. +1. [**`block.json`**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json) - How a block is defined using its `block.json` metadata and some relevant properties of this file. +1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. +1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How to set proper attributes to the block's markup wrapper. +1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/block-json.md b/docs/getting-started/fundamentals/block-json.md new file mode 100644 index 00000000000000..3d65a8f016914e --- /dev/null +++ b/docs/getting-started/fundamentals/block-json.md @@ -0,0 +1,115 @@ +# block.json + +The `block.json` file simplifies the processs of defining and registering a block by using the same block's definition in JSON format to register the block in both the server and the client. + +[![Open block.json diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-json.png)](https://excalidraw.com/#json=v1GrIkGsYGKv8P14irBy6,Yy0vl8q7DTTL2VsH5Ww27A "Open block.json diagram in excalidraw") + +
+Click here to see a full block example and check its block.json +
+ +Besides simplifying a block's registration, using a `block.json` has [several benefits](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#benefits-using-the-metadata-file), including improved performance and development. + +At [**Metadata in block.json**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) you can find a detailed explanation of all the properties you can set in a `block.json` for a block. With these properties you can define things such as: + +- Basic metadata of the block +- Files for the block's behavior, style, or output +- Data Storage in the Block +- Setting UI panels for the block + +## Basic metadata of the block + +Through properties of the `block.json`, we can define how the block will be uniquely identified, how it can be found, and the info displayed for the block in the Block Editor. Some of these properties are: + +- `apiVersion`: the version of [the API](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/) used by the block (current version is 2). +- `name`: a unique identifier for a block, including a namespace. +- `title`: a display title for a block. +- `category`: a block category for the block in the Inserter panel. +- `icon`: a [Dashicon](https://developer.wordpress.org/resource/dashicons) slug or a custom SVG icon. +- `description`: a short description visible in the block inspector. +- `keywords`: to locate the block in the inserter. +- `textdomain`: the plugin text-domain (important for things such as translations). + +## Files for the block's behavior, output, or style + +The `editorScript` and `editorStyle` properties allow defining Javascript and CSS files to be enqueued and loaded **only in the editor**. + +The `script` and `style` properties allow the definition of Javascript and CSS files to be enqueued and loaded **in both the editor and the front end**. + +The `viewScript` property allow us to define the Javascript file or files to be enqueued and loaded **only in the front end**. + +All these properties (`editorScript`, `editorStyle`, `script` `style`,`viewScript`) accept as a value a path for the file, a handle registered with `wp_register_script` or `wp_register_style`, or an array with a mix of both. Paths values in `block.json` are prefixed with `file:`. + +The `render` property ([introduced on WordPress 6.1](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/)) sets the path of a `.php` template file that will render the markup returned to the front end. This only method will be used to return the markup for the block on request only if `$render_callback` function has not been passed to the `register_block_type` function. + +## Data Storage in the Block with `attributes` + +The [`attributes` property](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/) allows a block to declare "variables" that store data or content for the block. + +_Example: Attributes as defined in block.json_ +```json +"attributes": { + "fallbackCurrentYear": { + "type": "string" + }, + "showStartingYear": { + "type": "boolean" + }, + "startingYear": { + "type": "string" + } +}, +``` +By default `attributes` are serialized and stored in the block's delimiter but this [can be configured](https://developer.wordpress.org/news/2023/09/understanding-block-attributes/). + +_Example: Atributes stored in the Markup representation of the block_ +```html + + +x +``` + +These attributes are passed to the React component `Edit`(to display in the Block Editor) and the `save` function (to return the markup saved to the DB) of the block, and to any server-side render definition for the block (see `render` prop above). + +The `Edit` component receives exclusively the capability of updating the attributes via the `setAttributes` function. + +_See how the attributes are passed to the [`Edit` component](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/edit.js), [the `save` function](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/save.js) and [the `render.php`](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/render.php) in this [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/copyright-date-block-09aac3) of the code above_ + +
+Check the attributes reference page for full info about the Attributes API. +
+ +[![Open Attributes diagram in excalidraw](https://developer.wordpress.org/files/2023/11/attributes.png)](https://excalidraw.com/#json=pSgCZy8q9GbH7r0oz2fL1,MFCLd6ddQHqi_UqNp5ZSgg "Open Attributes diagram in excalidraw") + + +## Enable UI settings panels for the block with `supports` + +The `supports` property allows a block to declare support for certain features, enabling users to customize specific settings (like colors or margins) from the Settings Sidebar. + +_Example: Supports as defined in block.json_ + +```json +"supports": { + "color": { + "text": true, + "link": true, + "background": true + } +} +``` + +The use of `supports` generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data. + +_Example: Supports custom settings stored in the Markup representation of the block_ + +```html + +

Hello World

+ +``` + +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/block-supports-6aa4dd/src/block.json)_ + +
+Check the supports reference page for full info about the Supports API. +
diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md new file mode 100644 index 00000000000000..b391d758b7e546 --- /dev/null +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -0,0 +1,114 @@ +# The block wrapper + +Each block's markup is wrapped by a container HTML tag that needs to have the proper attributes to fully work in the Block Editor and to reflect the proper block's style settings when rendered in the Block Editor and the front end. As developers, we have full control over the block's markup, and WordPress provides the tools to add the attributes that need to exist on the wrapper to our block's markup. + +Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`. + +
+The use of supports generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data +
+ +A block can have three sets of markup defined, each one of them with a specific target and purpose: + +- The one for the **Block Editor**, defined through a `edit` React component passed to `registerBlockType` when registering the block in the client. +- The one used to **save the block in the DB**, defined through a `save` function passed to `registerBlockType` when registering the block in the client. + - This markup will be returned to the front end on request if no dynamic render has been defined for the block. +- The one used to **dynamically render the markup of the block** returned to the front end on request, defined through the `render_callback` on `register_block_type` or the `render` PHP file in `block.json` + - If defined, this server-side generated markup will be returned to the front end, ignoring the markup stored in DB. + +For the React component `edit` and the `save` function, the block wrapper element should be a native DOM element (like `
`) or a React component that forwards any additional props to native DOM elements. Using a or component, for instance, would be invalid. + + +## The Edit component's markup + +The `useBlockProps()` hook available on the `@wordpress/block-editor` allows passing the required attributes for the Block Editor to the `edit` block's outer wrapper. + +Among other things, the `useBlockProps()` hook takes care of including in this wrapper: +- An `id` for the block's markup +- Some accesibility and `data-` attributes +- Classes and inline styles reflecting custom settings, which include by default: + - The `wp-block` class + - A class that contains the name of the block with its namespace + +For example, for the following piece of code of a block's registration in the client... + +```js +const Edit = () =>

Hello World - Block Editor

; + +registerBlockType( ..., { + edit: Edit +} ); +``` +_(see the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda))_ + +...the markup of the block in the Block Editor could look like this: +```html +

Hello World - Block Editor

+``` + +Any additional classes and attributes for the `Edit` component of the block should be passed as an argument of `useBlockProps` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/edit.js)). When you add `support` for any feature, they get added to the object returned by the `useBlockProps` hook. + + +## The Save component's markup + +When saving the markup in the DB, it’s important to add the block props returned by `useBlockProps.save()` to the wrapper element of your block. `useBlockProps.save()` ensures that the block class name is rendered properly in addition to any HTML attribute injected by the block supports API. + +For example, for the following piece of code of a block's registration in the client that defines the markup desired for the DB (and returned to the front end by default)... + +```js +const Edit = () =>

Hello World - Block Editor

; +const save = () =>

Hello World - Frontend

; + +registerBlockType( ..., { + edit: Edit, + save, +} ); +``` + +_(see the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda))_ + + +...the markup of the block in the front end could look like this: +```html +

Hello World – Frontend

+``` + +Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). + +When you add `support` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. + +```html +

Hello World

+``` + +_(check the [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd) that generated the HTML above in the front end)_ + +## The server-side render markup + +Any markup in the server-side render definition for the block can use the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) to generate the string of attributes required to reflect the block settings. function (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/copyright-date-block-09aac3/src/render.php#L31)). + +```php +

> + +

+``` \ No newline at end of file diff --git a/docs/getting-started/fundamentals/file-structure-of-a-block.md b/docs/getting-started/fundamentals/file-structure-of-a-block.md new file mode 100644 index 00000000000000..130483ae5af70f --- /dev/null +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -0,0 +1,86 @@ +# File structure of a block + +It is recommended to **register blocks within plugins** to ensure they stay available when a theme gets switched. With the [`create-block` tool](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) you can quickly scaffold the structure of the files required to create a plugin that registers a block. + +The files generated by `create-block` are a good reference of the files that can be involved in the definition and registration of a block. + +[![Open File Structure of a Block Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-block.png)](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw "Open File Structure of a Block Diagram in excalidraw") + +### `.php` + +A block is usually added to the block editor using a WordPress plugin. In the main PHP file of the plugin the block is usually registered on the server side. + +
+For more on creating a WordPress plugin see Plugin Basics, and Plugin Header requirements for explanation and additional fields you can include in your plugin header. +
+ +### `package.json` + +[`package.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) is a configuration file for a Node.js project. In this file you define the NPM dependencies of the block and the scripts used for local work. + +### `src` folder + +In a standard project you'll place your block files in the `src` folder. By default, the build process with `wp-scripts `will take files from this folder and will generate the bundled files in the `build` folder. + +### `block.json` + +This file contains the metadata of the block, and it's used to simplify the definition and registration of the block both in the client and on the server. + +Among other data it provides properties to define the paths of the files involved in the block's behaviour, output and style. If there's a build process involved, this `block.json` along with the generated files are placed into a destination folder (usually the `build` folder) so the paths provided target to the bundled versions of these files. + +The most relevant properties that can be defined in a `block.json` to set the files involved in the block's behaviour, output or style are: +- The `editorScript` property, usually set with the path of a bundled `index.js` file (output build from `src/index.js`). +- The `style` property, usually set with the path of a bundled `style-index.css` file (output build from `src/style.(css|scss|sass)`). +- The `editorStyle` property, usually set with the path of a bundled `index.css` (output build from `src/editor.(css|scss|sass)`). +- The `render` property, usually set with the path of a bundled `render.php` (output copied from `src/render.php`). +- The `viewScript` property, usually set with the path of a bundled `view.js` (output copied from `src/view.php`). + +[![Open Build Output Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-build-output.png)](https://excalidraw.com/#json=c22LROgcG4JkD-7SkuE-N,rQW_ViJBq0Yk3qhCgqD6zQ "Open Build Output Diagram in excalidraw") + +### `index.js` + +The `index.js` file (or any other file defined in the `editorScript` property of `block.json`) is the entry point file for javascript that should only get loaded in the editor. It is responsible for calling the `registerBlockType` function to register the block on the client. In a standard structure it imports the `edit.js` and `save.js` files to get functions required in block registration. + +### `edit.js` + +The `edit.js` commonly gets used to contain the React component that gets used in the editor for our block. It usually exports a single component that then gets passed to the `edit` property of the `registerBlockType` function in the `index.js` file. + +### `save.js` + +The `save.js` exports the function that returns the static HTML markup that gets saved to the Database. + +### `style.(css|scss|sass)` + +A `style` file with any of the extensions `.css`, `.scss` or `.sass`, contains the styles of the block that will be loaded in both the editor and the frontend. In the build process this file is converted into `style-index.css` which is usually defined at `style` property in `block.json` + +
+ The webpack config used internally by wp-scripts includes a css-loader chained with postcss-loader and sass-loader that allows it to process CSS, SASS or SCSS files. Check Default webpack config for more info +
+ + +### `editor.(css|scss|sass)` + +An `editor` file with any of the extensions `.css`, `.scss` or `.sass`, contains the additional styles applied to the block only in the editor’s context. In the build process this file is converted into `index.css` which is usually defined at `editorStyle` property in `block.json` + +### `render.php` + +The `render.php` file (or any other file defined in the `render` property of `block.json`) defines the server side process that returns the markup for the block when there is a request from the frontend. If this file is defined, it will take precedence over any other ways to render the block's markup for the frontend. + +### `view.js` + +The `view.js` file (or any other file defined in the `viewScript` property of `block.json`) will be loaded in the front-end when the block is displayed. + +### `build` folder + +In a standard project, the `build` folder contains the generated files in the build process triggered by the `build` or `start` commands of `wp-scripts`. + +
+ You can use webpack-src-dir and output-path option of wp-scripts build commands to customize the entry and output points +
+ +## Additional resources + +- [Metadata in block.json](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) +- [`wp-scripts build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) +- [`wp-scripts start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) +- [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md new file mode 100644 index 00000000000000..73c6a6c56e6328 --- /dev/null +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -0,0 +1,51 @@ +# Working with Javascript for the Block Editor + +A JavaScript Build Process is recommended for most cases when working with Javascript for the Block Editor. With a build process, you'll be able to work with ESNext and JSX (among others) syntaxes and features in your code while producing code ready for the majority of the browsers. + +## JavaScript Build Process + +["ESNext"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/JavaScript_technologies_overview#standardization_process) is a dynamic name that refers to Javascript's latest syntax and features. ["JSX"](https://react.dev/learn/writing-markup-with-jsx) is a custom syntax extension to JavaScript, created by React project, that allows you to write JavaScript using a familiar HTML tag-like syntax. + +Browsers cannot interpret or run ESNext and JSX syntaxes, so a transformation step is needed to convert these syntaxes to code that browsers can understand. + +["webpack"](https://webpack.js.org/concepts/why-webpack/) is a pluggable tool that processes JavaScript and creates a compiled bundle that runs in a browser. ["babel"](https://babeljs.io/) transforms JavaScript from one format to another. Babel is a webpack plugin to transform ESNext and JSX to production-ready JavaScript. + +[`@wordpress/scripts`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) package abstracts these libraries away to standardize and simplify development, so you won’t need to handle the details for configuring webpack or babel. Check the [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) intro guide. + + +Among other things, with `wp-scripts` package you can use Javascript modules to distribute your code among different files and get a few bundled files at the end of the build process (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)). + +[![Build Process Diagram](https://developer.wordpress.org/files/2023/11/build-process.png)](https://excalidraw.com/#json=4aNG9JUti3pMnsfoga35b,ihEAI8p5dwkpjWr6gQmjuw "Open Build Process Diagram in Excalidraw") + +With the [proper `package.json` scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/#basic-usage) you can launch the build process with `wp-scripts` in production and development mode: + +- **`npm run build` for "production" mode build** - This process [minifies the code](https://developer.mozilla.org/en-US/docs/Glossary/Minification) so it downloads faster in the browser. +- **`npm run start` for "development" mode build** - This process does not minify the code of the bundled files, provides [source maps files](https://firefox-source-docs.mozilla.org/devtools-user/debugger/how_to/use_a_source_map/index.html) for them, and additionally continues a running process to watch the source file for more changes and rebuilds as you develop. + +
+ You can provide your own custom webpack.config.js to wp-scripts to customize the build process to suit your needs +
+ +## Javascript without a build process + +Using Javascript without a build process may be another good option for code developments with few requirements (especially those not requiring JSX). + +Without a build process, you access the methods directly from the `wp` global object and must enqueue the script manually. [WordPress Javascript packages](https://developer.wordpress.org/block-editor/reference-guides/packages/) can be accessed through the `wp` [global variable](https://developer.mozilla.org/en-US/docs/Glossary/Global_variable) but every script that wants to use them through this `wp` object is responsible for adding [the handle of that package](https://developer.wordpress.org/block-editor/contributors/code/scripts/) to the dependency array when registered. + +So, for example if a script wants to register a block variation using the `registerBlockVariation` method out of the ["blocks" package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/), the `wp-blocks` handle would need to get added to the dependency array to ensure that `wp.blocks.registerBlockVariation` is defined when the script tries to access it (see [example](https://github.com/wptrainingteam/block-theme-examples/blob/master/example-block-variation/functions.php)). + +
+ Try running wp.data.select('core/editor').getBlocks()) in your browser's dev tools while editing a post or a site. The entire editor is available from the console. +
+ +Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/hooks/enqueue_block_editor_assets/) hook coupled with the standard [`wp_enqueue_script`](https://developer.wordpress.org/reference/functions/wp_enqueue_script/) (and [`wp_register_script`](https://developer.wordpress.org/reference/functions/wp_register_script/)) to enqueue javascript assets for the Editor with access to these packages via `wp` (see [example](https://github.com/wptrainingteam/block-theme-examples/tree/master/example-block-variation)). Refer to [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) for more info. + +## Additional resources + +- [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) +- [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) +- [Wordpress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) +- [Javascript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript) | MDN Web Docs +- [block-development-examples](https://github.com/WordPress/block-development-examples) | GitHub repository +- [block-theme-examples](https://github.com/wptrainingteam/block-theme-examples) | GitHub repository +- [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md new file mode 100644 index 00000000000000..7cc8e6bcbe8b06 --- /dev/null +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -0,0 +1,98 @@ +# Registration of a block + +A block is usually registered through a plugin on both the server and client-side using its `block.json` metadata. + +Although technically, blocks could be registered only in the client, **registering blocks on both the server and in the client is a strong recommendation**. Some server-side features like Dynamic Rendering, Block Supports, Block Hooks, or Block style variations require the block to "exist" on the server, and they won't work properly without server registration of the block. + +For example, to allow a block [to be styled via `theme.json`](https://developer.wordpress.org/themes/global-settings-and-styles/settings/blocks/), it needs to be registered on the server, otherwise, any styles assigned to it in `theme.json` will be ignored. + +[![Open Block Registration diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png)](https://excalidraw.com/#json=PUQu7jpvbKsUHYfpHWn7s,61QnhpZtjykp3s44lbUN_g "Open Block Registration diagram in excalidraw") + +### Registration of the block with PHP (server-side) + +Block registration on the server usually takes place in the main plugin PHP file with the `register_block_type` function called on the [init hook](https://developer.wordpress.org/reference/hooks/init/). + +The [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) function aims to simplify block type registration on the server by reading metadata stored in the `block.json` file. + +This function takes two params relevant in this context (`$block_type` accepts more types and variants): + +- `$block_type` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently. +- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default: + - `$render_callback` (`callable`) – callback used to render blocks of this block type, it's an alternative to the `render` field in `block.json`. + +As part of the build process, the `block.json` file is usually copied from the `src` folder to the `build` folder, so the path to the `block.json` of your registered block should refer to the `build` folder. + +`register_block_type` returns the registered block type (`WP_Block_Type`) on success or `false` on failure. + +**Example:** +```php +register_block_type( + __DIR__ . '/notice', + array( + 'render_callback' => 'render_block_core_notice', + ) +); +``` + +**Example:** +```php +function minimal_block_ca6eda___register_block() { + register_block_type( __DIR__ . '/build' ); +} + +add_action( 'init', 'minimal_block_ca6eda___register_block' ); +``` +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/index.php)_ + +### Registration of the block with JavaScript (client-side) + +When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. + +**Example:** + +```js +registerBlockType( 'my-plugin/notice', { + edit: Edit, + // ...other client-side settings +} ); +``` + +Although registering the block also on the server with PHP is still recommended for the reasons mentioned at ["Benefits using the metadata file"](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#benefits-using-the-metadata-file), if you want to register it only client-side you can use [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) method from `@wordpress/blocks` package to register a block type using the metadata loaded from `block.json` file. + +The function takes two params: + +- `$blockNameOrMetadata` (`string`|`Object`) – block type name or the metadata object loaded from the `block.json` +- `$settings` (`Object`) – client-side block settings. + +
+The content of block.json (or any other .json file) can be imported directly in Javascript files when using a build process like the one available with wp-scripts +
+ +The client-side block settings object passed as a second parameter include two properties that are especially relevant: +- `edit`: The React component that gets used in the editor for our block. +- `save`: The function that returns the static HTML markup that gets saved to the Database. + +`registerBlockType` returns the registered block type (`WPBlock`) on success or `undefined` on failure. + +**Example:** + +```js +import { registerBlockType } from '@wordpress/blocks'; +import { useBlockProps } from '@wordpress/block-editor'; +import metadata from './block.json'; + +const Edit = () =>

Hello World - Block Editor

; +const save = () =>

Hello World - Frontend

; + +registerBlockType( metadata.name, { + edit: Edit, + save, +} ); +``` +_See the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/src/index.js) in [an example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda)_ + +## Additional resources + +- [`register_block_type` PHP function](https://developer.wordpress.org/reference/functions/register_block_type/) +- [`registerBlockType` JS function](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) +- [Why a block needs to be registered in both the server and the client?](https://github.com/WordPress/gutenberg/discussions/55884) | GitHub Discussion \ No newline at end of file diff --git a/docs/getting-started/quick-start-guide.md b/docs/getting-started/quick-start-guide.md index 4ad3998e7c27d3..e978b250ab8aff 100644 --- a/docs/getting-started/quick-start-guide.md +++ b/docs/getting-started/quick-start-guide.md @@ -16,7 +16,7 @@ Next, use the [`@wordpress/create-block`](https://developer.wordpress.org/block- Choose the folder where you want to create the plugin, and then execute the following command in the terminal from within that folder: ```sh -npx @wordpress/create-block copyright-date-block --template create-block-tutorial-template +npx @wordpress/create-block copyright-date-block --template @wordpress/create-block-tutorial-template ``` The `slug` provided (`copyright-date-block`) defines the folder name for the scaffolded plugin and the internal block name. @@ -41,4 +41,4 @@ When you are finished making changes, run the `npm run build` command. This opti - [Get started with create-block](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) - [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) -- [Get started with wp-env](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) \ No newline at end of file +- [Get started with wp-env](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/) diff --git a/docs/how-to-guides/block-tutorial/README.md b/docs/how-to-guides/block-tutorial/README.md index 95aa4182430c07..8688bd09416d7b 100644 --- a/docs/how-to-guides/block-tutorial/README.md +++ b/docs/how-to-guides/block-tutorial/README.md @@ -2,9 +2,9 @@ The purpose of this tutorial is to step through the fundamentals of creating a new block type. Beginning with the simplest possible example, each new section will incrementally build upon the last to include more of the common functionality you could expect to need when implementing your own block types. -To follow along with this tutorial, you can download the [accompanying WordPress plugin](https://github.com/WordPress/gutenberg-examples) which includes all of the examples for you to try on your own site. At each step along the way, experiment by modifying the examples with your own ideas, and observe the effects they have on the block's behavior. +To follow along with this tutorial, you can download the [accompanying WordPress plugin](https://github.com/WordPress/block-development-examples) which includes all of the examples for you to try on your own site. At each step along the way, experiment by modifying the examples with your own ideas, and observe the effects they have on the block's behavior. -> To find the latest version of the .zip file go to the repo's [releases page](https://github.com/WordPress/gutenberg-examples/releases) and look in the latest release under 'Assets'. +> To find the latest version of the .zip file go to the repo's [releases page](https://github.com/WordPress/block-development-examples/releases) and look in the latest release under 'Assets'. Code snippets are provided in two formats "JSX" and "Plain". JSX refers to JavaScript code that uses JSX syntax which requires a build step. Plain refers to "classic" JavaScript that does not require building. You can change between them using tabs found above each code example. Using JSX, does require you to run [the JavaScript build step](/docs/how-to-guides/javascript/js-build-setup/) to compile your code to a browser compatible format. diff --git a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md index 2cd79198b70b9f..697984c9456e02 100644 --- a/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md +++ b/docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md @@ -18,9 +18,6 @@ The first method shows adding the style inline. This transforms the defined styl The `useBlockProps` React hook is used to set and apply properties on the block's wrapper element. The following example shows how: -{% codetabs %} -{% JSX %} - ```jsx import { registerBlockType } from '@wordpress/blocks'; import { useBlockProps } from '@wordpress/block-editor'; @@ -55,49 +52,6 @@ registerBlockType( 'gutenberg-examples/example-02-stylesheets', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - - blocks.registerBlockType( 'gutenberg-examples/example-02-stylesheets', { - edit: function ( props ) { - const greenBackground = { - backgroundColor: '#090', - color: '#fff', - padding: '20px', - }; - const blockProps = blockEditor.useBlockProps( { - style: greenBackground, - } ); - return el( - 'p', - blockProps, - 'Hello World (from the editor, in green).' - ); - }, - save: function () { - const redBackground = { - backgroundColor: '#090', - color: '#fff', - padding: '20px', - }; - const blockProps = blockEditor.useBlockProps.save( { - style: redBackground, - } ); - return el( - 'p', - blockProps, - 'Hello World (from the frontend, in red).' - ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - ## Method 2: Block classname The inline style works well for a small amount of CSS to apply. If you have much more than the above you will likely find that it is easier to manage with them in a separate stylesheet file. @@ -106,9 +60,6 @@ The `useBlockProps` hooks includes the classname for the block automatically, it For example the block name: `gutenberg-examples/example-02-stylesheets` would get the classname: `wp-block-gutenberg-examples-example-02-stylesheets`. It might be a bit long but best to avoid conflicts with other blocks. -{% codetabs %} -{% JSX %} - ```jsx import { registerBlockType } from '@wordpress/blocks'; import { useBlockProps } from '@wordpress/block-editor'; @@ -131,66 +82,15 @@ registerBlockType( 'gutenberg-examples/example-02-stylesheets', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - - blocks.registerBlockType( 'gutenberg-examples/example-02-stylesheets', { - edit: function ( props ) { - var blockProps = blockEditor.useBlockProps(); - return el( - 'p', - blockProps, - 'Hello World (from the editor, in green).' - ); - }, - save: function () { - var blockProps = blockEditor.useBlockProps.save(); - return el( - 'p', - blockProps, - 'Hello World (from the frontend, in red).' - ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - ### Build or add dependency In order to include the blockEditor as a dependency, make sure to run the build step, or update the asset php file. -{% codetabs %} -{% JSX %} - Build the scripts and update the asset file which is used to keep track of dependencies and the build version. ```bash npm run build ``` -{% Plain %} - -Edit the asset file to include the block-editor dependency for the scripts. - -```php - - array( - 'react', - 'wp-blocks', - 'wp-block-editor', - 'wp-polyfill' - ), - 'version' => '0.1' - ); -``` - -{% end %} - ### Enqueue stylesheets Like scripts, you can enqueue your block's styles using the `block.json` file. @@ -199,7 +99,7 @@ Use the `editorStyle` property to a CSS file you want to load in the editor view It is worth noting that, if the editor content is iframed, both of these will load in the iframe. `editorStyle` will also load outside the iframe, so it can -be used for editor content as well as UI. +be used for editor content as well as UI. For example: @@ -249,4 +149,4 @@ The files will automatically be enqueued when specified in the block.json. This guide showed a couple of different ways to apply styles to your block, by either inline or in its own style sheet. Both of these methods use the `useBlockProps` hook, see the [block wrapper reference documentation](/docs/reference-guides/block-api/block-edit-save.md#block-wrapper-props) for additional details. -See the complete [example-02-stylesheets](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/02-stylesheets) code in the [gutenberg-examples repository](https://github.com/WordPress/gutenberg-examples). +See the complete [stylesheets-79a4c3](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/stylesheets-79a4c3) code in the [block-development-examples repository](https://github.com/WordPress/block-development-examples). diff --git a/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md b/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md index 1b3a54592b9967..4436696b552619 100644 --- a/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md +++ b/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md @@ -10,9 +10,6 @@ When the user selects a block, a number of control buttons may be shown in a too You can also customize the toolbar to include controls specific to your block type. If the return value of your block type's `edit` function includes a `BlockControls` element, those controls will be shown in the selected block's toolbar. -{% codetabs %} -{% JSX %} - ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -92,95 +89,6 @@ registerBlockType( 'gutenberg-examples/example-04-controls-esnext', { } ); ``` -{% Plain %} - -```js -( function ( blocks, blockEditor, React ) { - var el = React.createElement; - var RichText = blockEditor.RichText; - var AlignmentToolbar = blockEditor.AlignmentToolbar; - var BlockControls = blockEditor.BlockControls; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-04-controls', { - title: 'Example: Controls', - icon: 'universal-access-alt', - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - alignment: { - type: 'string', - default: 'none', - }, - }, - example: { - attributes: { - content: 'Hello World', - alignment: 'right', - }, - }, - edit: function ( props ) { - var content = props.attributes.content; - var alignment = props.attributes.alignment; - - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - function onChangeAlignment( newAlignment ) { - props.setAttributes( { - alignment: - newAlignment === undefined ? 'none' : newAlignment, - } ); - } - - return el( - 'div', - useBlockProps(), - el( - BlockControls, - { key: 'controls' }, - el( AlignmentToolbar, { - value: alignment, - onChange: onChangeAlignment, - } ) - ), - el( RichText, { - key: 'richtext', - tagName: 'p', - style: { textAlign: alignment }, - onChange: onChangeContent, - value: content, - } ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - - return el( - 'div', - blockProps, - el( RichText.Content, { - tagName: 'p', - className: - 'gutenberg-examples-align-' + - props.attributes.alignment, - value: props.attributes.content, - } ) - ); - }, - } ); -} )( window.wp.blocks, window.wp.blockEditor, window.React ); -``` - -{% end %} - Note that `BlockControls` is only visible when the block is currently selected and in visual editing mode. `BlockControls` are not shown when editing a block in HTML editing mode. ## Settings Sidebar diff --git a/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md b/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md index a6350470bb797c..47fa3a86b75eb9 100644 --- a/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md +++ b/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md @@ -8,8 +8,6 @@ Let's take the block we wrote in the previous chapter (example 3) and with just Here's the exact same code we used to register the block previously. -{% codetabs %} -{% JSX %} ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -64,65 +62,6 @@ registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { } ); ``` -{% Plain %} - -```js -( function ( blocks, blockEditor, React ) { - var el = React.createElement; - var RichText = blockEditor.RichText; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-03-editable', { - apiVersion: 3, - title: 'Example: Basic with block supports', - icon: 'universal-access-alt', - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - example: { - attributes: { - content: 'Hello World', - }, - }, - edit: function ( props ) { - var blockProps = useBlockProps(); - var content = props.attributes.content; - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - return el( - RichText, - Object.assign( blockProps, { - tagName: 'p', - onChange: onChangeContent, - value: content, - } ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - return el( - RichText.Content, - Object.assign( blockProps, { - tagName: 'p', - value: props.attributes.content, - } ) - ); - }, - } ); -} )( window.wp.blocks, window.wp.blockEditor, window.React ); -``` - -{% end %} - Now, let's alter the block.json file for that block, and add the supports key. (If you're not using a block.json file, you can also add the key to the `registerBlockType` function call) ```json diff --git a/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md b/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md index f8d4041c4542e4..89ef666abe494f 100644 --- a/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md +++ b/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md @@ -17,8 +17,7 @@ Block attributes can be used for any content or setting you want to save for tha The following code example shows how to create a dynamic block that shows only the last post as a link. -{% codetabs %} -{% JSX %} + ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -52,47 +51,7 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, data, blockEditor ) { - var el = React.createElement, - registerBlockType = blocks.registerBlockType, - useSelect = data.useSelect, - useBlockProps = blockEditor.useBlockProps; - - registerBlockType( 'gutenberg-examples/example-dynamic', { - apiVersion: 3, - title: 'Example: last post', - icon: 'megaphone', - category: 'widgets', - edit: function () { - var content; - var blockProps = useBlockProps(); - var posts = useSelect( function ( select ) { - return select( 'core' ).getEntityRecords( 'postType', 'post' ); - }, [] ); - if ( ! posts ) { - content = 'Loading...'; - } else if ( posts.length === 0 ) { - content = 'No posts'; - } else { - var post = posts[ 0 ]; - content = el( 'a', { href: post.link }, post.title.rendered ); - } - - return el( 'div', blockProps, content ); - }, - } ); -} )( - window.wp.blocks, - window.React, - window.wp.data, - window.wp.blockEditor -); -``` -{% end %} Because it is a dynamic block it doesn't need to override the default `save` implementation on the client. Instead, it needs a server component. The contents in the front of your site depend on the function called by the `render_callback` property of `register_block_type`. @@ -156,8 +115,7 @@ Gutenberg 2.8 added the [``](/packages/server-side-render/READ _Server-side render is meant as a fallback; client-side rendering in JavaScript is always preferred (client rendering is faster and allows better editor manipulation)._ -{% codetabs %} -{% JSX %} + ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -184,41 +142,6 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, serverSideRender, blockEditor ) { - var el = React.createElement, - registerBlockType = blocks.registerBlockType, - ServerSideRender = serverSideRender, - useBlockProps = blockEditor.useBlockProps; - - registerBlockType( 'gutenberg-examples/example-dynamic', { - apiVersion: 3, - title: 'Example: last post', - icon: 'megaphone', - category: 'widgets', - - edit: function ( props ) { - var blockProps = useBlockProps(); - return el( - 'div', - blockProps, - el( ServerSideRender, { - block: 'gutenberg-examples/example-dynamic', - attributes: props.attributes, - } ) - ); - }, - } ); -} )( - window.wp.blocks, - window.React, - window.wp.serverSideRender, - window.wp.blockEditor -); -``` -{% end %} -Note that this code uses the `wp-server-side-render` package but not `wp-data`. Make sure to update the dependencies in the PHP code. You can use wp-scripts to automatically build dependencies (see the [gutenberg-examples repo](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/01-basic-esnext) for PHP code setup). +Note that this code uses the `wp-server-side-render` package but not `wp-data`. Make sure to update the dependencies in the PHP code. You can use wp-scripts to automatically build dependencies (see the [block-development-examples repo](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/basic-esnext-a2ab62) for PHP code setup). diff --git a/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md b/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md index 7586081af4216d..3d8e10cae7ab2a 100644 --- a/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md +++ b/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md @@ -52,8 +52,6 @@ Because `RichText` allows for nested nodes, you'll most often use it in conjunct Here is the complete block definition for Example 03. -{% codetabs %} -{% JSX %} ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -107,62 +105,3 @@ registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { }, } ); ``` - -{% Plain %} - -```js -( function ( blocks, blockEditor, React ) { - var el = React.createElement; - var RichText = blockEditor.RichText; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-03-editable', { - apiVersion: 3, - title: 'Example: Editable', - icon: 'universal-access-alt', - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - example: { - attributes: { - content: 'Hello World', - }, - }, - edit: function ( props ) { - var blockProps = useBlockProps(); - var content = props.attributes.content; - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - return el( - RichText, - Object.assign( blockProps, { - tagName: 'p', - onChange: onChangeContent, - value: content, - } ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - return el( - RichText.Content, - Object.assign( blockProps, { - tagName: 'p', - value: props.attributes.content, - } ) - ); - }, - } ); -} )( window.wp.blocks, window.wp.blockEditor, window.React ); -``` - -{% end %} diff --git a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md index e2c37f341427e5..9dc7f1f324743f 100644 --- a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md +++ b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md @@ -6,8 +6,6 @@ Note: A single block can only contain one `InnerBlocks` component. Here is the basic InnerBlocks usage. -{% codetabs %} -{% JSX %} ```js import { registerBlockType } from '@wordpress/blocks'; @@ -38,35 +36,6 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - title: 'Example: Inner Blocks', - category: 'design', - - edit: function () { - var blockProps = useBlockProps(); - - return el( 'div', blockProps, el( InnerBlocks ) ); - }, - - save: function () { - var blockProps = useBlockProps.save(); - - return el( 'div', blockProps, el( InnerBlocks.Content ) ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - ## Allowed Blocks Using the `allowedBlocks` property, you can define the set of blocks allowed in your InnerBlock. This restricts the blocks that can be included only to those listed, all other blocks will not show in the inserter. @@ -101,8 +70,6 @@ By default this behavior is disabled until the `directInsert` prop is set to `tr Use the template property to define a set of blocks that prefill the InnerBlocks component when inserted. You can set attributes on the blocks to define their use. The example below shows a book review template using InnerBlocks component and setting placeholders values to show the block usage. -{% codetabs %} -{% JSX %} ```js const MY_TEMPLATE = [ @@ -123,29 +90,6 @@ const MY_TEMPLATE = [ }, ``` -{% Plain %} - -```js -const MY_TEMPLATE = [ - [ 'core/image', {} ], - [ 'core/heading', { placeholder: 'Book Title' } ], - [ 'core/paragraph', { placeholder: 'Summary' } ], -]; - -//... - - edit: function( props ) { - return el( - InnerBlocks, - { - template: MY_TEMPLATE, - templateLock: "all", - } - ); - }, -``` - -{% end %} Use the `templateLock` property to lock down the template. Using `all` locks the template completely so no changes can be made. Using `insert` prevents additional blocks from being inserted, but existing blocks can be reordered. See [templateLock documentation](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md#templatelock) for additional information. @@ -167,7 +111,7 @@ add_action( 'init', function() { ## Using Parent and Ancestor Relationships in Blocks -A common pattern for using InnerBlocks is to create a custom block that will be only be available if its parent block is inserted. This allows builders to establish a relationship between blocks, while limiting a nested block's discoverability. Currently, there are two relationships builders can use: `parent` and `ancestor`. The differences are: +A common pattern for using InnerBlocks is to create a custom block that will be only be available if its parent block is inserted. This allows builders to establish a relationship between blocks, while limiting a nested block's discoverability. Currently, there are two relationships builders can use: `parent` and `ancestor`. The differences are: - If you assign a `parent` then you’re stating that the nested block can only be used and inserted as a __direct descendant of the parent__. - If you assign an `ancestor` then you’re stating that the nested block can only be used and inserted as a __descendent of the parent__. @@ -214,8 +158,7 @@ The `useInnerBlocksProps` is exported from the `@wordpress/block-editor` package Here is the basic `useInnerBlocksProps` hook usage. -{% codetabs %} -{% JSX %} + ```js import { registerBlockType } from '@wordpress/blocks'; @@ -248,42 +191,9 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - var useInnerBlocksProps = blockEditor.useInnerBlocksProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - title: 'Example: Inner Blocks', - category: 'design', - - edit: function () { - var blockProps = useBlockProps(); - var innerBlocksProps = useInnerBlocksProps(); - - return el( 'div', blockProps, el( 'div', innerBlocksProps ) ); - }, - - save: function () { - var blockProps = useBlockProps.save(); - var innerBlocksProps = useInnerBlocksProps.save(); - - return el( 'div', blockProps, el( 'div', innerBlocksProps ) ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} - This hook can also pass objects returned from the `useBlockProps` hook to the `useInnerBlocksProps` hook. This reduces the number of elements we need to create. -{% codetabs %} -{% JSX %} + ```js import { registerBlockType } from '@wordpress/blocks'; @@ -312,36 +222,6 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - var useInnerBlocksProps = blockEditor.useInnerBlocksProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - // ... - - edit: function () { - var blockProps = useBlockProps(); - var innerBlocksProps = useInnerBlocksProps(); - - return el( 'div', innerBlocksProps ); - }, - - save: function () { - var blockProps = useBlockProps.save(); - var innerBlocksProps = useInnerBlocksProps.save(); - - return el( 'div', innerBlocksProps ); - }, - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} The above code will render to the following markup in the editor: @@ -353,8 +233,6 @@ The above code will render to the following markup in the editor: Another benefit to using the hook approach is using the returned value, which is just an object, and deconstruct to get the react children from the object. This property contains the actual child inner blocks thus we can place elements on the same level as our inner blocks. -{% codetabs %} -{% JSX %} ```js import { registerBlockType } from '@wordpress/blocks'; @@ -379,39 +257,6 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -{% Plain %} - -```js -( function ( blocks, React, blockEditor ) { - var el = React.createElement; - var InnerBlocks = blockEditor.InnerBlocks; - var useBlockProps = blockEditor.useBlockProps; - var useInnerBlocksProps = blockEditor.useInnerBlocksProps; - - blocks.registerBlockType( 'gutenberg-examples/example-06', { - // ... - - edit: function () { - var blockProps = useBlockProps(); - var { children, ...innerBlocksProps } = useInnerBlocksProps( blockProps ); - - return el( - 'div', - innerBlocksProps, - children, - el( - 'div', - {}, - '', - ) - ); - }, - // ... - } ); -} )( window.wp.blocks, window.React, window.wp.blockEditor ); -``` - -{% end %} ```html
diff --git a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md index a9dfc0d51a682c..4a690984011e0f 100644 --- a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md +++ b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md @@ -64,8 +64,6 @@ The `block.json` file should be added to your plugin. To start a new plugin, cre Create a basic `block.json` file there: -{% codetabs %} -{% JSX %} ```json { @@ -77,22 +75,6 @@ Create a basic `block.json` file there: "editorScript": "file:./build/index.js" } ``` - -{% Plain %} - -```json -{ - "apiVersion": 3, - "title": "Example: Basic", - "name": "gutenberg-examples/example-01-basic", - "category": "layout", - "icon": "universal-access-alt", - "editorScript": "file:./block.js" -} -``` - -{% end %} - ### Step 2: Register block in plugin With the `block.json` in place, the registration for the block is a single function call in PHP, this will setup the block and JavaScript file specified in the `editorScript` property to load in the editor. @@ -118,8 +100,6 @@ The `edit` function is a component that is shown in the editor when the block is The `save` function is a component that defines the final markup returned by the block and saved in `post_content`. -{% codetabs %} -{% JSX %} Add the following in `src/index.js` @@ -140,33 +120,12 @@ registerBlockType( 'gutenberg-examples/example-01-basic-esnext', { } ); ``` -{% Plain %} - -Add the following to `block.js` - -```js -( function ( blocks, React ) { - var el = React.createElement; - - blocks.registerBlockType( 'gutenberg-examples/example-01-basic', { - edit: function () { - return el( 'p', {}, 'Hello World (from the editor).' ); - }, - save: function () { - return el( 'p', {}, 'Hola mundo (from the frontend).' ); - }, - } ); -} )( window.wp.blocks, window.React ); -``` - -{% end %} ### Step 4: Build or add dependency In order to register the block, an asset php file is required in the same directory as the directory used in `register_block_type()` and must begin with the script's filename. -{% codetabs %} -{% JSX %} + Build the scripts and asset file which is used to keep track of dependencies and the build version. @@ -174,23 +133,6 @@ Build the scripts and asset file which is used to keep track of dependencies and npm run build ``` -{% Plain %} - -Create the asset file to load the dependencies for the scripts. The name of this file should be the name of the js file then .asset.php. For this example, create `block.asset.php` with the following: - -```php - - array( - 'react', - 'wp-blocks', - 'wp-polyfill' - ), - 'version' => '0.1' - ); -``` - -{% end %} ### Step 5: Confirm @@ -207,11 +149,11 @@ When you save the post and view it published, you will see the `Hola mundo (from ## Conclusion -This shows the most basic static block. The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository has complete examples for both. +This shows the most basic static block. The [block-development-examples](https://github.com/WordPress/block-development-examples) repository has complete examples for both. -- [Basic Example with JSX build](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/01-basic-esnext) +- [Basic Example with JSX build](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/basic-esnext-a2ab62) -- [Basic Example Plain JavaScript](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/01-basic), +- [Basic Example Plain JavaScript](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-no-build-e621a6), **NOTE:** The examples include a more complete block setup with translation features included, it is recommended to follow those examples for a production block. The internationalization features were left out of this guide for simplicity and focusing on the very basics of a block. @@ -219,7 +161,7 @@ This shows the most basic static block. The [gutenberg-examples](https://github. A couple of things to note when creating your blocks: -- A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. In this example, the namespace is `gutenberg-examples`. +- A block name must be prefixed with a namespace specific to your plugin. This helps prevent conflicts when more than one plugin registers a block with the same name. In this example, the namespace is `block-development-examples`. - Block names _must_ include only lowercase alphanumeric characters or dashes and start with a letter. Example: `my-plugin/my-custom-block`. diff --git a/docs/how-to-guides/data-basics/1-data-basics-setup.md b/docs/how-to-guides/data-basics/1-data-basics-setup.md index 3657b65791a658..e61db83c4ecbd5 100644 --- a/docs/how-to-guides/data-basics/1-data-basics-setup.md +++ b/docs/how-to-guides/data-basics/1-data-basics-setup.md @@ -212,4 +212,4 @@ Congratulations! You are now ready to start building the app! - Previous part: [Introduction](/docs/how-to-guides/data-basics/README.md) - Next part: [Building a basic list of pages](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md) -- (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +- (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md index aee5575cdb5adb..8a0d172e45f453 100644 --- a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md +++ b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md @@ -446,4 +446,4 @@ All that’s left is to refresh the page and enjoy the brand new status indicato * **Previous part:** [Setup](/docs/how-to-guides/data-basics/1-data-basics-setup.md) * **Next part:** [Building an edit form](/docs/how-to-guides/data-basics/3-building-an-edit-form.md) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/3-building-an-edit-form.md b/docs/how-to-guides/data-basics/3-building-an-edit-form.md index 754a31f1bc4921..68c87381701515 100644 --- a/docs/how-to-guides/data-basics/3-building-an-edit-form.md +++ b/docs/how-to-guides/data-basics/3-building-an-edit-form.md @@ -540,4 +540,4 @@ function EditPageForm( { pageId, onCancel, onSaveFinished } ) { * **Previous part:** [Building a list of pages](/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md) * **Next part:** Building a *New Page* form (coming soon) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/4-building-a-create-page-form.md b/docs/how-to-guides/data-basics/4-building-a-create-page-form.md index 19aada07c2fc78..33c6e9a5ccff5b 100644 --- a/docs/how-to-guides/data-basics/4-building-a-create-page-form.md +++ b/docs/how-to-guides/data-basics/4-building-a-create-page-form.md @@ -389,4 +389,4 @@ All that’s left is to refresh the page and enjoy the form: * **Next part:** [Adding a delete button](/docs/how-to-guides/data-basics/5-adding-a-delete-button.md) * **Previous part:** [Building an edit form](/docs/how-to-guides/data-basics/3-building-an-edit-form.md) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/5-adding-a-delete-button.md b/docs/how-to-guides/data-basics/5-adding-a-delete-button.md index 07b10ac822c546..e0a0b0d1e93370 100644 --- a/docs/how-to-guides/data-basics/5-adding-a-delete-button.md +++ b/docs/how-to-guides/data-basics/5-adding-a-delete-button.md @@ -446,4 +446,4 @@ function DeletePageButton( { pageId } ) { ## What's next? * **Previous part:** [Building a *Create page form*](/docs/how-to-guides/data-basics/4-building-a-create-page-form.md) -* (optional) Review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository +* (optional) Review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository diff --git a/docs/how-to-guides/data-basics/README.md b/docs/how-to-guides/data-basics/README.md index 88e901a90e11db..3e92a216b60c55 100644 --- a/docs/how-to-guides/data-basics/README.md +++ b/docs/how-to-guides/data-basics/README.md @@ -2,9 +2,10 @@ This tutorial aims to get you comfortable with the Gutenberg data layer. It guides you through building a simple React application that enables the user to manage their WordPress pages. The finished app will look like this: -![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg) -You may review the [finished app](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/09-code-data-basics-esnext) in the gutenberg-examples repository. +[![Open demo in WordPress Playground](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg)](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/WordPress/block-development-examples/trunk/plugins/data-basics-59c8f8/_playground/blueprint.json "Opens demo in WordPress Playground") + +You may review the [finished app](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8) in the block-development-examples repository. ### Table of Contents diff --git a/docs/how-to-guides/format-api.md b/docs/how-to-guides/format-api.md index a23293bbb27e3f..00e1b82675c006 100644 --- a/docs/how-to-guides/format-api.md +++ b/docs/how-to-guides/format-api.md @@ -18,7 +18,7 @@ You will need: - A minimal plugin activated and setup ready to edit - JavaScript setup for building and enqueuing -The [complete format-api example](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/format-api) is available that you can use as a reference for your setup. +The [complete format-api example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/format-api-f14b86) is available that you can use as a reference for your setup. ## Step-by-step guide @@ -234,4 +234,4 @@ Reference documentation used in this guide: The guide showed you how to add a button to the toolbar and have it apply a format to the selected text. Try it out and see what you can build with it in your next plugin. -Download the [format-api example](https://github.com/WordPress/gutenberg-examples/tree/trunk/non-block-examples/format-api) from the [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository. +Download the [format-api example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/format-api-f14b86) from the [block-development-examples](https://github.com/WordPress/block-development-examples) repository. diff --git a/docs/how-to-guides/internationalization.md b/docs/how-to-guides/internationalization.md index c3194f309fca63..08ce46edb3f581 100644 --- a/docs/how-to-guides/internationalization.md +++ b/docs/how-to-guides/internationalization.md @@ -37,9 +37,6 @@ add_action( 'init', 'myguten_block_init' ); In your code, you can include the i18n functions. The most common function is **\_\_** (a double underscore) which provides translation of a simple string. Here is a basic block example: -{% codetabs %} -{% JSX %} - ```js import { __ } from '@wordpress/i18n'; import { registerBlockType } from '@wordpress/blocks'; @@ -64,33 +61,6 @@ registerBlockType( 'myguten/simple', { } ); ``` -{% Plain %} - -```js -const el = React.createElement; -const { __ } = wp.i18n; -const { registerBlockType } = wp.blocks; -const { useBlockProps } = wp.blockEditor; - -registerBlockType( 'myguten/simple', { - title: __( 'Simple Block', 'myguten' ), - category: 'widgets', - - edit: function () { - const blockProps = useBlockProps( { style: { color: 'red' } } ); - - return el( 'p', blockProps, __( 'Hello World', 'myguten' ) ); - }, - - save: function () { - const blockProps = useBlockProps.save( { style: { color: 'red' } } ); - return el( 'p', blockProps, __( 'Hello World', 'myguten' ) ); - }, -} ); -``` - -{% end %} - In the above example, the function will use the first argument for the string to be translated. The second argument is the text domain which must match the text domain slug specified by your plugin. Common functions available, these mirror their PHP counterparts are: diff --git a/docs/how-to-guides/javascript/js-build-setup.md b/docs/how-to-guides/javascript/js-build-setup.md index b915f4dd444f90..cf49154b590a96 100644 --- a/docs/how-to-guides/javascript/js-build-setup.md +++ b/docs/how-to-guides/javascript/js-build-setup.md @@ -22,7 +22,7 @@ The [@wordpress/scripts](https://www.npmjs.com/package/@wordpress/scripts) packa ## Quick Start -If you prefer a quick start, you can use one of the examples from the [Gutenberg Examples repository](https://github.com/wordpress/gutenberg-examples/) and skip below. Each one of the `-esnext` directories in the examples repository contain the necessary files for working with ESNext and JSX. +If you prefer a quick start, you can use one of the examples from the [Block Development Examples repository](https://github.com/wordpress/block-development-examples/) and skip below. Each one of the `-esnext` directories in the examples repository contain the necessary files for working with ESNext and JSX. ## Setup @@ -168,7 +168,7 @@ wp_register_script( ); ``` -See [ESNext blocks in gutenberg-examples repo](https://github.com/WordPress/gutenberg-examples) for full examples. +See [blocks in the block-development-examples repo](https://github.com/WordPress/block-development-examples) for full examples. ## Summary diff --git a/docs/how-to-guides/metabox.md b/docs/how-to-guides/metabox.md index 7a8686968d2cf2..e0402b1180c1c3 100644 --- a/docs/how-to-guides/metabox.md +++ b/docs/how-to-guides/metabox.md @@ -26,7 +26,7 @@ You will need: - A minimal plugin activated and ready to edit - JavaScript setup for building and enqueuing -A [complete meta-block example](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/meta-block) is available that you can use as a reference for your setup. +A [complete meta-block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/meta-block-bb1e55) is available that you can use as a reference for your setup. ## Step-by-step guide diff --git a/docs/how-to-guides/plugin-sidebar-0.md b/docs/how-to-guides/plugin-sidebar-0.md index 9543eaf8761549..bf084680c3d1b7 100644 --- a/docs/how-to-guides/plugin-sidebar-0.md +++ b/docs/how-to-guides/plugin-sidebar-0.md @@ -379,7 +379,7 @@ Functions used in this guide: You now have a custom sidebar that you can use to update `post_meta` content. -A complete example is available, download the [plugin-sidebar example](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/plugin-sidebar) from the [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository. +A complete example is available, download the [plugin-sidebar example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/plugin-sidebar-9ee4a6) from the [block-development-examples](https://github.com/WordPress/block-development-examples) repository. ### Note @@ -407,4 +407,4 @@ return el( TextControl, { document.querySelector( {the-value-textarea} ).innerHTML = content; }, } ); -``` \ No newline at end of file +``` diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index 024564b7c9eaeb..bd27abca1494c6 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -103,10 +103,10 @@ body { } ``` -{% end %} - - **Custom properties**: there's also a mechanism to create your own CSS Custom Properties. +{% end %} + {% codetabs %} {% Input %} @@ -309,7 +309,6 @@ The settings section has the following structure: } } ``` - {% end %} Each block can configure any of these settings separately, providing a more fine-grained control over what exists via `add_theme_support`. The settings declared at the top-level affect to all blocks, unless a particular block overwrites it. It's a way to provide inheritance and configure all blocks at once. @@ -376,6 +375,7 @@ The naming schema for the classes and the custom properties is as follows: - Custom Properties: `--wp--preset--{preset-category}--{preset-slug}` such as `--wp--preset--color--black` - Classes: `.has-{preset-slug}-{preset-category}` such as `.has-black-color`. + {% codetabs %} {% Input %} @@ -539,7 +539,6 @@ body { .wp-block-group.has-white-border-color { border-color: #444 !important; } ``` - {% end %} To maintain backward compatibility, the presets declared via `add_theme_support` will also generate the CSS Custom Properties. If the `theme.json` contains any presets, these will take precedence over the ones declared via `add_theme_support`. @@ -703,7 +702,6 @@ The tabs below show WordPress 5.8 supported styles and the ones supported by the Each block declares which style properties it exposes via the [block supports mechanism](/docs/reference-guides/block-api/block-supports.md). The support declarations are used to automatically generate the UI controls for the block in the editor. Themes can use any style property via the `theme.json` for any block ― it's the theme's responsibility to verify that it works properly according to the block markup, etc. {% codetabs %} - {% WordPress %} ```json @@ -783,7 +781,6 @@ Each block declares which style properties it exposes via the [block supports me } } ``` - {% Gutenberg %} ```json @@ -872,9 +869,7 @@ Each block declares which style properties it exposes via the [block supports me } } ``` - {% end %} - ### Top-level styles Styles found at the top-level will be enqueued using the `body` selector. @@ -902,7 +897,6 @@ body { ``` {% end %} - ### Block styles Styles found within a block will be enqueued using the block selector. @@ -948,7 +942,6 @@ p { /* The core/paragraph opts out from the default behaviour and uses p as a se color: var( --wp--preset--color--tertiary ); } ``` - {% end %} #### Referencing a style @@ -996,6 +989,7 @@ Supported by WordPress: If they're found in the top-level the element selector will be used. If they're found within a block, the selector to be used will be the element's appended to the corresponding block. + {% codetabs %} {% Input %} @@ -1065,9 +1059,7 @@ h3 { font-size: var( --wp--preset--font-size--smaller ); } ``` - {% end %} - ##### Element pseudo selectors Pseudo selectors `:hover`, `:focus`, `:visited`, `:active`, `:link`, `:any-link` are supported by Gutenberg. @@ -1261,7 +1253,6 @@ body { --wp--custom--font-primary: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"; } ``` - {% end %} A few notes about this process: diff --git a/docs/manifest.json b/docs/manifest.json index ba345e7716ee37..3ab4cefb2b533c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -95,6 +95,42 @@ "markdown_source": "../docs/getting-started/create-block/submitting-to-block-directory.md", "parent": "create-block" }, + { + "title": "Fundamentals of Block Development", + "slug": "fundamentals", + "markdown_source": "../docs/getting-started/fundamentals/README.md", + "parent": "getting-started" + }, + { + "title": "File structure of a block", + "slug": "file-structure-of-a-block", + "markdown_source": "../docs/getting-started/fundamentals/file-structure-of-a-block.md", + "parent": "fundamentals" + }, + { + "title": "block.json", + "slug": "block-json", + "markdown_source": "../docs/getting-started/fundamentals/block-json.md", + "parent": "fundamentals" + }, + { + "title": "Registration of a block", + "slug": "registration-of-a-block", + "markdown_source": "../docs/getting-started/fundamentals/registration-of-a-block.md", + "parent": "fundamentals" + }, + { + "title": "The block wrapper", + "slug": "block-wrapper", + "markdown_source": "../docs/getting-started/fundamentals/block-wrapper.md", + "parent": "fundamentals" + }, + { + "title": "Working with Javascript for the Block Editor", + "slug": "javascript-in-the-block-editor", + "markdown_source": "../docs/getting-started/fundamentals/javascript-in-the-block-editor.md", + "parent": "fundamentals" + }, { "title": "Glossary", "slug": "glossary", @@ -785,6 +821,12 @@ "markdown_source": "../packages/components/src/confirm-dialog/README.md", "parent": "components" }, + { + "title": "CustomSelectControlV2", + "slug": "custom-select-control-v2", + "markdown_source": "../packages/components/src/custom-select-control-v2/README.md", + "parent": "components" + }, { "title": "CustomSelectControl", "slug": "custom-select-control", diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index 765d69584a6690..35ec1c1e7c64e4 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -375,7 +375,7 @@ Attribute definition: From here, meta attributes can be read and written by a block using the same interface as any attribute: -{% codetabs %} + {% JSX %} ```js @@ -388,22 +388,6 @@ edit( { attributes, setAttributes } ) { }, ``` -{% Plain %} - -```js -edit: function( props ) { - function onChange( event ) { - props.setAttributes( { author: event.target.value } ); - } - - return el( 'input', { - value: props.attributes.author, - onChange: onChange, - } ); -}, -``` - -{% end %} #### Considerations diff --git a/docs/reference-guides/block-api/block-deprecation.md b/docs/reference-guides/block-api/block-deprecation.md index a1497ec346936d..4d69d9d46843cd 100644 --- a/docs/reference-guides/block-api/block-deprecation.md +++ b/docs/reference-guides/block-api/block-deprecation.md @@ -61,9 +61,6 @@ It's important to note that attributes, supports, and ### Example: -{% codetabs %} -{% JSX %} - ```js const { registerBlockType } = wp.blocks; const attributes = { @@ -101,46 +98,6 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { } ); ``` -{% Plain %} - -```js -var el = React.createElement, - registerBlockType = wp.blocks.registerBlockType, - attributes = { - text: { - type: 'string', - default: 'some random value', - }, - }, - supports = { - className: false, - }; - -registerBlockType( 'gutenberg/block-with-deprecated-version', { - // ... other block properties go here - - attributes: attributes, - - supports: supports, - - save: function ( props ) { - return el( 'div', {}, props.attributes.text ); - }, - - deprecated: [ - { - attributes: attributes, - - save: function ( props ) { - return el( 'p', {}, props.attributes.text ); - }, - }, - ], -} ); -``` - -{% end %} - In the example above we updated the markup of the block to use a `div` instead of `p`. ## Changing the attributes set @@ -149,8 +106,6 @@ Sometimes, you need to update the attributes set to rename or modify old attribu ### Example: -{% codetabs %} -{% JSX %} ```js const { registerBlockType } = wp.blocks; @@ -192,50 +147,6 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { } ); ``` -{% Plain %} - -```js -var el = React.createElement, - registerBlockType = wp.blocks.registerBlockType; - -registerBlockType( 'gutenberg/block-with-deprecated-version', { - // ... other block properties go here - - attributes: { - content: { - type: 'string', - default: 'some random value', - }, - }, - - save: function ( props ) { - return el( 'div', {}, props.attributes.content ); - }, - - deprecated: [ - { - attributes: { - text: { - type: 'string', - default: 'some random value', - }, - }, - - migrate: function ( attributes ) { - return { - content: attributes.text, - }; - }, - - save: function ( props ) { - return el( 'p', {}, props.attributes.text ); - }, - }, - ], -} ); -``` - -{% end %} In the example above we updated the markup of the block to use a `div` instead of `p` and rename the `text` attribute to `content`. @@ -246,9 +157,6 @@ E.g: a block wants to migrate a title attribute to a paragraph innerBlock. ### Example: -{% codetabs %} -{% JSX %} - ```js const { registerBlockType } = wp.blocks; @@ -292,49 +200,6 @@ registerBlockType( 'gutenberg/block-with-deprecated-version', { } ); ``` -{% Plain %} - -```js -var el = React.createElement, - registerBlockType = wp.blocks.registerBlockType; - -registerBlockType( 'gutenberg/block-with-deprecated-version', { - // ... block properties go here - - deprecated: [ - { - attributes: { - title: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - - migrate: function ( attributes, innerBlocks ) { - const { title, ...restAttributes } = attributes; - - return [ - restAttributes, - [ - createBlock( 'core/paragraph', { - content: attributes.title, - fontSize: 'large', - } ), - ].concat( innerBlocks ), - ]; - }, - - save: function ( props ) { - return el( 'p', {}, props.attributes.title ); - }, - }, - ], -} ); -``` - -{% end %} - In the example above we updated the block to use an inner Paragraph block with a title instead of a title attribute. _Above are example cases of block deprecation. For more, real-world examples, check for deprecations in the [core block library](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src). Core blocks have been updated across releases and contain simple and complex deprecations._ diff --git a/docs/reference-guides/block-api/block-edit-save.md b/docs/reference-guides/block-api/block-edit-save.md index 35bbd5ae13e1e0..a8b6f9171bdef3 100644 --- a/docs/reference-guides/block-api/block-edit-save.md +++ b/docs/reference-guides/block-api/block-edit-save.md @@ -6,8 +6,6 @@ When registering a block with JavaScript on the client, the `edit` and `save` fu The `edit` function describes the structure of your block in the context of the editor. This represents what the editor will render when the block is used. -{% codetabs %} -{% JSX %} ```jsx import { useBlockProps } from '@wordpress/block-editor'; @@ -26,32 +24,12 @@ const blockSettings = { }; ``` -{% Plain %} - -```js -var blockSettings = { - apiVersion: 3, - - // ... - - edit: function () { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( 'div', blockProps, 'Your block.' ); - }, -}; -``` - -{% end %} - ### block wrapper props The first thing to notice here is the use of the `useBlockProps` React hook on the block wrapper element. In the example above, the block wrapper renders a "div" in the editor, but in order for the Gutenberg editor to know how to manipulate the block, add any extra classNames that are needed for the block... the block wrapper element should apply props retrieved from the `useBlockProps` react hook call. The block wrapper element should be a native DOM element, like `
` and ``, or a React component that forwards any additional props to native DOM elements. Using a `` or `` component, for instance, would be invalid. If the element wrapper needs any extra custom HTML attributes, these need to be passed as an argument to the `useBlockProps` hook. For example to add a `my-random-classname` className to the wrapper, you can use the following code: -{% codetabs %} -{% JSX %} ```jsx import { useBlockProps } from '@wordpress/block-editor'; @@ -72,25 +50,6 @@ const blockSettings = { }; ``` -{% Plain %} - -```js -var blockSettings = { - apiVersion: 3, - - // ... - - edit: function () { - var blockProps = wp.blockEditor.useBlockProps( { - className: 'my-random-classname', - } ); - - return React.createElement( 'div', blockProps, 'Your block.' ); - }, -}; -``` - -{% end %} ### attributes @@ -100,8 +59,6 @@ The `attributes` property surfaces all the available attributes and their corres In this case, assuming we had defined an attribute of `content` during block registration, we would receive and use that value in our edit function: -{% codetabs %} -{% JSX %} ```js edit: ( { attributes } ) => { @@ -111,21 +68,6 @@ edit: ( { attributes } ) => { }; ``` -{% Plain %} - -```js -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( - 'div', - blockProps, - props.attributes.content - ); -} -``` - -{% end %} The value of `attributes.content` will be displayed inside the `div` when inserting the block in the editor. @@ -133,8 +75,6 @@ The value of `attributes.content` will be displayed inside the `div` when insert The isSelected property is an boolean that communicates whether the block is currently selected. -{% codetabs %} -{% JSX %} ```jsx edit: ( { attributes, isSelected } ) => { @@ -151,35 +91,10 @@ edit: ( { attributes, isSelected } ) => { }; ``` -{% Plain %} - -```js -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( - 'div', - blockProps, - [ - 'Your block.', - props.isSelected ? React.createElement( - 'span', - null, - 'Shows only when the block is selected.' - ) - ] - ); -} -``` - -{% end %} - ### setAttributes This function allows the block to update individual attributes based on user interactions. -{% codetabs %} -{% JSX %} ```jsx edit: ( { attributes, setAttributes, isSelected } ) => { @@ -201,40 +116,8 @@ edit: ( { attributes, setAttributes, isSelected } ) => { }; ``` -{% Plain %} - -```js -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - // Simplify access to attributes - let content = props.attributes.content; - let mySetting = props.attributes.mySetting; - - // Toggle a setting when the user clicks the button - let toggleSetting = () => props.setAttributes( { mySetting: ! mySetting } ); - return React.createElement( - 'div', - blockProps, - [ - content, - props.isSelected ? React.createElement( - 'button', - { onClick: toggleSetting }, - 'Toggle setting' - ) : null - ] - ); -}, -``` - -{% end %} - When using attributes that are objects or arrays it's a good idea to copy or clone the attribute prior to updating it: -{% codetabs %} -{% JSX %} - ```js // Good - a new array is created from the old list attribute and a new list item: const { list } = attributes; @@ -249,25 +132,6 @@ const addListItem = ( newListItem ) => { }; ``` -{% Plain %} - -```js -// Good - cloning the old list -var newList = attributes.list.slice(); - -var addListItem = function ( newListItem ) { - setAttributes( { list: newList.concat( [ newListItem ] ) } ); -}; - -// Bad - the list from the existing attribute is modified directly to add the new list item: -var list = attributes.list; -var addListItem = function ( newListItem ) { - list.push( newListItem ); - setAttributes( { list: list } ); -}; -``` - -{% end %} Why do this? In JavaScript, arrays and objects are passed by reference, so this practice ensures changes won't affect other code that might hold references to the same data. Furthermore, the Gutenberg project follows the philosophy of the Redux library that [state should be immutable](https://redux.js.org/faq/immutable-data#what-are-the-benefits-of-immutability)—data should not be changed directly, but instead a new version of the data created containing the changes. @@ -275,8 +139,6 @@ Why do this? In JavaScript, arrays and objects are passed by reference, so this The `save` function defines the way in which the different attributes should be combined into the final markup, which is then serialized into `post_content`. -{% codetabs %} -{% JSX %} ```jsx save: () => { @@ -286,21 +148,6 @@ save: () => { }; ``` -{% Plain %} - -```js -save: function() { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( - 'div', - blockProps, - 'Your block.' - ); -} -``` - -{% end %} For most blocks, the return value of `save` should be an [instance of WordPress Element](/packages/element/README.md) representing how the block is to appear on the front of the site. @@ -326,8 +173,6 @@ Like the `edit` function, when rendering static blocks, it's important to add th As with `edit`, the `save` function also receives an object argument including attributes which can be inserted into the markup. -{% codetabs %} -{% JSX %} ```jsx save: ( { attributes } ) => { @@ -337,21 +182,7 @@ save: ( { attributes } ) => { }; ``` -{% Plain %} -```js -save: function( props ) { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( - 'div', - blockProps, - props.attributes.content - ); -} -``` - -{% end %} When saving your block, you want to save the attributes in the same format specified by the attribute source definition. If no attribute source is specified, the attribute will be saved to the block's comment delimiter. See the [Block Attributes documentation](/docs/reference-guides/block-api/block-attributes.md) for more details. @@ -361,8 +192,6 @@ Here are a couple examples of using attributes, edit, and save all together. For ### Saving Attributes to Child Elements -{% codetabs %} -{% JSX %} ```jsx attributes: { @@ -396,46 +225,6 @@ save: ( { attributes } ) => { }, ``` -{% Plain %} - -```js -attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p' - } -}, - -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - var updateFieldValue = function( val ) { - props.setAttributes( { content: val } ); - } - - return React.createElement( - 'div', - blockProps, - React.createElement( - wp.components.TextControl, - { - label: 'My Text Field', - value: props.attributes.content, - onChange: updateFieldValue, - - } - ) - ); -}, - -save: function( props ) { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( 'div', blockProps, props.attributes.content ); -}, -``` - -{% end %} ### Saving Attributes via Serialization @@ -443,8 +232,6 @@ Ideally, the attributes saved should be included in the markup. However, there a This example could be for a dynamic block, such as the [Latest Posts block](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-library/src/latest-posts/index.js), which renders the markup server-side. The save function is still required, however in this case it simply returns null since the block is not saving content from the editor. -{% codetabs %} -{% JSX %} ```jsx attributes: { @@ -474,41 +261,6 @@ save: () => { } ``` -{% Plain %} - -```js -attributes: { - postsToShow: { - type: 'number', - } -}, - -edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createEleement( - 'div', - blockProps, - React.createElement( - wp.components.TextControl, - { - label: 'Number Posts to Show', - value: props.attributes.postsToShow, - onChange: function( val ) { - props.setAttributes( { postsToShow: parseInt( val ) } ); - }, - } - ) - ); -}, - -save: function() { - return null; -} -``` - -{% end %} - ## Validation When the editor loads, all blocks within post content are validated to determine their accuracy in order to protect against content loss. This is closely related to the saving implementation of a block, as a user may unintentionally remove or modify their content if the editor is unable to restore a block correctly. During editor initialization, the saved markup for each block is regenerated using the attributes that were parsed from the post's content. If the newly-generated markup does not match what was already stored in post content, the block is marked as invalid. This is because we assume that unless the user makes edits, the markup should remain identical to the saved content. diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index f380683f39ccdd..edc61d138128e6 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -77,65 +77,9 @@ Development is improved by using a defined schema definition file. Supported edi "$schema": "https://schemas.wp.org/trunk/block.json" ``` -## Block registration - -### PHP (server-side) - -The [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) function that aims to simplify the block type registration on the server, can read metadata stored in the `block.json` file. - -This function takes two params relevant in this context (`$block_type` accepts more types and variants): - -- `$block_type` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently. -- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default: - - `$render_callback` (`callable`) – callback used to render blocks of this block type, it's an alternative to the `render` field in `block.json`. - -It returns the registered block type (`WP_Block_Type`) on success or `false` on failure. - -**Example:** - -```php -register_block_type( - __DIR__ . '/notice', - array( - 'render_callback' => 'render_block_core_notice', - ) -); -``` - -### JavaScript (client-side) - -When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. - -**Example:** - -```js -registerBlockType( 'my-plugin/notice', { - edit: Edit, - // ...other client-side settings -} ); -``` - -Although registering the block also on the server with PHP is still recommended for the reasons above, if you want to register it only client-side you can now use `registerBlockType` method from `@wordpress/blocks` package to register a block type using the metadata loaded from `block.json` file. - -The function takes two params: - -- `$blockNameOrMetadata` (`string`|`Object`) – block type name (supported previously) or the metadata object loaded from the `block.json` file with a bundler (e.g., webpack) or a custom Babel plugin. -- `$settings` (`Object`) – client-side block settings. - -It returns the registered block type (`WPBlock`) on success or `undefined` on failure. - -**Example:** - -```js -import { registerBlockType } from '@wordpress/blocks'; -import Edit from './edit'; -import metadata from './block.json'; - -registerBlockType( metadata, { - edit: Edit, - // ...other client-side settings -} ); -``` +
+Check Registration of a block to learn more about how to register a block using its metadata. +
## Block API diff --git a/docs/reference-guides/block-api/block-supports.md b/docs/reference-guides/block-api/block-supports.md index a58c56d7a8a94f..7fd0e68c9bd8c0 100644 --- a/docs/reference-guides/block-api/block-supports.md +++ b/docs/reference-guides/block-api/block-supports.md @@ -556,6 +556,7 @@ supports: { - `allowVerticalAlignment`: type `boolean`, default value `true` - `allowJustification`: type `boolean`, default value `true` - `allowOrientation`: type `boolean`, default value `true` + - `allowCustomContentAndWideSize`: type `boolean`, default value `true` This value only applies to blocks that are containers for inner blocks. If set to `true` the layout type will be `flow`. For other layout types it's necessary to set the `type` explicitly inside the `default` object. @@ -615,6 +616,13 @@ For the `flex` layout type, determines display of the justification control in t For the `flex` layout type only, determines display of the orientation control in the block toolbar. +### layout.allowCustomContentAndWideSize + +- Type: `boolean` +- Default value: `true` + +For the `constrained` layout type only, determines display of the custom content and wide size controls in the block sidebar. + ## multiple diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index 0ae5979b797047..4f42550ba4cfbc 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -378,8 +378,8 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre - **Name:** core/image - **Category:** media -- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone) -- **Attributes:** align, alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width +- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone) +- **Attributes:** alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width ## Latest Comments @@ -470,7 +470,7 @@ Add a page, link, or another item to your navigation. ([Source](https://github.c - **Name:** core/navigation-link - **Category:** design - **Parent:** core/navigation -- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~reusable~~ +- **Supports:** typography (fontSize, lineHeight), ~~html~~, ~~renaming~~, ~~reusable~~ - **Attributes:** description, id, isTopLevelLink, kind, label, opensInNewTab, rel, title, type, url ## Submenu diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md index 21cd5b2beb7b69..7a0d67f9db0be0 100644 --- a/docs/reference-guides/data/data-core-edit-site.md +++ b/docs/reference-guides/data/data-core-edit-site.md @@ -132,11 +132,9 @@ _Returns_ ### hasPageContentFocus -Whether or not the editor allows only page content to be edited. - -_Parameters_ +> **Deprecated** -- _state_ `Object`: Global application state. +Whether or not the editor allows only page content to be edited. _Returns_ diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 5dbcb095bbf085..4774934651b139 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -501,6 +501,18 @@ _Related_ - getPreviousBlockClientId in core/block-editor store. +### getRenderingMode + +Returns the post editor's rendering mode. + +_Parameters_ + +- _state_ `Object`: Editor state. + +_Returns_ + +- `string`: Rendering mode. + ### getSelectedBlock _Related_ @@ -1241,6 +1253,19 @@ _Related_ - selectBlock in core/block-editor store. +### setRenderingMode + +Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: + +- `all`: This is the default mode. It renders the post editor with all the features available. If a template is provided, it's preferred over the post. +- `template-only`: This mode renders the editor with only the template blocks visible. +- `post-only`: This mode extracts the post blocks from the template and renders only those. The idea is to allow the user to edit the post/page in isolation without the wrapping template. +- `template-locked`: This mode renders both the template and the post blocks but the template blocks are locked and can't be edited. The post blocks are editable. + +_Parameters_ + +- _mode_ `string`: Mode (one of 'template-only', 'post-only', 'template-locked' or 'all'). + ### setTemplateValidity _Related_ diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index ea97ce28e4d85c..b80703dcc67b18 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -128,6 +128,8 @@ _Returns_ ### getCurrentThemeGlobalStylesRevisions +> **Deprecated** since WordPress 6.5.0. Callers should use `select( 'core' ).getRevisions( 'root', 'globalStyles', ${ recordKey } )` instead, where `recordKey` is the id of the global styles parent post. + Returns the revisions of the current global styles theme. _Parameters_ @@ -420,6 +422,39 @@ _Returns_ - A value whose reference will change only when an edit occurs. +### getRevision + +Returns a single, specific revision of a parent entity. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _revisionKey_ `EntityRecordKey`: The revision's key. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [entity kind]". + +_Returns_ + +- `RevisionRecord | Record< PropertyKey, never > | undefined`: Record. + +### getRevisions + +Returns an entity's revisions. + +_Parameters_ + +- _state_ `State`: State tree +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name. +- _recordKey_ `EntityRecordKey`: The key of the entity record whose revisions you want to fetch. +- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see revisions schema in [the REST API Handbook](https://developer.wordpress.org/rest-api/reference/). Then see the arguments available "Retrieve a [Entity kind]". + +_Returns_ + +- `RevisionRecord[] | null`: Record. + ### getThemeSupports Return theme supports data in the index. @@ -704,6 +739,20 @@ _Returns_ - `Object`: Action object. +### receiveRevisions + +Action triggered to receive revision items. + +_Parameters_ + +- _kind_ `string`: Kind of the received entity record revisions. +- _name_ `string`: Name of the received entity record revisions. +- _recordKey_ `number|string`: The key of the entity record whose revisions you want to fetch. +- _records_ `Array|Object`: Revisions received. +- _query_ `?Object`: Query Object. +- _invalidateCache_ `?boolean`: Should invalidate query caches. +- _meta_ `?Object`: Meta information about pagination. + ### receiveThemeSupports > **Deprecated** since WP 5.9, this is not useful anymore, use the selector direclty. diff --git a/docs/reference-guides/filters/autocomplete-filters.md b/docs/reference-guides/filters/autocomplete-filters.md index 1ce47219529181..85581f62e4af01 100644 --- a/docs/reference-guides/filters/autocomplete-filters.md +++ b/docs/reference-guides/filters/autocomplete-filters.md @@ -8,8 +8,7 @@ The `Autocomplete` component found in `@wordpress/block-editor` applies this fil Here is an example of using the `editor.Autocomplete.completers` filter to add an acronym completer. You can find full documentation for the autocompleter interface with the `Autocomplete` component in the `@wordpress/components` package. -{% codetabs %} -{% JSX %} + ```jsx // Our completer @@ -45,48 +44,3 @@ wp.hooks.addFilter( appendAcronymCompleter ); ``` - -{% Plain %} - -```js -// Our completer -var acronymCompleter = { - name: 'acronyms', - triggerPrefix: '::', - options: [ - { letters: 'FYI', expansion: 'For Your Information' }, - { letters: 'AFAIK', expansion: 'As Far As I Know' }, - { letters: 'IIRC', expansion: 'If I Recall Correctly' }, - ], - getOptionKeywords: function ( abbr ) { - var expansionWords = abbr.expansion.split( /\s+/ ); - return [ abbr.letters ].concat( expansionWords ); - }, - getOptionLabel: function ( acronym ) { - return acronym.letters; - }, - getOptionCompletion: function ( abbr ) { - return React.createElement( - 'abbr', - { title: abbr.expansion }, - abbr.letters - ); - }, -}; - -// Our filter function -function appendAcronymCompleter( completers, blockName ) { - return blockName === 'my-plugin/foo' - ? completers.concat( acronymCompleter ) - : completers; -} - -// Adding the filter -wp.hooks.addFilter( - 'editor.Autocomplete.completers', - 'my-plugin/autocompleters/acronyms', - appendAcronymCompleter -); -``` - -{% end %} diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index 912403c4838941..e269ba9ef19917 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -179,8 +179,6 @@ Used to modify the block's `edit` component. It receives the original block `Blo _Example:_ -{% codetabs %} -{% JSX %} ```js const { createHigherOrderComponent } = wp.compose; @@ -207,36 +205,6 @@ wp.hooks.addFilter( ); ``` -{% Plain %} - -```js -var el = React.createElement; - -var withMyPluginControls = wp.compose.createHigherOrderComponent( function ( - BlockEdit -) { - return function ( props ) { - return el( - React.Fragment, - {}, - el( BlockEdit, props ), - el( - wp.blockEditor.InspectorControls, - {}, - el( wp.components.PanelBody, {}, 'My custom control' ) - ) - ); - }; -}, 'withMyPluginControls' ); - -wp.hooks.addFilter( - 'editor.BlockEdit', - 'my-plugin/with-inspector-controls', - withMyPluginControls -); -``` - -{% end %} Note that as this hook is run for _all blocks_, consuming it has potential for performance regressions particularly around block selection metrics. @@ -267,9 +235,6 @@ Used to modify the block's wrapper component containing the block's `edit` compo _Example:_ -{% codetabs %} -{% JSX %} - ```js const { createHigherOrderComponent } = wp.compose; @@ -294,39 +259,10 @@ wp.hooks.addFilter( ); ``` -{% Plain %} - -```js -var el = React.createElement; - -var withClientIdClassName = wp.compose.createHigherOrderComponent( function ( - BlockListBlock -) { - return function ( props ) { - var newProps = { - ...props, - className: 'block-' + props.clientId, - }; - - return el( BlockListBlock, newProps ); - }; -}, 'withClientIdClassName' ); - -wp.hooks.addFilter( - 'editor.BlockListBlock', - 'my-plugin/with-client-id-class-name', - withClientIdClassName -); -``` - -{% end %} - Adding new properties to the block's wrapper component can be achieved by adding them to the `wrapperProps` property of the returned component. _Example:_ -{% codetabs %} -{% JSX %} ```js const { createHigherOrderComponent } = wp.compose; @@ -346,32 +282,6 @@ wp.hooks.addFilter( ); ``` -{% Plain %} - -```js -var el = React.createElement; -var hoc = wp.compose.createHigherOrderComponent; - -var withMyWrapperProp = hoc( function ( BlockListBlock ) { - return function ( props ) { - var newProps = { - ...props, - wrapperProps: { - ...props.wrapperProps, - 'data-my-property': 'the-value', - }, - }; - return el( BlockListBlock, newProps ); - }; -}, 'withMyWrapperProp' ); -wp.hooks.addFilter( - 'editor.BlockListBlock', - 'my-plugin/with-my-wrapper-prop', - withMyWrapperProp -); -``` - -{% end %} ## Removing Blocks @@ -379,8 +289,6 @@ wp.hooks.addFilter( Adding blocks is easy enough, removing them is as easy. Plugin or theme authors have the possibility to "unregister" blocks. -{% codetabs %} -{% JSX %} ```js // my-plugin.js @@ -392,16 +300,6 @@ domReady( function () { } ); ``` -{% Plain %} - -```js -// my-plugin.js -wp.domReady( function () { - wp.blocks.unregisterBlockType( 'core/verse' ); -} ); -``` - -{% end %} and load this script in the Editor diff --git a/docs/reference-guides/richtext.md b/docs/reference-guides/richtext.md index f908c7585bc1b8..1a4509318b72b7 100644 --- a/docs/reference-guides/richtext.md +++ b/docs/reference-guides/richtext.md @@ -25,8 +25,7 @@ There are a number of core blocks using the RichText component. The JavaScript e ## Example -{% codetabs %} -{% JSX %} + ```jsx import { registerBlockType } from '@wordpress/blocks'; @@ -66,46 +65,6 @@ registerBlockType( /* ... */, { } ); ``` -{% Plain %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'h2', - }, - }, - - edit: function( props ) { - var blockProps = wp.blockEditor.useBlockProps(); - - return React.createElement( wp.blockEditor.RichText, Object.assign( blockProps, { - tagName: 'h2', // The tag here is the element output and editable in the admin - value: props.attributes.content, // Any existing content, either from the database or an attribute default - allowedFormats: [ 'core/bold', 'core/italic' ], // Allow the content to be made bold or italic, but do not allow other formatting options - onChange: function( content ) { - props.setAttributes( { content: content } ); // Store updated content as a block attribute - }, - placeholder: __( 'Heading...' ), // Display this text before any content has been added by the user - } ) ); - }, - - save: function( props ) { - var blockProps = wp.blockEditor.useBlockProps.save(); - - return React.createElement( wp.blockEditor.RichText.Content, Object.assign( blockProps, { - tagName: 'h2', value: props.attributes.content // Saves

Content added in the editor...

to the database for frontend display - } ) ); - } -} ); -``` - -{% end %} - ## Common Issues & Solutions While using the RichText component a number of common issues tend to appear. diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 4890386ca8333e..24a5845381bfda 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -95,6 +95,8 @@ Settings related to colors. | link | boolean | false | | | palette | array | | color, name, slug | | text | boolean | true | | +| heading | boolean | true | | +| button | boolean | true | | --- @@ -127,6 +129,7 @@ Settings related to layout. | contentSize | string | | | | wideSize | string | | | | allowEditing | boolean | true | | +| allowCustomContentAndWideSize | boolean | true | | --- diff --git a/docs/toc.json b/docs/toc.json index 8a29d2d4f10aff..91017ce69643c3 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -46,6 +46,25 @@ } ] }, + { + "docs/getting-started/fundamentals/README.md": [ + { + "docs/getting-started/fundamentals/file-structure-of-a-block.md": [] + }, + { + "docs/getting-started/fundamentals/block-json.md": [] + }, + { + "docs/getting-started/fundamentals/registration-of-a-block.md": [] + }, + { + "docs/getting-started/fundamentals/block-wrapper.md": [] + }, + { + "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] + } + ] + }, { "docs/getting-started/glossary.md": [] }, { "docs/getting-started/faq.md": [] } ] diff --git a/gutenberg.php b/gutenberg.php index 8a8b1db6f85b92..ad8bdcebc852aa 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,9 +3,9 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. - * Requires at least: 6.2 + * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.0.2 + * Version: 17.1.3 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 80be02db68360e..d35c963d0bed48 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -617,6 +617,9 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { $processor->add_class( $class_name ); } return $processor->get_updated_html(); + } elseif ( ! $block_supports_layout ) { + // Ensure layout classnames are not injected if there is no layout support. + return $block_content; } $global_settings = gutenberg_get_global_settings(); @@ -865,17 +868,63 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) { return $block_content; } - $replace_regex = sprintf( + /** + * This filter runs after the layout classnames have been added to the block, so they + * have to be removed from the outer wrapper and then added to the inner. + */ + $layout_classes = array(); + $processor = new WP_HTML_Tag_Processor( $block_content ); + + if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) { + if ( method_exists( $processor, 'class_list' ) ) { + foreach ( $processor->class_list() as $class_name ) { + if ( str_contains( $class_name, 'layout' ) ) { + array_push( $layout_classes, $class_name ); + $processor->remove_class( $class_name ); + } + } + } else { + /** + * The class_list method was only added in 6.4 so this needs a temporary fallback. + * This fallback should be removed when the minimum supported version is 6.4. + */ + $classes = $processor->get_attribute( 'class' ); + if ( $classes ) { + $classes = explode( ' ', $classes ); + foreach ( $classes as $class_name ) { + if ( str_contains( $class_name, 'layout' ) ) { + array_push( $layout_classes, $class_name ); + $processor->remove_class( $class_name ); + } + } + } + } + } + + $content_without_layout_classes = $processor->get_updated_html(); + $replace_regex = sprintf( '/(^\s*<%1$s\b[^>]*wp-block-group[^>]*>)(.*)(<\/%1$s>\s*$)/ms', preg_quote( $tag_name, '/' ) ); - $updated_content = preg_replace_callback( + $updated_content = preg_replace_callback( $replace_regex, static function ( $matches ) { return $matches[1] . '
' . $matches[2] . '
' . $matches[3]; }, - $block_content + $content_without_layout_classes ); + + // Add layout classes to inner wrapper. + if ( ! empty( $layout_classes ) ) { + $processor = new WP_HTML_Tag_Processor( $updated_content ); + if ( $processor->next_tag( array( 'class_name' => 'wp-block-group__inner-container' ) ) ) { + foreach ( $layout_classes as $class_name ) { + $processor->add_class( $class_name ); + } + } + $updated_content = $processor->get_updated_html(); + } + return $updated_content; } diff --git a/lib/class-wp-duotone-gutenberg.php b/lib/class-wp-duotone-gutenberg.php index 11f6c6dcccdd93..93714e59740063 100644 --- a/lib/class-wp-duotone-gutenberg.php +++ b/lib/class-wp-duotone-gutenberg.php @@ -938,9 +938,17 @@ public static function output_footer_assets() { echo self::get_svg_definitions( self::$used_svg_filter_data ); } - // This is for classic themes - in block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. - if ( ! wp_is_block_theme() && ! empty( self::$used_global_styles_presets ) ) { - wp_add_inline_style( 'core-block-supports', self::get_global_styles_presets( self::$used_global_styles_presets ) ); + // In block themes, the CSS is added in the head via wp_add_inline_style in the wp_enqueue_scripts action. + if ( ! wp_is_block_theme() ) { + $style_tag_id = 'core-block-supports-duotone'; + wp_register_style( $style_tag_id, false ); + if ( ! empty( self::$used_global_styles_presets ) ) { + wp_add_inline_style( $style_tag_id, self::get_global_styles_presets( self::$used_global_styles_presets ) ); + } + if ( ! empty( self::$block_css_declarations ) ) { + wp_add_inline_style( $style_tag_id, gutenberg_style_engine_get_stylesheet_from_css_rules( self::$block_css_declarations ) ); + } + wp_enqueue_style( $style_tag_id ); } } diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 8c2857fa89d0ca..9311001f2edd14 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -383,9 +383,10 @@ class WP_Theme_JSON_Gutenberg { 'minHeight' => null, ), 'layout' => array( - 'contentSize' => null, - 'wideSize' => null, - 'allowEditing' => null, + 'contentSize' => null, + 'wideSize' => null, + 'allowEditing' => null, + 'allowCustomContentAndWideSize' => null, ), 'lightbox' => array( 'enabled' => null, @@ -425,6 +426,31 @@ class WP_Theme_JSON_Gutenberg { ), ); + const FONT_FAMILY_SCHEMA = array( + array( + 'fontFamily' => null, + 'name' => null, + 'slug' => null, + 'fontFace' => array( + array( + 'ascentOverride' => null, + 'descentOverride' => null, + 'fontDisplay' => null, + 'fontFamily' => null, + 'fontFeatureSettings' => null, + 'fontStyle' => null, + 'fontStretch' => null, + 'fontVariationSettings' => null, + 'fontWeight' => null, + 'lineGapOverride' => null, + 'sizeAdjust' => null, + 'src' => null, + 'unicodeRange' => null, + ), + ), + ), + ); + /** * The valid properties under the styles key. * @@ -549,6 +575,52 @@ class WP_Theme_JSON_Gutenberg { 'typography' => 'typography', ); + /** + * Return the input schema at the root and per origin. + * + * @since 6.5.0 + * + * @param array $schema The base schema. + * @return array The schema at the root and per origin. + * + * Example: + * schema_in_root_and_per_origin( + * array( + * 'fontFamily' => null, + * 'slug' => null, + * ) + * ) + * + * Returns: + * array( + * 'fontFamily' => null, + * 'slug' => null, + * 'default' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'blocks' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'theme' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * 'custom' => array( + * 'fontFamily' => null, + * 'slug' => null, + * ), + * ) + */ + protected static function schema_in_root_and_per_origin( $schema ) { + $schema_in_root_and_per_origin = $schema; + foreach ( static::VALID_ORIGINS as $origin ) { + $schema_in_root_and_per_origin[ $origin ] = $schema; + } + return $schema_in_root_and_per_origin; + } + /** * Returns a class name by an element name. * @@ -790,11 +862,12 @@ protected static function sanitize( $input, $valid_block_names, $valid_element_n $schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations; } - $schema['styles'] = static::VALID_STYLES; - $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['elements'] = $schema_styles_elements; - $schema['settings'] = static::VALID_SETTINGS; - $schema['settings']['blocks'] = $schema_settings_blocks; + $schema['styles'] = static::VALID_STYLES; + $schema['styles']['blocks'] = $schema_styles_blocks; + $schema['styles']['elements'] = $schema_styles_elements; + $schema['settings'] = static::VALID_SETTINGS; + $schema['settings']['blocks'] = $schema_settings_blocks; + $schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA ); // Remove anything that's not present in the schema. foreach ( array( 'styles', 'settings' ) as $subtree ) { @@ -966,18 +1039,39 @@ protected static function get_blocks_metadata() { * @return array The modified $tree. */ protected static function remove_keys_not_in_schema( $tree, $schema ) { - $tree = array_intersect_key( $tree, $schema ); + if ( ! is_array( $tree ) ) { + return $tree; + } - foreach ( $schema as $key => $data ) { - if ( ! isset( $tree[ $key ] ) ) { + foreach ( $tree as $key => $value ) { + // Remove keys not in the schema or with null/empty values. + if ( ! array_key_exists( $key, $schema ) ) { + unset( $tree[ $key ] ); continue; } - if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { - $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); + // Check if the value is an array and requires further processing. + if ( is_array( $value ) && is_array( $schema[ $key ] ) ) { + // Determine if it is an associative or indexed array. + $schema_is_assoc = self::is_assoc( $value ); - if ( empty( $tree[ $key ] ) ) { - unset( $tree[ $key ] ); + if ( $schema_is_assoc ) { + // If associative, process as a single object. + $tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] ); + + if ( empty( $tree[ $key ] ) ) { + unset( $tree[ $key ] ); + } + } else { + // If indexed, process each item in the array. + foreach ( $value as $item_key => $item_value ) { + if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) { + $tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] ); + } else { + // If the schema does not define a further structure, keep the value as is. + $tree[ $key ][ $item_key ] = $item_value; + } + } } } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { unset( $tree[ $key ] ); @@ -987,6 +1081,20 @@ protected static function remove_keys_not_in_schema( $tree, $schema ) { return $tree; } + /** + * Checks if the given array is associative. + * + * @since 6.5.0 + * @param array $data The array to check. + * @return bool True if the array is associative, false otherwise. + */ + protected static function is_assoc( $data ) { + if ( array() === $data ) { + return false; + } + return array_keys( $data ) !== range( 0, count( $data ) - 1 ); + } + /** * Returns the existing settings for each block. * diff --git a/lib/compat/wordpress-6.3/block-editor-settings.php b/lib/compat/wordpress-6.3/block-editor-settings.php deleted file mode 100644 index b478d022c16dd9..00000000000000 --- a/lib/compat/wordpress-6.3/block-editor-settings.php +++ /dev/null @@ -1,89 +0,0 @@ -slug ) { - $page_slug = 'page'; - } - if ( 'single' === $template_type->slug ) { - $post_slug = 'single'; - } - } - - $what_post_type = get_post_type( $post_ID ); - switch ( $what_post_type ) { - case 'page': - $template_slug = $page_slug; - break; - default: - $template_slug = $post_slug; - break; - } - } - - $current_template = get_block_templates( array( 'slug__in' => array( $template_slug ) ) ); - - if ( ! empty( $current_template ) ) { - $template_blocks = parse_blocks( $current_template[0]->content ); - $post_content_block = gutenberg_find_first_block( 'core/post-content', $template_blocks ); - - if ( isset( $post_content_block['attrs'] ) ) { - $settings['postContentAttributes'] = $post_content_block['attrs']; - } - } - - return $settings; -} - -add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_experimental', PHP_INT_MAX ); diff --git a/lib/compat/wordpress-6.3/block-patterns.php b/lib/compat/wordpress-6.3/block-patterns.php deleted file mode 100644 index bf7ef632846c2d..00000000000000 --- a/lib/compat/wordpress-6.3/block-patterns.php +++ /dev/null @@ -1,165 +0,0 @@ -is_block_editor ) { - return; - } - } - - $supports_core_patterns = get_theme_support( 'core-block-patterns' ); - - /** - * Filter to disable remote block patterns. - * - * @since 5.8.0 - * - * @param bool $should_load_remote - */ - $should_load_remote = apply_filters( 'should_load_remote_block_patterns', true ); - - if ( $supports_core_patterns && $should_load_remote ) { - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $core_keyword_id = 11; // 11 is the ID for "core". - $request->set_param( 'keyword', $core_keyword_id ); - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - - foreach ( $patterns as $pattern ) { - $pattern['source'] = 'pattern-directory/core'; // Added in 6.3.0. - $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); - $pattern_name = 'core/' . sanitize_title( $normalized_pattern['title'] ); - register_block_pattern( $pattern_name, (array) $normalized_pattern ); - } - } -} - -/** - * Register `Featured` (category) patterns from wordpress.org/patterns. - * - * @since 5.9.0 - * @since 6.2.0 Normalize the pattern from the API (snake_case) to the format expected by `register_block_pattern` (camelCase). - * @since 6.3.0 Add 'pattern-directory/featured' to the pattern's 'source'. - */ -function gutenberg_load_remote_featured_patterns() { - $supports_core_patterns = get_theme_support( 'core-block-patterns' ); - - /** This filter is documented in wp-includes/block-patterns.php */ - $should_load_remote = apply_filters( 'should_load_remote_block_patterns', true ); - - if ( ! $should_load_remote || ! $supports_core_patterns ) { - return; - } - - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $featured_cat_id = 26; // This is the `Featured` category id from pattern directory. - $request->set_param( 'category', $featured_cat_id ); - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - $registry = WP_Block_Patterns_Registry::get_instance(); - foreach ( $patterns as $pattern ) { - $pattern['source'] = 'pattern-directory/featured'; // Added in 6.3.0. - $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); - $pattern_name = sanitize_title( $normalized_pattern['title'] ); - // Some patterns might be already registered as core patterns with the `core` prefix. - $is_registered = $registry->is_registered( $pattern_name ) || $registry->is_registered( "core/$pattern_name" ); - if ( ! $is_registered ) { - register_block_pattern( $pattern_name, (array) $normalized_pattern ); - } - } -} - -/** - * Registers patterns from Pattern Directory provided by a theme's - * `theme.json` file. - * - * @since 6.0.0 - * @since 6.2.0 Normalize the pattern from the API (snake_case) to the format expected by `register_block_pattern` (camelCase). - * @since 6.3.0 Add 'pattern-directory/theme' to the pattern's 'source'. - * @access private - */ -function gutenberg_register_remote_theme_patterns() { - /** This filter is documented in wp-includes/block-patterns.php */ - if ( ! apply_filters( 'should_load_remote_block_patterns', true ) ) { - return; - } - - if ( ! wp_theme_has_theme_json() ) { - return; - } - - $pattern_settings = gutenberg_get_theme_directory_pattern_slugs(); - if ( empty( $pattern_settings ) ) { - return; - } - - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - $request['slug'] = $pattern_settings; - $response = rest_do_request( $request ); - if ( $response->is_error() ) { - return; - } - $patterns = $response->get_data(); - $patterns_registry = WP_Block_Patterns_Registry::get_instance(); - foreach ( $patterns as $pattern ) { - $pattern['source'] = 'pattern-directory/theme'; // Added in 6.3.0. - $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); - $pattern_name = sanitize_title( $normalized_pattern['title'] ); - // Some patterns might be already registered as core patterns with the `core` prefix. - $is_registered = $patterns_registry->is_registered( $pattern_name ) || $patterns_registry->is_registered( "core/$pattern_name" ); - if ( ! $is_registered ) { - register_block_pattern( $pattern_name, (array) $normalized_pattern ); - } - } -} diff --git a/lib/compat/wordpress-6.3/block-template-utils.php b/lib/compat/wordpress-6.3/block-template-utils.php deleted file mode 100644 index d5f69593e473f5..00000000000000 --- a/lib/compat/wordpress-6.3/block-template-utils.php +++ /dev/null @@ -1,150 +0,0 @@ - _x( 'Index', 'Template name', 'gutenberg' ), - 'description' => __( - 'Used as a fallback template for all pages when a more specific template is not defined.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['home'] ) ) { - $default_template_types['home'] = array( - 'title' => _x( 'Blog Home', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays the latest posts as either the site homepage or as the "Posts page" as defined under reading settings. If it exists, the Front Page template overrides this template when posts are shown on the homepage.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['front-page'] ) ) { - $default_template_types['front-page'] = array( - 'title' => _x( 'Front Page', 'Template name', 'gutenberg' ), - 'description' => __( - "Displays your site's homepage, whether it is set to display latest posts or a static page. The Front Page template takes precedence over all templates.", - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['singular'] ) ) { - $default_template_types['singular'] = array( - 'title' => _x( 'Single Entries', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays any single entry, such as a post or a page. This template will serve as a fallback when a more specific template (e.g. Single Post, Page, or Attachment) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['single'] ) ) { - $default_template_types['single'] = array( - 'title' => _x( 'Single Posts', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays single posts on your website unless a custom template has been applied to that post or a dedicated template exists.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['page'] ) ) { - $default_template_types['page'] = array( - 'title' => _x( 'Pages', 'Template name', 'gutenberg' ), - 'description' => __( 'Display all static pages unless a custom template has been applied or a dedicated template exists.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['archive'] ) ) { - $default_template_types['archive'] = array( - 'title' => _x( 'All Archives', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays any archive, including posts by a single author, category, tag, taxonomy, custom post type, and date. This template will serve as a fallback when more specific templates (e.g. Category or Tag) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['author'] ) ) { - $default_template_types['author'] = array( - 'title' => _x( 'Author Archives', 'Template name', 'gutenberg' ), - 'description' => __( - "Displays a single author's post archive. This template will serve as a fallback when a more a specific template (e.g. Author: Admin) cannot be found.", - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['category'] ) ) { - $default_template_types['category'] = array( - 'title' => _x( 'Category Archives', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays a post category archive. This template will serve as a fallback when more specific template (e.g. Category: Recipes) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['taxonomy'] ) ) { - $default_template_types['taxonomy'] = array( - 'title' => _x( 'Taxonomy', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays a custom taxonomy archive. Like categories and tags, taxonomies have terms which you use to classify things. For example: a taxonomy named "Art" can have multiple terms, such as "Modern" and "18th Century." This template will serve as a fallback when a more specific template (e.g. Taxonomy: Art) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['date'] ) ) { - $default_template_types['date'] = array( - 'title' => _x( 'Date Archives', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a post archive when a specific date is visited (e.g. example.com/2023/).', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['tag'] ) ) { - $default_template_types['tag'] = array( - 'title' => _x( 'Tag Archives', 'Template name', 'gutenberg' ), - 'description' => __( - 'Displays a post tag archive. This template will serve as a fallback when more specific template (e.g. Tag: Pizza) cannot be found.', - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['attachment'] ) ) { - $default_template_types['attachment'] = array( - 'title' => _x( 'Attachment Pages', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when a visitor views the dedicated page that exists for any media attachment.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['search'] ) ) { - $default_template_types['search'] = array( - 'title' => _x( 'Search Results', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when a visitor performs a search on your website.', 'gutenberg' ), - ); - } - if ( isset( $default_template_types['privacy-policy'] ) ) { - $default_template_types['privacy-policy'] = array( - 'title' => _x( 'Privacy Policy', 'Template name', 'gutenberg' ), - 'description' => __( - "Displays your site's Privacy Policy page.", - 'gutenberg' - ), - ); - } - if ( isset( $default_template_types['404'] ) ) { - $default_template_types['404'] = array( - 'title' => _x( 'Page: 404', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when a visitor views a non-existent page, such as a dead link or a mistyped URL.', 'gutenberg' ), - ); - } - - return $default_template_types; -} -add_filter( 'default_template_types', 'gutenberg_get_default_block_template_types', 10 ); diff --git a/lib/compat/wordpress-6.3/blocks.php b/lib/compat/wordpress-6.3/blocks.php deleted file mode 100644 index 001416b42566f7..00000000000000 --- a/lib/compat/wordpress-6.3/blocks.php +++ /dev/null @@ -1,122 +0,0 @@ -= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/46496 - * - * @param array $settings Current block type settings. - * @param array $metadata Block metadata as read in via block.json. - * - * @return array Filtered block type settings. - */ -function gutenberg_add_selectors_property_to_block_type_settings( $settings, $metadata ) { - if ( ! isset( $settings['selectors'] ) && isset( $metadata['selectors'] ) ) { - $settings['selectors'] = $metadata['selectors']; - } - - return $settings; -} -add_filter( 'block_type_metadata_settings', 'gutenberg_add_selectors_property_to_block_type_settings', 10, 2 ); - -/** - * Renames Reusable block CPT to Pattern. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/51144 - * - * @param array $args Register post type args. - * @param string $post_type The post type string. - * - * @return array Register post type args. - */ -function gutenberg_rename_reusable_block_cpt_to_pattern( $args, $post_type ) { - if ( 'wp_block' === $post_type ) { - $args['labels']['name'] = _x( 'Patterns', 'post type general name' ); - $args['labels']['singular_name'] = _x( 'Pattern', 'post type singular name' ); - $args['labels']['add_new_item'] = __( 'Add new Pattern' ); - $args['labels']['new_item'] = __( 'New Pattern' ); - $args['labels']['edit_item'] = __( 'Edit Block Pattern' ); - $args['labels']['view_item'] = __( 'View Pattern' ); - $args['labels']['view_items'] = __( 'View Patterns' ); - $args['labels']['all_items'] = __( 'All Patterns' ); - $args['labels']['search_items'] = __( 'Search Patterns' ); - $args['labels']['not_found'] = __( 'No Patterns found.' ); - $args['labels']['not_found_in_trash'] = __( 'No Patterns found in Trash.' ); - $args['labels']['filter_items_list'] = __( 'Filter Patterns list' ); - $args['labels']['items_list_navigation'] = __( 'Patterns list navigation' ); - $args['labels']['items_list'] = __( 'Patterns list' ); - $args['labels']['item_published'] = __( 'Pattern published.' ); - $args['labels']['item_published_privately'] = __( 'Pattern published privately.' ); - $args['labels']['item_reverted_to_draft'] = __( 'Pattern reverted to draft.' ); - $args['labels']['item_scheduled'] = __( 'Pattern scheduled.' ); - $args['labels']['item_updated'] = __( 'Pattern updated.' ); - $args['rest_controller_class'] = 'Gutenberg_REST_Blocks_Controller'; - } - - return $args; -} - -add_filter( 'register_post_type_args', 'gutenberg_rename_reusable_block_cpt_to_pattern', 10, 2 ); - -/** - * Adds custom fields support to the wp_block post type so an unsynced option can be added. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/51144 - * - * @param array $args Register post type args. - * @param string $post_type The post type string. - * - * @return array Register post type args. - */ -function gutenberg_add_custom_fields_to_wp_block( $args, $post_type ) { - if ( 'wp_block' === $post_type ) { - array_push( $args['supports'], 'custom-fields' ); - } - - return $args; -} -add_filter( 'register_post_type_args', 'gutenberg_add_custom_fields_to_wp_block', 10, 2 ); - -/** - * Adds wp_pattern_sync_status meta fields to the wp_block post type so an unsynced option can be added. - * - * Note: This should be removed when the minimum required WP version is >= 6.3. - * - * @see https://github.com/WordPress/gutenberg/pull/51144 - * - * @return void - */ -function gutenberg_wp_block_register_post_meta() { - $post_type = 'wp_block'; - register_post_meta( - $post_type, - 'wp_pattern_sync_status', - array( - 'auth_callback' => function () { - return current_user_can( 'edit_posts' ); - }, - 'sanitize_callback' => 'sanitize_text_field', - 'single' => true, - 'type' => 'string', - 'show_in_rest' => array( - 'schema' => array( - 'type' => 'string', - 'enum' => array( 'partial', 'unsynced' ), - ), - ), - ) - ); -} -add_action( 'init', 'gutenberg_wp_block_register_post_meta' ); diff --git a/lib/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php b/lib/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php deleted file mode 100644 index 8677f9abaee170..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php +++ /dev/null @@ -1,123 +0,0 @@ -term_id, array( 'update_post_term_cache' => false ) ); - - if ( empty( $menu_items ) ) { - return array(); - } - - // Set up the $menu_item variables. - // Adds the class property classes for the current context, if applicable. - _wp_menu_item_classes_by_context( $menu_items ); - - $menu_items_by_parent_id = static::group_by_parent_id( $menu_items ); - - $first_menu_item = isset( $menu_items_by_parent_id[0] ) - ? $menu_items_by_parent_id[0] - : array(); - - $inner_blocks = static::to_blocks( - $first_menu_item, - $menu_items_by_parent_id - ); - - return serialize_blocks( $inner_blocks ); - } - - /** - * Returns an array of menu items grouped by the id of the parent menu item. - * - * @param array $menu_items An array of menu items. - * @return array - */ - private static function group_by_parent_id( $menu_items ) { - $menu_items_by_parent_id = array(); - - foreach ( $menu_items as $menu_item ) { - $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item; - } - - return $menu_items_by_parent_id; - } - - /** - * Turns menu item data into a nested array of parsed blocks - * - * @param array $menu_items An array of menu items that represent - * an individual level of a menu. - * @param array $menu_items_by_parent_id An array keyed by the id of the - * parent menu where each element is an - * array of menu items that belong to - * that parent. - * @return array An array of parsed block data. - */ - private static function to_blocks( $menu_items, $menu_items_by_parent_id ) { - - if ( empty( $menu_items ) ) { - return array(); - } - - $blocks = array(); - - foreach ( $menu_items as $menu_item ) { - $class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null; - $id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null; - $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target; - $rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null; - $kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom'; - - $block = array( - 'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link', - 'attrs' => array( - 'className' => $class_name, - 'description' => $menu_item->description, - 'id' => $id, - 'kind' => $kind, - 'label' => $menu_item->title, - 'opensInNewTab' => $opens_in_new_tab, - 'rel' => $rel, - 'title' => $menu_item->attr_title, - 'type' => $menu_item->object, - 'url' => $menu_item->url, - ), - ); - - $block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] ) - ? static::to_blocks( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id ) - : array(); - $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] ); - - $blocks[] = $block; - } - - return $blocks; - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php b/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php deleted file mode 100644 index fcd70da61f57ed..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php +++ /dev/null @@ -1,237 +0,0 @@ - 'wp_navigation', - 'no_found_rows' => true, - 'update_post_meta_cache' => false, - 'update_post_term_cache' => false, - 'order' => 'DESC', - 'orderby' => 'date', - 'post_status' => 'publish', - 'posts_per_page' => 1, - ); - - $navigation_post = new WP_Query( $parsed_args ); - - if ( count( $navigation_post->posts ) > 0 ) { - return $navigation_post->posts[0]; - } - - return null; - } - - /** - * Creates a Navigation Menu post from a Classic Menu. - * - * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object. - */ - private static function create_classic_menu_fallback() { - // See if we have a classic menu. - $classic_nav_menu = static::get_fallback_classic_menu(); - - if ( ! $classic_nav_menu ) { - return new WP_Error( 'no_classic_menus', __( 'No Classic Menus found.', 'gutenberg' ) ); - } - - // If there is a classic menu then convert it to blocks. - $classic_nav_menu_blocks = Gutenberg_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu ); - - if ( empty( $classic_nav_menu_blocks ) ) { - return new WP_Error( 'cannot_convert_classic_menu', __( 'Unable to convert Classic Menu to blocks.', 'gutenberg' ) ); - } - - // Create a new navigation menu from the classic menu. - $classic_menu_fallback = wp_insert_post( - array( - 'post_content' => $classic_nav_menu_blocks, - 'post_title' => $classic_nav_menu->name, - 'post_name' => $classic_nav_menu->slug, - 'post_status' => 'publish', - 'post_type' => 'wp_navigation', - ), - true // So that we can check whether the result is an error. - ); - - return $classic_menu_fallback; - } - - /** - * Determine the most appropriate classic navigation menu to use as a fallback. - * - * @return WP_Term|null The most appropriate classic navigation menu to use as a fallback. - */ - private static function get_fallback_classic_menu() { - $classic_nav_menus = wp_get_nav_menus(); - - if ( ! $classic_nav_menus || is_wp_error( $classic_nav_menus ) ) { - return null; - } - - $nav_menu = static::get_nav_menu_at_primary_location(); - - if ( $nav_menu ) { - return $nav_menu; - } - - $nav_menu = static::get_nav_menu_with_primary_slug( $classic_nav_menus ); - - if ( $nav_menu ) { - return $nav_menu; - } - - return static::get_most_recently_created_nav_menu( $classic_nav_menus ); - } - - - /** - * Sorts the classic menus and returns the most recently created one. - * - * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects. - * @return WP_Term The most recently created classic nav menu. - */ - private static function get_most_recently_created_nav_menu( $classic_nav_menus ) { - usort( - $classic_nav_menus, - static function ( $a, $b ) { - return $b->term_id - $a->term_id; - } - ); - - return $classic_nav_menus[0]; - } - - /** - * Returns the classic menu with the slug `primary` if it exists. - * - * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects. - * @return WP_Term|null The classic nav menu with the slug `primary` or null. - */ - private static function get_nav_menu_with_primary_slug( $classic_nav_menus ) { - foreach ( $classic_nav_menus as $classic_nav_menu ) { - if ( 'primary' === $classic_nav_menu->slug ) { - return $classic_nav_menu; - } - } - - return null; - } - - - /** - * Gets the classic menu assigned to the `primary` navigation menu location - * if it exists. - * - * @return WP_Term|null The classic nav menu assigned to the `primary` location or null. - */ - private static function get_nav_menu_at_primary_location() { - $locations = get_nav_menu_locations(); - - if ( isset( $locations['primary'] ) ) { - $primary_menu = wp_get_nav_menu_object( $locations['primary'] ); - - if ( $primary_menu ) { - return $primary_menu; - } - } - - return null; - } - - /** - * Creates a default Navigation Block Menu fallback. - * - * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object. - */ - private static function create_default_fallback() { - - $default_blocks = static::get_default_fallback_blocks(); - - // Create a new navigation menu from the fallback blocks. - $default_fallback = wp_insert_post( - array( - 'post_content' => $default_blocks, - 'post_title' => _x( 'Navigation', 'Title of a Navigation menu', 'gutenberg' ), - 'post_name' => 'navigation', - 'post_status' => 'publish', - 'post_type' => 'wp_navigation', - ), - true // So that we can check whether the result is an error. - ); - - return $default_fallback; - } - - /** - * Gets the rendered markup for the default fallback blocks. - * - * @return string default blocks markup to use a the fallback. - */ - private static function get_default_fallback_blocks() { - $registry = WP_Block_Type_Registry::get_instance(); - - // If `core/page-list` is not registered then use empty blocks. - return $registry->is_registered( 'core/page-list' ) ? '' : ''; - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php deleted file mode 100644 index 0a5b026cded3b9..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php +++ /dev/null @@ -1,198 +0,0 @@ -get_fields_for_response( $request ); - $keys = array( - 'name' => 'name', - 'title' => 'title', - 'description' => 'description', - 'viewportWidth' => 'viewport_width', - 'blockTypes' => 'block_types', - 'postTypes' => 'post_types', - 'categories' => 'categories', - 'keywords' => 'keywords', - 'content' => 'content', - 'inserter' => 'inserter', - 'templateTypes' => 'template_types', - 'source' => 'source', - ); - $data = array(); - foreach ( $keys as $item_key => $rest_key ) { - if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) { - $data[ $rest_key ] = $item[ $item_key ]; - } - } - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - return rest_ensure_response( $data ); - } - - /** - * Retrieves the block pattern schema, conforming to JSON Schema. - * - * @since 6.0.0 - * @since 6.1.0 Added `post_types` property. - * @since 6.3.0 Added `source` property. - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'block-pattern', - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'description' => __( 'The pattern name.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'title' => array( - 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'description' => array( - 'description' => __( 'The pattern detailed description.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'viewport_width' => array( - 'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ), - 'type' => 'number', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'block_types' => array( - 'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'post_types' => array( - 'description' => __( 'An array of post types that the pattern is restricted to be used with.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'categories' => array( - 'description' => __( 'The pattern category slugs.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'keywords' => array( - 'description' => __( 'The pattern keywords.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'template_types' => array( - 'description' => __( 'An array of template types where the pattern fits.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'content' => array( - 'description' => __( 'The pattern content.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'inserter' => array( - 'description' => __( 'Determines whether the pattern is visible in inserter.', 'gutenberg' ), - 'type' => 'boolean', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'source' => array( - 'description' => __( 'Where the pattern comes from e.g. core', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - 'enum' => array( - 'core', - 'plugin', - 'theme', - 'pattern-directory/core', - 'pattern-directory/theme', - 'pattern-directory/featured', - ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Retrieves all block patterns. - * - * @since 6.0.0 - * @since 6.2.0 Added migration for old core pattern categories to the new ones. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_items( $request ) { - if ( ! $this->remote_patterns_loaded ) { - // Load block patterns from w.org. - gutenberg_load_remote_block_patterns(); // Patterns with the `core` keyword. - gutenberg_load_remote_featured_patterns(); // Patterns in the `featured` category. - gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme. - - $this->remote_patterns_loaded = true; - } - - $response = array(); - $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered(); - foreach ( $patterns as $pattern ) { - $migrated_pattern = $this->migrate_pattern_categories( $pattern ); - $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request ); - $response[] = $this->prepare_response_for_collection( $prepared_pattern ); - } - return rest_ensure_response( $response ); - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php deleted file mode 100644 index 5279a2c3a829ec..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php +++ /dev/null @@ -1,51 +0,0 @@ -parent_post_type = 'wp_global_styles'; - $this->rest_base = 'revisions'; - $this->parent_base = 'global-styles'; - $this->namespace = 'wp/v2'; - } - - /** - * Registers the controllers routes. - * - * @return void - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, - array( - 'args' => array( - 'parent' => array( - 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Retrieves the query params for collections. - * - * Inherits from WP_REST_Controller::get_collection_params(), - * also reflects changes to return value WP_REST_Revisions_Controller::get_collection_params(). - * - * @since 6.3.0 - * - * @return array Collection parameters. - */ - public function get_collection_params() { - $collection_params = parent::get_collection_params(); - $collection_params['context']['default'] = 'view'; - $collection_params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.' ), - 'type' => 'integer', - ); - unset( $collection_params['search'] ); - unset( $collection_params['per_page']['default'] ); - - return $collection_params; - } - - /** - * Returns decoded JSON from post content string, - * or a 404 if not found. - * - * @since 6.3.0 - * - * @param string $raw_json Encoded JSON from global styles custom post content. - * @return Array|WP_Error - */ - protected function get_decoded_global_styles_json( $raw_json ) { - $decoded_json = json_decode( $raw_json, true ); - - if ( is_array( $decoded_json ) && isset( $decoded_json['isGlobalStylesUserThemeJSON'] ) && true === $decoded_json['isGlobalStylesUserThemeJSON'] ) { - return $decoded_json; - } - - return new WP_Error( - 'rest_global_styles_not_found', - __( 'Cannot find user global styles revisions.' ), - array( 'status' => 404 ) - ); - } - - /** - * Returns revisions of the given global styles config custom post type. - * - * @since 6.3.0 - * - * @param WP_REST_Request $request The request instance. - * - * @return WP_REST_Response|WP_Error - */ - public function get_items( $request ) { - $parent = $this->get_parent( $request['parent'] ); - - if ( is_wp_error( $parent ) ) { - return $parent; - } - - $global_styles_config = $this->get_decoded_global_styles_json( $parent->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - if ( wp_revisions_enabled( $parent ) ) { - $registered = $this->get_collection_params(); - $query_args = array( - 'post_parent' => $parent->ID, - 'post_type' => 'revision', - 'post_status' => 'inherit', - 'posts_per_page' => -1, - 'orderby' => 'date ID', - 'order' => 'DESC', - ); - - $parameter_mappings = array( - 'offset' => 'offset', - 'page' => 'paged', - 'per_page' => 'posts_per_page', - ); - - foreach ( $parameter_mappings as $api_param => $wp_param ) { - if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { - $query_args[ $wp_param ] = $request[ $api_param ]; - } - } - - $revisions_query = new WP_Query(); - $revisions = $revisions_query->query( $query_args ); - $offset = isset( $query_args['offset'] ) ? (int) $query_args['offset'] : 0; - $page = (int) $query_args['paged']; - $total_revisions = $revisions_query->found_posts; - - if ( $total_revisions < 1 ) { - // Out-of-bounds, run the query again without LIMIT for total count. - unset( $query_args['paged'], $query_args['offset'] ); - $count_query = new WP_Query(); - $count_query->query( $query_args ); - - $total_revisions = $count_query->found_posts; - } - - if ( $revisions_query->query_vars['posts_per_page'] > 0 ) { - $max_pages = ceil( $total_revisions / (int) $revisions_query->query_vars['posts_per_page'] ); - } else { - $max_pages = $total_revisions > 0 ? 1 : 0; - } - if ( $total_revisions > 0 ) { - if ( $offset >= $total_revisions ) { - return new WP_Error( - 'rest_revision_invalid_offset_number', - __( 'The offset number requested is larger than or equal to the number of available revisions.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } elseif ( ! $offset && $page > $max_pages ) { - return new WP_Error( - 'rest_revision_invalid_page_number', - __( 'The page number requested is larger than the number of pages available.', 'gutenberg' ), - array( 'status' => 400 ) - ); - } - } - } else { - $revisions = array(); - $total_revisions = 0; - $max_pages = 0; - $page = (int) $request['page']; - } - - $response = array(); - - foreach ( $revisions as $revision ) { - $data = $this->prepare_item_for_response( $revision, $request ); - $response[] = $this->prepare_response_for_collection( $data ); - } - - $response = rest_ensure_response( $response ); - - $response->header( 'X-WP-Total', (int) $total_revisions ); - $response->header( 'X-WP-TotalPages', (int) $max_pages ); - - $request_params = $request->get_query_params(); - $base_path = rest_url( sprintf( '%s/%s/%d/%s', $this->namespace, $this->parent_base, $request['parent'], $this->rest_base ) ); - $base = add_query_arg( urlencode_deep( $request_params ), $base_path ); - - if ( $page > 1 ) { - $prev_page = $page - 1; - - if ( $prev_page > $max_pages ) { - $prev_page = $max_pages; - } - - $prev_link = add_query_arg( 'page', $prev_page, $base ); - $response->link_header( 'prev', $prev_link ); - } - if ( $max_pages > $page ) { - $next_page = $page + 1; - $next_link = add_query_arg( 'page', $next_page, $base ); - - $response->link_header( 'next', $next_link ); - } - - return $response; - } - - /** - * A direct copy of WP_REST_Revisions_Controller->prepare_date_response(). - * Checks the post_date_gmt or modified_gmt and prepare any post or - * modified date for single post output. - * - * @since 6.3.0 - * - * @param string $date_gmt GMT publication time. - * @param string|null $date Optional. Local publication time. Default null. - * @return string|null ISO8601/RFC3339 formatted datetime, otherwise null. - */ - protected function prepare_date_response( $date_gmt, $date = null ) { - if ( '0000-00-00 00:00:00' === $date_gmt ) { - return null; - } - - if ( isset( $date ) ) { - return mysql_to_rfc3339( $date ); - } - - return mysql_to_rfc3339( $date_gmt ); - } - - /** - * Prepares the revision for the REST response. - * - * @since 6.3.0 - * - * @param WP_Post $post Post revision object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object. - */ - public function prepare_item_for_response( $post, $request ) { - $parent = $this->get_parent( $request['parent'] ); - $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) { - $global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data(); - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass(); - } - if ( rest_is_field_included( 'styles', $fields ) ) { - $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass(); - } - } - - if ( rest_is_field_included( 'author', $fields ) ) { - $data['author'] = (int) $post->post_author; - } - - if ( rest_is_field_included( 'date', $fields ) ) { - $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); - } - - if ( rest_is_field_included( 'date_gmt', $fields ) ) { - $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt ); - } - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $post->ID; - } - - if ( rest_is_field_included( 'modified', $fields ) ) { - $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); - } - - if ( rest_is_field_included( 'modified_gmt', $fields ) ) { - $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt ); - } - - if ( rest_is_field_included( 'parent', $fields ) ) { - $data['parent'] = (int) $parent->ID; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - return rest_ensure_response( $data ); - } - - /** - * Retrieves the revision's schema, conforming to JSON Schema. - * - * @since 6.3.0 - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => "{$this->parent_post_type}-revision", - 'type' => 'object', - // Base properties for every Revision. - 'properties' => array( - - /* - * Adds settings and styles from the WP_REST_Revisions_Controller item fields. - * Leaves out GUID as global styles shouldn't be accessible via URL. - */ - 'author' => array( - 'description' => __( 'The ID for the author of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date' => array( - 'description' => __( "The date the revision was published, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date_gmt' => array( - 'description' => __( 'The date the revision was published, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'id' => array( - 'description' => __( 'Unique identifier for the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'modified' => array( - 'description' => __( "The date the revision was last modified, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'modified_gmt' => array( - 'description' => __( 'The date the revision was last modified, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'parent' => array( - 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - - // Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema. - 'styles' => array( - 'description' => __( 'Global styles.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - 'settings' => array( - 'description' => __( 'Global settings.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Checks if a given request has access to read a single global style. - * - * @since 6.3.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - */ - public function get_item_permissions_check( $request ) { - $post = $this->get_parent( $request['parent'] ); - if ( is_wp_error( $post ) ) { - return $post; - } - - /* - * The same check as WP_REST_Global_Styles_Controller->get_item_permissions_check. - */ - if ( ! current_user_can( 'read_post', $post->ID ) ) { - return new WP_Error( - 'rest_cannot_view', - __( 'Sorry, you are not allowed to view revisions for this global style.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } - - /** - * Get the parent post, if the ID is valid. Copied from WP_REST_Revisions_Controller. - * - * @since 6.3.0 - * - * @param int $parent_post_id Supplied ID. - * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. - */ - protected function get_parent( $parent_post_id ) { - $error = new WP_Error( - 'rest_post_invalid_parent', - __( 'Invalid post parent ID.', 'gutenberg' ), - array( 'status' => 404 ) - ); - - if ( (int) $parent_post_id <= 0 ) { - return $error; - } - - $parent_post = get_post( (int) $parent_post_id ); - - if ( empty( $parent_post ) || empty( $parent_post->ID ) - || $this->parent_post_type !== $parent_post->post_type - ) { - return $error; - } - - return $parent_post; - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php deleted file mode 100644 index 2cac70f2ea1c34..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php +++ /dev/null @@ -1,178 +0,0 @@ -namespace = 'wp-block-editor/v1'; - $this->rest_base = 'navigation-fallback'; - $this->post_type = 'wp_navigation'; - } - - /** - * Registers the controllers routes. - * - * @return void - */ - public function register_routes() { - - // Lists a single nav item based on the given id or slug. - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::READABLE ), - ), - 'schema' => array( $this, 'get_item_schema' ), - ) - ); - } - - /** - * Checks if a given request has access to read fallbacks. - * - * @param WP_REST_Request $request Full details about the request. - * @return true|WP_Error True if the request has read access, WP_Error object otherwise. - */ - public function get_item_permissions_check( $request ) { - - $post_type = get_post_type_object( $this->post_type ); - - // Getting fallbacks requires creating and reading `wp_navigation` posts. - if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( 'edit_theme_options' ) || ! current_user_can( 'edit_posts' ) ) { - return new WP_Error( - 'rest_cannot_create', - __( 'Sorry, you are not allowed to create Navigation Menus as this user.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - if ( 'edit' === $request['context'] && ! current_user_can( $post_type->cap->edit_posts ) ) { - return new WP_Error( - 'rest_forbidden_context', - __( 'Sorry, you are not allowed to edit Navigation Menus as this user.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } - - /** - * Gets the most appropriate fallback Navigation Menu. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_item( $request ) { - $post = Gutenberg_Navigation_Fallback::get_fallback(); - - if ( empty( $post ) ) { - return rest_ensure_response( new WP_Error( 'no_fallback_menu', __( 'No fallback menu found.', 'gutenberg' ), array( 'status' => 404 ) ) ); - } - - $response = $this->prepare_item_for_response( $post, $request ); - - return $response; - } - - /** - * Retrieves the fallbacks' schema, conforming to JSON Schema. - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $this->schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'navigation-fallback', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'The unique identifier for the Navigation Menu.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ); - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Matches the post data to the schema we want. - * - * @param WP_Post $item The wp_navigation Post object whose response is being prepared. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response $response The response data. - */ - public function prepare_item_for_response( $item, $request ) { - $data = array(); - - $fields = $this->get_fields_for_response( $request ); - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $item->ID; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - $response = rest_ensure_response( $data ); - - if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { - $links = $this->prepare_links( $item ); - $response->add_links( $links ); - } - - return $response; - } - - /** - * Prepares the links for the request. - * - * @param WP_Post $post the Navigation Menu post object. - * @return array Links for the given request. - */ - private function prepare_links( $post ) { - return array( - 'self' => array( - 'href' => rest_url( rest_get_route_for_post( $post->ID ) ), - 'embeddable' => true, - ), - ); - } -} diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php deleted file mode 100644 index a92cbd1e2c1716..00000000000000 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php +++ /dev/null @@ -1,68 +0,0 @@ -namespace, - '/' . $this->rest_base . '/lookup', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_template_fallback' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'slug' => array( - 'description' => __( 'The slug of the template to get the fallback for', 'gutenberg' ), - 'type' => 'string', - 'required' => true, - ), - 'is_custom' => array( - 'description' => __( 'Indicates if a template is custom or part of the template hierarchy', 'gutenberg' ), - 'type' => 'boolean', - ), - 'template_prefix' => array( - 'description' => __( 'The template prefix for the created template. This is used to extract the main template type ex. in `taxonomy-books` we extract the `taxonomy`', 'gutenberg' ), - 'type' => 'string', - ), - ), - ), - ) - ); - parent::register_routes(); - // Get fallback template content. - } - - /** - * Returns the fallback template for a given slug. - * - * @param WP_REST_Request $request The request instance. - * - * @return WP_REST_Response|WP_Error - */ - public function get_template_fallback( $request ) { - $hierarchy = get_template_hierarchy( $request['slug'], $request['is_custom'], $request['template_prefix'] ); - $fallback_template = null; - do { - $fallback_template = resolve_block_template( $request['slug'], $hierarchy, '' ); - array_shift( $hierarchy ); - } while ( ! empty( $hierarchy ) && empty( $fallback_template->content ) ); - $response = $this->prepare_item_for_response( $fallback_template, $request ); - return rest_ensure_response( $response ); - } -} diff --git a/lib/compat/wordpress-6.3/footnotes.php b/lib/compat/wordpress-6.3/footnotes.php deleted file mode 100644 index 6225b280e1b6c2..00000000000000 --- a/lib/compat/wordpress-6.3/footnotes.php +++ /dev/null @@ -1,32 +0,0 @@ -\s*\d+\s*_', - '', - $content - ); -} - -add_filter( 'the_content', 'gutenberg_trim_footnotes' ); diff --git a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php b/lib/compat/wordpress-6.3/get-global-styles-and-settings.php deleted file mode 100644 index 009fa6253f79d4..00000000000000 --- a/lib/compat/wordpress-6.3/get-global-styles-and-settings.php +++ /dev/null @@ -1,114 +0,0 @@ -selectors ); - - // Root Selector. - - // Calculated before returning as it can be used as fallback for - // feature selectors later on. - $root_selector = null; - - if ( $has_selectors && isset( $block_type->selectors['root'] ) ) { - // Use the selectors API if available. - $root_selector = $block_type->selectors['root']; - } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && is_string( $block_type->supports['__experimentalSelector'] ) ) { - // Use the old experimental selector supports property if set. - $root_selector = $block_type->supports['__experimentalSelector']; - } else { - // If no root selector found, generate default block class selector. - $block_name = str_replace( '/', '-', str_replace( 'core/', '', $block_type->name ) ); - $root_selector = ".wp-block-{$block_name}"; - } - - // Return selector if it's the root target we are looking for. - if ( 'root' === $target ) { - return $root_selector; - } - - // If target is not `root` we have a feature or subfeature as the target. - // If the target is a string convert to an array. - if ( is_string( $target ) ) { - $target = explode( '.', $target ); - } - - // Feature Selectors ( May fallback to root selector ). - if ( 1 === count( $target ) ) { - $fallback_selector = $fallback ? $root_selector : null; - - // Prefer the selectors API if available. - if ( $has_selectors ) { - // Look for selector under `feature.root`. - $path = array_merge( $target, array( 'root' ) ); - $feature_selector = _wp_array_get( $block_type->selectors, $path, null ); - - if ( $feature_selector ) { - return $feature_selector; - } - - // Check if feature selector is set via shorthand. - $feature_selector = _wp_array_get( $block_type->selectors, $target, null ); - - return is_string( $feature_selector ) ? $feature_selector : $fallback_selector; - } - - // Try getting old experimental supports selector value. - $path = array_merge( $target, array( '__experimentalSelector' ) ); - $feature_selector = _wp_array_get( $block_type->supports, $path, null ); - - // Nothing to work with, provide fallback or null. - if ( null === $feature_selector ) { - return $fallback_selector; - } - - // Scope the feature selector by the block's root selector. - return WP_Theme_JSON_Gutenberg::scope_selector( $root_selector, $feature_selector ); - } - - // Subfeature selector - // This may fallback either to parent feature or root selector. - $subfeature_selector = null; - - // Use selectors API if available. - if ( $has_selectors ) { - $subfeature_selector = _wp_array_get( $block_type->selectors, $target, null ); - } - - // Only return if we have a subfeature selector. - if ( $subfeature_selector ) { - return $subfeature_selector; - } - - // To this point we don't have a subfeature selector. If a fallback - // has been requested, remove subfeature from target path and return - // results of a call for the parent feature's selector. - if ( $fallback ) { - return wp_get_block_css_selector( $block_type, $target[0], $fallback ); - } - - // We tried... - return null; - } -} diff --git a/lib/compat/wordpress-6.3/kses.php b/lib/compat/wordpress-6.3/kses.php deleted file mode 100644 index b0b7356d2dac1c..00000000000000 --- a/lib/compat/wordpress-6.3/kses.php +++ /dev/null @@ -1,29 +0,0 @@ -post_type || 'wp_template_part' === $post->post_type ) { - $post_type_object = get_post_type_object( $post->post_type ); - $slug = urlencode( get_stylesheet() . '//' . $post->post_name ); - $link = admin_url( sprintf( $post_type_object->_edit_link, $slug ) ); - } - - return $link; -} - -add_filter( 'get_edit_post_link', 'gutenberg_update_get_edit_post_link', 10, 2 ); - - - -/** - * Modifies the edit link for the `wp_navigation` custom post type. - * - * This has not been backported to Core. - * - * @param string $link The edit link. - * @param int $post_id Post ID. - * @return string|null The edit post link for the given post. Null if the post type does not exist - * or does not allow an editing UI. - */ -function gutenberg_update_navigation_get_edit_post_link( $link, $post_id ) { - $post = get_post( $post_id ); - - if ( 'wp_navigation' === $post->post_type ) { - $post_type_object = get_post_type_object( $post->post_type ); - $id = $post->ID; - $link = admin_url( sprintf( $post_type_object->_edit_link, $id ) ); - } - - return $link; -} -add_filter( 'get_edit_post_link', 'gutenberg_update_navigation_get_edit_post_link', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/navigation-block-preloading.php b/lib/compat/wordpress-6.3/navigation-block-preloading.php deleted file mode 100644 index 22b4a97526793e..00000000000000 --- a/lib/compat/wordpress-6.3/navigation-block-preloading.php +++ /dev/null @@ -1,48 +0,0 @@ -name ) && 'core/edit-site' !== $context->name ) { - return $preload_paths; - } - - $navigation_rest_route = rest_get_route_for_post_type_items( - 'wp_navigation' - ); - - // Preload the OPTIONS request for all Navigation posts request. - $preload_paths[] = array( $navigation_rest_route, 'OPTIONS' ); - - // Preload request for all menus in Browse Mode sidebar "Navigation" section. - $preload_paths[] = array( - add_query_arg( - array( - 'context' => 'edit', - 'per_page' => 100, - 'order' => 'desc', - 'orderby' => 'date', - 'status[0]' => 'publish', - 'status[1]' => 'draft', - ), - $navigation_rest_route - ), - 'GET', - ); - - return $preload_paths; -} -add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_preload_navigation_posts', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/navigation-fallback.php b/lib/compat/wordpress-6.3/navigation-fallback.php deleted file mode 100644 index 5cc84f4a1c848c..00000000000000 --- a/lib/compat/wordpress-6.3/navigation-fallback.php +++ /dev/null @@ -1,54 +0,0 @@ - $post_type, - 'postId' => '%s', - 'canvas' => 'edit', - ) - ); - $args['_edit_link'] = $template_edit_link; - } - - if ( in_array( $post_type, array( 'wp_global_styles' ), true ) ) { - $args['_edit_link'] = '/site-editor.php?canvas=edit'; - } - - if ( 'wp_navigation' === $post_type ) { - $navigation_edit_link = 'site-editor.php?' . build_query( - array( - 'postId' => '%s', - 'postType' => 'wp_navigation', - 'canvas' => 'edit', - ) - ); - $args['_edit_link'] = $navigation_edit_link; - } - - return $args; -} -add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 ); - -if ( ! function_exists( 'add_modified_wp_template_schema' ) ) { - /** - * Add the `modified` value to the `wp_template` schema. - * - * @since 6.3.0 Added 'modified' property and response value. - */ - function add_modified_wp_template_schema() { - register_rest_field( - array( 'wp_template', 'wp_template_part' ), - 'modified', - array( - 'schema' => array( - 'description' => __( "The date the template was last modified, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'get_callback' => function ( $template_object ) { - if ( ! empty( $template_object['wp_id'] ) ) { - $post = get_post( $template_object['wp_id'] ); - if ( $post && isset( $post->post_modified ) ) { - return mysql_to_rfc3339( $post->post_modified ); - } - } - return null; - }, - ) - ); - } -} -add_filter( 'rest_api_init', 'add_modified_wp_template_schema' ); - -/** - * Registers the Navigation Fallbacks REST API routes. - */ -function gutenberg_register_rest_navigation_fallbacks() { - $editor_settings = new Gutenberg_REST_Navigation_Fallback_Controller(); - $editor_settings->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_navigation_fallbacks' ); - -/** - * Add extra collection params to themes requests. - * - * @param array $query_params JSON Schema-formatted collection parameters. - * @return array Updated parameters. - */ -function gutenberg_themes_collection_params_6_3( $query_params ) { - $query_params['is_block_theme'] = array( - 'description' => __( 'Whether the theme is a block-based theme.' ), - 'type' => 'boolean', - 'readonly' => true, - ); - return $query_params; -} -add_filter( 'rest_themes_collection_params', 'gutenberg_themes_collection_params_6_3' ); - -/** - * Updates REST API response for the themes and adds the `is_block_theme` flag. - * - * @param WP_REST_Response $response The response object. - * @param WP_Theme $theme Theme object used to create response. - * @return WP_REST_Response $response Updated response object. - */ -function gutenberg_modify_rest_themes_response( $response, $theme ) { - $response->data['is_block_theme'] = $theme->is_block_theme(); - return $response; -} -add_filter( 'rest_prepare_theme', 'gutenberg_modify_rest_themes_response', 10, 2 ); diff --git a/lib/compat/wordpress-6.3/script-loader.php b/lib/compat/wordpress-6.3/script-loader.php deleted file mode 100644 index c8de16efd06875..00000000000000 --- a/lib/compat/wordpress-6.3/script-loader.php +++ /dev/null @@ -1,14 +0,0 @@ -errors() ) ) { - if ( current_filter() === 'template' ) { - $theme_path = $wp_theme->get_template(); - } else { - $theme_path = $wp_theme->get_stylesheet(); - } - - return sanitize_text_field( $theme_path ); - } - - return $current_stylesheet; -} - -/** - * Adds a middleware to the REST API to set the theme for the preview. - */ -function gutenberg_attach_theme_preview_middleware() { - // Don't allow non-admins to preview themes. - if ( ! current_user_can( 'switch_themes' ) ) { - return; - } - - wp_add_inline_script( - 'wp-api-fetch', - sprintf( - 'wp.apiFetch.use( wp.apiFetch.createThemePreviewMiddleware( %s ) );', - wp_json_encode( sanitize_text_field( $_GET['wp_theme_preview'] ) ) - ), - 'after' - ); -} - -if ( ! function_exists( 'add_live_preview_button' ) ) { - /** - * Temporary function to add a live preview button to block themes. - * Remove when https://core.trac.wordpress.org/ticket/58190 lands. - */ - function add_live_preview_button() { - global $pagenow; - if ( 'themes.php' === $pagenow ) { - ?> - - - - next_tag( array( 'class' => 'wp-group-block' ) ) ) { + * if ( $tags->next_tag( array( 'class_name' => 'wp-group-block' ) ) ) { * $tags->set_attribute( 'title', 'This groups the contained content.' ); * $tags->remove_attribute( 'data-test-id' ); * } @@ -2031,8 +2031,8 @@ public function set_attribute( $name, $value ) { * * @see https://html.spec.whatwg.org/#attributes-2 * - * @TODO as the only regex pattern maybe we should take it out? are - * Unicode patterns available broadly in Core? + * @todo As the only regex pattern maybe we should take it out? + * Are Unicode patterns available broadly in Core? */ if ( preg_match( '~[' . diff --git a/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php b/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php index e53e64c80e2e02..d1c8b9e82c708a 100644 --- a/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php +++ b/lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php @@ -103,12 +103,16 @@ * * The following list specifies the HTML tags that _are_ supported: * + * - Containers: ADDRESS, BLOCKQUOTE, DETAILS, DIALOG, DIV, FOOTER, HEADER, MAIN, MENU, SPAN, SUMMARY. + * - Form elements: BUTTON, FIELDSET, SEARCH. + * - Formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. + * - Heading elements: HGROUP. * - Links: A. - * - The formatting elements: B, BIG, CODE, EM, FONT, I, SMALL, STRIKE, STRONG, TT, U. - * - Containers: DIV, FIGCAPTION, FIGURE, SPAN. - * - Form elements: BUTTON. + * - Lists: DL. + * - Media elements: FIGCAPTION, FIGURE, IMG. * - Paragraph: P. - * - Void elements: IMG. + * - Sectioning elements: ARTICLE, ASIDE, NAV, SECTION + * - Deprecated elements: CENTER, DIR * * ### Supported markup * @@ -346,7 +350,7 @@ public function get_last_error() { /** * Finds the next tag matching the $query. * - * @TODO: Support matching the class name and tag name. + * @todo Support matching the class name and tag name. * * @since 6.4.0 * @@ -555,9 +559,9 @@ public function step( $node_to_process = self::PROCESS_NEXT_NODE ) { * Breadcrumbs start at the outermost parent and descend toward the matched element. * They always include the entire path from the root HTML node to the matched element. * - * @TODO: It could be more efficient to expose a generator-based version of this function - * to avoid creating the array copy on tag iteration. If this is done, it would likely - * be more useful to walk up the stack when yielding instead of starting at the top. + * @todo It could be more efficient to expose a generator-based version of this function + * to avoid creating the array copy on tag iteration. If this is done, it would likely + * be more useful to walk up the stack when yielding instead of starting at the top. * * Example * @@ -625,11 +629,29 @@ private function step_in_body() { * > "fieldset", "figcaption", "figure", "footer", "header", "hgroup", * > "main", "menu", "nav", "ol", "p", "search", "section", "summary", "ul" */ + case '+ADDRESS': + case '+ARTICLE': + case '+ASIDE': case '+BLOCKQUOTE': + case '+CENTER': + case '+DETAILS': + case '+DIALOG': + case '+DIR': case '+DIV': + case '+DL': + case '+FIELDSET': case '+FIGCAPTION': case '+FIGURE': + case '+FOOTER': + case '+HEADER': + case '+HGROUP': + case '+MAIN': + case '+MENU': + case '+NAV': case '+P': + case '+SEARCH': + case '+SECTION': + case '+SUMMARY': if ( $this->state->stack_of_open_elements->has_p_in_button_scope() ) { $this->close_a_p_element(); } @@ -643,11 +665,29 @@ private function step_in_body() { * > "figcaption", "figure", "footer", "header", "hgroup", "listing", "main", * > "menu", "nav", "ol", "pre", "search", "section", "summary", "ul" */ + case '-ADDRESS': + case '-ARTICLE': + case '-ASIDE': case '-BLOCKQUOTE': case '-BUTTON': + case '-CENTER': + case '-DETAILS': + case '-DIALOG': + case '-DIR': case '-DIV': + case '-DL': + case '-FIELDSET': case '-FIGCAPTION': case '-FIGURE': + case '-FOOTER': + case '-HEADER': + case '-HGROUP': + case '-MAIN': + case '-MENU': + case '-NAV': + case '-SEARCH': + case '-SECTION': + case '-SUMMARY': if ( ! $this->state->stack_of_open_elements->has_element_in_scope( $tag_name ) ) { // @TODO: Report parse error. // Ignore the token. diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php index 0e9166a7c7d548..52ec4f508246ac 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -170,7 +170,7 @@ private static function get_inner_blocks_html( $attributes, $inner_blocks ) { // Add directives to the submenu if needed. if ( $has_submenus && $should_load_view_script ) { $tags = new WP_HTML_Tag_Processor( $inner_blocks_html ); - $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes ); + $inner_blocks_html = gutenberg_block_core_navigation_add_directives_to_submenu( $tags, $attributes ); } return $inner_blocks_html; @@ -195,7 +195,7 @@ private static function get_inner_blocks_from_navigation_post( $attributes ) { // 'parse_blocks' includes a null block with '\n\n' as the content when // it encounters whitespace. This code strips it. - $compacted_blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); + $compacted_blocks = gutenberg_block_core_navigation_filter_out_empty_blocks( $parsed_blocks ); // TODO - this uses the full navigation block attributes for the // context which could be refined. @@ -210,7 +210,7 @@ private static function get_inner_blocks_from_navigation_post( $attributes ) { * @return WP_Block_List Returns the inner blocks for the navigation block. */ private static function get_inner_blocks_from_fallback( $attributes ) { - $fallback_blocks = block_core_navigation_get_fallback_blocks(); + $fallback_blocks = gutenberg_block_core_navigation_get_fallback_blocks(); // Fallback my have been filtered so do basic test for validity. if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { @@ -245,9 +245,9 @@ private static function get_inner_blocks( $attributes, $block ) { defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && array_key_exists( '__unstableLocation', $attributes ) && ! array_key_exists( 'ref', $attributes ) && - ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) + ! empty( gutenberg_block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) ) ) { - $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); + $inner_blocks = gutenberg_block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ); } // Load inner blocks from the navigation post. @@ -270,7 +270,7 @@ private static function get_inner_blocks( $attributes, $block ) { */ $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks ); - $post_ids = block_core_navigation_get_post_ids( $inner_blocks ); + $post_ids = gutenberg_block_core_navigation_get_post_ids( $inner_blocks ); if ( $post_ids ) { _prime_post_caches( $post_ids, false, false ); } @@ -353,8 +353,8 @@ private static function get_layout_class( $attributes ) { private static function get_classes( $attributes ) { // Restore legacy classnames for submenu positioning. $layout_class = static::get_layout_class( $attributes ); - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); + $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes ); $is_responsive_menu = static::is_responsive( $attributes ); // Manually add block support text decoration as CSS class. @@ -378,8 +378,8 @@ private static function get_classes( $attributes ) { * @return string Returns the styles for the navigation block. */ private static function get_styles( $attributes ) { - $colors = block_core_navigation_build_css_colors( $attributes ); - $font_sizes = block_core_navigation_build_css_font_sizes( $attributes ); + $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); + $font_sizes = gutenberg_block_core_navigation_build_css_font_sizes( $attributes ); $block_styles = isset( $attributes['styles'] ) ? $attributes['styles'] : ''; return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles']; } @@ -394,7 +394,7 @@ private static function get_styles( $attributes ) { */ private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) { $should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks ); - $colors = block_core_navigation_build_css_colors( $attributes ); + $colors = gutenberg_block_core_navigation_build_css_colors( $attributes ); $modal_unique_id = wp_unique_id( 'modal-' ); $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu']; @@ -429,25 +429,25 @@ private static function get_responsive_container_markup( $attributes, $inner_blo $close_button_directives = ''; if ( $should_load_view_script ) { $open_button_directives = ' - data-wp-on--click="actions.core.navigation.openMenuOnClick" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" + data-wp-on--click="actions.openMenuOnClick" + data-wp-on--keydown="actions.handleMenuKeydown" '; $responsive_container_directives = ' - data-wp-class--has-modal-open="selectors.core.navigation.isMenuOpen" - data-wp-class--is-menu-open="selectors.core.navigation.isMenuOpen" - data-wp-effect="effects.core.navigation.initMenu" - data-wp-on--keydown="actions.core.navigation.handleMenuKeydown" - data-wp-on--focusout="actions.core.navigation.handleMenuFocusout" + data-wp-class--has-modal-open="state.isMenuOpen" + data-wp-class--is-menu-open="state.isMenuOpen" + data-wp-watch="callbacks.initMenu" + data-wp-on--keydown="actions.handleMenuKeydown" + data-wp-on--focusout="actions.handleMenuFocusout" tabindex="-1" '; $responsive_dialog_directives = ' - data-wp-bind--aria-modal="selectors.core.navigation.ariaModal" - data-wp-bind--aria-label="selectors.core.navigation.ariaLabel" - data-wp-bind--role="selectors.core.navigation.roleAttribute" - data-wp-effect="effects.core.navigation.focusFirstElement" + data-wp-bind--aria-modal="state.ariaModal" + data-wp-bind--aria-label="state.ariaLabel" + data-wp-bind--role="state.roleAttribute" + data-wp-watch="callbacks.focusFirstElement" '; $close_button_directives = ' - data-wp-on--click="actions.core.navigation.closeMenuOnClick" + data-wp-on--click="actions.closeMenuOnClick" '; } @@ -521,19 +521,15 @@ private static function get_nav_element_directives( $should_load_view_script ) { // When adding to this array be mindful of security concerns. $nav_element_context = wp_json_encode( array( - 'core' => array( - 'navigation' => array( - 'overlayOpenedBy' => array(), - 'type' => 'overlay', - 'roleAttribute' => '', - 'ariaLabel' => __( 'Menu' ), - ), - ), + 'overlayOpenedBy' => array(), + 'type' => 'overlay', + 'roleAttribute' => '', + 'ariaLabel' => __( 'Menu' ), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP ); return ' - data-wp-interactive + data-wp-interactive=\'{"namespace":"core/navigation"}\' data-wp-context=\'' . $nav_element_context . '\' '; } @@ -627,7 +623,7 @@ public static function render( $attributes, $content, $block ) { $inner_blocks = static::get_inner_blocks( $attributes, $block ); // Prevent navigation blocks referencing themselves from rendering. - if ( block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { + if ( gutenberg_block_core_navigation_block_contains_core_navigation( $inner_blocks ) ) { return ''; } diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index dd372eff7943b7..3b82815c41e420 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -19,3 +19,124 @@ function gutenberg_register_global_styles_revisions_endpoints() { } add_action( 'rest_api_init', 'gutenberg_register_global_styles_revisions_endpoints' ); + +/** + * Registers additional fields for wp_template and wp_template_part rest api. + * + * @access private + * @internal + * + * @param array $template_object Template object. + * @return string Original source of the template one of theme, plugin, site, or user. + */ +function _gutenberg_get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object['type'] || 'wp_template_part' === $template_object['type'] ) { + // Added by theme. + // Template originally provided by a theme, but customized by a user. + // Templates originally didn't have the 'origin' field so identify + // older customized templates by checking for no origin and a 'theme' + // or 'custom' source. + if ( $template_object['has_theme_file'] && + ( 'theme' === $template_object['origin'] || ( + empty( $template_object['origin'] ) && in_array( + $template_object['source'], + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + if ( $template_object['has_theme_file'] && 'plugin' === $template_object['origin'] ) { + return 'plugin'; + } + + // Added by site. + // Template was created from scratch, but has no author. Author support + // was only added to templates in WordPress 5.9. Fallback to showing the + // site logo and title. + if ( empty( $template_object['has_theme_file'] ) && 'custom' === $template_object['source'] && empty( $template_object['author'] ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; +} + +/** + * Registers additional fields for wp_template and wp_template_part rest api. + * + * @access private + * @internal + * + * @param array $template_object Template object. + * @return string Human readable text for the author. + */ +function _gutenberg_get_wp_templates_author_text_field( $template_object ) { + $original_source = _gutenberg_get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object['theme'] )->get( 'Name' ); + return empty( $theme_name ) ? $template_object['theme'] : $theme_name; + case 'plugin': + $plugins = get_plugins(); + $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object['theme'] . '.php' ) ) ]; + return empty( $plugin['Name'] ) ? $template_object['theme'] : $plugin['Name']; + case 'site': + return get_bloginfo( 'name' ); + case 'user': + return get_user_by( 'id', $template_object['author'] )->get( 'display_name' ); + } +} + +/** + * Registers additional fields for wp_template and wp_template_part rest api. + * + * @access private + * @internal + */ +function _gutenberg_register_wp_templates_additional_fields() { + register_rest_field( + array( 'wp_template', 'wp_template_part' ), + 'author_text', + array( + 'get_callback' => '_gutenberg_get_wp_templates_author_text_field', + 'update_callback' => null, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'Human readable text for the author.', 'gutenberg' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); + + register_rest_field( + array( 'wp_template', 'wp_template_part' ), + 'original_source', + array( + 'get_callback' => '_gutenberg_get_wp_templates_original_source_field', + 'update_callback' => null, + 'schema' => array( + 'description' => __( 'Where the template originally comes from e.g. \'theme\'', 'gutenberg' ), + 'type' => 'string', + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + 'enum' => array( + 'theme', + 'plugin', + 'site', + 'user', + ), + ), + ) + ); +} + +add_action( 'rest_api_init', '_gutenberg_register_wp_templates_additional_fields' ); diff --git a/lib/experimental/class-gutenberg-rest-template-revision-count.php b/lib/experimental/class-gutenberg-rest-template-revision-count.php index f3080f27af3d7c..17fb34e05ecfe1 100644 --- a/lib/experimental/class-gutenberg-rest-template-revision-count.php +++ b/lib/experimental/class-gutenberg-rest-template-revision-count.php @@ -13,7 +13,7 @@ * When merging into core, prepare_revision_links() should be merged with * WP_REST_Templates_Controller::prepare_links(). */ -class Gutenberg_REST_Template_Revision_Count extends Gutenberg_REST_Templates_Controller_6_3 { +class Gutenberg_REST_Template_Revision_Count extends WP_REST_Templates_Controller { /** * Add revisions to the response. * diff --git a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php index 8a8ee1d4ddb5f1..7d954e79e96a3c 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family-utils.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family-utils.php @@ -90,4 +90,36 @@ public static function has_font_mime_type( $filepath ) { return in_array( $filetype['type'], $allowed_mime_types, true ); } + + /** + * Format font family to make it valid CSS. + * + * @since 6.5.0 + * + * @param string $font_family Font family attribute. + * @return string The formatted font family attribute. + */ + public static function format_font_family( $font_family ) { + if ( $font_family ) { + $font_families = explode( ',', $font_family ); + $wrapped_font_families = array_map( + function ( $family ) { + $trimmed = trim( $family ); + if ( ! empty( $trimmed ) && strpos( $trimmed, ' ' ) !== false && strpos( $trimmed, "'" ) === false && strpos( $trimmed, '"' ) === false ) { + return "'" . $trimmed . "'"; + } + return $trimmed; + }, + $font_families + ); + + if ( count( $wrapped_font_families ) === 1 ) { + $font_family = $wrapped_font_families[0]; + } else { + $font_family = implode( ', ', $wrapped_font_families ); + } + } + + return $font_family; + } } diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php index 2897811f3b70b5..58d4f476e834d1 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -300,18 +300,27 @@ private function sanitize() { 'version' => '2', 'settings' => array( 'typography' => array( - 'fontFamilies' => array( $this->data ), + 'fontFamilies' => array( + 'custom' => array( + $this->data, + ), + ), ), ), ); + // Creates a new WP_Theme_JSON object with the new fonts to // leverage sanitization and validation. + $fonts_json = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $fonts_json ); $theme_json = new WP_Theme_JSON_Gutenberg( $fonts_json ); $theme_data = $theme_json->get_data(); $sanitized_font = ! empty( $theme_data['settings']['typography']['fontFamilies'] ) ? $theme_data['settings']['typography']['fontFamilies'][0] : array(); - $this->data = $sanitized_font; + + $sanitized_font['slug'] = _wp_to_kebab_case( $sanitized_font['slug'] ); + $sanitized_font['fontFamily'] = WP_Font_Family_Utils::format_font_family( $sanitized_font['fontFamily'] ); + $this->data = $sanitized_font; return $this->data; } @@ -394,6 +403,11 @@ private function download_or_move_font_faces( $files ) { continue; } + // If the font face requires the use of the filesystem, create the fonts dir if it doesn't exist. + if ( ! empty( $font_face['downloadFromUrl'] ) && ! empty( $font_face['uploadedFile'] ) ) { + wp_mkdir_p( WP_Font_Library::get_fonts_dir() ); + } + // If installing google fonts, download the font face assets. if ( ! empty( $font_face['downloadFromUrl'] ) ) { $new_font_face = $this->download_font_face_assets( $new_font_face ); diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index 7d9ccdca453b8f..e717b2e5539431 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -27,17 +27,26 @@ class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 { * * @var array */ - public static $root_blocks = array(); + public static $root_block = null; /** - * Add a root block to the list. + * Add a root block to the variable. * * @param array $block The block to add. * * @return void */ - public static function add_root_block( $block ) { - self::$root_blocks[] = md5( serialize( $block ) ); + public static function mark_root_block( $block ) { + self::$root_block = md5( serialize( $block ) ); + } + + /** + * Remove a root block to the variable. + * + * @return void + */ + public static function unmark_root_block() { + self::$root_block = null; } /** @@ -47,8 +56,17 @@ public static function add_root_block( $block ) { * * @return bool True if block is a root block, false otherwise. */ - public static function is_root_block( $block ) { - return in_array( md5( serialize( $block ) ), self::$root_blocks, true ); + public static function is_marked_as_root_block( $block ) { + return md5( serialize( $block ) ) === self::$root_block; + } + + /** + * Check if a root block has already been defined. + * + * @return bool True if block is a root block, false otherwise. + */ + public static function has_root_block() { + return isset( self::$root_block ); } @@ -92,6 +110,75 @@ public function next_balanced_closer() { return false; } + /** + * Traverses the HTML searching for Interactivity API directives and processing + * them. + * + * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. + * @param string $prefix Attribute prefix. + * @param string[] $directives Directives. + * + * @return WP_Directive_Processor The modified instance of the + * WP_Directive_Processor. + */ + public function process_rendered_html( $tags, $prefix, $directives ) { + $context = new WP_Directive_Context(); + $tag_stack = array(); + + while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + $tag_name = $tags->get_tag(); + + // Is this a tag that closes the latest opening tag? + if ( $tags->is_tag_closer() ) { + if ( 0 === count( $tag_stack ) ) { + continue; + } + + list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); + if ( $latest_opening_tag_name === $tag_name ) { + array_pop( $tag_stack ); + + // If the matching opening tag didn't have any directives, we move on. + if ( 0 === count( $attributes ) ) { + continue; + } + } + } else { + $attributes = array(); + foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { + /* + * Removes the part after the double hyphen before looking for + * the directive processor inside `$directives`, e.g., "wp-bind" + * from "wp-bind--src" and "wp-context" from "wp-context" etc... + */ + list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); + if ( array_key_exists( $type, $directives ) ) { + $attributes[] = $type; + } + } + + /* + * If this is an open tag, and if it either has directives, or if + * we're inside a tag that does, take note of this tag and its + * directives so we can call its directive processor once we + * encounter the matching closing tag. + */ + if ( + ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && + ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) + ) { + $tag_stack[] = array( $tag_name, $attributes ); + } + } + + foreach ( $attributes as $attribute ) { + call_user_func( $directives[ $attribute ], $tags, $context ); + } + } + + return $tags; + } + /** * Return the content between two balanced tags. * diff --git a/lib/experimental/interactivity-api/class-wp-interactivity-store.php b/lib/experimental/interactivity-api/class-wp-interactivity-store.php index 89cb58700554a9..c53701b14e8aff 100644 --- a/lib/experimental/interactivity-api/class-wp-interactivity-store.php +++ b/lib/experimental/interactivity-api/class-wp-interactivity-store.php @@ -62,7 +62,7 @@ public static function render() { return; } echo sprintf( - '', + '', wp_json_encode( self::$store, JSON_HEX_TAG | JSON_HEX_AMP ) ); } diff --git a/lib/experimental/interactivity-api/directive-processing.php b/lib/experimental/interactivity-api/directive-processing.php index eae731e2438913..064fc8ea62cbb2 100644 --- a/lib/experimental/interactivity-api/directive-processing.php +++ b/lib/experimental/interactivity-api/directive-processing.php @@ -8,9 +8,9 @@ */ /** - * Process the Interactivity API directives using the root blocks of the - * outermost rendering, ignoring the root blocks of inner blocks like Patterns, - * Template Parts or Content. + * Mark if the block is a root block. Checks that there is already a root block + * in order not to mark template-parts or synced patterns as root blocks, where + * the parent is null. * * @param array $parsed_block The parsed block. * @param array $source_block The source block. @@ -18,121 +18,44 @@ * * @return array The parsed block. */ -function gutenberg_interactivity_process_directives( $parsed_block, $source_block, $parent_block ) { - static $is_inside_root_block = false; - static $process_directives_in_root_blocks = null; - - if ( ! isset( $process_directives_in_root_blocks ) ) { - /** - * Process directives in each root block. - * - * @param string $block_content The block content. - * @param array $block The full block. - * - * @return string Filtered block content. - */ - $process_directives_in_root_blocks = static function ( $block_content, $block ) use ( &$is_inside_root_block ) { - - if ( WP_Directive_Processor::is_root_block( $block ) ) { - - $directives = array( - 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', - 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', - 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', - 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', - 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', - ); - - $tags = new WP_Directive_Processor( $block_content ); - $tags = gutenberg_interactivity_process_rendered_html( $tags, 'data-wp-', $directives ); - $is_inside_root_block = false; - return $tags->get_updated_html(); - - } - - return $block_content; - }; - add_filter( 'render_block', $process_directives_in_root_blocks, 10, 2 ); - } - - if ( ! isset( $parent_block ) && ! $is_inside_root_block ) { - WP_Directive_Processor::add_root_block( $parsed_block ); - $is_inside_root_block = true; +function gutenberg_interactivity_mark_root_blocks( $parsed_block, $source_block, $parent_block ) { + if ( ! isset( $parent_block ) && ! WP_Directive_Processor::has_root_block() ) { + WP_Directive_Processor::mark_root_block( $parsed_block ); } return $parsed_block; } -add_filter( 'render_block_data', 'gutenberg_interactivity_process_directives', 10, 3 ); - +add_filter( 'render_block_data', 'gutenberg_interactivity_mark_root_blocks', 10, 3 ); /** - * Traverses the HTML searching for Interactivity API directives and processing - * them. + * Process directives in each root block. * - * @param WP_Directive_Processor $tags An instance of the WP_Directive_Processor. - * @param string $prefix Attribute prefix. - * @param string[] $directives Directives. + * @param string $block_content The block content. + * @param array $block The full block. * - * @return WP_Directive_Processor The modified instance of the - * WP_Directive_Processor. + * @return string Filtered block content. */ -function gutenberg_interactivity_process_rendered_html( $tags, $prefix, $directives ) { - $context = new WP_Directive_Context(); - $tag_stack = array(); +function gutenberg_process_directives_in_root_blocks( $block_content, $block ) { + if ( WP_Directive_Processor::is_marked_as_root_block( $block ) ) { + WP_Directive_Processor::unmark_root_block(); + $directives = array( + 'data-wp-bind' => 'gutenberg_interactivity_process_wp_bind', + 'data-wp-context' => 'gutenberg_interactivity_process_wp_context', + 'data-wp-class' => 'gutenberg_interactivity_process_wp_class', + 'data-wp-style' => 'gutenberg_interactivity_process_wp_style', + 'data-wp-text' => 'gutenberg_interactivity_process_wp_text', + ); + + $tags = new WP_Directive_Processor( $block_content ); + $tags = $tags->process_rendered_html( $tags, 'data-wp-', $directives ); + return $tags->get_updated_html(); - while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $tags->get_tag(); - - // Is this a tag that closes the latest opening tag? - if ( $tags->is_tag_closer() ) { - if ( 0 === count( $tag_stack ) ) { - continue; - } - - list( $latest_opening_tag_name, $attributes ) = end( $tag_stack ); - if ( $latest_opening_tag_name === $tag_name ) { - array_pop( $tag_stack ); - - // If the matching opening tag didn't have any directives, we move on. - if ( 0 === count( $attributes ) ) { - continue; - } - } - } else { - $attributes = array(); - foreach ( $tags->get_attribute_names_with_prefix( $prefix ) as $name ) { - /* - * Removes the part after the double hyphen before looking for - * the directive processor inside `$directives`, e.g., "wp-bind" - * from "wp-bind--src" and "wp-context" from "wp-context" etc... - */ - list( $type ) = WP_Directive_Processor::parse_attribute_name( $name ); - if ( array_key_exists( $type, $directives ) ) { - $attributes[] = $type; - } - } - - /* - * If this is an open tag, and if it either has directives, or if - * we're inside a tag that does, take note of this tag and its - * directives so we can call its directive processor once we - * encounter the matching closing tag. - */ - if ( - ! WP_Directive_Processor::is_html_void_element( $tags->get_tag() ) && - ( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) ) - ) { - $tag_stack[] = array( $tag_name, $attributes ); - } - } - - foreach ( $attributes as $attribute ) { - call_user_func( $directives[ $attribute ], $tags, $context ); - } } - return $tags; + return $block_content; } +add_filter( 'render_block', 'gutenberg_process_directives_in_root_blocks', 10, 2 ); + /** * Resolve the reference using the store and the context from the provided path. diff --git a/lib/load.php b/lib/load.php index 38111d9ed5d3d5..e2d804befe7d79 100644 --- a/lib/load.php +++ b/lib/load.php @@ -35,21 +35,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/experimental/class-wp-rest-block-editor-settings-controller.php'; } - // WordPress 6.3 compat. - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php'; - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php'; - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-global-styles-revisions-controller-6-3.php'; - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-classic-to-block-menu-converter.php'; - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-navigation-fallback.php'; - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-navigation-fallback-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.3/rest-api.php'; - require_once __DIR__ . '/compat/wordpress-6.3/theme-previews.php'; - require_once __DIR__ . '/compat/wordpress-6.3/navigation-block-preloading.php'; - require_once __DIR__ . '/compat/wordpress-6.3/link-template.php'; - require_once __DIR__ . '/compat/wordpress-6.3/block-patterns.php'; - require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-blocks-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.3/footnotes.php'; - // WordPress 6.4 compat. require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; @@ -105,15 +90,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-processor.php'; } -// WordPress 6.3 compat. -require __DIR__ . '/compat/wordpress-6.3/get-global-styles-and-settings.php'; -require __DIR__ . '/compat/wordpress-6.3/block-template-utils.php'; -require __DIR__ . '/compat/wordpress-6.3/script-loader.php'; -require __DIR__ . '/compat/wordpress-6.3/blocks.php'; -require __DIR__ . '/compat/wordpress-6.3/navigation-fallback.php'; -require __DIR__ . '/compat/wordpress-6.3/block-editor-settings.php'; -require_once __DIR__ . '/compat/wordpress-6.3/kses.php'; - // WordPress 6.4 compat. require __DIR__ . '/compat/wordpress-6.4/blocks.php'; require __DIR__ . '/compat/wordpress-6.4/block-hooks.php'; @@ -248,12 +224,12 @@ function () { // Copied package PHP files. if ( is_dir( __DIR__ . '/../build/style-engine' ) ) { - require_once __DIR__ . '/../build/style-engine/style-engine-gutenberg.php'; - require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-declarations-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rule-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-css-rules-store-gutenberg.php'; require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-processor-gutenberg.php'; + require_once __DIR__ . '/../build/style-engine/class-wp-style-engine-gutenberg.php'; + require_once __DIR__ . '/../build/style-engine/style-engine-gutenberg.php'; } // Block supports overrides. diff --git a/package-lock.json b/package-lock.json index 28f51a1408f64d..4f293d8e6f64bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.0.2", + "version": "17.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.0.2", + "version": "17.1.3", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -10378,6 +10378,14 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@sideway/formula": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", @@ -20266,15 +20274,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -32691,58 +32690,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-dev-server": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-6.0.2.tgz", - "integrity": "sha512-cVpBu4KvNnefZsiustbaQmgGEKn72K6yk0OMgXDJ3yMT9N24ZM16TG0QgEacHPkrHKirOXZa1GN9Mi/lNl9Y+Q==", - "dev": true, - "dependencies": { - "chalk": "^4.1.2", - "cwd": "^0.10.0", - "find-process": "^1.4.5", - "prompts": "^2.4.1", - "spawnd": "^6.0.2", - "tree-kill": "^1.2.2", - "wait-on": "^6.0.0" - } - }, - "node_modules/jest-dev-server/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-dev-server/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-dev-server/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-diff": { "version": "29.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", @@ -33423,14 +33370,6 @@ "@sideway/pinpoint": "^2.0.0" } }, - "node_modules/joi/node_modules/@sideway/address": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", - "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", @@ -48763,17 +48702,6 @@ "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", "dev": true }, - "node_modules/spawnd": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-6.0.2.tgz", - "integrity": "sha512-+YJtx0dvy2wt304MrHD//tASc84zinBUYU1jacPBzrjhZUd7RsDo25krxr4HUHAQzEQFuMAs4/p+yLYU5ciZ1w==", - "dev": true, - "dependencies": { - "exit": "^0.1.2", - "signal-exit": "^3.0.6", - "tree-kill": "^1.2.2" - } - }, "node_modules/spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -52062,34 +51990,6 @@ "node": ">=14" } }, - "node_modules/wait-on": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", - "integrity": "sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==", - "dev": true, - "dependencies": { - "axios": "^0.21.1", - "joi": "^17.4.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^7.1.0" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/wait-on/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/wait-port": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", @@ -54404,7 +54304,7 @@ }, "packages/a11y": { "name": "@wordpress/a11y", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54417,7 +54317,7 @@ }, "packages/annotations": { "name": "@wordpress/annotations", - "version": "2.45.0", + "version": "2.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54445,7 +54345,7 @@ }, "packages/api-fetch": { "name": "@wordpress/api-fetch", - "version": "6.42.0", + "version": "6.43.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54458,7 +54358,7 @@ }, "packages/autop": { "name": "@wordpress/autop", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54469,7 +54369,7 @@ }, "packages/babel-plugin-import-jsx-pragma": { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.28.0", + "version": "4.29.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54481,7 +54381,7 @@ }, "packages/babel-plugin-makepot": { "name": "@wordpress/babel-plugin-makepot", - "version": "5.29.0", + "version": "5.30.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54498,7 +54398,7 @@ }, "packages/babel-preset-default": { "name": "@wordpress/babel-preset-default", - "version": "7.29.0", + "version": "7.30.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54521,13 +54421,13 @@ }, "packages/base-styles": { "name": "@wordpress/base-styles", - "version": "4.36.0", + "version": "4.37.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/blob": { "name": "@wordpress/blob", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54538,7 +54438,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54573,7 +54473,7 @@ }, "packages/block-editor": { "name": "@wordpress/block-editor", - "version": "12.13.0", + "version": "12.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54670,7 +54570,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.22.0", + "version": "8.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54732,7 +54632,7 @@ }, "packages/block-serialization-default-parser": { "name": "@wordpress/block-serialization-default-parser", - "version": "4.45.0", + "version": "4.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54743,7 +54643,7 @@ }, "packages/block-serialization-spec-parser": { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.45.0", + "version": "4.46.0", "license": "GPL-2.0-or-later", "dependencies": { "pegjs": "^0.10.0", @@ -54755,7 +54655,7 @@ }, "packages/blocks": { "name": "@wordpress/blocks", - "version": "12.22.0", + "version": "12.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54775,7 +54675,6 @@ "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", - "deepmerge": "^4.3.0", "fast-deep-equal": "^3.1.3", "hpq": "^1.3.0", "is-plain-object": "^5.0.0", @@ -54803,7 +54702,7 @@ }, "packages/browserslist-config": { "name": "@wordpress/browserslist-config", - "version": "5.28.0", + "version": "5.29.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54812,7 +54711,7 @@ }, "packages/commands": { "name": "@wordpress/commands", - "version": "0.16.0", + "version": "0.17.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54837,7 +54736,7 @@ }, "packages/components": { "name": "@wordpress/components", - "version": "25.11.0", + "version": "25.12.0", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.5", @@ -54943,7 +54842,7 @@ }, "packages/compose": { "name": "@wordpress/compose", - "version": "6.22.0", + "version": "6.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54969,7 +54868,7 @@ }, "packages/core-commands": { "name": "@wordpress/core-commands", - "version": "0.14.0", + "version": "0.15.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54994,7 +54893,7 @@ }, "packages/core-data": { "name": "@wordpress/core-data", - "version": "6.22.0", + "version": "6.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55038,7 +54937,7 @@ }, "packages/create-block": { "name": "@wordpress/create-block", - "version": "4.29.0", + "version": "4.30.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55066,13 +54965,13 @@ }, "packages/create-block-tutorial-template": { "name": "@wordpress/create-block-tutorial-template", - "version": "2.33.0", + "version": "3.0.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55109,7 +55008,7 @@ }, "packages/data": { "name": "@wordpress/data", - "version": "9.15.0", + "version": "9.16.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55137,7 +55036,7 @@ }, "packages/data-controls": { "name": "@wordpress/data-controls", - "version": "3.14.0", + "version": "3.15.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55154,7 +55053,7 @@ }, "packages/date": { "name": "@wordpress/date", - "version": "4.45.0", + "version": "4.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55168,7 +55067,7 @@ }, "packages/dependency-extraction-webpack-plugin": { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "4.28.0", + "version": "4.29.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55184,7 +55083,7 @@ }, "packages/deprecated": { "name": "@wordpress/deprecated", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55196,7 +55095,7 @@ }, "packages/docgen": { "name": "@wordpress/docgen", - "version": "1.54.0", + "version": "1.55.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55214,7 +55113,7 @@ }, "packages/dom": { "name": "@wordpress/dom", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55226,7 +55125,7 @@ }, "packages/dom-ready": { "name": "@wordpress/dom-ready", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55237,7 +55136,7 @@ }, "packages/e2e-test-utils": { "name": "@wordpress/e2e-test-utils", - "version": "10.16.0", + "version": "10.17.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55259,7 +55158,7 @@ }, "packages/e2e-test-utils-playwright": { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.13.0", + "version": "0.14.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55282,7 +55181,7 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "7.16.0", + "version": "7.17.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55320,7 +55219,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.22.0", + "version": "7.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55368,7 +55267,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.22.0", + "version": "5.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55433,7 +55332,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.22.0", + "version": "5.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55475,7 +55374,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.22.0", + "version": "13.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55525,7 +55424,7 @@ }, "packages/element": { "name": "@wordpress/element", - "version": "5.22.0", + "version": "5.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55543,7 +55442,7 @@ }, "packages/env": { "name": "@wordpress/env", - "version": "8.11.0", + "version": "8.12.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55656,7 +55555,7 @@ }, "packages/escape-html": { "name": "@wordpress/escape-html", - "version": "2.45.0", + "version": "2.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55667,7 +55566,7 @@ }, "packages/eslint-plugin": { "name": "@wordpress/eslint-plugin", - "version": "17.2.0", + "version": "17.3.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55710,7 +55609,7 @@ }, "packages/format-library": { "name": "@wordpress/format-library", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55736,7 +55635,7 @@ }, "packages/hooks": { "name": "@wordpress/hooks", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55747,7 +55646,7 @@ }, "packages/html-entities": { "name": "@wordpress/html-entities", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55758,7 +55657,7 @@ }, "packages/i18n": { "name": "@wordpress/i18n", - "version": "4.45.0", + "version": "4.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55777,7 +55676,7 @@ }, "packages/icons": { "name": "@wordpress/icons", - "version": "9.36.0", + "version": "9.37.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55790,7 +55689,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "2.6.0", + "version": "2.7.0", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.1.3", @@ -55803,7 +55702,7 @@ }, "packages/interface": { "name": "@wordpress/interface", - "version": "5.22.0", + "version": "5.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55830,7 +55729,7 @@ }, "packages/is-shallow-equal": { "name": "@wordpress/is-shallow-equal", - "version": "4.45.0", + "version": "4.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55841,7 +55740,7 @@ }, "packages/jest-console": { "name": "@wordpress/jest-console", - "version": "7.16.0", + "version": "7.17.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55857,7 +55756,7 @@ }, "packages/jest-preset-default": { "name": "@wordpress/jest-preset-default", - "version": "11.16.0", + "version": "11.17.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55874,7 +55773,7 @@ }, "packages/jest-puppeteer-axe": { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.16.0", + "version": "6.17.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55896,7 +55795,7 @@ }, "packages/keyboard-shortcuts": { "name": "@wordpress/keyboard-shortcuts", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55914,7 +55813,7 @@ }, "packages/keycodes": { "name": "@wordpress/keycodes", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55927,7 +55826,7 @@ }, "packages/lazy-import": { "name": "@wordpress/lazy-import", - "version": "1.32.0", + "version": "1.33.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55941,7 +55840,7 @@ }, "packages/list-reusable-blocks": { "name": "@wordpress/list-reusable-blocks", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55963,7 +55862,7 @@ }, "packages/media-utils": { "name": "@wordpress/media-utils", - "version": "4.36.0", + "version": "4.37.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55978,7 +55877,7 @@ }, "packages/notices": { "name": "@wordpress/notices", - "version": "4.13.0", + "version": "4.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55994,7 +55893,7 @@ }, "packages/npm-package-json-lint-config": { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.30.0", + "version": "4.31.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56006,7 +55905,7 @@ }, "packages/nux": { "name": "@wordpress/nux", - "version": "8.7.0", + "version": "8.8.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56029,7 +55928,7 @@ }, "packages/patterns": { "name": "@wordpress/patterns", - "version": "1.6.0", + "version": "1.7.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56058,7 +55957,7 @@ }, "packages/plugins": { "name": "@wordpress/plugins", - "version": "6.13.0", + "version": "6.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56080,7 +55979,7 @@ }, "packages/postcss-plugins-preset": { "name": "@wordpress/postcss-plugins-preset", - "version": "4.29.0", + "version": "4.30.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56096,7 +55995,7 @@ }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", - "version": "5.28.0", + "version": "5.29.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56108,7 +56007,7 @@ }, "packages/preferences": { "name": "@wordpress/preferences", - "version": "3.22.0", + "version": "3.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56130,7 +56029,7 @@ }, "packages/preferences-persistence": { "name": "@wordpress/preferences-persistence", - "version": "1.37.0", + "version": "1.38.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56142,7 +56041,7 @@ }, "packages/prettier-config": { "name": "@wordpress/prettier-config", - "version": "3.2.0", + "version": "3.3.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56154,7 +56053,7 @@ }, "packages/primitives": { "name": "@wordpress/primitives", - "version": "3.43.0", + "version": "3.44.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56167,7 +56066,7 @@ }, "packages/priority-queue": { "name": "@wordpress/priority-queue", - "version": "2.45.0", + "version": "2.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56179,7 +56078,7 @@ }, "packages/private-apis": { "name": "@wordpress/private-apis", - "version": "0.27.0", + "version": "0.28.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56190,7 +56089,7 @@ }, "packages/project-management-automation": { "name": "@wordpress/project-management-automation", - "version": "1.44.0", + "version": "1.45.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56203,7 +56102,7 @@ }, "packages/react-i18n": { "name": "@wordpress/react-i18n", - "version": "3.43.0", + "version": "3.44.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56217,7 +56116,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.108.0", + "version": "1.109.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56230,7 +56129,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.108.0", + "version": "1.109.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56241,7 +56140,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.108.0", + "version": "1.109.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56351,7 +56250,7 @@ }, "packages/readable-js-assets-webpack-plugin": { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.28.0", + "version": "2.29.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56363,7 +56262,7 @@ }, "packages/redux-routine": { "name": "@wordpress/redux-routine", - "version": "4.45.0", + "version": "4.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56407,7 +56306,7 @@ }, "packages/reusable-blocks": { "name": "@wordpress/reusable-blocks", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56433,7 +56332,7 @@ }, "packages/rich-text": { "name": "@wordpress/rich-text", - "version": "6.22.0", + "version": "6.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56457,7 +56356,7 @@ }, "packages/router": { "name": "@wordpress/router", - "version": "0.14.0", + "version": "0.15.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56475,7 +56374,7 @@ }, "packages/scripts": { "name": "@wordpress/scripts", - "version": "26.16.0", + "version": "26.17.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56510,7 +56409,7 @@ "fast-glob": "^3.2.7", "filenamify": "^4.2.0", "jest": "^29.6.2", - "jest-dev-server": "^6.0.2", + "jest-dev-server": "^9.0.1", "jest-environment-jsdom": "^29.6.2", "jest-environment-node": "^29.6.2", "markdownlint-cli": "^0.31.1", @@ -56551,6 +56450,73 @@ "react-dom": "^18.0.0" } }, + "packages/scripts/node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "packages/scripts/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "packages/scripts/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "packages/scripts/node_modules/jest-dev-server": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-9.0.1.tgz", + "integrity": "sha512-eqpJKSvVl4M0ojHZUPNbka8yEzLNbIMiINXDsuMF3lYfIdRO2iPqy+ASR4wBQ6nUyR3OT24oKPWhpsfLhgAVyg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.7", + "prompts": "^2.4.2", + "spawnd": "^9.0.1", + "tree-kill": "^1.2.2", + "wait-on": "^7.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "packages/scripts/node_modules/joi": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "packages/scripts/node_modules/playwright-core": { "version": "1.39.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", @@ -56563,9 +56529,74 @@ "node": ">=16" } }, + "packages/scripts/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "packages/scripts/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/scripts/node_modules/spawnd": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-9.0.1.tgz", + "integrity": "sha512-vaMk8E9CpbjTYToBxLXowDeArGf1+yI7A6PU6Nr57b2g8BVY8nRi5vTBj3bMF8UkCrMdTMyf/Lh+lrcrW2z7pw==", + "dev": true, + "dependencies": { + "signal-exit": "^4.1.0", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=16" + } + }, + "packages/scripts/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "packages/scripts/node_modules/wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "dependencies": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "packages/server-side-render": { "name": "@wordpress/server-side-render", - "version": "4.22.0", + "version": "4.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56590,7 +56621,7 @@ }, "packages/shortcode": { "name": "@wordpress/shortcode", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56602,7 +56633,7 @@ }, "packages/style-engine": { "name": "@wordpress/style-engine", - "version": "1.28.0", + "version": "1.29.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56614,7 +56645,7 @@ }, "packages/stylelint-config": { "name": "@wordpress/stylelint-config", - "version": "21.28.0", + "version": "21.29.0", "dev": true, "license": "MIT", "dependencies": { @@ -56630,7 +56661,7 @@ }, "packages/sync": { "name": "@wordpress/sync", - "version": "0.7.0", + "version": "0.8.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56650,7 +56681,7 @@ }, "packages/token-list": { "name": "@wordpress/token-list", - "version": "2.45.0", + "version": "2.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56661,7 +56692,7 @@ }, "packages/undo-manager": { "name": "@wordpress/undo-manager", - "version": "0.5.0", + "version": "0.6.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56673,7 +56704,7 @@ }, "packages/url": { "name": "@wordpress/url", - "version": "3.46.0", + "version": "3.47.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56685,7 +56716,7 @@ }, "packages/viewport": { "name": "@wordpress/viewport", - "version": "5.22.0", + "version": "5.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56702,7 +56733,7 @@ }, "packages/warning": { "name": "@wordpress/warning", - "version": "2.45.0", + "version": "2.46.0", "license": "GPL-2.0-or-later", "engines": { "node": ">=12" @@ -56710,7 +56741,7 @@ }, "packages/widgets": { "name": "@wordpress/widgets", - "version": "3.22.0", + "version": "3.23.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56734,7 +56765,7 @@ }, "packages/wordcount": { "name": "@wordpress/wordcount", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -64014,6 +64045,14 @@ } } }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@sideway/formula": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", @@ -70054,7 +70093,6 @@ "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", - "deepmerge": "^4.3.0", "fast-deep-equal": "^3.1.3", "hpq": "^1.3.0", "is-plain-object": "^5.0.0", @@ -71211,7 +71249,7 @@ "fast-glob": "^3.2.7", "filenamify": "^4.2.0", "jest": "^29.6.2", - "jest-dev-server": "^6.0.2", + "jest-dev-server": "^9.0.1", "jest-environment-jsdom": "^29.6.2", "jest-environment-node": "^29.6.2", "markdownlint-cli": "^0.31.1", @@ -71240,11 +71278,113 @@ "webpack-dev-server": "^4.15.1" }, "dependencies": { + "axios": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jest-dev-server": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-9.0.1.tgz", + "integrity": "sha512-eqpJKSvVl4M0ojHZUPNbka8yEzLNbIMiINXDsuMF3lYfIdRO2iPqy+ASR4wBQ6nUyR3OT24oKPWhpsfLhgAVyg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.7", + "prompts": "^2.4.2", + "spawnd": "^9.0.1", + "tree-kill": "^1.2.2", + "wait-on": "^7.0.1" + } + }, + "joi": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "playwright-core": { "version": "1.39.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz", "integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==", "dev": true + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "spawnd": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-9.0.1.tgz", + "integrity": "sha512-vaMk8E9CpbjTYToBxLXowDeArGf1+yI7A6PU6Nr57b2g8BVY8nRi5vTBj3bMF8UkCrMdTMyf/Lh+lrcrW2z7pw==", + "dev": true, + "requires": { + "signal-exit": "^4.1.0", + "tree-kill": "^1.2.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wait-on": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", + "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", + "dev": true, + "requires": { + "axios": "^1.6.1", + "joi": "^17.11.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.1" + } } } }, @@ -72629,15 +72769,6 @@ "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==", "dev": true }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, - "requires": { - "follow-redirects": "^1.14.0" - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -82108,48 +82239,6 @@ } } }, - "jest-dev-server": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-6.0.2.tgz", - "integrity": "sha512-cVpBu4KvNnefZsiustbaQmgGEKn72K6yk0OMgXDJ3yMT9N24ZM16TG0QgEacHPkrHKirOXZa1GN9Mi/lNl9Y+Q==", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "cwd": "^0.10.0", - "find-process": "^1.4.5", - "prompts": "^2.4.1", - "spawnd": "^6.0.2", - "tree-kill": "^1.2.2", - "wait-on": "^6.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "jest-diff": { "version": "29.6.2", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", @@ -82666,16 +82755,6 @@ "@sideway/address": "^4.1.3", "@sideway/formula": "^3.0.0", "@sideway/pinpoint": "^2.0.0" - }, - "dependencies": { - "@sideway/address": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", - "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", - "requires": { - "@hapi/hoek": "^9.0.0" - } - } } }, "jpeg-js": { @@ -94500,17 +94579,6 @@ "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", "dev": true }, - "spawnd": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-6.0.2.tgz", - "integrity": "sha512-+YJtx0dvy2wt304MrHD//tASc84zinBUYU1jacPBzrjhZUd7RsDo25krxr4HUHAQzEQFuMAs4/p+yLYU5ciZ1w==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "signal-exit": "^3.0.6", - "tree-kill": "^1.2.2" - } - }, "spdx-correct": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", @@ -96996,30 +97064,6 @@ "xml-name-validator": "^4.0.0" } }, - "wait-on": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", - "integrity": "sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==", - "dev": true, - "requires": { - "axios": "^0.21.1", - "joi": "^17.4.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^7.1.0" - }, - "dependencies": { - "rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - } - } - }, "wait-port": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", diff --git a/package.json b/package.json index 2c3313b9337d41..d46d146cb9bafb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.0.2", + "version": "17.1.3", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/a11y/CHANGELOG.md b/packages/a11y/CHANGELOG.md index 2be0c0b64bb149..ef2826ea2f87bc 100644 --- a/packages/a11y/CHANGELOG.md +++ b/packages/a11y/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.46.0 (2023-11-16) + ## 3.45.0 (2023-11-02) ## 3.44.0 (2023-10-18) diff --git a/packages/a11y/package.json b/packages/a11y/package.json index a48168ac575783..10964d4f47800a 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "3.45.0", + "version": "3.46.0", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index a45627849418b3..ccb8a4eb3b22f9 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.46.0 (2023-11-16) + ## 2.45.0 (2023-11-02) ## 2.44.0 (2023-10-18) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 32b3eee0e2ecb6..653414da29014e 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.45.0", + "version": "2.46.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 5806a41860d5c9..07e50ad2cb451f 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.43.0 (2023-11-16) + ## 6.42.0 (2023-11-02) ## 6.41.0 (2023-10-18) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index e5413887a61ceb..c572e343edab86 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "6.42.0", + "version": "6.43.0", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/CHANGELOG.md b/packages/autop/CHANGELOG.md index a3cba5718404ba..c3ffd7dd4bb171 100644 --- a/packages/autop/CHANGELOG.md +++ b/packages/autop/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.46.0 (2023-11-16) + ## 3.45.0 (2023-11-02) ## 3.44.0 (2023-10-18) diff --git a/packages/autop/package.json b/packages/autop/package.json index 13eb7cac02d54c..d444d2d5a4779f 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "3.45.0", + "version": "3.46.0", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md index 292314b1c9fe10..8070de13c5955d 100644 --- a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md +++ b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.29.0 (2023-11-16) + ## 4.28.0 (2023-11-02) ## 4.27.0 (2023-10-18) diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index 00a621f31b1e06..a49af835b912fc 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.28.0", + "version": "4.29.0", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index 0b57ac2d9b0a1d..1b8137ab96e7fe 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.30.0 (2023-11-16) + ## 5.29.0 (2023-11-02) ## 5.28.0 (2023-10-18) diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index 4acc61c4819e63..298a05f3f4425f 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "5.29.0", + "version": "5.30.0", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index e5a4c06b4c92b6..20ff5b49dbebe9 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.30.0 (2023-11-16) + ## 7.29.0 (2023-11-02) ## 7.28.0 (2023-10-18) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index dd38c349bb1716..d53a2603593233 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "7.29.0", + "version": "7.30.0", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index b6135581dbd731..bac407def2873b 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.37.0 (2023-11-16) + ## 4.36.0 (2023-11-02) ## 4.35.0 (2023-10-18) diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index fe604a7cb02115..8af7e6b7e3db17 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "4.36.0", + "version": "4.37.0", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index 680d53971c018d..7fae8a61cabb1f 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.46.0 (2023-11-16) + ### New feature - Add `downloadBlob` function and remove `downloadjs` dependency ([#56024](https://github.com/WordPress/gutenberg/pull/56024)). diff --git a/packages/blob/README.md b/packages/blob/README.md index ff28e8879602f3..64520a98bd6a79 100644 --- a/packages/blob/README.md +++ b/packages/blob/README.md @@ -40,9 +40,9 @@ const fileContent = JSON.stringify( null, 2 ); -const fileName = 'file.json'; +const filename = 'file.json'; -downloadBlob( 'file.json', fileContent, 'application/json' ); +downloadBlob( filename, fileContent, 'application/json' ); ``` _Parameters_ diff --git a/packages/blob/package.json b/packages/blob/package.json index c3e89fb9918237..ba0a355e19a47d 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "3.45.0", + "version": "3.46.0", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/src/index.js b/packages/blob/src/index.js index edc2e43729a23f..2493f81fc4c65b 100644 --- a/packages/blob/src/index.js +++ b/packages/blob/src/index.js @@ -85,9 +85,9 @@ export function isBlobURL( url ) { * null, * 2 * ); - * const fileName = 'file.json'; + * const filename = 'file.json'; * - * downloadBlob( 'file.json', fileContent, 'application/json' ); + * downloadBlob( filename, fileContent, 'application/json' ); * ``` * * @param {string} filename File name. diff --git a/packages/block-directory/CHANGELOG.md b/packages/block-directory/CHANGELOG.md index eb3a68dd24cc81..7cebd71db2fbc3 100644 --- a/packages/block-directory/CHANGELOG.md +++ b/packages/block-directory/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.23.0 (2023-11-16) + ## 4.22.0 (2023-11-02) ## 4.21.0 (2023-10-18) diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 71018738fac083..438b0680db283b 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.22.0", + "version": "4.23.0", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 94e0306e175659..497e419453f6a2 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.14.0 (2023-11-16) + ## 12.13.0 (2023-11-02) - Deprecated the `useSetting` function in favor of new `useSettings` one that can retrieve multiple settings at once ([#55337](https://github.com/WordPress/gutenberg/pull/55337)). diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 9c7a72f0897143..2d6a5627a52a44 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -954,7 +954,7 @@ _Parameters_ ### useSetting -> **Deprecated** 6.4.0 Use useSettings instead. +> **Deprecated** 6.5.0 Use useSettings instead. Hook that retrieves the given setting for the block instance in use. diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index baebc824086525..a80fd2f9bca969 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "12.13.0", + "version": "12.14.0", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/src/components/block-heading-level-dropdown/index.js b/packages/block-editor/src/components/block-heading-level-dropdown/index.js index be580042bb85e1..a8296d48ad2683 100644 --- a/packages/block-editor/src/components/block-heading-level-dropdown/index.js +++ b/packages/block-editor/src/components/block-heading-level-dropdown/index.js @@ -56,7 +56,7 @@ export default function HeadingLevelDropdown( { isPressed={ isActive } /> ), - label: + title: targetLevel === 0 ? __( 'Paragraph' ) : sprintf( diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index c6cce290985c22..03a84d530ba12a 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -605,6 +605,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { } moveFirstItemUp( rootClientId ); + } else { + removeBlock( clientId ); } } }, diff --git a/packages/block-editor/src/components/block-parent-selector/index.js b/packages/block-editor/src/components/block-parent-selector/index.js index 31e0b1b8fd8cf4..80b314eeb42e5c 100644 --- a/packages/block-editor/src/components/block-parent-selector/index.js +++ b/packages/block-editor/src/components/block-parent-selector/index.js @@ -74,7 +74,7 @@ export default function BlockParentSelector() { onClick={ () => selectBlock( firstParentClientId ) } label={ sprintf( /* translators: %s: Name of the block's parent. */ - __( 'Select %s' ), + __( 'Select parent block: %s' ), blockInformation?.title ) } showTooltip diff --git a/packages/block-editor/src/components/block-pattern-setup/index.js b/packages/block-editor/src/components/block-pattern-setup/index.js index 22d51466b3b6e9..edd55e90dc3e27 100644 --- a/packages/block-editor/src/components/block-pattern-setup/index.js +++ b/packages/block-editor/src/components/block-pattern-setup/index.js @@ -5,9 +5,7 @@ import { useDispatch } from '@wordpress/data'; import { cloneBlock } from '@wordpress/blocks'; import { VisuallyHidden, - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, - __unstableCompositeItem as CompositeItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useState } from '@wordpress/element'; @@ -22,6 +20,13 @@ import BlockPreview from '../block-preview'; import SetupToolbar from './setup-toolbar'; import usePatternsSetup from './use-patterns-setup'; import { VIEWMODES } from './constants'; +import { unlock } from '../../lock-unlock'; + +const { + CompositeV2: Composite, + CompositeItemV2: CompositeItem, + useCompositeStoreV2: useCompositeStore, +} = unlock( componentsPrivateApis ); const SetupContent = ( { viewMode, @@ -30,8 +35,9 @@ const SetupContent = ( { onBlockPatternSelect, showTitles, } ) => { - const composite = useCompositeState(); + const compositeStore = useCompositeStore(); const containerClass = 'block-editor-block-pattern-setup__container'; + if ( viewMode === VIEWMODES.carousel ) { const slideClass = new Map( [ [ activeSlide, 'active-slide' ], @@ -41,23 +47,25 @@ const SetupContent = ( { return (
-
    +
    { patterns.map( ( pattern, index ) => ( ) ) } -
+
); } + return (
) ) } @@ -76,7 +83,7 @@ const SetupContent = ( { ); }; -function BlockPattern( { pattern, onSelect, composite, showTitles } ) { +function BlockPattern( { pattern, onSelect, showTitles } ) { const baseClassName = 'block-editor-block-pattern-setup-list'; const { blocks, description, viewportWidth = 700 } = pattern; const descriptionId = useInstanceId( @@ -84,16 +91,19 @@ function BlockPattern( { pattern, onSelect, composite, showTitles } ) { `${ baseClassName }__item-description` ); return ( -
+
+ } + id={ `${ baseClassName }__pattern__${ pattern.name }` } role="option" - as="div" - { ...composite } - className={ `${ baseClassName }__item` } onClick={ () => onSelect( blocks ) } > ) } - +
); } @@ -178,10 +190,14 @@ const BlockPatternSetup = ( { activeSlide={ activeSlide } totalSlides={ patterns.length } handleNext={ () => { - setActiveSlide( ( active ) => active + 1 ); + setActiveSlide( ( active ) => + Math.min( active + 1, patterns.length - 1 ) + ); } } handlePrevious={ () => { - setActiveSlide( ( active ) => active - 1 ); + setActiveSlide( ( active ) => + Math.max( active - 1, 0 ) + ); } } onBlockPatternSelect={ () => { onPatternSelectCallback( diff --git a/packages/block-editor/src/components/block-pattern-setup/setup-toolbar.js b/packages/block-editor/src/components/block-pattern-setup/setup-toolbar.js index 69922f9560ab0b..91b68456cda71c 100644 --- a/packages/block-editor/src/components/block-pattern-setup/setup-toolbar.js +++ b/packages/block-editor/src/components/block-pattern-setup/setup-toolbar.js @@ -35,12 +35,14 @@ const CarouselNavigation = ( { label={ __( 'Previous pattern' ) } onClick={ handlePrevious } disabled={ activeSlide === 0 } + __experimentalIsFocusable />
); diff --git a/packages/block-editor/src/components/block-pattern-setup/style.scss b/packages/block-editor/src/components/block-pattern-setup/style.scss index 792e6872a2d594..3474eed5be5176 100644 --- a/packages/block-editor/src/components/block-pattern-setup/style.scss +++ b/packages/block-editor/src/components/block-pattern-setup/style.scss @@ -32,6 +32,8 @@ } .block-editor-block-pattern-setup-list__item { + scroll-margin: 5px 0; + &:hover .block-editor-block-preview__container { box-shadow: 0 0 0 2px var(--wp-admin-theme-color); } @@ -44,6 +46,7 @@ color: var(--wp-admin-theme-color); } } + .block-editor-block-pattern-setup-list__list-item { break-inside: avoid-column; margin-bottom: $grid-unit-30; @@ -85,7 +88,7 @@ align-items: center; justify-content: space-between; border-top: 1px solid $gray-300; - align-self: flex-end; + align-self: stretch; .block-editor-block-pattern-setup__display-controls { display: flex; diff --git a/packages/block-editor/src/components/block-patterns-list/README.md b/packages/block-editor/src/components/block-patterns-list/README.md index 7b30dcecc7bbd4..8b798f93b7190a 100644 --- a/packages/block-editor/src/components/block-patterns-list/README.md +++ b/packages/block-editor/src/components/block-patterns-list/README.md @@ -1,6 +1,6 @@ # Block Patterns List -The `BlockPatternList` component makes a list of the different registered block patterns. It uses the `BlockPreview` component to display a preview for each block pattern. +The `BlockPatternsList` component makes a list of the different registered block patterns. It uses the `BlockPreview` component to display a preview for each block pattern. For more infos about blocks patterns, read [this](https://make.wordpress.org/core/2020/07/16/block-patterns-in-wordpress-5-5/). @@ -18,10 +18,10 @@ For more infos about blocks patterns, read [this](https://make.wordpress.org/cor Renders a block patterns list. ```jsx -import { BlockPatternList } from '@wordpress/block-editor'; +import { BlockPatternsList } from '@wordpress/block-editor'; -const MyBlockPatternList = () => ( - ( + { if ( showTooltip ) { @@ -34,11 +40,11 @@ const WithToolTip = ( { showTooltip, title, children } ) => { }; function BlockPattern( { + id, isDraggable, pattern, onClick, onHover, - composite, showTooltip, } ) { const [ isDragging, setIsDragging ] = useState( false ); @@ -71,20 +77,33 @@ function BlockPattern( { } } > + } + id={ id } onClick={ () => { onClick( pattern, blocks ); onHover?.( null ); @@ -96,10 +115,6 @@ function BlockPattern( { onHover?.( pattern ); } } onMouseLeave={ () => onHover?.( null ) } - aria-label={ pattern.title } - aria-describedby={ - pattern.description ? descriptionId : undefined - } > - { pattern.id && ! pattern.syncStatus && ( -
- -
- ) } - { ( ! showTooltip || pattern.id ) && ( + { pattern.type === PATTERN_TYPES.user && + ! pattern.syncStatus && ( +
+ +
+ ) } + { ( ! showTooltip || + pattern.type === PATTERN_TYPES.user ) && (
{ pattern.title }
@@ -141,7 +158,7 @@ function BlockPatternPlaceholder() { ); } -function BlockPatternList( +function BlockPatternsList( { isDraggable, blockPatterns, @@ -155,10 +172,19 @@ function BlockPatternList( }, ref ) { - const composite = useCompositeState( { orientation } ); + const compositeStore = useCompositeStore( { orientation } ); + const { setActiveId } = compositeStore; + + useEffect( () => { + // We reset the active composite item whenever the + // available patterns change, to make sure that + // focus is put back to the start. + setActiveId( undefined ); + }, [ setActiveId, shownPatterns, blockPatterns ] ); + return ( ) : ( @@ -185,4 +211,4 @@ function BlockPatternList( ); } -export default forwardRef( BlockPatternList ); +export default forwardRef( BlockPatternsList ); diff --git a/packages/block-editor/src/components/block-patterns-list/style.scss b/packages/block-editor/src/components/block-patterns-list/style.scss index e3b38deff5ef7a..8009dfbcce1f23 100644 --- a/packages/block-editor/src/components/block-patterns-list/style.scss +++ b/packages/block-editor/src/components/block-patterns-list/style.scss @@ -18,6 +18,13 @@ .block-editor-block-patterns-list__item { height: 100%; + // This is derived from the top padding set on + // `.block-editor-block-patterns-explorer__list` + scroll-margin-top: $grid-unit-30; + // This is derived from the bottom padding set on + // `.block-editor-block-patterns-explorer__list` and + // the bottom margin set on `...__list-item` above + scroll-margin-bottom: ($grid-unit-40 + $grid-unit-30); .block-editor-block-preview__container { display: flex; diff --git a/packages/block-editor/src/components/block-quick-navigation/index.js b/packages/block-editor/src/components/block-quick-navigation/index.js index de33c8a427f257..7a0e7984b83cb7 100644 --- a/packages/block-editor/src/components/block-quick-navigation/index.js +++ b/packages/block-editor/src/components/block-quick-navigation/index.js @@ -5,7 +5,9 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { Button, __experimentalVStack as VStack, - __experimentalHStack as HStack, + __experimentalTruncate as Truncate, + Flex, + FlexBlock, FlexItem, } from '@wordpress/components'; import { @@ -72,10 +74,14 @@ function BlockQuickNavigationItem( { clientId } ) { isPressed={ isSelected } onClick={ () => selectBlock( clientId ) } > - - - { name } - + + + + + + { name } + + ); } diff --git a/packages/block-editor/src/components/block-rename/index.js b/packages/block-editor/src/components/block-rename/index.js new file mode 100644 index 00000000000000..0379893d412ec9 --- /dev/null +++ b/packages/block-editor/src/components/block-rename/index.js @@ -0,0 +1,3 @@ +export { default as BlockRenameControl } from './rename-control'; +export { default as BlockRenameModal } from './modal'; +export { default as useBlockRename } from './use-block-rename'; diff --git a/packages/block-editor/src/components/block-rename/is-empty-string.js b/packages/block-editor/src/components/block-rename/is-empty-string.js new file mode 100644 index 00000000000000..42d88be77b96e5 --- /dev/null +++ b/packages/block-editor/src/components/block-rename/is-empty-string.js @@ -0,0 +1,3 @@ +export default function isEmptyString( testString ) { + return testString?.trim()?.length === 0; +} diff --git a/packages/block-editor/src/components/block-rename/modal.js b/packages/block-editor/src/components/block-rename/modal.js new file mode 100644 index 00000000000000..a1e9193f348fd0 --- /dev/null +++ b/packages/block-editor/src/components/block-rename/modal.js @@ -0,0 +1,115 @@ +/** + * WordPress dependencies + */ +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, + Button, + TextControl, + Modal, +} from '@wordpress/components'; +import { useInstanceId } from '@wordpress/compose'; +import { __, sprintf } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; + +/** + * Internal dependencies + */ +import isEmptyString from './is-empty-string'; + +export default function BlockRenameModal( { + blockName, + originalBlockName, + onClose, + onSave, +} ) { + const [ editedBlockName, setEditedBlockName ] = useState( blockName ); + + const nameHasChanged = editedBlockName !== blockName; + const nameIsOriginal = editedBlockName === originalBlockName; + const nameIsEmpty = isEmptyString( editedBlockName ); + + const isNameValid = nameHasChanged || nameIsOriginal; + + const autoSelectInputText = ( event ) => event.target.select(); + + const dialogDescription = useInstanceId( + BlockRenameModal, + `block-editor-rename-modal__description` + ); + + const handleSubmit = () => { + const message = + nameIsOriginal || nameIsEmpty + ? sprintf( + /* translators: %s: new name/label for the block */ + __( 'Block name reset to: "%s".' ), + editedBlockName + ) + : sprintf( + /* translators: %s: new name/label for the block */ + __( 'Block name changed to: "%s".' ), + editedBlockName + ); + + // Must be assertive to immediately announce change. + speak( message, 'assertive' ); + onSave( editedBlockName ); + + // Immediate close avoids ability to hit save multiple times. + onClose(); + }; + + return ( + +

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

+
{ + e.preventDefault(); + + if ( ! isNameValid ) { + return; + } + + handleSubmit(); + } } + > + + + + + + + + + +
+ ); +} diff --git a/packages/block-editor/src/components/block-rename/rename-control.js b/packages/block-editor/src/components/block-rename/rename-control.js new file mode 100644 index 00000000000000..1f646126d14a4b --- /dev/null +++ b/packages/block-editor/src/components/block-rename/rename-control.js @@ -0,0 +1,80 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { useBlockDisplayInformation } from '..'; +import isEmptyString from './is-empty-string'; +import BlockRenameModal from './modal'; + +export default function BlockRenameControl( { clientId } ) { + const [ renamingBlock, setRenamingBlock ] = useState( false ); + + const { metadata } = useSelect( + ( select ) => { + const { getBlockAttributes } = select( blockEditorStore ); + + const _metadata = getBlockAttributes( clientId )?.metadata; + return { + metadata: _metadata, + }; + }, + [ clientId ] + ); + + const { updateBlockAttributes } = useDispatch( blockEditorStore ); + + const customName = metadata?.name; + + function onChange( newName ) { + updateBlockAttributes( [ clientId ], { + metadata: { + ...( metadata && metadata ), + name: newName, + }, + } ); + } + + const blockInformation = useBlockDisplayInformation( clientId ); + + 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 || + isEmptyString( newName ) + ) { + newName = undefined; + } + + onChange( newName ); + } } + /> + ) } + + ); +} diff --git a/packages/block-editor/src/hooks/block-rename-ui.scss b/packages/block-editor/src/components/block-rename/style.scss similarity index 100% rename from packages/block-editor/src/hooks/block-rename-ui.scss rename to packages/block-editor/src/components/block-rename/style.scss diff --git a/packages/block-editor/src/components/block-rename/use-block-rename.js b/packages/block-editor/src/components/block-rename/use-block-rename.js new file mode 100644 index 00000000000000..a3fca66f13670e --- /dev/null +++ b/packages/block-editor/src/components/block-rename/use-block-rename.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { getBlockSupport } from '@wordpress/blocks'; + +export default function useBlockRename( name ) { + return { + canRename: getBlockSupport( name, 'renaming', true ), + }; +} 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 9063765e72a031..d7a0b001c294da 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 @@ -22,6 +22,8 @@ import { BlockLockMenuItem, useBlockLock } from '../block-lock'; import { store as blockEditorStore } from '../../store'; import BlockModeToggle from '../block-settings-menu/block-mode-toggle'; +import { BlockRenameControl, useBlockRename } from '../block-rename'; + const { Fill, Slot } = createSlotFill( 'BlockSettingsMenuControls' ); const BlockSettingsMenuControlsSlot = ( { @@ -44,7 +46,9 @@ const BlockSettingsMenuControlsSlot = ( { ); const { canLock } = useBlockLock( selectedClientIds[ 0 ] ); + const { canRename } = useBlockRename( selectedBlocks[ 0 ] ); const showLockButton = selectedClientIds.length === 1 && canLock; + const showRenameButton = selectedClientIds.length === 1 && canRename; // Check if current selection of blocks is Groupable or Ungroupable // and pass this props down to ConvertToGroupButton. @@ -84,6 +88,11 @@ const BlockSettingsMenuControlsSlot = ( { clientId={ selectedClientIds[ 0 ] } /> ) } + { showRenameButton && ( + + ) } { fills } { fillProps?.canMove && ! fillProps?.onlyBlock && ( { copyMenuItemLabel }; } +function ParentSelectorMenuItem( { parentClientId, parentBlockType } ) { + const isSmallViewport = useViewportMatch( 'medium', '<' ); + const { selectBlock } = useDispatch( blockEditorStore ); + + // Allows highlighting the parent block outline when focusing or hovering + // the parent block selector within the child. + const menuItemRef = useRef(); + const gesturesProps = useShowHoveredOrFocusedGestures( { + ref: menuItemRef, + highlightParent: true, + } ); + + if ( ! isSmallViewport ) { + return null; + } + + return ( + } + onClick={ () => selectBlock( parentClientId ) } + > + { sprintf( + /* translators: %s: Name of the block's parent. */ + __( 'Select parent block (%s)' ), + parentBlockType.title + ) } + + ); +} + export function BlockSettingsDropdown( { block, clientIds, @@ -132,8 +164,6 @@ export function BlockSettingsDropdown( { }; }, [] ); const isMatch = __unstableUseShortcutEventMatch(); - - const { selectBlock } = useDispatch( blockEditorStore ); const hasSelectedBlocks = selectedBlockClientIds.length > 0; const updateSelectionAfterDuplicate = useCallback( @@ -175,14 +205,6 @@ export function BlockSettingsDropdown( { const removeBlockLabel = count === 1 ? __( 'Delete' ) : __( 'Delete blocks' ); - // Allows highlighting the parent block outline when focusing or hovering - // the parent block selector within the child. - const selectParentButtonRef = useRef(); - const showParentOutlineGestures = useShowHoveredOrFocusedGestures( { - ref: selectParentButtonRef, - highlightParent: true, - } ); - // This can occur when the selected block (the parent) // displays child blocks within a List View. const parentBlockIsSelected = @@ -297,30 +319,12 @@ export function BlockSettingsDropdown( { /> { ! parentBlockIsSelected && !! firstParentClientId && ( - - } - onClick={ () => - selectBlock( - firstParentClientId - ) + - { sprintf( - /* translators: %s: Name of the block's parent. */ - __( - 'Select parent block (%s)' - ), - parentBlockType.title - ) } - + parentBlockType={ parentBlockType } + /> ) } { count === 1 && ( ) ) }
); } -function BlockPattern( { pattern, onSelect, composite } ) { +function BlockPattern( { pattern, onSelect } ) { // TODO check pattern/preview width... const baseClassName = 'block-editor-block-switcher__preview-patterns-container'; @@ -104,14 +108,16 @@ function BlockPattern( { pattern, onSelect, composite } ) { return (
} - className={ `${ baseClassName }-list__item` } onClick={ () => onSelect( pattern.transformedBlocks ) } > 1 - ? __( 'Detach patterns' ) - : __( 'Detach pattern' ), + label: __( 'Detach' ), value: 'convertToRegularBlocksOption', onSelect: () => { /* translators: %s: name of the synced block */ diff --git a/packages/block-editor/src/components/block-types-list/index.js b/packages/block-editor/src/components/block-types-list/index.js index 40e04b040d5a80..0be6f82a653d18 100644 --- a/packages/block-editor/src/components/block-types-list/index.js +++ b/packages/block-editor/src/components/block-types-list/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { getBlockMenuDefaultClassName } from '@wordpress/blocks'; +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies @@ -25,11 +26,10 @@ function BlockTypesList( { label, isDraggable = true, } ) { + const className = 'block-editor-block-types-list'; + const listId = useInstanceId( BlockTypesList, className ); return ( - + { chunk( items, 3 ).map( ( row, i ) => ( { row.map( ( item, j ) => ( @@ -43,6 +43,7 @@ function BlockTypesList( { onHover={ onHover } isDraggable={ isDraggable && ! item.isDisabled } isFirst={ i === 0 && j === 0 } + rowId={ `${ listId }-${ i }` } /> ) ) } diff --git a/packages/block-editor/src/components/button-block-appender/content.scss b/packages/block-editor/src/components/button-block-appender/content.scss index 941ccb7dd1ad3a..50d93234b93f5b 100644 --- a/packages/block-editor/src/components/button-block-appender/content.scss +++ b/packages/block-editor/src/components/button-block-appender/content.scss @@ -39,8 +39,8 @@ .is-layout-constrained.block-editor-block-list__block:not(.is-selected) > &, .is-layout-flow.block-editor-block-list__block:not(.is-selected) > &, // Legacy groups have an inner container so need to be targeted separately - .is-layout-constrained.block-editor-block-list__block:not(.is-selected) > .wp-block-group__inner-container > &, - .is-layout-flow.block-editor-block-list__block:not(.is-selected) > .wp-block-group__inner-container > & { + .block-editor-block-list__block:not(.is-selected) > .is-layout-constrained.wp-block-group__inner-container > &, + .block-editor-block-list__block:not(.is-selected) > .is-layout-flow.wp-block-group__inner-container > & { pointer-events: none; &::after { diff --git a/packages/block-editor/src/components/date-format-picker/index.js b/packages/block-editor/src/components/date-format-picker/index.js index 82a7b4cf760fc7..7a2ed46b4a9b04 100644 --- a/packages/block-editor/src/components/date-format-picker/index.js +++ b/packages/block-editor/src/components/date-format-picker/index.js @@ -78,12 +78,19 @@ function NonDefaultControls( { format, onChange } ) { // formats. const suggestedFormats = [ ...new Set( [ + /* translators: See https://www.php.net/manual/datetime.format.php */ 'Y-m-d', + /* translators: See https://www.php.net/manual/datetime.format.php */ _x( 'n/j/Y', 'short date format' ), + /* translators: See https://www.php.net/manual/datetime.format.php */ _x( 'n/j/Y g:i A', 'short date format with time' ), + /* translators: See https://www.php.net/manual/datetime.format.php */ _x( 'M j, Y', 'medium date format' ), + /* translators: See https://www.php.net/manual/datetime.format.php */ _x( 'M j, Y g:i A', 'medium date format with time' ), + /* translators: See https://www.php.net/manual/datetime.format.php */ _x( 'F j, Y', 'long date format' ), + /* translators: See https://www.php.net/manual/datetime.format.php */ _x( 'M j', 'short date format without the year' ), ] ), ]; diff --git a/packages/block-editor/src/components/editable-text/README.md b/packages/block-editor/src/components/editable-text/README.md index 86607349ae8176..aa5a2f4b1962b8 100644 --- a/packages/block-editor/src/components/editable-text/README.md +++ b/packages/block-editor/src/components/editable-text/README.md @@ -47,40 +47,6 @@ _Optional._ Called when the block can be removed. `forward` is true when the sel ## Example -{% codetabs %} -{% ES5 %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - content: { - source: 'html', - selector: 'div', - }, - }, - - edit: function( props ) { - return React.createElement( wp.editor.EditableText, { - className: props.className, - value: props.attributes.content, - onChange: function( content ) { - props.setAttributes( { content: content } ); - } - } ); - }, - - save: function( props ) { - return React.createElement( wp.editor.EditableText.Content, { - value: props.attributes.content - } ); - } -} ); -``` - -{% ESNext %} - ```js const { registerBlockType } = wp.blocks; const { EditableText } = wp.editor; @@ -110,5 +76,3 @@ registerBlockType( /* ... */, { } } ); ``` - -{% end %} diff --git a/packages/block-editor/src/components/global-styles/advanced-panel.js b/packages/block-editor/src/components/global-styles/advanced-panel.js index 4460de03513180..af43552c0a3eba 100644 --- a/packages/block-editor/src/components/global-styles/advanced-panel.js +++ b/packages/block-editor/src/components/global-styles/advanced-panel.js @@ -30,7 +30,7 @@ export default function AdvancedPanel( { } ); if ( cssError ) { const [ transformed ] = transformStyles( - [ { css: value } ], + [ { css: newValue } ], '.editor-styles-wrapper' ); if ( transformed ) { diff --git a/packages/block-editor/src/components/image-size-control/index.js b/packages/block-editor/src/components/image-size-control/index.js index d929b129313938..46e87de60f2fc8 100644 --- a/packages/block-editor/src/components/image-size-control/index.js +++ b/packages/block-editor/src/components/image-size-control/index.js @@ -8,7 +8,6 @@ import { __experimentalNumberControl as NumberControl, __experimentalHStack as HStack, } from '@wordpress/components'; -import deprecated from '@wordpress/deprecated'; import { __ } from '@wordpress/i18n'; /** @@ -31,11 +30,6 @@ export default function ImageSizeControl( { onChange, onChangeImage = noop, } ) { - deprecated( 'wp.blockEditor.__experimentalImageSizeControl', { - since: '6.3', - alternative: - 'wp.blockEditor.privateApis.DimensionsTool and wp.blockEditor.privateApis.ResolutionTool', - } ); const { currentHeight, currentWidth, updateDimension, updateDimensions } = useDimensionHandler( height, width, imageHeight, imageWidth, onChange ); diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index f5f216d6072e4b..d637a16f363602 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -169,8 +169,11 @@ const ForwardedInnerBlocks = forwardRef( ( props, ref ) => { * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inner-blocks/README.md */ export function useInnerBlocksProps( props = {}, options = {} ) { - const { __unstableDisableLayoutClassNames, __unstableDisableDropZone } = - options; + const { + __unstableDisableLayoutClassNames, + __unstableDisableDropZone, + dropZoneElement, + } = options; const { clientId, layout = null, @@ -211,6 +214,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { ); const blockDropZoneRef = useBlockDropZone( { + dropZoneElement, rootClientId: clientId, } ); diff --git a/packages/block-editor/src/components/inner-blocks/warning-max-depth-exceeded.native.js b/packages/block-editor/src/components/inner-blocks/warning-max-depth-exceeded.native.js index 57a7b7a60483ce..e363db4961c7c3 100644 --- a/packages/block-editor/src/components/inner-blocks/warning-max-depth-exceeded.native.js +++ b/packages/block-editor/src/components/inner-blocks/warning-max-depth-exceeded.native.js @@ -8,7 +8,7 @@ import { TouchableWithoutFeedback, View } from 'react-native'; */ import { __, sprintf } from '@wordpress/i18n'; import { useState } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -18,54 +18,71 @@ import UnsupportedBlockDetails from '../unsupported-block-details'; import { store as blockEditorStore } from '../../store'; import { MAX_NESTING_DEPTH } from './constants'; import useUnsupportedBlockEditor from '../use-unsupported-block-editor'; +import { + useConvertToGroupButtons, + useConvertToGroupButtonProps, +} from '../convert-to-group-buttons'; + +const EMPTY_ARRAY = []; const WarningMaxDepthExceeded = ( { clientId } ) => { const [ showDetails, setShowDetails ] = useState( false ); - const { isSelected, innerBlocks } = useSelect( - ( select ) => { - const { getBlock, isBlockSelected } = select( blockEditorStore ); - return { - innerBlocks: getBlock( clientId )?.innerBlocks || [], - isSelected: isBlockSelected( clientId ), - }; - }, + const isSelected = useSelect( + ( select ) => select( blockEditorStore ).isBlockSelected( clientId ), [ clientId ] ); - const { replaceBlocks } = useDispatch( blockEditorStore ); + + // We rely on the logic related to the Group/Ungroup buttons used in the block options to + // determine whether to use the Ungroup action. + const convertToGroupButtonProps = useConvertToGroupButtonProps( [ + clientId, + ] ); + const { isUngroupable } = convertToGroupButtonProps; + const convertToGroupButtons = useConvertToGroupButtons( { + ...convertToGroupButtonProps, + } ); + const onUngroup = convertToGroupButtons.ungroup.onSelect; const { isUnsupportedBlockEditorSupported, canEnableUnsupportedBlockEditor, } = useUnsupportedBlockEditor( clientId ); - const onUngroup = () => { - if ( ! innerBlocks.length ) { - return; - } - - replaceBlocks( clientId, innerBlocks ); - }; - - let description; - // When UBE can't be used, the description mentions using the web browser to edit the block. + /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ + const descriptionFormat = __( + 'Blocks nested deeper than %d levels may not render properly in the mobile editor.' + ); + let description = sprintf( descriptionFormat, MAX_NESTING_DEPTH ); if ( ! isUnsupportedBlockEditorSupported && ! canEnableUnsupportedBlockEditor ) { - /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ - const descriptionFormat = __( - 'Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using your web browser.' - ); - description = sprintf( descriptionFormat, MAX_NESTING_DEPTH ); + // When UBE can't be used, the description mentions using the web browser to edit the block. + description += + ' ' + + /* translators: Recommendation included in a warning related to having blocks deeply nested. */ + __( + 'For this reason, we recommend editing the block using your web browser.' + ); } // Otherwise, the description mentions using the web editor (i.e. UBE). else { - /* translators: Warning related to having blocks deeply nested. %d: The deepest nesting level. */ - const descriptionFormat = __( - 'Blocks nested deeper than %d levels may not render properly in the mobile editor. For this reason, we recommend flattening the content by ungrouping the block or editing the block using the web editor.' - ); - description = sprintf( descriptionFormat, MAX_NESTING_DEPTH ); + description += + ' ' + + /* translators: Recommendation included in a warning related to having blocks deeply nested. */ + __( + 'For this reason, we recommend editing the block using the web editor.' + ); + } + // If the block can be flattened, we also suggest to ungroup the block. + if ( isUngroupable ) { + description += + ' ' + + /* translators: Alternative option included in a warning related to having blocks deeply nested. */ + __( + 'Alternatively, you can flatten the content by ungrouping the block.' + ); } return ( @@ -88,9 +105,16 @@ const WarningMaxDepthExceeded = ( { clientId } ) => { onCloseSheet={ () => setShowDetails( false ) } title={ __( 'Deeply nested block' ) } description={ description } - customActions={ [ - { label: __( 'Ungroup block' ), onPress: onUngroup }, - ] } + customActions={ + isUngroupable + ? [ + { + label: __( 'Ungroup block' ), + onPress: onUngroup, + }, + ] + : EMPTY_ARRAY + } /> diff --git a/packages/block-editor/src/components/inserter-listbox/index.js b/packages/block-editor/src/components/inserter-listbox/index.js index 6345cb38c494ac..6af26a1d746bfb 100644 --- a/packages/block-editor/src/components/inserter-listbox/index.js +++ b/packages/block-editor/src/components/inserter-listbox/index.js @@ -1,26 +1,30 @@ /** * WordPress dependencies */ -import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ -import InserterListboxContext from './context'; +import { unlock } from '../../lock-unlock'; export { default as InserterListboxGroup } from './group'; export { default as InserterListboxRow } from './row'; export { default as InserterListboxItem } from './item'; +const { CompositeV2: Composite, useCompositeStoreV2: useCompositeStore } = + unlock( componentsPrivateApis ); + function InserterListbox( { children } ) { - const compositeState = useCompositeState( { - shift: true, - wrap: 'horizontal', + const store = useCompositeStore( { + focusShift: true, + focusWrap: 'horizontal', } ); + return ( - + }> { children } - + ); } diff --git a/packages/block-editor/src/components/inserter-listbox/item.js b/packages/block-editor/src/components/inserter-listbox/item.js index 50adb4a7880387..951eb86223ce8f 100644 --- a/packages/block-editor/src/components/inserter-listbox/item.js +++ b/packages/block-editor/src/components/inserter-listbox/item.js @@ -3,32 +3,31 @@ */ import { Button, - __unstableCompositeItem as CompositeItem, + privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { forwardRef, useContext } from '@wordpress/element'; +import { forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import InserterListboxContext from './context'; +import { unlock } from '../../lock-unlock'; + +const { CompositeItemV2: CompositeItem } = unlock( componentsPrivateApis ); function InserterListboxItem( { isFirst, as: Component, children, ...props }, ref ) { - const state = useContext( InserterListboxContext ); return ( - { ( htmlProps ) => { + render={ ( htmlProps ) => { const propsWithTabIndex = { ...htmlProps, tabIndex: isFirst ? 0 : htmlProps.tabIndex, @@ -45,7 +44,7 @@ function InserterListboxItem( } return ; } } - + /> ); } diff --git a/packages/block-editor/src/components/inserter-listbox/row.js b/packages/block-editor/src/components/inserter-listbox/row.js index 710267660199d7..f9827f08b3fa39 100644 --- a/packages/block-editor/src/components/inserter-listbox/row.js +++ b/packages/block-editor/src/components/inserter-listbox/row.js @@ -1,24 +1,18 @@ /** * WordPress dependencies */ -import { forwardRef, useContext } from '@wordpress/element'; -import { __unstableCompositeGroup as CompositeGroup } from '@wordpress/components'; +import { forwardRef } from '@wordpress/element'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ -import InserterListboxContext from './context'; +import { unlock } from '../../lock-unlock'; + +const { CompositeGroupV2: CompositeGroup } = unlock( componentsPrivateApis ); function InserterListboxRow( props, ref ) { - const state = useContext( InserterListboxContext ); - return ( - - ); + return ; } export default forwardRef( InserterListboxRow ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js index 7cd2320a4fd1f0..bf2867be5cdf3c 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-list.js @@ -18,6 +18,7 @@ import { searchItems } from '../search-items'; import BlockPatternsPaging from '../../block-patterns-paging'; import usePatternsPaging from '../hooks/use-patterns-paging'; import { + PATTERN_TYPES, allPatternsCategory, myPatternsCategory, } from '../block-patterns-tab/utils'; @@ -70,7 +71,10 @@ function PatternList( { searchValue, selectedCategory, patternCategories } ) { if ( selectedCategory === allPatternsCategory.name ) { return true; } - if ( selectedCategory === myPatternsCategory.name && pattern.id ) { + if ( + selectedCategory === myPatternsCategory.name && + pattern.type === PATTERN_TYPES.user + ) { return true; } if ( selectedCategory === 'uncategorized' ) { diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js index 2fef53cfa2a193..9e5b6373e54d8c 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js @@ -22,7 +22,7 @@ import { * Internal dependencies */ import usePatternsState from '../hooks/use-patterns-state'; -import BlockPatternList from '../../block-patterns-list'; +import BlockPatternsList from '../../block-patterns-list'; import usePatternsPaging from '../hooks/use-patterns-paging'; import { PatternsFilter } from './patterns-filter'; import { usePatternCategories } from './use-pattern-categories'; @@ -30,6 +30,7 @@ import { isPatternFiltered, allPatternsCategory, myPatternsCategory, + PATTERN_TYPES, } from './utils'; const noop = () => {}; @@ -69,7 +70,10 @@ export function PatternCategoryPreviews( { if ( category.name === allPatternsCategory.name ) { return true; } - if ( category.name === myPatternsCategory.name && pattern.id ) { + if ( + category.name === myPatternsCategory.name && + pattern.type === PATTERN_TYPES.user + ) { return true; } if ( category.name !== 'uncategorized' ) { @@ -155,7 +159,7 @@ export function PatternCategoryPreviews( { { currentCategoryPatterns.length > 0 && ( - pattern.id ) ) { + if ( + filteredPatterns.some( + ( pattern ) => pattern.type === PATTERN_TYPES.user + ) + ) { categories.unshift( myPatternsCategory ); } if ( filteredPatterns.length > 0 ) { diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js index 9f222c6a2f93cd..b1e5a99bbe6dc6 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/utils.js @@ -21,7 +21,7 @@ export const SYNC_TYPES = { export const allPatternsCategory = { name: 'allPatterns', - label: __( 'All patterns' ), + label: __( 'All' ), }; export const myPatternsCategory = { @@ -53,9 +53,11 @@ export function isPatternFiltered( pattern, sourceFilter, syncFilter ) { return true; } - // If user source selected, filter out theme patterns. Any pattern without - // an id wasn't created by a user. - if ( sourceFilter === PATTERN_TYPES.user && ! pattern.id ) { + // If user source selected, filter out theme patterns. + if ( + sourceFilter === PATTERN_TYPES.user && + pattern.type !== PATTERN_TYPES.user + ) { return true; } diff --git a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js index 1924187e04179f..576768c76abca9 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-patterns-state.js @@ -11,6 +11,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import { store as blockEditorStore } from '../../../store'; +import { PATTERN_TYPES } from '../block-patterns-tab/utils'; /** * Retrieves the block patterns inserter state. @@ -57,7 +58,8 @@ const usePatternsState = ( onInsert, rootClientId ) => { const onClickPattern = useCallback( ( pattern, blocks ) => { const patternBlocks = - pattern.id && pattern.syncStatus !== 'unsynced' + pattern.type === PATTERN_TYPES.user && + pattern.syncStatus !== 'unsynced' ? [ createBlock( 'core/block', { ref: pattern.id } ) ] : blocks; onInsert( diff --git a/packages/block-editor/src/components/inserter/media-tab/media-list.js b/packages/block-editor/src/components/inserter/media-tab/media-list.js index b745a54e25e9c0..bfc858bc8c4de7 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-list.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-list.js @@ -1,16 +1,17 @@ /** * WordPress dependencies */ -import { - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, -} from '@wordpress/components'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { MediaPreview } from './media-preview'; +import { unlock } from '../../../lock-unlock'; + +const { CompositeV2: Composite, useCompositeStoreV2: useCompositeStore } = + unlock( componentsPrivateApis ); function MediaList( { mediaList, @@ -18,10 +19,10 @@ function MediaList( { onClick, label = __( 'Media List' ), } ) { - const composite = useCompositeState(); + const compositeStore = useCompositeStore(); return ( ) ) } diff --git a/packages/block-editor/src/components/inserter/media-tab/media-preview.js b/packages/block-editor/src/components/inserter/media-tab/media-preview.js index 88648bf96531b6..9efed229f0adf2 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-preview.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-preview.js @@ -7,7 +7,6 @@ import classnames from 'classnames'; * WordPress dependencies */ import { - __unstableCompositeItem as CompositeItem, Tooltip, DropdownMenu, MenuGroup, @@ -17,6 +16,7 @@ import { Flex, FlexItem, Button, + privateApis as componentsPrivateApis, __experimentalVStack as VStack, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; @@ -33,6 +33,7 @@ import { isBlobURL } from '@wordpress/blob'; import InserterDraggableBlocks from '../../inserter-draggable-blocks'; import { getBlockAndPreviewFromMedia } from './utils'; import { store as blockEditorStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; const ALLOWED_MEDIA_TYPES = [ 'image' ]; const MAXIMUM_TITLE_LENGTH = 25; @@ -42,6 +43,8 @@ const MEDIA_OPTIONS_POPOVER_PROPS = { 'block-editor-inserter__media-list__item-preview-options__popover', }; +const { CompositeItemV2: CompositeItem } = unlock( componentsPrivateApis ); + function MediaPreviewOptions( { category, media } ) { if ( ! category.getReportUrl ) { return null; @@ -113,7 +116,7 @@ function InsertExternalImageModal( { onClose, onSubmit } ) { ); } -export function MediaPreview( { media, onClick, composite, category } ) { +export function MediaPreview( { media, onClick, category } ) { const [ showExternalUploadModal, setShowExternalUploadModal ] = useState( false ); const [ isHovered, setIsHovered ] = useState( false ); @@ -216,20 +219,22 @@ export function MediaPreview( { media, onClick, composite, category } ) { onDragStart={ onDragStart } onDragEnd={ onDragEnd } > - - { /* Adding `is-hovered` class to the wrapper element is needed - because the options Popover is rendered outside of this node. */ } -
+ { /* Adding `is-hovered` class to the wrapper element is needed + because the options Popover is rendered outside of this node. */ } +
+ + } onClick={ () => onMediaInsert( block ) } - aria-label={ title } >
{ preview } @@ -240,14 +245,14 @@ export function MediaPreview( { media, onClick, composite, category } ) { ) }
- { ! isInserting && ( - - ) } -
- + + { ! isInserting && ( + + ) } +
) } diff --git a/packages/block-editor/src/components/list-view/use-list-view-expand-selected-item.js b/packages/block-editor/src/components/list-view/use-list-view-expand-selected-item.js index 09b5e09e4713a3..f84419dc1db933 100644 --- a/packages/block-editor/src/components/list-view/use-list-view-expand-selected-item.js +++ b/packages/block-editor/src/components/list-view/use-list-view-expand-selected-item.js @@ -27,12 +27,6 @@ export default function useListViewExpandSelectedItem( { [ firstSelectedBlockClientId ] ); - const parentClientIds = - Array.isArray( selectedBlockParentClientIds ) && - selectedBlockParentClientIds.length - ? selectedBlockParentClientIds - : null; - // Expand tree when a block is selected. useEffect( () => { // If the selectedTreeId is the same as the selected block, @@ -42,7 +36,7 @@ export default function useListViewExpandSelectedItem( { } // If the selected block has parents, get the top-level parent. - if ( parentClientIds ) { + if ( selectedBlockParentClientIds?.length ) { // If the selected block has parents, // expand the tree branch. setExpandedState( { @@ -50,7 +44,12 @@ export default function useListViewExpandSelectedItem( { clientIds: selectedBlockParentClientIds, } ); } - }, [ firstSelectedBlockClientId ] ); + }, [ + firstSelectedBlockClientId, + selectedBlockParentClientIds, + selectedTreeId, + setExpandedState, + ] ); return { setSelectedTreeId, diff --git a/packages/block-editor/src/components/plain-text/README.md b/packages/block-editor/src/components/plain-text/README.md index 4e59789fd612c7..aa15758118afdc 100644 --- a/packages/block-editor/src/components/plain-text/README.md +++ b/packages/block-editor/src/components/plain-text/README.md @@ -20,33 +20,6 @@ _Optional._ The component forwards the `ref` property to the `TextareaAutosize` ## Example -{% codetabs %} -{% ES5 %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - content: { - type: 'string', - }, - }, - - edit: function( props ) { - return React.createElement( wp.blockEditor.PlainText, { - className: props.className, - value: props.attributes.content, - onChange: function( content ) { - props.setAttributes( { content: content } ); - }, - } ); - }, -} ); -``` - -{% ESNext %} - ```js import { registerBlockType } from '@wordpress/blocks'; import { PlainText } from '@wordpress/block-editor'; @@ -72,4 +45,3 @@ registerBlockType( /* ... */, { } ); ``` -{% end %} diff --git a/packages/block-editor/src/components/preview-options/index.js b/packages/block-editor/src/components/preview-options/index.js index 8dc5a70a91397d..8ffdd4327de27c 100644 --- a/packages/block-editor/src/components/preview-options/index.js +++ b/packages/block-editor/src/components/preview-options/index.js @@ -19,6 +19,7 @@ export default function PreviewOptions( { deviceType, setDeviceType, label, + showIconLabels, } ) { const isMobile = useViewportMatch( 'small', '<' ); if ( isMobile ) return null; @@ -35,6 +36,7 @@ export default function PreviewOptions( { disabled: ! isEnabled, __experimentalIsFocusable: ! isEnabled, children: viewLabel, + showTooltip: ! showIconLabels, }; const menuProps = { 'aria-label': __( 'View options' ), diff --git a/packages/block-editor/src/components/provider/use-block-sync.js b/packages/block-editor/src/components/provider/use-block-sync.js index 58aca847d80de0..4f2300f380892e 100644 --- a/packages/block-editor/src/components/provider/use-block-sync.js +++ b/packages/block-editor/src/components/provider/use-block-sync.js @@ -76,18 +76,11 @@ export default function useBlockSync( { resetBlocks, resetSelection, replaceInnerBlocks, - selectBlock, setHasControlledInnerBlocks, __unstableMarkNextChangeAsNotPersistent, } = registry.dispatch( blockEditorStore ); - const { - hasSelectedBlock, - getBlockName, - getBlocks, - getSelectionStart, - getSelectionEnd, - getBlock, - } = registry.select( blockEditorStore ); + const { getBlockName, getBlocks, getSelectionStart, getSelectionEnd } = + registry.select( blockEditorStore ); const isControlled = useSelect( ( select ) => { return ( @@ -180,9 +173,6 @@ export default function useBlockSync( { // bound sync, unset the outbound value to avoid considering it in // subsequent renders. pendingChanges.current.outgoing = []; - const hadSelection = hasSelectedBlock(); - const selectionAnchor = getSelectionStart(); - const selectionFocus = getSelectionEnd(); setControlledBlocks(); if ( controlledSelection ) { @@ -191,15 +181,6 @@ export default function useBlockSync( { controlledSelection.selectionEnd, controlledSelection.initialPosition ); - } else { - const selectionStillExists = getBlock( - selectionAnchor.clientId - ); - if ( hadSelection && ! selectionStillExists ) { - selectBlock( clientId ); - } else { - resetSelection( selectionAnchor, selectionFocus ); - } } } }, [ controlledBlocks, clientId ] ); diff --git a/packages/block-editor/src/components/rich-text/README.md b/packages/block-editor/src/components/rich-text/README.md index d17f987a34cf0e..4251debfa16c54 100644 --- a/packages/block-editor/src/components/rich-text/README.md +++ b/packages/block-editor/src/components/rich-text/README.md @@ -80,41 +80,6 @@ trimmed. ## Example -{% codetabs %} -{% ES5 %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - content: { - source: 'html', - selector: 'h2', - }, - }, - - edit: function( props ) { - return React.createElement( wp.blockEditor.RichText, { - tagName: 'h2', - className: props.className, - value: props.attributes.content, - onChange: function( content ) { - props.setAttributes( { content: content } ); - } - } ); - }, - - save: function( props ) { - return React.createElement( wp.blockEditor.RichText.Content, { - tagName: 'h2', value: props.attributes.content - } ); - } -} ); -``` - -{% ESNext %} - ```js import { registerBlockType } from '@wordpress/blocks'; import { RichText } from '@wordpress/block-editor'; @@ -146,7 +111,6 @@ registerBlockType( /* ... */, { } ); ``` -{% end %} ## RichTextToolbarButton @@ -154,26 +118,6 @@ Slot to extend the format toolbar. Use it in the edit function of a `registerFor ### Example -{% codetabs %} -{% ES5 %} - -```js -wp.richText.registerFormatType( /* ... */, { - /* ... */ - edit: function( props ) { - return React.createElement( - wp.blockEditor.RichTextToolbarButton, { - icon: 'editor-code', - title: 'My formatting button', - onClick: function() { /* ... */ } - isActive: props.isActive, - } ); - }, - /* ... */ -} ); -``` - -{% ESNext %} ```js import { registerFormatType } from '@wordpress/rich-text'; @@ -194,5 +138,3 @@ registerFormatType( /* ... */, { /* ... */ } ); ``` - -{% end %} diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index aab10e9ab65476..9427962eced198 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -223,7 +223,7 @@ function RichTextWrapper( // an intentional user interaction distinguishing between Backspace and // Delete to remove the empty field, but also to avoid merge & remove // causing destruction of two fields (merge, then removed merged). - if ( onRemove && isEmpty( value ) && isReverse ) { + else if ( onRemove && isEmpty( value ) && isReverse ) { onRemove( ! isReverse ); } }, diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 2381b9809eca86..ab465b24411549 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -650,6 +650,40 @@ export class RichText extends Component { return shouldDrop; } + /** + * Determines whether the text input should receive focus after an update. + * For cases where a RichText with a value is merged with an empty one. + * + * @param {Object} prevProps - The previous props of the component. + * @return {boolean} True if the text input should receive focus, false otherwise. + */ + shouldFocusTextInputAfterMerge( prevProps ) { + const { + __unstableIsSelected: isSelected, + blockIsSelected, + selectionStart, + selectionEnd, + __unstableMobileNoFocusOnMount, + } = this.props; + + const { + __unstableIsSelected: prevIsSelected, + blockIsSelected: prevBlockIsSelected, + } = prevProps; + + const noSelectionValues = + selectionStart === undefined && selectionEnd === undefined; + const textInputWasNotFocused = ! prevIsSelected && ! isSelected; + + return ( + ! __unstableMobileNoFocusOnMount && + noSelectionValues && + textInputWasNotFocused && + ! prevBlockIsSelected && + blockIsSelected + ); + } + onSelectionChangeFromAztec( start, end, text, event ) { if ( this.shouldDropEventFromAztec( event, 'onSelectionChange' ) ) { return; @@ -843,9 +877,8 @@ export class RichText extends Component { if ( this.props.value !== this.value ) { this.value = this.props.value; } - const { __unstableIsSelected: isSelected } = this.props; - const { __unstableIsSelected: prevIsSelected } = prevProps; + const { __unstableIsSelected: isSelected } = this.props; if ( isSelected && ! prevIsSelected ) { this._editor.focus(); @@ -855,6 +888,16 @@ export class RichText extends Component { this.props.selectionStart || 0, this.props.selectionEnd || 0 ); + } else if ( this.shouldFocusTextInputAfterMerge( prevProps ) ) { + // Since this is happening when merging blocks, the selection should be at the last character position. + // As a fallback the internal selectionEnd value is used. + const lastCharacterPosition = + this.value?.length ?? this.selectionEnd; + this._editor.focus(); + this.props.onSelectionChange( + lastCharacterPosition, + lastCharacterPosition + ); } else if ( ! isSelected && prevIsSelected ) { this._editor.blur(); } diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 58432c01f9683b..5aa47e7c7b4d74 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -3,7 +3,7 @@ */ import { useRef } from '@wordpress/element'; import { useRefEffect } from '@wordpress/compose'; -import { insert, toHTMLString } from '@wordpress/rich-text'; +import { insert, isCollapsed, toHTMLString } from '@wordpress/rich-text'; import { getBlockTransforms, findTransform } from '@wordpress/blocks'; import { useDispatch } from '@wordpress/data'; @@ -42,6 +42,34 @@ function findSelection( blocks ) { return []; } +/** + * An input rule that replaces two spaces with an en space, and an en space + * followed by a space with an em space. + * + * @param {Object} value Value to replace spaces in. + * + * @return {Object} Value with spaces replaced. + */ +function replacePrecedingSpaces( value ) { + if ( ! isCollapsed( value ) ) { + return value; + } + + const { text, start } = value; + const lastTwoCharacters = text.slice( start - 2, start ); + + // Replace two spaces with an em space. + if ( lastTwoCharacters === ' ' ) { + return insert( value, '\u2002', start - 2, start ); + } + // Replace an en space followed by a space with an em space. + else if ( lastTwoCharacters === '\u2002 ' ) { + return insert( value, '\u2003', start - 2, start ); + } + + return value; +} + export function useInputRules( props ) { const { __unstableMarkLastChangeAsPersistent, @@ -122,7 +150,7 @@ export function useInputRules( props ) { return accumlator; }, - preventEventDiscovery( value ) + preventEventDiscovery( replacePrecedingSpaces( value ) ) ); if ( transformed !== value ) { diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js index a7efd10bce7125..7ef5c17f82943c 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js +++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js @@ -139,7 +139,7 @@ export default function SpacingInputControl( { useMemo( () => parseQuantityAndUnitFromRawValue( currentValue ), [ currentValue ] - )[ 1 ] || units[ 0 ].value; + )[ 1 ] || units[ 0 ]?.value; const setInitialValue = () => { if ( value === undefined ) { diff --git a/packages/block-editor/src/components/url-input/README.md b/packages/block-editor/src/components/url-input/README.md index 46f673ecd35545..9d3e340371a608 100644 --- a/packages/block-editor/src/components/url-input/README.md +++ b/packages/block-editor/src/components/url-input/README.md @@ -36,41 +36,6 @@ This prop is passed directly to the `URLInput` component. ## Example -{% codetabs %} -{% ES5 %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - url: { - type: 'string' - }, - text: { - type: 'string' - } - }, - - edit: function( props ) { - return React.createElement( wp.blockEditor.URLInputButton, { - className: props.className, - url: props.attributes.url, - onChange: function( url, post ) { - props.setAttributes( { url: url, text: (post && post.title) || 'Click here' } ); - } - } ); - }, - - save: function( props ) { - return React.createElement( 'a', { - href: props.attributes.url, - }, props.attributes.text ); - } -} ); -``` - -{% ESNext %} ```js import { registerBlockType } from '@wordpress/blocks'; @@ -103,7 +68,6 @@ registerBlockType( /* ... */, { } ); ``` -{% end %} # `URLInput` @@ -139,7 +103,7 @@ _Required._ Called when the value changes. The second parameter is `null` unless } ``` -### `onKeydown`: `( event: KeyboardEvent ) => void` +### `onKeyDown`: `( event: KeyboardEvent ) => void` A callback invoked on the keydown event. @@ -172,41 +136,6 @@ Start opting into the new margin-free styles that will become the default in a f ## Example -{% codetabs %} -{% ES5 %} - -```js -wp.blocks.registerBlockType( /* ... */, { - // ... - - attributes: { - url: { - type: 'string' - }, - text: { - type: 'string' - } - }, - - edit: function( props ) { - return React.createElement( wp.blockEditor.URLInput, { - className: props.className, - value: props.attributes.url, - onChange: function( url, post ) { - props.setAttributes( { url: url, text: (post && post.title) || 'Click here' } ); - } - } ); - }, - - save: function( props ) { - return React.createElement( 'a', { - href: props.attributes.url, - }, props.attributes.text ); - } -} ); -``` - -{% ESNext %} ```js import { registerBlockType } from '@wordpress/blocks'; @@ -240,5 +169,3 @@ registerBlockType( /* ... */, { } } ); ``` - -{% end %} diff --git a/packages/block-editor/src/components/url-popover/image-url-input-ui.js b/packages/block-editor/src/components/url-popover/image-url-input-ui.js index 2f849eaad78847..7caa218658b24c 100644 --- a/packages/block-editor/src/components/url-popover/image-url-input-ui.js +++ b/packages/block-editor/src/components/url-popover/image-url-input-ui.js @@ -249,6 +249,7 @@ const ImageURLInputUI = ( { aria-expanded={ isOpen } onClick={ openLinkUI } ref={ setPopoverAnchor } + isActive={ !! url } /> { isOpen && ( { const result = runHook( () => useSetting( 'layout.contentSize' ) ); expect( result ).toBe( '840px' ); expect( console ).toHaveWarnedWith( - 'wp.blockEditor.useSetting is deprecated since version 6.4. Please use wp.blockEditor.useSettings instead.' + 'wp.blockEditor.useSetting is deprecated since version 6.5. Please use wp.blockEditor.useSettings instead.' ); } ); } ); diff --git a/packages/block-editor/src/hooks/block-rename-ui.js b/packages/block-editor/src/hooks/block-rename-ui.js deleted file mode 100644 index 836df953256c10..00000000000000 --- a/packages/block-editor/src/hooks/block-rename-ui.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * WordPress dependencies - */ -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; -import { addFilter } from '@wordpress/hooks'; -import { __, sprintf } from '@wordpress/i18n'; -import { hasBlockSupport } 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 = () => { - const message = - nameIsOriginal || nameIsEmpty - ? sprintf( - /* translators: %s: new name/label for the block */ - __( 'Block name reset to: "%s".' ), - editedBlockName - ) - : sprintf( - /* translators: %s: new name/label for the block */ - __( 'Block name changed to: "%s".' ), - editedBlockName - ); - - // Must be assertive to immediately announce change. - speak( message, 'assertive' ); - onSave( editedBlockName ); - - // Immediate close avoids ability to hit save multiple times. - onClose(); - }; - - return ( - -

- { __( 'Enter 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 withBlockRenameControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { clientId, name, attributes, setAttributes, isSelected } = props; - - const supportsBlockNaming = hasBlockSupport( name, 'renaming', true ); - - return ( - <> - { isSelected && supportsBlockNaming && ( - <> - { - setAttributes( { - metadata: { - ...attributes?.metadata, - name: newName, - }, - } ); - } } - /> - - ) } - - - - ); - }, - 'withBlockRenameControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/block-rename-ui/with-block-rename-controls', - withBlockRenameControls -); diff --git a/packages/block-editor/src/hooks/block-renaming.js b/packages/block-editor/src/hooks/block-renaming.js index 5db06d1a652d41..48e3b801d4eb91 100644 --- a/packages/block-editor/src/hooks/block-renaming.js +++ b/packages/block-editor/src/hooks/block-renaming.js @@ -3,6 +3,15 @@ */ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +import { TextControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { InspectorControls } from '../components'; +import { useBlockRename } from '../components/block-rename'; /** * Filters registered block settings, adding an `__experimentalLabel` callback if one does not already exist. @@ -38,6 +47,44 @@ export function addLabelCallback( settings ) { return settings; } +export const withBlockRenameControl = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { name, attributes, setAttributes, isSelected } = props; + + const { canRename } = useBlockRename( name ); + + return ( + <> + { isSelected && canRename && ( + + { + setAttributes( { + metadata: { + ...attributes?.metadata, + name: newName, + }, + } ); + } } + /> + + ) } + + + ); + }, + 'withToolbarControls' +); + +addFilter( + 'editor.BlockEdit', + 'core/block-rename-ui/with-block-rename-control', + withBlockRenameControl +); + addFilter( 'blocks.registerBlockType', 'core/metadata/addLabelCallback', diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 31721569781b6c..adb9df15824a77 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -34,6 +34,65 @@ function addAttribute( settings ) { return settings; } +function CustomFieldsControl( props ) { + const blockEditingMode = useBlockEditingMode(); + if ( blockEditingMode !== 'default' ) { + return null; + } + + // If the block is a paragraph or image block, we need to know which + // attribute to use for the connection. Only the `content` attribute + // of the paragraph block and the `url` attribute of the image block are supported. + let attributeName; + if ( props.name === 'core/paragraph' ) attributeName = 'content'; + if ( props.name === 'core/image' ) attributeName = 'url'; + + return ( + + + { + if ( nextValue === '' ) { + props.setAttributes( { + connections: undefined, + [ attributeName ]: undefined, + placeholder: undefined, + } ); + } else { + props.setAttributes( { + connections: { + attributes: { + // The attributeName will be either `content` or `url`. + [ attributeName ]: { + // Source will be variable, could be post_meta, user_meta, term_meta, etc. + // Could even be a custom source like a social media attribute. + source: 'meta_fields', + value: nextValue, + }, + }, + }, + [ attributeName ]: undefined, + placeholder: sprintf( + 'This content will be replaced on the frontend by the value of "%s" custom field.', + nextValue + ), + } ); + } + } } + /> + + + ); +} + /** * Override the default edit UI to include a new block inspector control for * assigning a connection to blocks that has support for connections. @@ -46,7 +105,6 @@ function addAttribute( settings ) { */ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { - const blockEditingMode = useBlockEditingMode(); const hasCustomFieldsSupport = hasBlockSupport( props.name, '__experimentalConnections', @@ -56,72 +114,17 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { // Check if the current block is a paragraph or image block. // Currently, only these two blocks are supported. if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) { - return ; + return ; } - // If the block is a paragraph or image block, we need to know which - // attribute to use for the connection. Only the `content` attribute - // of the paragraph block and the `url` attribute of the image block are supported. - let attributeName; - if ( props.name === 'core/paragraph' ) attributeName = 'content'; - if ( props.name === 'core/image' ) attributeName = 'url'; - - if ( hasCustomFieldsSupport && props.isSelected ) { - return ( - <> - - { blockEditingMode === 'default' && ( - - - { - if ( nextValue === '' ) { - props.setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - props.setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source: 'meta_fields', - value: nextValue, - }, - }, - }, - [ attributeName ]: undefined, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - nextValue - ), - } ); - } - } } - /> - - - ) } - - ); - } - - return ; + return ( + <> + + { hasCustomFieldsSupport && props.isSelected && ( + + ) } + + ); }; }, 'withCustomFieldsControls' ); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 730f0defe0a635..c088216c0645cb 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -22,7 +22,6 @@ import './metadata'; import './custom-fields'; import './block-hooks'; import './block-renaming'; -import './block-rename-ui'; export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index f4730702e9adb7..8120de137f9793 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -135,10 +135,9 @@ export function useLayoutStyles( blockAttributes = {}, blockName, selector ) { function LayoutPanel( { setAttributes, attributes, name: blockName } ) { const settings = useBlockSettings( blockName ); - const { - layout: { allowEditing: allowEditingSetting }, - } = settings; - + // Block settings come from theme.json under settings.[blockName]. + const { layout: layoutSettings } = settings; + // Layout comes from block attributes. const { layout } = attributes; const [ defaultThemeLayout ] = useSettings( 'layout' ); const { themeSupportsLayout } = useSelect( ( select ) => { @@ -153,17 +152,22 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { return null; } + // Layout block support comes from the block's block.json. const layoutBlockSupport = getBlockSupport( blockName, layoutBlockSupportKey, {} ); + const blockSupportAndThemeSettings = { + ...layoutSettings, + ...layoutBlockSupport, + }; const { allowSwitching, - allowEditing = allowEditingSetting ?? true, + allowEditing = true, allowInheriting = true, default: defaultBlockLayout, - } = layoutBlockSupport; + } = blockSupportAndThemeSettings; if ( ! allowEditing ) { return null; @@ -260,14 +264,14 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { ) } { constrainedType && displayControlsForLegacyLayouts && ( ) } diff --git a/packages/block-editor/src/hooks/test/__snapshots__/align.native.js.snap b/packages/block-editor/src/hooks/test/__snapshots__/align.native.js.snap index 2782d6ae0b984b..045fb0e3bd944a 100644 --- a/packages/block-editor/src/hooks/test/__snapshots__/align.native.js.snap +++ b/packages/block-editor/src/hooks/test/__snapshots__/align.native.js.snap @@ -19,25 +19,25 @@ exports[`Align options for group block sets Wide width option 1`] = ` `; exports[`Align options for media block sets Align center option 1`] = ` -" +"
" `; exports[`Align options for media block sets Align left option 1`] = ` -" +"
" `; exports[`Align options for media block sets Align right option 1`] = ` -" +"
" `; exports[`Align options for media block sets Full width option 1`] = ` -" +"
" `; @@ -49,7 +49,7 @@ exports[`Align options for media block sets None option 1`] = ` `; exports[`Align options for media block sets Wide width option 1`] = ` -" +"
" `; diff --git a/packages/block-editor/src/layouts/constrained.js b/packages/block-editor/src/layouts/constrained.js index 7fe0d5ff0b5268..f86791391176f9 100644 --- a/packages/block-editor/src/layouts/constrained.js +++ b/packages/block-editor/src/layouts/constrained.js @@ -36,7 +36,10 @@ export default { layoutBlockSupport = {}, } ) { const { wideSize, contentSize, justifyContent = 'center' } = layout; - const { allowJustification = true } = layoutBlockSupport; + const { + allowJustification = true, + allowCustomContentAndWideSize = true, + } = layoutBlockSupport; const onJustificationChange = ( value ) => { onChange( { ...layout, @@ -66,55 +69,59 @@ export default { } ); return ( <> -
-
- { - nextWidth = - 0 > parseFloat( nextWidth ) - ? '0' - : nextWidth; - onChange( { - ...layout, - contentSize: nextWidth, - } ); - } } - units={ units } - /> - -
-
- { - nextWidth = - 0 > parseFloat( nextWidth ) - ? '0' - : nextWidth; - onChange( { - ...layout, - wideSize: nextWidth, - } ); - } } - units={ units } - /> - -
-
-

- { __( - 'Customize the width for all elements that are assigned to the center or wide columns.' - ) } -

+ { allowCustomContentAndWideSize && ( + <> +
+
+ { + nextWidth = + 0 > parseFloat( nextWidth ) + ? '0' + : nextWidth; + onChange( { + ...layout, + contentSize: nextWidth, + } ); + } } + units={ units } + /> + +
+
+ { + nextWidth = + 0 > parseFloat( nextWidth ) + ? '0' + : nextWidth; + onChange( { + ...layout, + wideSize: nextWidth, + } ); + } } + units={ units } + /> + +
+
+

+ { __( + 'Customize the width for all elements that are assigned to the center or wide columns.' + ) } +

+ + ) } { allowJustification && ( categories && categories.get( catId ) diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 93ab3b69a7aad3..a55756ae6f53d7 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -15,6 +15,7 @@ @import "./components/block-patterns-paging/style.scss"; @import "./components/block-popover/style.scss"; @import "./components/block-preview/style.scss"; +@import "./components/block-rename/style.scss"; @import "./components/block-settings-menu/style.scss"; @import "./components/block-styles/style.scss"; @import "./components/block-switcher/style.scss"; @@ -56,7 +57,6 @@ @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/test/transform-styles.js b/packages/block-editor/src/utils/test/transform-styles.js index f162a0b2f6048c..38eaa1947d2a8c 100644 --- a/packages/block-editor/src/utils/test/transform-styles.js +++ b/packages/block-editor/src/utils/test/transform-styles.js @@ -4,6 +4,55 @@ import transformStyles from '../transform-styles'; describe( 'transformStyles', () => { + describe( 'error handling', () => { + beforeEach( () => { + // Intentionally suppress the expected console errors and warnings to reduce + // noise in the test output. + jest.spyOn( console, 'warn' ).mockImplementation( jest.fn() ); + } ); + + it( 'should not throw error in case of invalid css', () => { + const run = () => + transformStyles( + [ + { + css: 'h1 { color: red;', // invalid CSS + }, + ], + '.my-namespace' + ); + + expect( run ).not.toThrow(); + expect( console ).toHaveWarned(); + } ); + + it( 'should warn invalid css in the console', () => { + const run = () => + transformStyles( + [ + { + css: 'h1 { color: red; }', // valid CSS + }, + { + css: 'h1 { color: red;', // invalid CSS + }, + ], + '.my-namespace' + ); + + const [ validCSS, invalidCSS ] = run(); + + expect( validCSS ).toBe( '.my-namespace h1 { color: red; }' ); + expect( invalidCSS ).toBe( null ); + + expect( console ).toHaveWarnedWith( + 'wp.blockEditor.transformStyles Failed to transform CSS.', + ':1:1: Unclosed block\n> 1 | h1 { color: red;\n | ^' + // ^^^^ In PostCSS, a tab is equal four spaces + ); + } ); + } ); + describe( 'selector wrap', () => { it( 'should wrap regular selectors', () => { const input = `h1 { color: red; }`; diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index 8f5e1702307a45..742e3a9becaef3 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import postcss from 'postcss'; +import postcss, { CssSyntaxError } from 'postcss'; import wrap from 'postcss-prefixwrap'; import rebaseUrl from 'postcss-urlrebase'; @@ -19,18 +19,44 @@ import rebaseUrl from 'postcss-urlrebase'; */ const transformStyles = ( styles, wrapperSelector = '' ) => { return styles.map( ( { css, ignoredSelectors = [], baseURL } ) => { - return postcss( - [ - wrapperSelector && - wrap( wrapperSelector, { - ignoredSelectors: [ - ...ignoredSelectors, - wrapperSelector, - ], - } ), - baseURL && rebaseUrl( { rootUrl: baseURL } ), - ].filter( Boolean ) - ).process( css, {} ).css; // use sync PostCSS API + // When there is no wrapper selector or base URL, there is no need + // to transform the CSS. This is most cases because in the default + // iframed editor, no wrapping is needed, and not many styles + // provide a base URL. + if ( ! wrapperSelector && ! baseURL ) { + return css; + } + + try { + return postcss( + [ + wrapperSelector && + wrap( wrapperSelector, { + ignoredSelectors: [ + ...ignoredSelectors, + wrapperSelector, + ], + } ), + baseURL && rebaseUrl( { rootUrl: baseURL } ), + ].filter( Boolean ) + ).process( css, {} ).css; // use sync PostCSS API + } catch ( error ) { + if ( error instanceof CssSyntaxError ) { + // eslint-disable-next-line no-console + console.warn( + 'wp.blockEditor.transformStyles Failed to transform CSS.', + error.message + '\n' + error.showSourceCode( false ) + ); + } else { + // eslint-disable-next-line no-console + console.warn( + 'wp.blockEditor.transformStyles Failed to transform CSS.', + error + ); + } + + return null; + } } ); }; diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 22f02cfcd62885..da6f0fa75152cf 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.23.0 (2023-11-16) + ## 8.22.0 (2023-11-02) ## 8.21.0 (2023-10-18) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 5fdec495443efa..558566bccaae0a 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.22.0", + "version": "8.23.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/block/edit.js b/packages/block-library/src/block/edit.js index 395e4bff4680bd..979ae04c62282c 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -114,33 +114,28 @@ export default function ReusableBlockEdit( { : InnerBlocks.ButtonBlockAppender, } ); + let children = null; if ( hasAlreadyRendered ) { - return ( -
- - { __( 'Block cannot be rendered inside itself.' ) } - -
+ children = ( + + { __( 'Block cannot be rendered inside itself.' ) } + ); } if ( isMissing ) { - return ( -
- - { __( 'Block has been deleted or is unavailable.' ) } - -
+ children = ( + + { __( 'Block has been deleted or is unavailable.' ) } + ); } if ( ! hasResolved ) { - return ( -
- - - -
+ children = ( + + + ); } @@ -157,7 +152,11 @@ export default function ReusableBlockEdit( { /> -
+ { children === null ? ( +
+ ) : ( +
{ children }
+ ) } ); } diff --git a/packages/block-library/src/block/edit.native.js b/packages/block-library/src/block/edit.native.js index ddc22c01e40def..9ab6ccf86a1e19 100644 --- a/packages/block-library/src/block/edit.native.js +++ b/packages/block-library/src/block/edit.native.js @@ -78,7 +78,7 @@ export default function ReusableBlockEdit( { styles.spinnerDark ); - const { hasResolved, isEditing, isMissing, innerBlockCount } = useSelect( + const { hasResolved, isEditing, isMissing } = useSelect( ( select ) => { const persistedBlock = select( coreStore ).getEntityRecord( 'postType', @@ -176,20 +176,12 @@ export default function ReusableBlockEdit( { { infoTitle } - { innerBlockCount > 1 - ? __( - 'Alternatively, you can detach and edit these blocks separately by tapping “Detach patterns”.' - ) - : __( - 'Alternatively, you can detach and edit this block separately by tapping “Detach pattern”.' - ) } + { __( + 'Alternatively, you can detach and edit this block separately by tapping “Detach”.' + ) } 1 - ? __( 'Detach patterns' ) - : __( 'Detach pattern' ) - } + label={ __( 'Detach' ) } separatorType="topFullWidth" onPress={ onConvertToRegularBlocks } labelStyle={ actionButtonStyle } diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 81861e44997a4a..d41d917d5b8fe1 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; +import removeAnchorTag from '../utils/remove-anchor-tag'; /** * WordPress dependencies @@ -33,11 +34,17 @@ import { __experimentalGetSpacingClassesAndStyles as useSpacingProps, __experimentalLinkControl as LinkControl, __experimentalGetElementClassName, + store as blockEditorStore, } from '@wordpress/block-editor'; -import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes'; +import { displayShortcut, isKeyboardEvent, ENTER } from '@wordpress/keycodes'; import { link, linkOff } from '@wordpress/icons'; -import { createBlock } from '@wordpress/blocks'; -import { useMergeRefs } from '@wordpress/compose'; +import { + createBlock, + cloneBlock, + getDefaultBlockName, +} from '@wordpress/blocks'; +import { useMergeRefs, useRefEffect } from '@wordpress/compose'; +import { useSelect, useDispatch } from '@wordpress/data'; const LINK_SETTINGS = [ ...LinkControl.DEFAULT_LINK_SETTINGS, @@ -47,6 +54,62 @@ const LINK_SETTINGS = [ }, ]; +function useEnter( props ) { + const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore ); + const { getBlock, getBlockRootClientId, getBlockIndex } = + useSelect( blockEditorStore ); + const propsRef = useRef( props ); + propsRef.current = props; + return useRefEffect( ( element ) => { + function onKeyDown( event ) { + if ( event.defaultPrevented || event.keyCode !== ENTER ) { + return; + } + const { content, clientId } = propsRef.current; + if ( content.length ) { + return; + } + event.preventDefault(); + const topParentListBlock = getBlock( + getBlockRootClientId( clientId ) + ); + const blockIndex = getBlockIndex( clientId ); + const head = cloneBlock( { + ...topParentListBlock, + innerBlocks: topParentListBlock.innerBlocks.slice( + 0, + blockIndex + ), + } ); + const middle = createBlock( getDefaultBlockName() ); + const after = topParentListBlock.innerBlocks.slice( + blockIndex + 1 + ); + const tail = after.length + ? [ + cloneBlock( { + ...topParentListBlock, + innerBlocks: after, + } ), + ] + : []; + replaceBlocks( + topParentListBlock.clientId, + [ head, middle, ...tail ], + 1 + ); + // We manually change the selection here because we are replacing + // a different block than the selected one. + selectionChange( middle.clientId ); + } + + element.addEventListener( 'keydown', onKeyDown ); + return () => { + element.removeEventListener( 'keydown', onKeyDown ); + }; + }, [] ); +} + function WidthPanel( { selectedWidth, setAttributes } ) { function handleChange( newWidth ) { // Check if we are toggling the width off @@ -88,6 +151,7 @@ function ButtonEdit( props ) { isSelected, onReplace, mergeBlocks, + clientId, } = props; const { tagName, @@ -103,11 +167,6 @@ function ButtonEdit( props ) { const TagName = tagName || 'a'; - function setButtonText( newText ) { - // Remove anchor tags from button text content. - setAttributes( { text: newText.replace( /<\/?a[^>]*>/g, '' ) } ); - } - function onKeyDown( event ) { if ( isKeyboardEvent.primary( event, 'k' ) ) { startEditing( event ); @@ -164,6 +223,9 @@ function ButtonEdit( props ) { [ url, opensInNewTab, nofollow ] ); + const useEnterRef = useEnter( { content: text, clientId } ); + const mergedRef = useMergeRefs( [ useEnterRef, richTextRef ] ); + return ( <>
setButtonText( value ) } + onChange={ ( value ) => + setAttributes( { + text: removeAnchorTag( value ), + } ) + } withoutInteractiveFormatting className={ classnames( className, diff --git a/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap index 25867634d12d8e..1a55c807225d9d 100644 --- a/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/buttons/test/__snapshots__/edit.native.js.snap @@ -71,9 +71,3 @@ exports[`Buttons block when a button is shown removing button along with buttons

" `; - -exports[`Buttons block when a button is shown removing button along with buttons block removes the button and buttons block when deleting the block using the delete (backspace) key 1`] = ` -" -

-" -`; diff --git a/packages/block-library/src/buttons/test/edit.native.js b/packages/block-library/src/buttons/test/edit.native.js index 2fe70d034aa747..f393a31c7330ad 100644 --- a/packages/block-library/src/buttons/test/edit.native.js +++ b/packages/block-library/src/buttons/test/edit.native.js @@ -18,7 +18,6 @@ import { */ import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; import { registerCoreBlocks } from '@wordpress/block-library'; -import { BACKSPACE } from '@wordpress/keycodes'; const BUTTONS_HTML = `
@@ -238,32 +237,6 @@ describe( 'Buttons block', () => { expect( getEditorHtml() ).toMatchSnapshot(); } ); - - it( 'removes the button and buttons block when deleting the block using the delete (backspace) key', async () => { - const screen = await initializeEditor( { - initialHtml: BUTTONS_HTML, - } ); - - // Get block - const buttonsBlock = await getBlock( screen, 'Buttons' ); - triggerBlockListLayout( buttonsBlock ); - - // Get inner button block - const buttonBlock = await getBlock( screen, 'Button' ); - fireEvent.press( buttonBlock ); - - const buttonInput = - within( buttonBlock ).getByLabelText( 'Text input. Empty' ); - - // Delete block - fireEvent( buttonInput, 'onKeyDown', { - nativeEvent: {}, - preventDefault() {}, - keyCode: BACKSPACE, - } ); - - expect( getEditorHtml() ).toMatchSnapshot(); - } ); } ); } ); diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index ab49d58b766b09..8c5488584094c6 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -335,6 +335,7 @@ function CoverEdit( { templateInsertUpdatesSelection: true, allowedBlocks, templateLock, + dropZoneElement: ref.current, } ); diff --git a/packages/block-library/src/cover/style.scss b/packages/block-library/src/cover/style.scss index d9312a71f0faad..837e3834e2e1ba 100644 --- a/packages/block-library/src/cover/style.scss +++ b/packages/block-library/src/cover/style.scss @@ -123,7 +123,7 @@ h4, h5, h6 { - &:not(.has-text-color) { + &:where(:not(.has-text-color)) { color: inherit; } } diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index d1febe3c6fa115..879b77636255a3 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -53,7 +53,7 @@ async function createAndSelectBlock() { ); await userEvent.click( screen.getByRole( 'button', { - name: 'Select Cover', + name: 'Select parent block: Cover', } ) ); } diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 5f3d962ae7afae..e3642868034a5a 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -65,7 +65,7 @@ // This CSS Custom Properties aren't used anymore as defaults, // but we still need to keep them for backward compatibility. -.editor-styles-wrapper { +:where(.editor-styles-wrapper) { --wp--preset--font-size--normal: 16px; --wp--preset--font-size--huge: 42px; } @@ -74,19 +74,19 @@ // // The reason we add the editor class wrapper here is // to avoid enqueing the classes twice: here and in ./editor.scss -.editor-styles-wrapper .has-regular-font-size { +:where(.editor-styles-wrapper) .has-regular-font-size { font-size: 16px; } -.editor-styles-wrapper .has-larger-font-size { +:where(.editor-styles-wrapper) .has-larger-font-size { font-size: 42px; } -.editor-styles-wrapper .has-normal-font-size { +:where(.editor-styles-wrapper) .has-normal-font-size { font-size: var(--wp--preset--font-size--normal); } -.editor-styles-wrapper .has-huge-font-size { +:where(.editor-styles-wrapper) .has-huge-font-size { font-size: var(--wp--preset--font-size--huge); } @@ -98,6 +98,6 @@ */ // Remove the browser default border for iframe in Custom HTML block, Embed block, etc. -.editor-styles-wrapper iframe:not([frameborder]) { +:where(.editor-styles-wrapper) iframe:not([frameborder]) { border: 0; } diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index 733cdf9a9351fc..e3328fd9851c38 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -35,6 +35,7 @@ import { store as noticesStore } from '@wordpress/notices'; */ import FileBlockInspector from './inspector'; import { browserSupportsPdfs } from './utils'; +import removeAnchorTag from '../utils/remove-anchor-tag'; export const MIN_PREVIEW_HEIGHT = 200; export const MAX_PREVIEW_HEIGHT = 2000; @@ -102,7 +103,9 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { } if ( downloadButtonText === undefined ) { - changeDownloadButtonText( _x( 'Download', 'button label' ) ); + setAttributes( { + downloadButtonText: _x( 'Download', 'button label' ), + } ); } }, [] ); @@ -148,13 +151,6 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { setAttributes( { showDownloadButton: newValue } ); } - function changeDownloadButtonText( newValue ) { - // Remove anchor tags from button text content. - setAttributes( { - downloadButtonText: newValue.replace( /<\/?a[^>]*>/g, '' ), - } ); - } - function changeDisplayPreview( newValue ) { setAttributes( { displayPreview: newValue } ); } @@ -277,7 +273,9 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { placeholder={ __( 'Write file name…' ) } withoutInteractiveFormatting onChange={ ( text ) => - setAttributes( { fileName: text } ) + setAttributes( { + fileName: removeAnchorTag( text ), + } ) } href={ textLinkHref } /> @@ -301,7 +299,10 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { withoutInteractiveFormatting placeholder={ __( 'Add text…' ) } onChange={ ( text ) => - changeDownloadButtonText( text ) + setAttributes( { + downloadButtonText: + removeAnchorTag( text ), + } ) } />
diff --git a/packages/block-library/src/file/index.php b/packages/block-library/src/file/index.php index 042ea899707360..8bcce69ef8968d 100644 --- a/packages/block-library/src/file/index.php +++ b/packages/block-library/src/file/index.php @@ -57,9 +57,9 @@ static function ( $matches ) { if ( $should_load_view_script ) { $processor = new WP_HTML_Tag_Processor( $content ); $processor->next_tag(); - $processor->set_attribute( 'data-wp-interactive', '' ); + $processor->set_attribute( 'data-wp-interactive', '{"namespace":"core/file"}' ); $processor->next_tag( 'object' ); - $processor->set_attribute( 'data-wp-bind--hidden', '!selectors.core.file.hasPdfPreview' ); + $processor->set_attribute( 'data-wp-bind--hidden', '!state.hasPdfPreview' ); $processor->set_attribute( 'hidden', true ); return $processor->get_updated_html(); } diff --git a/packages/block-library/src/file/view.js b/packages/block-library/src/file/view.js index 9d09ca2b7f4340..79340223f007cb 100644 --- a/packages/block-library/src/file/view.js +++ b/packages/block-library/src/file/view.js @@ -5,14 +5,12 @@ import { store } from '@wordpress/interactivity'; /** * Internal dependencies */ -import { browserSupportsPdfs as hasPdfPreview } from './utils'; +import { browserSupportsPdfs } from './utils'; -store( { - selectors: { - core: { - file: { - hasPdfPreview, - }, +store( 'core/file', { + state: { + get hasPdfPreview() { + return browserSupportsPdfs(); }, }, } ); diff --git a/packages/block-library/src/gallery/style.scss b/packages/block-library/src/gallery/style.scss index 3f5a462e3206c0..afbc3088049858 100644 --- a/packages/block-library/src/gallery/style.scss +++ b/packages/block-library/src/gallery/style.scss @@ -57,6 +57,7 @@ figure.wp-block-gallery.has-nested-images { text-align: center; width: 100%; box-sizing: border-box; + @include custom-scrollbars-on-hover(transparent, rgba($white, 0.8)); img { display: inline; diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 277fa6872fa82e..9c8690c4e0e8e2 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -71,13 +71,7 @@ function GroupEditControls( { tagName, onSelectTagName } ) { ); } -function GroupEdit( { - attributes, - name, - setAttributes, - clientId, - __unstableLayoutClassNames: layoutClassNames, -} ) { +function GroupEdit( { attributes, name, setAttributes, clientId } ) { const { hasInnerBlocks, themeSupportsLayout } = useSelect( ( select ) => { const { getBlock, getSettings } = select( blockEditorStore ); @@ -103,9 +97,8 @@ function GroupEdit( { themeSupportsLayout || type === 'flex' || type === 'grid'; // Hooks. - const blockProps = useBlockProps( { - className: ! layoutSupportEnabled ? layoutClassNames : null, - } ); + const blockProps = useBlockProps(); + const [ showPlaceholder, setShowPlaceholder ] = useShouldShowPlaceHolder( { attributes, usedLayoutType: type, @@ -134,7 +127,6 @@ function GroupEdit( { templateLock, allowedBlocks, renderAppender, - __unstableDisableLayoutClassNames: ! layoutSupportEnabled, } ); diff --git a/packages/block-library/src/heading/test/__snapshots__/index.native.js.snap b/packages/block-library/src/heading/test/__snapshots__/index.native.js.snap index 308aa8ac729bff..c0397e823d4511 100644 --- a/packages/block-library/src/heading/test/__snapshots__/index.native.js.snap +++ b/packages/block-library/src/heading/test/__snapshots__/index.native.js.snap @@ -6,6 +6,12 @@ exports[`Heading block inserts block 1`] = ` " `; +exports[`Heading block should merge with an empty Paragraph block and keep being the Heading block 1`] = ` +" +

A quick brown fox jumps over the lazy dog.

+" +`; + exports[`Heading block should set a background color 1`] = ` "

A quick brown fox jumps over the lazy dog.

diff --git a/packages/block-library/src/heading/test/index.native.js b/packages/block-library/src/heading/test/index.native.js index 5b7abbc91ad94a..1582e96aae0f4d 100644 --- a/packages/block-library/src/heading/test/index.native.js +++ b/packages/block-library/src/heading/test/index.native.js @@ -17,6 +17,7 @@ import { */ import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks'; import { registerCoreBlocks } from '@wordpress/block-library'; +import { BACKSPACE, ENTER } from '@wordpress/keycodes'; beforeAll( () => { // Register all core blocks @@ -134,4 +135,43 @@ describe( 'Heading block', () => { ) ).toBeVisible(); } ); + + it( 'should merge with an empty Paragraph block and keep being the Heading block', async () => { + // Arrange + const screen = await initializeEditor(); + await addBlock( screen, 'Paragraph' ); + + // Act + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + fireEvent( paragraphTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: ENTER, + } ); + + await addBlock( screen, 'Heading' ); + const headingBlock = getBlock( screen, 'Heading', { rowIndex: 2 } ); + fireEvent.press( headingBlock ); + + const headingTextInput = + within( headingBlock ).getByPlaceholderText( 'Heading' ); + typeInRichText( + headingTextInput, + 'A quick brown fox jumps over the lazy dog.', + { finalSelectionStart: 0, finalSelectionEnd: 0 } + ); + + fireEvent( headingTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: BACKSPACE, + } ); + + // Assert + expect( getEditorHtml() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index d665a8a8f77085..b46829e5059a25 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -9,9 +9,6 @@ "keywords": [ "img", "photo", "picture" ], "textdomain": "default", "attributes": { - "align": { - "type": "string" - }, "url": { "type": "string", "source": "attribute", @@ -95,6 +92,7 @@ } }, "supports": { + "align": [ "left", "center", "right", "wide", "full" ], "anchor": true, "color": { "text": false, diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index c58a96e5949112..d189af32efcbec 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -10,8 +10,6 @@ import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; import { Placeholder } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { - BlockAlignmentControl, - BlockControls, BlockIcon, MediaPlaceholder, useBlockProps, @@ -106,13 +104,13 @@ export function ImageEdit( { url = '', alt, caption, - align, id, width, height, sizeSlug, aspectRatio, scale, + align, } = attributes; const [ temporaryURL, setTemporaryURL ] = useState(); @@ -126,6 +124,21 @@ export function ImageEdit( { captionRef.current = caption; }, [ caption ] ); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + + useEffect( () => { + if ( [ 'wide', 'full' ].includes( align ) ) { + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { + width: undefined, + height: undefined, + aspectRatio: undefined, + scale: undefined, + } ); + } + }, [ align ] ); + const ref = useRef(); const { imageDefaultSize, mediaUpload } = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); @@ -255,16 +268,6 @@ export function ImageEdit( { } } - function updateAlignment( nextAlign ) { - const extraUpdatedAttributes = [ 'wide', 'full' ].includes( nextAlign ) - ? { width: undefined, height: undefined } - : {}; - setAttributes( { - ...extraUpdatedAttributes, - align: nextAlign, - } ); - } - let isTemp = isTemporaryImage( id, url ); // Upload a temporary image on mount. @@ -375,14 +378,6 @@ export function ImageEdit( { clientId={ clientId } blockEditingMode={ blockEditingMode } /> - { ! url && blockEditingMode === 'default' && ( - - - - ) } } onSelect={ onSelectImage } diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 804ae9e1671f6e..44ebfda67d8750 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -44,7 +44,6 @@ import { MEDIA_TYPE_IMAGE, BlockControls, InspectorControls, - BlockAlignmentToolbar, BlockStyles, store as blockEditorStore, blockSettingsScreens, @@ -212,7 +211,6 @@ export class ImageEdit extends Component { this.onSetFeatured = this.onSetFeatured.bind( this ); this.onFocusCaption = this.onFocusCaption.bind( this ); this.onSelectURL = this.onSelectURL.bind( this ); - this.updateAlignment = this.updateAlignment.bind( this ); this.accessibilityLabelCreator = this.accessibilityLabelCreator.bind( this ); this.setMappedAttributes = this.setMappedAttributes.bind( this ); @@ -305,6 +303,20 @@ export class ImageEdit extends Component { this.replacedFeaturedImage = false; setFeaturedImage( id ); } + + const { align } = attributes; + const { __unstableMarkNextChangeAsNotPersistent } = this.props; + + // Update the attributes if the align is wide or full + if ( [ 'wide', 'full' ].includes( align ) ) { + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { + width: undefined, + height: undefined, + aspectRatio: undefined, + scale: undefined, + } ); + } } static getDerivedStateFromProps( props, state ) { @@ -391,18 +403,6 @@ export class ImageEdit extends Component { } ); } - updateAlignment( nextAlign ) { - const extraUpdatedAttributes = Object.values( - WIDE_ALIGNMENTS.alignments - ).includes( nextAlign ) - ? { width: undefined, height: undefined } - : {}; - this.props.setAttributes( { - ...extraUpdatedAttributes, - align: nextAlign, - } ); - } - onSetNewTab( value ) { const updatedLinkTarget = getUpdatedLinkTargetSettings( value, @@ -711,10 +711,6 @@ export class ImageEdit extends Component { onClick={ open } /> - ); @@ -941,8 +937,11 @@ export default compose( [ } ), withDispatch( ( dispatch ) => { const { createErrorNotice } = dispatch( noticesStore ); + const { __unstableMarkNextChangeAsNotPersistent } = + dispatch( blockEditorStore ); return { + __unstableMarkNextChangeAsNotPersistent, createErrorNotice, closeSettingsBottomSheet() { dispatch( editPostStore ).closeGeneralSidebar(); diff --git a/packages/block-library/src/image/editor.scss b/packages/block-library/src/image/editor.scss index e1721928362149..934682ed91b7de 100644 --- a/packages/block-library/src/image/editor.scss +++ b/packages/block-library/src/image/editor.scss @@ -62,13 +62,6 @@ figure.wp-block-image:not(.wp-block) { left: 50%; transform: translate(-50%, -50%); } - - // When the Image block is linked, - // it's wrapped with a disabled tag. - // Restore cursor style so it doesn't appear 'clickable'. - > a { - cursor: default; - } } // This is necessary for the editor resize handles to accurately work on a non-floated, non-resized, small image. diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index ae5f749fff3b5f..11d460efd472cb 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -25,7 +25,6 @@ import { MediaReplaceFlow, store as blockEditorStore, useSettings, - BlockAlignmentControl, __experimentalImageEditor as ImageEditor, __experimentalGetElementClassName, __experimentalUseBorderProps as useBorderProps, @@ -83,9 +82,29 @@ const scaleOptions = [ }, ]; -const disabledClickProps = { - onClick: ( event ) => event.preventDefault(), - 'aria-disabled': true, +// If the image has a href, wrap in an tag to trigger any inherited link element styles. +const ImageWrapper = ( { href, children } ) => { + if ( ! href ) { + return children; + } + return ( + event.preventDefault() } + aria-disabled={ true } + style={ { + // When the Image block is linked, + // it's wrapped with a disabled tag. + // Restore cursor style so it doesn't appear 'clickable' + // and remove pointer events. Safari needs the display property. + pointerEvents: 'none', + cursor: 'default', + display: 'inline', + } } + > + { children } + + ); }; export default function Image( { @@ -333,21 +352,6 @@ export default function Image( { } ); } - function updateAlignment( nextAlign ) { - const extraUpdatedAttributes = [ 'wide', 'full' ].includes( nextAlign ) - ? { - width: undefined, - height: undefined, - aspectRatio: undefined, - scale: undefined, - } - : {}; - setAttributes( { - ...extraUpdatedAttributes, - align: nextAlign, - } ); - } - useEffect( () => { if ( ! isSelected ) { setIsEditingImage( false ); @@ -435,12 +439,6 @@ export default function Image( { const controls = ( <> - { hasNonContentControls && ( - - ) } { hasNonContentControls && ( { @@ -653,25 +651,31 @@ export default function Image( { if ( canEditImage && isEditingImage ) { img = ( - - setAttributes( imageAttributes ) - } - onFinishEditing={ () => { - setIsEditingImage( false ); - } } - borderProps={ isRounded ? undefined : borderProps } - /> + + + setAttributes( imageAttributes ) + } + onFinishEditing={ () => { + setIsEditingImage( false ); + } } + borderProps={ isRounded ? undefined : borderProps } + /> + ); } else if ( ! isResizable ) { - img =
{ img }
; + img = ( +
+ { img } +
+ ); } else { const numericRatio = aspectRatio && evalAspectRatio( aspectRatio ); const customRatio = numericWidth / numericHeight; @@ -774,7 +778,7 @@ export default function Image( { } } resizeRatio={ align === 'center' ? 2 : 1 } > - { img } + { img } ); } @@ -788,14 +792,7 @@ export default function Image( { { /* Hide controls during upload to avoid component remount, which causes duplicated image upload. */ } { ! temporaryURL && controls } - { /* If the image has a href, wrap in an tag to trigger any inherited link element styles */ } - { !! href ? ( - - { img } - - ) : ( - img - ) } + { img } { showCaption && ( ! RichText.isEmpty( caption ) || isSelected ) && ( next_tag( 'figure' ); $w->add_class( 'wp-lightbox-container' ); - $w->set_attribute( 'data-wp-interactive', true ); + $w->set_attribute( 'data-wp-interactive', '{"namespace":"core/image"}' ); $w->set_attribute( 'data-wp-context', sprintf( - '{ "core": - { "image": - { "imageLoaded": false, - "initialized": false, - "lightboxEnabled": false, - "hideAnimationEnabled": false, - "preloadInitialized": false, - "lightboxAnimation": "%s", - "imageUploadedSrc": "%s", - "imageCurrentSrc": "", - "targetWidth": "%s", - "targetHeight": "%s", - "scaleAttr": "%s", - "dialogLabel": "%s" - } - } + '{ "imageLoaded": false, + "initialized": false, + "lightboxEnabled": false, + "hideAnimationEnabled": false, + "preloadInitialized": false, + "lightboxAnimation": "%s", + "imageUploadedSrc": "%s", + "imageCurrentSrc": "", + "targetWidth": "%s", + "targetHeight": "%s", + "scaleAttr": "%s", + "dialogLabel": "%s" }', $lightbox_animation, $img_uploaded_src, @@ -218,14 +214,14 @@ function block_core_image_render_lightbox( $block_content, $block ) { ) ); $w->next_tag( 'img' ); - $w->set_attribute( 'data-wp-init', 'effects.core.image.initOriginImage' ); - $w->set_attribute( 'data-wp-on--load', 'actions.core.image.handleLoad' ); - $w->set_attribute( 'data-wp-effect', 'effects.core.image.setButtonStyles' ); + $w->set_attribute( 'data-wp-init', 'callbacks.initOriginImage' ); + $w->set_attribute( 'data-wp-on--load', 'actions.handleLoad' ); + $w->set_attribute( 'data-wp-watch', 'callbacks.setButtonStyles' ); // We need to set an event callback on the `img` specifically // because the `figure` element can also contain a caption, and // we don't want to trigger the lightbox when the caption is clicked. - $w->set_attribute( 'data-wp-on--click', 'actions.core.image.showLightbox' ); - $w->set_attribute( 'data-wp-effect--setStylesOnResize', 'effects.core.image.setStylesOnResize' ); + $w->set_attribute( 'data-wp-on--click', 'actions.showLightbox' ); + $w->set_attribute( 'data-wp-watch--setStylesOnResize', 'callbacks.setStylesOnResize' ); $body_content = $w->get_updated_html(); // Add a button alongside image in the body content. @@ -239,9 +235,9 @@ class="lightbox-trigger" type="button" aria-haspopup="dialog" aria-label="' . esc_attr( $aria_label ) . '" - data-wp-on--click="actions.core.image.showLightbox" - data-wp-style--right="context.core.image.imageButtonRight" - data-wp-style--top="context.core.image.imageButtonTop" + data-wp-on--click="actions.showLightbox" + data-wp-style--right="context.imageButtonRight" + data-wp-style--top="context.imageButtonTop" > @@ -267,8 +263,8 @@ class="lightbox-trigger" // use the exact same image as in the content when the lightbox is first opened while // we wait for the larger image to load. $m->set_attribute( 'src', '' ); - $m->set_attribute( 'data-wp-bind--src', 'context.core.image.imageCurrentSrc' ); - $m->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); + $m->set_attribute( 'data-wp-bind--src', 'context.imageCurrentSrc' ); + $m->set_attribute( 'data-wp-style--object-fit', 'state.lightboxObjectFit' ); $initial_image_content = $m->get_updated_html(); $q = new WP_HTML_Tag_Processor( $block_content ); @@ -283,8 +279,8 @@ class="lightbox-trigger" // and Chrome (see https://github.com/WordPress/gutenberg/pull/52765#issuecomment-1674008151). Until that // is resolved, manually setting the 'src' seems to be the best solution to load the large image on demand. $q->set_attribute( 'src', '' ); - $q->set_attribute( 'data-wp-bind--src', 'selectors.core.image.enlargedImgSrc' ); - $q->set_attribute( 'data-wp-style--object-fit', 'selectors.core.image.lightboxObjectFit' ); + $q->set_attribute( 'data-wp-bind--src', 'state.enlargedImgSrc' ); + $q->set_attribute( 'data-wp-style--object-fit', 'state.lightboxObjectFit' ); $enlarged_image_content = $q->get_updated_html(); // If the current theme does NOT have a `theme.json`, or the colors are not defined, @@ -307,21 +303,21 @@ class="lightbox-trigger" $lightbox_html = << - diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss index 2b8631fffe3c93..303f43ce4ed5f1 100644 --- a/packages/block-library/src/image/style.scss +++ b/packages/block-library/src/image/style.scss @@ -215,7 +215,7 @@ left: 0; z-index: 100000; overflow: hidden; - width: 100vw; + width: 100%; height: 100vh; box-sizing: border-box; visibility: hidden; @@ -372,7 +372,7 @@ @keyframes lightbox-zoom-in { 0% { - transform: translate(calc(-50vw + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale)); + transform: translate(calc((-100vw + var(--wp--lightbox-scrollbar-width)) / 2 + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale)); } 100% { transform: translate(-50%, -50%) scale(1, 1); @@ -389,6 +389,6 @@ } 100% { visibility: hidden; - transform: translate(calc(-50vw + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale)); + transform: translate(calc((-100vw + var(--wp--lightbox-scrollbar-width)) / 2 + var(--wp--lightbox-initial-left-position)), calc(-50vh + var(--wp--lightbox-initial-top-position))) scale(var(--wp--lightbox-scale)); } } diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index ef46e932d2490b..315ed995f26cfc 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store } from '@wordpress/interactivity'; +import { store, getContext, getElement } from '@wordpress/interactivity'; const focusableSelectors = [ 'a[href]', @@ -17,7 +17,7 @@ const focusableSelectors = [ '[tabindex]:not([tabindex^="-"])', ]; -/* +/** * Stores a context-bound scroll handler. * * This callback could be defined inline inside of the store @@ -32,7 +32,7 @@ const focusableSelectors = [ */ let scrollCallback; -/* +/** * Tracks whether user is touching screen; used to * differentiate behavior for touch and mouse input. * @@ -40,7 +40,7 @@ let scrollCallback; */ let isTouching = false; -/* +/** * Tracks the last time the screen was touched; used to * differentiate behavior for touch and mouse input. * @@ -48,7 +48,7 @@ let isTouching = false; */ let lastTouchTime = 0; -/* +/** * Lightbox page-scroll handler: prevents scrolling. * * This handler is added to prevent scrolling behaviors that @@ -64,348 +64,296 @@ let lastTouchTime = 0; * instead to not rely on JavaScript, but this seems to be the best approach * for now that provides the best visual experience. * - * @param {Object} context Interactivity page context? + * @param {Object} ctx Context object with the `core/image` namespace. */ -function handleScroll( context ) { +function handleScroll( ctx ) { // We can't override the scroll behavior on mobile devices // because doing so breaks the pinch to zoom functionality, and we // want to allow users to zoom in further on the high-res image. if ( ! isTouching && Date.now() - lastTouchTime > 450 ) { // We are unable to use event.preventDefault() to prevent scrolling // because the scroll event can't be canceled, so we reset the position instead. - window.scrollTo( - context.core.image.scrollLeftReset, - context.core.image.scrollTopReset - ); + window.scrollTo( ctx.scrollLeftReset, ctx.scrollTopReset ); } } -store( - { - state: { - core: { - image: { - windowWidth: window.innerWidth, - windowHeight: window.innerHeight, - }, - }, +const { state, actions, callbacks } = store( 'core/image', { + state: { + windowWidth: window.innerWidth, + windowHeight: window.innerHeight, + get roleAttribute() { + const ctx = getContext(); + return ctx.lightboxEnabled ? 'dialog' : null; }, - actions: { - core: { - image: { - showLightbox: ( { context, event } ) => { - // We can't initialize the lightbox until the reference - // image is loaded, otherwise the UX is broken. - if ( ! context.core.image.imageLoaded ) { - return; - } - context.core.image.initialized = true; - context.core.image.lastFocusedElement = - window.document.activeElement; - context.core.image.scrollDelta = 0; - context.core.image.pointerType = event.pointerType; - - context.core.image.lightboxEnabled = true; - setStyles( context, context.core.image.imageRef ); - - context.core.image.scrollTopReset = - window.pageYOffset || - document.documentElement.scrollTop; - - // In most cases, this value will be 0, but this is included - // in case a user has created a page with horizontal scrolling. - context.core.image.scrollLeftReset = - window.pageXOffset || - document.documentElement.scrollLeft; - - // We define and bind the scroll callback here so - // that we can pass the context and as an argument. - // We may be able to change this in the future if we - // define the scroll callback in the store instead, but - // this approach seems to tbe clearest for now. - scrollCallback = handleScroll.bind( null, context ); - - // We need to add a scroll event listener to the window - // here because we are unable to otherwise access it via - // the Interactivity API directives. If we add a native way - // to access the window, we can remove this. - window.addEventListener( - 'scroll', - scrollCallback, - false - ); - }, - hideLightbox: async ( { context } ) => { - context.core.image.hideAnimationEnabled = true; - if ( context.core.image.lightboxEnabled ) { - // We want to wait until the close animation is completed - // before allowing a user to scroll again. The duration of this - // animation is defined in the styles.scss and depends on if the - // animation is 'zoom' or 'fade', but in any case we should wait - // a few milliseconds longer than the duration, otherwise a user - // may scroll too soon and cause the animation to look sloppy. - setTimeout( function () { - window.removeEventListener( - 'scroll', - scrollCallback - ); - // If we don't delay before changing the focus, - // the focus ring will appear on Firefox before - // the image has finished animating, which looks broken. - context.core.image.lightboxTriggerRef.focus( { - preventScroll: true, - } ); - }, 450 ); - - context.core.image.lightboxEnabled = false; - } - }, - handleKeydown: ( { context, actions, event } ) => { - if ( context.core.image.lightboxEnabled ) { - if ( event.key === 'Tab' || event.keyCode === 9 ) { - // If shift + tab it change the direction - if ( - event.shiftKey && - window.document.activeElement === - context.core.image.firstFocusableElement - ) { - event.preventDefault(); - context.core.image.lastFocusableElement.focus(); - } else if ( - ! event.shiftKey && - window.document.activeElement === - context.core.image.lastFocusableElement - ) { - event.preventDefault(); - context.core.image.firstFocusableElement.focus(); - } - } - - if ( - event.key === 'Escape' || - event.keyCode === 27 - ) { - actions.core.image.hideLightbox( { - context, - event, - } ); - } - } - }, - // This is fired just by lazily loaded - // images on the page, not all images. - handleLoad: ( { context, effects, ref } ) => { - context.core.image.imageLoaded = true; - context.core.image.imageCurrentSrc = ref.currentSrc; - effects.core.image.setButtonStyles( { - context, - ref, - } ); - }, - handleTouchStart: () => { - isTouching = true; - }, - handleTouchMove: ( { context, event } ) => { - // On mobile devices, we want to prevent triggering the - // scroll event because otherwise the page jumps around as - // we reset the scroll position. This also means that closing - // the lightbox requires that a user perform a simple tap. This - // may be changed in the future if we find a better alternative - // to override or reset the scroll position during swipe actions. - if ( context.core.image.lightboxEnabled ) { - event.preventDefault(); - } - }, - handleTouchEnd: () => { - // We need to wait a few milliseconds before resetting - // to ensure that pinch to zoom works consistently - // on mobile devices when the lightbox is open. - lastTouchTime = Date.now(); - isTouching = false; - }, - }, - }, + get ariaModal() { + const ctx = getContext(); + return ctx.lightboxEnabled ? 'true' : null; }, - selectors: { - core: { - image: { - roleAttribute: ( { context } ) => { - return context.core.image.lightboxEnabled - ? 'dialog' - : null; - }, - ariaModal: ( { context } ) => { - return context.core.image.lightboxEnabled - ? 'true' - : null; - }, - dialogLabel: ( { context } ) => { - return context.core.image.lightboxEnabled - ? context.core.image.dialogLabel - : null; - }, - lightboxObjectFit: ( { context } ) => { - if ( context.core.image.initialized ) { - return 'cover'; - } - }, - enlargedImgSrc: ( { context } ) => { - return context.core.image.initialized - ? context.core.image.imageUploadedSrc - : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; - }, - }, - }, + get dialogLabel() { + const ctx = getContext(); + return ctx.lightboxEnabled ? ctx.dialogLabel : null; }, - effects: { - core: { - image: { - initOriginImage: ( { context, ref } ) => { - context.core.image.imageRef = ref; - context.core.image.lightboxTriggerRef = - ref.parentElement.querySelector( - '.lightbox-trigger' - ); - if ( ref.complete ) { - context.core.image.imageLoaded = true; - context.core.image.imageCurrentSrc = ref.currentSrc; - } - }, - initLightbox: async ( { context, ref } ) => { - if ( context.core.image.lightboxEnabled ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); - context.core.image.firstFocusableElement = - focusableElements[ 0 ]; - context.core.image.lastFocusableElement = - focusableElements[ - focusableElements.length - 1 - ]; - - // Move focus to the dialog when opening it. - ref.focus(); - } - }, - setButtonStyles: ( { context, ref } ) => { - const { - naturalWidth, - naturalHeight, - offsetWidth, - offsetHeight, - } = ref; - - // If the image isn't loaded yet, we can't - // calculate where the button should be. - if ( naturalWidth === 0 || naturalHeight === 0 ) { - return; - } - - const figure = ref.parentElement; - const figureWidth = ref.parentElement.clientWidth; - - // We need special handling for the height because - // a caption will cause the figure to be taller than - // the image, which means we need to account for that - // when calculating the placement of the button in the - // top right corner of the image. - let figureHeight = ref.parentElement.clientHeight; - const caption = figure.querySelector( 'figcaption' ); - if ( caption ) { - const captionComputedStyle = - window.getComputedStyle( caption ); - if ( - ! [ 'absolute', 'fixed' ].includes( - captionComputedStyle.position - ) - ) { - figureHeight = - figureHeight - - caption.offsetHeight - - parseFloat( - captionComputedStyle.marginTop - ) - - parseFloat( - captionComputedStyle.marginBottom - ); - } - } - - const buttonOffsetTop = figureHeight - offsetHeight; - const buttonOffsetRight = figureWidth - offsetWidth; - - // In the case of an image with object-fit: contain, the - // size of the element can be larger than the image itself, - // so we need to calculate where to place the button. - if ( context.core.image.scaleAttr === 'contain' ) { - // Natural ratio of the image. - const naturalRatio = naturalWidth / naturalHeight; - // Offset ratio of the image. - const offsetRatio = offsetWidth / offsetHeight; - - if ( naturalRatio >= offsetRatio ) { - // If it reaches the width first, keep - // the width and compute the height. - const referenceHeight = - offsetWidth / naturalRatio; - context.core.image.imageButtonTop = - ( offsetHeight - referenceHeight ) / 2 + - buttonOffsetTop + - 16; - context.core.image.imageButtonRight = - buttonOffsetRight + 16; - } else { - // If it reaches the height first, keep - // the height and compute the width. - const referenceWidth = - offsetHeight * naturalRatio; - context.core.image.imageButtonTop = - buttonOffsetTop + 16; - context.core.image.imageButtonRight = - ( offsetWidth - referenceWidth ) / 2 + - buttonOffsetRight + - 16; - } - } else { - context.core.image.imageButtonTop = - buttonOffsetTop + 16; - context.core.image.imageButtonRight = - buttonOffsetRight + 16; - } - }, - setStylesOnResize: ( { state, context, ref } ) => { - if ( - context.core.image.lightboxEnabled && - ( state.core.image.windowWidth || - state.core.image.windowHeight ) - ) { - setStyles( context, ref ); - } - }, - }, - }, + get lightboxObjectFit() { + const ctx = getContext(); + if ( ctx.initialized ) { + return 'cover'; + } + }, + get enlargedImgSrc() { + const ctx = getContext(); + return ctx.initialized + ? ctx.imageUploadedSrc + : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; }, }, - { - afterLoad: ( { state } ) => { - window.addEventListener( - 'resize', - debounce( () => { - state.core.image.windowWidth = window.innerWidth; - state.core.image.windowHeight = window.innerHeight; - } ) - ); + actions: { + showLightbox( event ) { + const ctx = getContext(); + // We can't initialize the lightbox until the reference + // image is loaded, otherwise the UX is broken. + if ( ! ctx.imageLoaded ) { + return; + } + ctx.initialized = true; + ctx.lastFocusedElement = window.document.activeElement; + ctx.scrollDelta = 0; + ctx.pointerType = event.pointerType; + + ctx.lightboxEnabled = true; + setStyles( ctx, ctx.imageRef ); + + ctx.scrollTopReset = + window.pageYOffset || document.documentElement.scrollTop; + + // In most cases, this value will be 0, but this is included + // in case a user has created a page with horizontal scrolling. + ctx.scrollLeftReset = + window.pageXOffset || document.documentElement.scrollLeft; + + // We define and bind the scroll callback here so + // that we can pass the context and as an argument. + // We may be able to change this in the future if we + // define the scroll callback in the store instead, but + // this approach seems to tbe clearest for now. + scrollCallback = handleScroll.bind( null, ctx ); + + // We need to add a scroll event listener to the window + // here because we are unable to otherwise access it via + // the Interactivity API directives. If we add a native way + // to access the window, we can remove this. + window.addEventListener( 'scroll', scrollCallback, false ); }, - } + hideLightbox() { + const ctx = getContext(); + ctx.hideAnimationEnabled = true; + if ( ctx.lightboxEnabled ) { + // We want to wait until the close animation is completed + // before allowing a user to scroll again. The duration of this + // animation is defined in the styles.scss and depends on if the + // animation is 'zoom' or 'fade', but in any case we should wait + // a few milliseconds longer than the duration, otherwise a user + // may scroll too soon and cause the animation to look sloppy. + setTimeout( function () { + window.removeEventListener( 'scroll', scrollCallback ); + // If we don't delay before changing the focus, + // the focus ring will appear on Firefox before + // the image has finished animating, which looks broken. + ctx.lightboxTriggerRef.focus( { + preventScroll: true, + } ); + }, 450 ); + + ctx.lightboxEnabled = false; + } + }, + handleKeydown( event ) { + const ctx = getContext(); + if ( ctx.lightboxEnabled ) { + if ( event.key === 'Tab' || event.keyCode === 9 ) { + // If shift + tab it change the direction + if ( + event.shiftKey && + window.document.activeElement === + ctx.firstFocusableElement + ) { + event.preventDefault(); + ctx.lastFocusableElement.focus(); + } else if ( + ! event.shiftKey && + window.document.activeElement === + ctx.lastFocusableElement + ) { + event.preventDefault(); + ctx.firstFocusableElement.focus(); + } + } + + if ( event.key === 'Escape' || event.keyCode === 27 ) { + actions.hideLightbox( event ); + } + } + }, + // This is fired just by lazily loaded + // images on the page, not all images. + handleLoad() { + const ctx = getContext(); + const { ref } = getElement(); + ctx.imageLoaded = true; + ctx.imageCurrentSrc = ref.currentSrc; + callbacks.setButtonStyles(); + }, + handleTouchStart() { + isTouching = true; + }, + handleTouchMove( event ) { + const ctx = getContext(); + // On mobile devices, we want to prevent triggering the + // scroll event because otherwise the page jumps around as + // we reset the scroll position. This also means that closing + // the lightbox requires that a user perform a simple tap. This + // may be changed in the future if we find a better alternative + // to override or reset the scroll position during swipe actions. + if ( ctx.lightboxEnabled ) { + event.preventDefault(); + } + }, + handleTouchEnd() { + // We need to wait a few milliseconds before resetting + // to ensure that pinch to zoom works consistently + // on mobile devices when the lightbox is open. + lastTouchTime = Date.now(); + isTouching = false; + }, + }, + callbacks: { + initOriginImage() { + const ctx = getContext(); + const { ref } = getElement(); + ctx.imageRef = ref; + ctx.lightboxTriggerRef = + ref.parentElement.querySelector( '.lightbox-trigger' ); + if ( ref.complete ) { + ctx.imageLoaded = true; + ctx.imageCurrentSrc = ref.currentSrc; + } + }, + initLightbox() { + const ctx = getContext(); + const { ref } = getElement(); + if ( ctx.lightboxEnabled ) { + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + ctx.firstFocusableElement = focusableElements[ 0 ]; + ctx.lastFocusableElement = + focusableElements[ focusableElements.length - 1 ]; + + // Move focus to the dialog when opening it. + ref.focus(); + } + }, + setButtonStyles() { + const { ref } = getElement(); + const { naturalWidth, naturalHeight, offsetWidth, offsetHeight } = + ref; + + // If the image isn't loaded yet, we can't + // calculate where the button should be. + if ( naturalWidth === 0 || naturalHeight === 0 ) { + return; + } + + const figure = ref.parentElement; + const figureWidth = ref.parentElement.clientWidth; + + // We need special handling for the height because + // a caption will cause the figure to be taller than + // the image, which means we need to account for that + // when calculating the placement of the button in the + // top right corner of the image. + let figureHeight = ref.parentElement.clientHeight; + const caption = figure.querySelector( 'figcaption' ); + if ( caption ) { + const captionComputedStyle = window.getComputedStyle( caption ); + if ( + ! [ 'absolute', 'fixed' ].includes( + captionComputedStyle.position + ) + ) { + figureHeight = + figureHeight - + caption.offsetHeight - + parseFloat( captionComputedStyle.marginTop ) - + parseFloat( captionComputedStyle.marginBottom ); + } + } + + const buttonOffsetTop = figureHeight - offsetHeight; + const buttonOffsetRight = figureWidth - offsetWidth; + + const ctx = getContext(); + + // In the case of an image with object-fit: contain, the + // size of the element can be larger than the image itself, + // so we need to calculate where to place the button. + if ( ctx.scaleAttr === 'contain' ) { + // Natural ratio of the image. + const naturalRatio = naturalWidth / naturalHeight; + // Offset ratio of the image. + const offsetRatio = offsetWidth / offsetHeight; + + if ( naturalRatio >= offsetRatio ) { + // If it reaches the width first, keep + // the width and compute the height. + const referenceHeight = offsetWidth / naturalRatio; + ctx.imageButtonTop = + ( offsetHeight - referenceHeight ) / 2 + + buttonOffsetTop + + 16; + ctx.imageButtonRight = buttonOffsetRight + 16; + } else { + // If it reaches the height first, keep + // the height and compute the width. + const referenceWidth = offsetHeight * naturalRatio; + ctx.imageButtonTop = buttonOffsetTop + 16; + ctx.imageButtonRight = + ( offsetWidth - referenceWidth ) / 2 + + buttonOffsetRight + + 16; + } + } else { + ctx.imageButtonTop = buttonOffsetTop + 16; + ctx.imageButtonRight = buttonOffsetRight + 16; + } + }, + setStylesOnResize() { + const ctx = getContext(); + const { ref } = getElement(); + if ( + ctx.lightboxEnabled && + ( state.windowWidth || state.windowHeight ) + ) { + setStyles( ctx, ref ); + } + }, + }, +} ); + +window.addEventListener( + 'resize', + debounce( () => { + state.windowWidth = window.innerWidth; + state.windowHeight = window.innerHeight; + } ) ); -/* +/** * Computes styles for the lightbox and adds them to the document. * * @function - * @param {Object} context - An Interactivity API context - * @param {Object} event - A triggering event + * @param {Object} ctx - Context for the `core/image` namespace. + * @param {Object} ref - The element reference. */ -function setStyles( context, ref ) { +function setStyles( ctx, ref ) { // The reference img element lies adjacent // to the event target button in the DOM. let { @@ -423,7 +371,7 @@ function setStyles( context, ref ) { // If it has object-fit: contain, recalculate the original sizes // and the screen position without the blank spaces. - if ( context.core.image.scaleAttr === 'contain' ) { + if ( ctx.scaleAttr === 'contain' ) { if ( naturalRatio > originalRatio ) { const heightWithoutSpace = originalWidth / naturalRatio; // Recalculate screen position without the top space. @@ -443,14 +391,10 @@ function setStyles( context, ref ) { // the image's dimensions in the lightbox are the same // as those of the image in the content. let imgMaxWidth = parseFloat( - context.core.image.targetWidth !== 'none' - ? context.core.image.targetWidth - : naturalWidth + ctx.targetWidth !== 'none' ? ctx.targetWidth : naturalWidth ); let imgMaxHeight = parseFloat( - context.core.image.targetHeight !== 'none' - ? context.core.image.targetHeight - : naturalHeight + ctx.targetHeight !== 'none' ? ctx.targetHeight : naturalHeight ); // Ratio of the biggest image stored in the database. @@ -568,16 +512,19 @@ function setStyles( context, ref ) { --wp--lightbox-image-width: ${ lightboxImgWidth }px; --wp--lightbox-image-height: ${ lightboxImgHeight }px; --wp--lightbox-scale: ${ containerScale }; + --wp--lightbox-scrollbar-width: ${ + window.innerWidth - document.documentElement.clientWidth + }px; } `; } -/* +/** * Debounces a function call. * * @function * @param {Function} func - A function to be called - * @param {number} wait - The time to wait before calling the function + * @param {number} wait - The time to wait before calling the function */ function debounce( func, wait = 50 ) { let timeout; diff --git a/packages/block-library/src/navigation-link/block.json b/packages/block-library/src/navigation-link/block.json index b2cbeaed63d3e9..d8f2fe31aef9dd 100644 --- a/packages/block-library/src/navigation-link/block.json +++ b/packages/block-library/src/navigation-link/block.json @@ -71,7 +71,8 @@ "__experimentalDefaultControls": { "fontSize": true } - } + }, + "renaming": false }, "editorStyle": "wp-block-navigation-link-editor", "style": "wp-block-navigation-link" diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index 5e518d5c374148..6550d896656b1b 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -101,11 +101,11 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut ) ) ) { // Add directives to the parent `
  • `. - $tags->set_attribute( 'data-wp-interactive', true ); - $tags->set_attribute( 'data-wp-context', '{ "core": { "navigation": { "submenuOpenedBy": {}, "type": "submenu" } } }' ); - $tags->set_attribute( 'data-wp-effect', 'effects.core.navigation.initMenu' ); - $tags->set_attribute( 'data-wp-on--focusout', 'actions.core.navigation.handleMenuFocusout' ); - $tags->set_attribute( 'data-wp-on--keydown', 'actions.core.navigation.handleMenuKeydown' ); + $tags->set_attribute( 'data-wp-interactive', '{ "namespace": "core/navigation" }' ); + $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": {}, "type": "submenu" }' ); + $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' ); + $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' ); + $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' ); // This is a fix for Safari. Without it, Safari doesn't change the active // element when the user clicks on a button. It can be removed once we add @@ -114,8 +114,8 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut $tags->set_attribute( 'tabindex', '-1' ); if ( ! isset( $block_attributes['openSubmenusOnClick'] ) || false === $block_attributes['openSubmenusOnClick'] ) { - $tags->set_attribute( 'data-wp-on--mouseenter', 'actions.core.navigation.openMenuOnHover' ); - $tags->set_attribute( 'data-wp-on--mouseleave', 'actions.core.navigation.closeMenuOnHover' ); + $tags->set_attribute( 'data-wp-on--mouseenter', 'actions.openMenuOnHover' ); + $tags->set_attribute( 'data-wp-on--mouseleave', 'actions.closeMenuOnHover' ); } // Add directives to the toggle submenu button. @@ -125,8 +125,8 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut 'class_name' => 'wp-block-navigation-submenu__toggle', ) ) ) { - $tags->set_attribute( 'data-wp-on--click', 'actions.core.navigation.toggleMenuOnClick' ); - $tags->set_attribute( 'data-wp-bind--aria-expanded', 'selectors.core.navigation.isMenuOpen' ); + $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' ); + $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isMenuOpen' ); // The `aria-expanded` attribute for SSR is already added in the submenu block. } // Add directives to the submenu. @@ -136,7 +136,7 @@ function block_core_navigation_add_directives_to_submenu( $tags, $block_attribut 'class_name' => 'wp-block-navigation__submenu-container', ) ) ) { - $tags->set_attribute( 'data-wp-on--focus', 'actions.core.navigation.openMenuOnFocus' ); + $tags->set_attribute( 'data-wp-on--focus', 'actions.openMenuOnFocus' ); } // Iterate through subitems if exist. diff --git a/packages/block-library/src/navigation/view.js b/packages/block-library/src/navigation/view.js index bad36f6240134f..ba8e6d1a6683a4 100644 --- a/packages/block-library/src/navigation/view.js +++ b/packages/block-library/src/navigation/view.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { store as wpStore } from '@wordpress/interactivity'; +import { store, getContext, getElement } from '@wordpress/interactivity'; const focusableSelectors = [ 'a[href]', @@ -18,205 +18,172 @@ const focusableSelectors = [ // capture the clicks, instead of relying on the focusout event. document.addEventListener( 'click', () => {} ); -const openMenu = ( store, menuOpenedOn ) => { - const { context, selectors } = store; - selectors.core.navigation.menuOpenedBy( store )[ menuOpenedOn ] = true; - if ( context.core.navigation.type === 'overlay' ) { - // Add a `has-modal-open` class to the root. - document.documentElement.classList.add( 'has-modal-open' ); - } -}; - -const closeMenu = ( store, menuClosedOn ) => { - const { context, selectors } = store; - selectors.core.navigation.menuOpenedBy( store )[ menuClosedOn ] = false; - // Check if the menu is still open or not. - if ( ! selectors.core.navigation.isMenuOpen( store ) ) { - if ( - context.core.navigation.modal?.contains( - window.document.activeElement - ) - ) { - context.core.navigation.previousFocus?.focus(); - } - context.core.navigation.modal = null; - context.core.navigation.previousFocus = null; - if ( context.core.navigation.type === 'overlay' ) { - document.documentElement.classList.remove( 'has-modal-open' ); - } - } -}; - -wpStore( { - effects: { - core: { - navigation: { - initMenu: ( store ) => { - const { context, selectors, ref } = store; - if ( selectors.core.navigation.isMenuOpen( store ) ) { - const focusableElements = - ref.querySelectorAll( focusableSelectors ); - context.core.navigation.modal = ref; - context.core.navigation.firstFocusableElement = - focusableElements[ 0 ]; - context.core.navigation.lastFocusableElement = - focusableElements[ focusableElements.length - 1 ]; - } - }, - focusFirstElement: ( store ) => { - const { selectors, ref } = store; - if ( selectors.core.navigation.isMenuOpen( store ) ) { - ref.querySelector( - '.wp-block-navigation-item > *:first-child' - ).focus(); - } - }, - }, +const { state, actions } = store( 'core/navigation', { + state: { + get roleAttribute() { + const ctx = getContext(); + return ctx.type === 'overlay' && state.isMenuOpen ? 'dialog' : null; }, - }, - selectors: { - core: { - navigation: { - roleAttribute: ( store ) => { - const { context, selectors } = store; - return context.core.navigation.type === 'overlay' && - selectors.core.navigation.isMenuOpen( store ) - ? 'dialog' - : null; - }, - ariaModal: ( store ) => { - const { context, selectors } = store; - return context.core.navigation.type === 'overlay' && - selectors.core.navigation.isMenuOpen( store ) - ? 'true' - : null; - }, - ariaLabel: ( store ) => { - const { context, selectors } = store; - return context.core.navigation.type === 'overlay' && - selectors.core.navigation.isMenuOpen( store ) - ? context.core.navigation.ariaLabel - : null; - }, - isMenuOpen: ( { context } ) => - // The menu is opened if either `click`, `hover` or `focus` is true. - Object.values( - context.core.navigation[ - context.core.navigation.type === 'overlay' - ? 'overlayOpenedBy' - : 'submenuOpenedBy' - ] - ).filter( Boolean ).length > 0, - menuOpenedBy: ( { context } ) => - context.core.navigation[ - context.core.navigation.type === 'overlay' - ? 'overlayOpenedBy' - : 'submenuOpenedBy' - ], - }, + get ariaModal() { + const ctx = getContext(); + return ctx.type === 'overlay' && state.isMenuOpen ? 'true' : null; + }, + get ariaLabel() { + const ctx = getContext(); + return ctx.type === 'overlay' && state.isMenuOpen + ? ctx.ariaLabel + : null; + }, + get isMenuOpen() { + // The menu is opened if either `click`, `hover` or `focus` is true. + return ( + Object.values( state.menuOpenedBy ).filter( Boolean ).length > 0 + ); + }, + get menuOpenedBy() { + const ctx = getContext(); + return ctx.type === 'overlay' + ? ctx.overlayOpenedBy + : ctx.submenuOpenedBy; }, }, actions: { - core: { - navigation: { - openMenuOnHover( store ) { - const { navigation } = store.context.core; - if ( - navigation.type === 'submenu' && - // Only open on hover if the overlay is closed. - Object.values( - navigation.overlayOpenedBy || {} - ).filter( Boolean ).length === 0 - ) - openMenu( store, 'hover' ); - }, - closeMenuOnHover( store ) { - closeMenu( store, 'hover' ); - }, - openMenuOnClick( store ) { - const { context, ref } = store; - context.core.navigation.previousFocus = ref; - openMenu( store, 'click' ); - }, - closeMenuOnClick( store ) { - closeMenu( store, 'click' ); - closeMenu( store, 'focus' ); - }, - openMenuOnFocus( store ) { - openMenu( store, 'focus' ); - }, - toggleMenuOnClick: ( store ) => { - const { selectors, context, ref } = store; - // Safari won't send focus to the clicked element, so we need to manually place it: https://bugs.webkit.org/show_bug.cgi?id=22261 - if ( window.document.activeElement !== ref ) ref.focus(); - const menuOpenedBy = - selectors.core.navigation.menuOpenedBy( store ); - if ( menuOpenedBy.click || menuOpenedBy.focus ) { - closeMenu( store, 'click' ); - closeMenu( store, 'focus' ); - } else { - context.core.navigation.previousFocus = ref; - openMenu( store, 'click' ); - } - }, - handleMenuKeydown: ( store ) => { - const { context, selectors, event } = store; - if ( - selectors.core.navigation.menuOpenedBy( store ).click - ) { - // If Escape close the menu. - if ( event?.key === 'Escape' ) { - closeMenu( store, 'click' ); - closeMenu( store, 'focus' ); - return; - } - - // Trap focus if it is an overlay (main menu). - if ( - context.core.navigation.type === 'overlay' && - event.key === 'Tab' - ) { - // If shift + tab it change the direction. - if ( - event.shiftKey && - window.document.activeElement === - context.core.navigation - .firstFocusableElement - ) { - event.preventDefault(); - context.core.navigation.lastFocusableElement.focus(); - } else if ( - ! event.shiftKey && - window.document.activeElement === - context.core.navigation.lastFocusableElement - ) { - event.preventDefault(); - context.core.navigation.firstFocusableElement.focus(); - } - } - } - }, - handleMenuFocusout: ( store ) => { - const { context, event } = store; - // If focus is outside modal, and in the document, close menu - // event.target === The element losing focus - // event.relatedTarget === The element receiving focus (if any) - // When focusout is outsite the document, - // `window.document.activeElement` doesn't change. + openMenuOnHover() { + const { type, overlayOpenedBy } = getContext(); + if ( + type === 'submenu' && + // Only open on hover if the overlay is closed. + Object.values( overlayOpenedBy || {} ).filter( Boolean ) + .length === 0 + ) + actions.openMenu( 'hover' ); + }, + closeMenuOnHover() { + actions.closeMenu( 'hover' ); + }, + openMenuOnClick() { + const ctx = getContext(); + const { ref } = getElement(); + ctx.previousFocus = ref; + actions.openMenu( 'click' ); + }, + closeMenuOnClick() { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + }, + openMenuOnFocus() { + actions.openMenu( 'focus' ); + }, + toggleMenuOnClick() { + const ctx = getContext(); + const { ref } = getElement(); + // Safari won't send focus to the clicked element, so we need to manually place it: https://bugs.webkit.org/show_bug.cgi?id=22261 + if ( window.document.activeElement !== ref ) ref.focus(); + const { menuOpenedBy } = state; + if ( menuOpenedBy.click || menuOpenedBy.focus ) { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + } else { + ctx.previousFocus = ref; + actions.openMenu( 'click' ); + } + }, + handleMenuKeydown( event ) { + const { type, firstFocusableElement, lastFocusableElement } = + getContext(); + if ( state.menuOpenedBy.click ) { + // If Escape close the menu. + if ( event?.key === 'Escape' ) { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + return; + } - // The event.relatedTarget is null when something outside the navigation menu is clicked. This is only necessary for Safari. + // Trap focus if it is an overlay (main menu). + if ( type === 'overlay' && event.key === 'Tab' ) { + // If shift + tab it change the direction. if ( - event.relatedTarget === null || - ( ! context.core.navigation.modal?.contains( - event.relatedTarget - ) && - event.target !== window.document.activeElement ) + event.shiftKey && + window.document.activeElement === firstFocusableElement ) { - closeMenu( store, 'click' ); - closeMenu( store, 'focus' ); + event.preventDefault(); + lastFocusableElement.focus(); + } else if ( + ! event.shiftKey && + window.document.activeElement === lastFocusableElement + ) { + event.preventDefault(); + firstFocusableElement.focus(); } - }, - }, + } + } + }, + handleMenuFocusout( event ) { + const { modal } = getContext(); + // If focus is outside modal, and in the document, close menu + // event.target === The element losing focus + // event.relatedTarget === The element receiving focus (if any) + // When focusout is outsite the document, + // `window.document.activeElement` doesn't change. + + // The event.relatedTarget is null when something outside the navigation menu is clicked. This is only necessary for Safari. + if ( + event.relatedTarget === null || + ( ! modal?.contains( event.relatedTarget ) && + event.target !== window.document.activeElement ) + ) { + actions.closeMenu( 'click' ); + actions.closeMenu( 'focus' ); + } + }, + + openMenu( menuOpenedOn = 'click' ) { + const { type } = getContext(); + state.menuOpenedBy[ menuOpenedOn ] = true; + if ( type === 'overlay' ) { + // Add a `has-modal-open` class to the root. + document.documentElement.classList.add( 'has-modal-open' ); + } + }, + + closeMenu( menuClosedOn = 'click' ) { + const ctx = getContext(); + state.menuOpenedBy[ menuClosedOn ] = false; + // Check if the menu is still open or not. + if ( ! state.isMenuOpen ) { + if ( ctx.modal?.contains( window.document.activeElement ) ) { + ctx.previousFocus?.focus(); + } + ctx.modal = null; + ctx.previousFocus = null; + if ( ctx.type === 'overlay' ) { + document.documentElement.classList.remove( + 'has-modal-open' + ); + } + } + }, + }, + callbacks: { + initMenu() { + const ctx = getContext(); + const { ref } = getElement(); + if ( state.isMenuOpen ) { + const focusableElements = + ref.querySelectorAll( focusableSelectors ); + ctx.modal = ref; + ctx.firstFocusableElement = focusableElements[ 0 ]; + ctx.lastFocusableElement = + focusableElements[ focusableElements.length - 1 ]; + } + }, + focusFirstElement() { + const { ref } = getElement(); + if ( state.isMenuOpen ) { + ref.querySelector( + '.wp-block-navigation-item > *:first-child' + ).focus(); + } }, }, } ); diff --git a/packages/block-library/src/paragraph/test/edit.native.js b/packages/block-library/src/paragraph/test/edit.native.js index 8220ad0888c795..fdb082246171ba 100644 --- a/packages/block-library/src/paragraph/test/edit.native.js +++ b/packages/block-library/src/paragraph/test/edit.native.js @@ -17,11 +17,12 @@ import { waitForElementToBeRemoved, } from 'test/helpers'; import Clipboard from '@react-native-clipboard/clipboard'; +import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; /** * WordPress dependencies */ -import { ENTER } from '@wordpress/keycodes'; +import { BACKSPACE, ENTER } from '@wordpress/keycodes'; /** * Internal dependencies @@ -685,4 +686,39 @@ describe( 'Paragraph block', () => { " ` ); } ); + + it( 'should focus on the previous Paragraph block when backspacing in an empty Paragraph block', async () => { + // Arrange + const screen = await initializeEditor(); + await addBlock( screen, 'Paragraph' ); + + // Act + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + typeInRichText( paragraphTextInput, 'A quick brown fox jumps' ); + + await addBlock( screen, 'Paragraph' ); + const secondParagraphBlock = getBlock( screen, 'Paragraph', { + rowIndex: 2, + } ); + fireEvent.press( secondParagraphBlock ); + + // Clear mock history + TextInputState.focusTextInput.mockClear(); + + const secondParagraphTextInput = + within( secondParagraphBlock ).getByPlaceholderText( + 'Start writing…' + ); + fireEvent( secondParagraphTextInput, 'onKeyDown', { + nativeEvent: {}, + preventDefault() {}, + keyCode: BACKSPACE, + } ); + + // Assert + expect( TextInputState.focusTextInput ).toHaveBeenCalled(); + } ); } ); diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index 5fd1b427a5e3e9..e01fb37eb849e6 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -24,9 +24,11 @@ const PatternEdit = ( { attributes, clientId } ) => { [] ); - const { replaceBlocks, __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); - const { setBlockEditingMode } = useDispatch( blockEditorStore ); + const { + replaceBlocks, + setBlockEditingMode, + __unstableMarkNextChangeAsNotPersistent, + } = useDispatch( blockEditorStore ); const { getBlockRootClientId, getBlockEditingMode } = useSelect( blockEditorStore ); diff --git a/packages/block-library/src/post-template/edit.js b/packages/block-library/src/post-template/edit.js index 025a8bf4f6c93f..2e668674e85301 100644 --- a/packages/block-library/src/post-template/edit.js +++ b/packages/block-library/src/post-template/edit.js @@ -123,7 +123,7 @@ export default function PostTemplateEdit( { slug: templateSlug.replace( 'category-', '' ), } ); const query = { - offset: perPage ? perPage + offset : 0, + offset: offset || 0, order, orderby: orderBy, }; diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php index 768fde56ff06f3..ca134f62192f9e 100644 --- a/packages/block-library/src/query-pagination-next/index.php +++ b/packages/block-library/src/query-pagination-next/index.php @@ -72,9 +72,9 @@ function render_block_core_query_pagination_next( $attributes, $content, $block ) ) ) { $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' ); - $p->set_attribute( 'data-wp-effect', 'effects.core.query.prefetch' ); + $p->set_attribute( 'data-wp-on--click', 'core/query::actions.navigate' ); + $p->set_attribute( 'data-wp-on--mouseenter', 'core/query::actions.prefetch' ); + $p->set_attribute( 'data-wp-watch', 'core/query::callbacks.prefetch' ); $content = $p->get_updated_html(); } } diff --git a/packages/block-library/src/query-pagination-numbers/index.php b/packages/block-library/src/query-pagination-numbers/index.php index 98098533adac7d..2f9370751f6d25 100644 --- a/packages/block-library/src/query-pagination-numbers/index.php +++ b/packages/block-library/src/query-pagination-numbers/index.php @@ -98,7 +98,7 @@ function render_block_core_query_pagination_numbers( $attributes, $content, $blo 'class_name' => 'page-numbers', ) ) ) { - $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + $p->set_attribute( 'data-wp-on--click', 'core/query::actions.navigate' ); } $content = $p->get_updated_html(); } diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index fc1fee08e82148..b49130a44d8ddf 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -60,9 +60,9 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl ) ) ) { $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' ); - $p->set_attribute( 'data-wp-effect', 'effects.core.query.prefetch' ); + $p->set_attribute( 'data-wp-on--click', 'core/query::actions.navigate' ); + $p->set_attribute( 'data-wp-on--mouseenter', 'core/query::actions.prefetch' ); + $p->set_attribute( 'data-wp-watch', 'core/query::callbacks.prefetch' ); $content = $p->get_updated_html(); } } diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index b6a5733632ff44..6daf2411233bdb 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -21,19 +21,15 @@ function render_block_core_query( $attributes, $content, $block ) { $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag() ) { // Add the necessary directives. - $p->set_attribute( 'data-wp-interactive', true ); + $p->set_attribute( 'data-wp-interactive', '{"namespace":"core/query"}' ); $p->set_attribute( 'data-wp-navigation-id', 'query-' . $attributes['queryId'] ); // Use context to send translated strings. $p->set_attribute( 'data-wp-context', wp_json_encode( array( - 'core' => array( - 'query' => array( - 'loadingText' => __( 'Loading page, please wait.' ), - 'loadedText' => __( 'Page Loaded.' ), - ), - ), + 'loadingText' => __( 'Loading page, please wait.' ), + 'loadedText' => __( 'Page Loaded.' ), ), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP ) @@ -54,12 +50,12 @@ function render_block_core_query( $attributes, $content, $block ) { '
    ', $last_tag_position, 0 diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index 1dac448952b11e..ccf70810047673 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -1,7 +1,13 @@ /** * WordPress dependencies */ -import { store, navigate, prefetch } from '@wordpress/interactivity'; +import { + store, + getContext, + getElement, + navigate, + prefetch, +} from '@wordpress/interactivity'; const isValidLink = ( ref ) => ref && @@ -18,83 +24,70 @@ const isValidEvent = ( event ) => ! event.shiftKey && ! event.defaultPrevented; -store( { - selectors: { - core: { - query: { - startAnimation: ( { context } ) => - context.core.query.animation === 'start', - finishAnimation: ( { context } ) => - context.core.query.animation === 'finish', - }, +store( 'core/query', { + state: { + get startAnimation() { + return getContext().animation === 'start'; + }, + get finishAnimation() { + return getContext().animation === 'finish'; }, }, actions: { - core: { - query: { - navigate: async ( { event, ref, context } ) => { - const isDisabled = ref.closest( '[data-wp-navigation-id]' ) - ?.dataset.wpNavigationDisabled; + *navigate( event ) { + const ctx = getContext(); + const { ref } = getElement(); + const isDisabled = ref.closest( '[data-wp-navigation-id]' )?.dataset + .wpNavigationDisabled; - if ( - isValidLink( ref ) && - isValidEvent( event ) && - ! isDisabled - ) { - event.preventDefault(); + if ( isValidLink( ref ) && isValidEvent( event ) && ! isDisabled ) { + event.preventDefault(); - const id = ref.closest( '[data-wp-navigation-id]' ) - .dataset.wpNavigationId; + const id = ref.closest( '[data-wp-navigation-id]' ).dataset + .wpNavigationId; - // Don't announce the navigation immediately, wait 400 ms. - const timeout = setTimeout( () => { - context.core.query.message = - context.core.query.loadingText; - context.core.query.animation = 'start'; - }, 400 ); + // Don't announce the navigation immediately, wait 400 ms. + const timeout = setTimeout( () => { + ctx.message = ctx.loadingText; + ctx.animation = 'start'; + }, 400 ); - await navigate( ref.href ); + yield navigate( ref.href ); - // Dismiss loading message if it hasn't been added yet. - clearTimeout( timeout ); + // 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 = - context.core.query.loadedText + - ( context.core.query.message === - context.core.query.loadedText - ? '\u00A0' - : '' ); + // 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 + ctx.message = + ctx.loadedText + + ( ctx.message === ctx.loadedText ? '\u00A0' : '' ); - context.core.query.animation = 'finish'; - context.core.query.url = ref.href; + ctx.animation = 'finish'; + ctx.url = ref.href; - // Focus the first anchor of the Query block. - const firstAnchor = `[data-wp-navigation-id=${ id }] .wp-block-post-template a[href]`; - document.querySelector( firstAnchor )?.focus(); - } - }, - prefetch: async ( { ref } ) => { - const isDisabled = ref.closest( '[data-wp-navigation-id]' ) - ?.dataset.wpNavigationDisabled; - if ( isValidLink( ref ) && ! isDisabled ) { - await prefetch( ref.href ); - } - }, - }, + // Focus the first anchor of the Query block. + const firstAnchor = `[data-wp-navigation-id=${ id }] .wp-block-post-template a[href]`; + document.querySelector( firstAnchor )?.focus(); + } + }, + *prefetch() { + const { ref } = getElement(); + const isDisabled = ref.closest( '[data-wp-navigation-id]' )?.dataset + .wpNavigationDisabled; + if ( isValidLink( ref ) && ! isDisabled ) { + yield prefetch( ref.href ); + } }, }, - effects: { - core: { - query: { - prefetch: async ( { ref, context } ) => { - if ( context.core.query.url && isValidLink( ref ) ) { - await prefetch( ref.href ); - } - }, - }, + callbacks: { + *prefetch() { + const { url } = getContext(); + const { ref } = getElement(); + if ( url && isValidLink( ref ) ) { + yield prefetch( ref.href ); + } }, }, } ); diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index f00ecfe6abe1cc..ec7e763ecb1f60 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -80,8 +80,8 @@ function render_block_core_search( $attributes, $content, $block ) { $is_expandable_searchfield = 'button-only' === $button_position && 'expand-searchfield' === $button_behavior; if ( $is_expandable_searchfield ) { - $input->set_attribute( 'data-wp-bind--aria-hidden', '!context.core.search.isSearchInputVisible' ); - $input->set_attribute( 'data-wp-bind--tabindex', 'selectors.core.search.tabindex' ); + $input->set_attribute( 'data-wp-bind--aria-hidden', '!context.isSearchInputVisible' ); + $input->set_attribute( 'data-wp-bind--tabindex', 'state.tabindex' ); // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); @@ -145,11 +145,11 @@ function render_block_core_search( $attributes, $content, $block ) { if ( $button->next_tag() ) { $button->add_class( implode( ' ', $button_classes ) ); if ( 'expand-searchfield' === $attributes['buttonBehavior'] && 'button-only' === $attributes['buttonPosition'] ) { - $button->set_attribute( 'data-wp-bind--aria-label', 'selectors.core.search.ariaLabel' ); - $button->set_attribute( 'data-wp-bind--aria-controls', 'selectors.core.search.ariaControls' ); - $button->set_attribute( 'data-wp-bind--aria-expanded', 'context.core.search.isSearchInputVisible' ); - $button->set_attribute( 'data-wp-bind--type', 'selectors.core.search.type' ); - $button->set_attribute( 'data-wp-on--click', 'actions.core.search.openSearchInput' ); + $button->set_attribute( 'data-wp-bind--aria-label', 'state.ariaLabel' ); + $button->set_attribute( 'data-wp-bind--aria-controls', 'state.ariaControls' ); + $button->set_attribute( 'data-wp-bind--aria-expanded', 'context.isSearchInputVisible' ); + $button->set_attribute( 'data-wp-bind--type', 'state.type' ); + $button->set_attribute( 'data-wp-on--click', 'actions.openSearchInput' ); // Adding these attributes manually is needed until the Interactivity API SSR logic is added to core. $button->set_attribute( 'aria-label', __( 'Expand search field' ) ); $button->set_attribute( 'aria-controls', 'wp-block-search__input-' . $input_id ); @@ -176,11 +176,11 @@ function render_block_core_search( $attributes, $content, $block ) { $aria_label_expanded = __( 'Submit Search' ); $aria_label_collapsed = __( 'Expand search field' ); $form_directives = ' - data-wp-interactive - data-wp-context=\'{ "core": { "search": { "isSearchInputVisible": ' . $open_by_default . ', "inputId": "' . $input_id . '", "ariaLabelExpanded": "' . $aria_label_expanded . '", "ariaLabelCollapsed": "' . $aria_label_collapsed . '" } } }\' - data-wp-class--wp-block-search__searchfield-hidden="!context.core.search.isSearchInputVisible" - data-wp-on--keydown="actions.core.search.handleSearchKeydown" - data-wp-on--focusout="actions.core.search.handleSearchFocusout" + data-wp-interactive=\'{ "namespace": "core/search" }\' + data-wp-context=\'{ "isSearchInputVisible": ' . $open_by_default . ', "inputId": "' . $input_id . '", "ariaLabelExpanded": "' . $aria_label_expanded . '", "ariaLabelCollapsed": "' . $aria_label_collapsed . '" }\' + data-wp-class--wp-block-search__searchfield-hidden="!context.isSearchInputVisible" + data-wp-on--keydown="actions.handleSearchKeydown" + data-wp-on--focusout="actions.handleSearchFocusout" '; } diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index d99dfc5696ccbb..b633bf971f363a 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -1,73 +1,68 @@ /** * WordPress dependencies */ -import { store as wpStore } from '@wordpress/interactivity'; +import { store, getContext, getElement } from '@wordpress/interactivity'; -wpStore( { - selectors: { - core: { - search: { - ariaLabel: ( { context } ) => { - const { ariaLabelCollapsed, ariaLabelExpanded } = - context.core.search; - return context.core.search.isSearchInputVisible - ? ariaLabelExpanded - : ariaLabelCollapsed; - }, - ariaControls: ( { context } ) => { - return context.core.search.isSearchInputVisible - ? null - : context.core.search.inputId; - }, - type: ( { context } ) => { - return context.core.search.isSearchInputVisible - ? 'submit' - : 'button'; - }, - tabindex: ( { context } ) => { - return context.core.search.isSearchInputVisible - ? '0' - : '-1'; - }, - }, +const { actions } = store( 'core/search', { + state: { + get ariaLabel() { + const { + isSearchInputVisible, + ariaLabelCollapsed, + ariaLabelExpanded, + } = getContext(); + return isSearchInputVisible + ? ariaLabelExpanded + : ariaLabelCollapsed; + }, + get ariaControls() { + const { isSearchInputVisible, inputId } = getContext(); + return isSearchInputVisible ? null : inputId; + }, + get type() { + const { isSearchInputVisible } = getContext(); + return isSearchInputVisible ? 'submit' : 'button'; + }, + get tabindex() { + const { isSearchInputVisible } = getContext(); + return isSearchInputVisible ? '0' : '-1'; }, }, actions: { - core: { - search: { - openSearchInput: ( { context, event, ref } ) => { - if ( ! context.core.search.isSearchInputVisible ) { - event.preventDefault(); - context.core.search.isSearchInputVisible = true; - ref.parentElement.querySelector( 'input' ).focus(); - } - }, - closeSearchInput: ( { context } ) => { - context.core.search.isSearchInputVisible = false; - }, - handleSearchKeydown: ( store ) => { - const { actions, event, ref } = store; - // If Escape close the menu. - if ( event?.key === 'Escape' ) { - actions.core.search.closeSearchInput( store ); - ref.querySelector( 'button' ).focus(); - } - }, - handleSearchFocusout: ( store ) => { - const { actions, event, ref } = store; - // If focus is outside search form, and in the document, close menu - // event.target === The element losing focus - // event.relatedTarget === The element receiving focus (if any) - // When focusout is outside the document, - // `window.document.activeElement` doesn't change. - if ( - ! ref.contains( event.relatedTarget ) && - event.target !== window.document.activeElement - ) { - actions.core.search.closeSearchInput( store ); - } - }, - }, + openSearchInput( event ) { + const ctx = getContext(); + const { ref } = getElement(); + if ( ! ctx.isSearchInputVisible ) { + event.preventDefault(); + ctx.isSearchInputVisible = true; + ref.parentElement.querySelector( 'input' ).focus(); + } + }, + closeSearchInput() { + const ctx = getContext(); + ctx.isSearchInputVisible = false; + }, + handleSearchKeydown( event ) { + const { ref } = getElement(); + // If Escape close the menu. + if ( event?.key === 'Escape' ) { + actions.closeSearchInput(); + ref.querySelector( 'button' ).focus(); + } + }, + handleSearchFocusout( event ) { + const { ref } = getElement(); + // If focus is outside search form, and in the document, close menu + // event.target === The element losing focus + // event.relatedTarget === The element receiving focus (if any) + // When focusout is outside the document, + // `window.document.activeElement` doesn't change. + if ( + ! ref.contains( event.relatedTarget ) && + event.target !== window.document.activeElement + ) { + actions.closeSearchInput(); + } }, }, } ); diff --git a/packages/block-library/src/utils/remove-anchor-tag.js b/packages/block-library/src/utils/remove-anchor-tag.js new file mode 100644 index 00000000000000..31d1877082f50d --- /dev/null +++ b/packages/block-library/src/utils/remove-anchor-tag.js @@ -0,0 +1,10 @@ +/** + * Removes anchor tags from a string. + * + * @param {string} value The value to remove anchor tags from. + * + * @return {string} The value with anchor tags removed. + */ +export default function removeAnchorTag( value ) { + return value.replace( /<\/?a[^>]*>/g, '' ); +} diff --git a/packages/block-library/tsconfig.json b/packages/block-library/tsconfig.json index ddd88be5189a46..a9a30e9804e1f3 100644 --- a/packages/block-library/tsconfig.json +++ b/packages/block-library/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../html-entities" }, { "path": "../i18n" }, { "path": "../icons" }, + { "path": "../interactivity" }, { "path": "../notices" }, { "path": "../keycodes" }, { "path": "../primitives" }, diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index 7d8b2b6b582ae7..38fbb2b327d63b 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.46.0 (2023-11-16) + ## 4.45.0 (2023-11-02) ## 4.44.0 (2023-10-18) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 3072546e446d19..93fcef93acdd97 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.45.0", + "version": "4.46.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 f4a3dea0b491bf..5e44a000908e5d 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.46.0 (2023-11-16) + ## 4.45.0 (2023-11-02) ## 4.44.0 (2023-10-18) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 1056d871489c33..d5818b926323bc 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.45.0", + "version": "4.46.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 a0f9d61e0b4c2e..3b04d680cb3aa2 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.23.0 (2023-11-16) + ## 12.22.0 (2023-11-02) ## 12.21.0 (2023-10-18) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 813272be7ef6e3..414e40ca9458e7 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.22.0", + "version": "12.23.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -45,7 +45,6 @@ "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", - "deepmerge": "^4.3.0", "fast-deep-equal": "^3.1.3", "hpq": "^1.3.0", "is-plain-object": "^5.0.0", diff --git a/packages/blocks/src/api/raw-handling/image-corrector.js b/packages/blocks/src/api/raw-handling/image-corrector.js index d8de5e3a2ff50d..d3cf2ffb17486f 100644 --- a/packages/blocks/src/api/raw-handling/image-corrector.js +++ b/packages/blocks/src/api/raw-handling/image-corrector.js @@ -3,11 +3,6 @@ */ import { createBlobURL } from '@wordpress/blob'; -/** - * Browser dependencies - */ -const { atob, File } = window; - export default function imageCorrector( node ) { if ( node.nodeName !== 'IMG' ) { return; @@ -44,7 +39,7 @@ export default function imageCorrector( node ) { } const name = type.replace( '/', '.' ); - const file = new File( [ uint8Array ], name, { type } ); + const file = new window.File( [ uint8Array ], name, { type } ); node.src = createBlobURL( file ); } diff --git a/packages/blocks/src/api/raw-handling/ms-list-converter.js b/packages/blocks/src/api/raw-handling/ms-list-converter.js index fdbc48398a1cc6..03db53edc772ac 100644 --- a/packages/blocks/src/api/raw-handling/ms-list-converter.js +++ b/packages/blocks/src/api/raw-handling/ms-list-converter.js @@ -1,8 +1,3 @@ -/** - * Browser dependencies - */ -const { parseInt } = window; - /** * Internal dependencies */ diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js index 2f68a826931ab6..d0bf3e05979c63 100644 --- a/packages/blocks/src/api/raw-handling/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/paste-handler.js @@ -33,10 +33,7 @@ import { deepFilterHTML, isPlain, getBlockContentSchema } from './utils'; import emptyParagraphRemover from './empty-paragraph-remover'; import slackParagraphCorrector from './slack-paragraph-corrector'; -/** - * Browser dependencies - */ -const { console } = window; +const log = ( ...args ) => window?.console?.log?.( ...args ); /** * Filters HTML to only contain phrasing content. @@ -60,7 +57,7 @@ function filterInlineHTML( HTML ) { HTML = deepFilterHTML( HTML, [ htmlFormattingRemover, brRemover ] ); // Allows us to ask for this information when we get a report. - console.log( 'Processed inline HTML:\n\n', HTML ); + log( 'Processed inline HTML:\n\n', HTML ); return HTML; } @@ -214,7 +211,7 @@ export function pasteHandler( { ); // Allows us to ask for this information when we get a report. - console.log( 'Processed HTML piece:\n\n', piece ); + log( 'Processed HTML piece:\n\n', piece ); return htmlToBlocks( piece, pasteHandler ); } ) diff --git a/packages/blocks/src/api/raw-handling/utils.js b/packages/blocks/src/api/raw-handling/utils.js index 76818f2663627d..3f4fe32a1af248 100644 --- a/packages/blocks/src/api/raw-handling/utils.js +++ b/packages/blocks/src/api/raw-handling/utils.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import deepmerge from 'deepmerge'; - /** * WordPress dependencies */ @@ -14,41 +9,6 @@ import { isPhrasingContent, getPhrasingContentSchema } from '@wordpress/dom'; import { hasBlockSupport } from '..'; import { getRawTransforms } from './get-raw-transforms'; -const customMerge = ( key ) => { - return ( srcValue, objValue ) => { - switch ( key ) { - case 'children': { - if ( objValue === '*' || srcValue === '*' ) { - return '*'; - } - - return { ...objValue, ...srcValue }; - } - case 'attributes': - case 'require': { - return [ ...( objValue || [] ), ...( srcValue || [] ) ]; - } - case 'isMatch': { - // If one of the values being merge is undefined (matches everything), - // the result of the merge will be undefined. - if ( ! objValue || ! srcValue ) { - return undefined; - } - // When merging two isMatch functions, the result is a new function - // that returns if one of the source functions returns true. - return ( ...args ) => { - return objValue( ...args ) || srcValue( ...args ); - }; - } - } - - return deepmerge( objValue, srcValue, { - customMerge, - clone: false, - } ); - }; -}; - export function getBlockContentSchemaFromTransforms( transforms, context ) { const phrasingContentSchema = getPhrasingContentSchema( context ); const schemaArgs = { phrasingContentSchema, isPaste: context === 'paste' }; @@ -86,10 +46,56 @@ export function getBlockContentSchemaFromTransforms( transforms, context ) { ); } ); - return deepmerge.all( schemas, { - customMerge, - clone: false, - } ); + function mergeTagNameSchemaProperties( objValue, srcValue, key ) { + switch ( key ) { + case 'children': { + if ( objValue === '*' || srcValue === '*' ) { + return '*'; + } + + return { ...objValue, ...srcValue }; + } + case 'attributes': + case 'require': { + return [ ...( objValue || [] ), ...( srcValue || [] ) ]; + } + case 'isMatch': { + // If one of the values being merge is undefined (matches everything), + // the result of the merge will be undefined. + if ( ! objValue || ! srcValue ) { + return undefined; + } + // When merging two isMatch functions, the result is a new function + // that returns if one of the source functions returns true. + return ( ...args ) => { + return objValue( ...args ) || srcValue( ...args ); + }; + } + } + } + + // A tagName schema is an object with children, attributes, require, and + // isMatch properties. + function mergeTagNameSchemas( a, b ) { + for ( const key in b ) { + a[ key ] = a[ key ] + ? mergeTagNameSchemaProperties( a[ key ], b[ key ], key ) + : { ...b[ key ] }; + } + return a; + } + + // A schema is an object with tagName schemas by tag name. + function mergeSchemas( a, b ) { + for ( const key in b ) { + a[ key ] = a[ key ] + ? mergeTagNameSchemas( a[ key ], b[ key ] ) + : { ...b[ key ] }; + } + return a; + } + + return schemas.reduce( mergeSchemas, {} ); } /** diff --git a/packages/blocks/src/store/process-block-type.js b/packages/blocks/src/store/process-block-type.js index d69f9f8e5810fe..59b48979b07eb5 100644 --- a/packages/blocks/src/store/process-block-type.js +++ b/packages/blocks/src/store/process-block-type.js @@ -17,7 +17,8 @@ import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../api/constants'; /** @typedef {import('../api/registration').WPBlockType} WPBlockType */ -const { error, warn } = window.console; +const error = ( ...args ) => window?.console?.error?.( ...args ); +const warn = ( ...args ) => window?.console?.warn?.( ...args ); /** * Mapping of legacy category slugs to their latest normal values, used to diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index 28f88c40569bf8..1b32704cfd5776 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.29.0 (2023-11-16) + ## 5.28.0 (2023-11-02) ## 5.27.0 (2023-10-18) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index c7813290dedf5d..315594a8bda1bc 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.28.0", + "version": "5.29.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 a858b5af26543b..6641cb29ca6724 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.17.0 (2023-11-16) + ## 0.16.0 (2023-11-02) ## 0.15.0 (2023-10-18) diff --git a/packages/commands/package.json b/packages/commands/package.json index 0acd14f3de8ad7..3d18b9f47a7ed7 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.16.0", + "version": "0.17.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 a96c5dc5827c5c..08082f99a00b92 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,35 @@ ## Unreleased +### Enhancements + +- `Button`: Add focus rings to focusable disabled buttons ([#56383](https://github.com/WordPress/gutenberg/pull/56383)). + +### Bug Fix + +- `ToolsPanelItem`: Use useLayoutEffect to prevent rendering glitch for last panel item styling. ([#56536](https://github.com/WordPress/gutenberg/pull/56536)). +- `FormTokenField`: Fix broken suggestions scrollbar when the `__experimentalExpandOnFocus` prop is defined ([#56426](https://github.com/WordPress/gutenberg/pull/56426)). +- `FormTokenField`: `onFocus` prop is now typed as a React `FocusEvent` ([#56426](https://github.com/WordPress/gutenberg/pull/56426)). + +### Experimental + +- `Tabs`: Memoize and expose the component context ([#56224](https://github.com/WordPress/gutenberg/pull/56224)). +- `DropdownMenuV2`: Design tweaks ([#56041](https://github.com/WordPress/gutenberg/pull/56041)) + +### Internal + +- `Slot`: add `style` prop to `bubblesVirtually` version ([#56428](https://github.com/WordPress/gutenberg/pull/56428)) + +### Internal + +- Introduce experimental new version of `CustomSelectControl` based on `ariakit` ([#55790](https://github.com/WordPress/gutenberg/pull/55790)) + +## 25.12.0 (2023-11-16) + +### Bug Fix + +- `Toolbar`: Remove CSS rule that prevented focus outline to be visible for toolbar buttons in the `:active` state. ([#56123](https://github.com/WordPress/gutenberg/pull/56123)). + ### Internal - Migrate `Divider` from `reakit` to `ariakit` ([#55622](https://github.com/WordPress/gutenberg/pull/55622)) @@ -34,6 +63,7 @@ ### Bug Fix - `Autocomplete`: Add `aria-live` announcements for Mac and IOS Voiceover to fix lack of support for `aria-owns` ([#54902](https://github.com/WordPress/gutenberg/pull/54902)). +- Improve Button saving state accessibility. ([#55547](https://github.com/WordPress/gutenberg/pull/55547)) ### Internal diff --git a/packages/components/package.json b/packages/components/package.json index 2980553f5a2846..7e8c237b700244 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.11.0", + "version": "25.12.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/src/angle-picker-control/index.tsx b/packages/components/src/angle-picker-control/index.tsx index f90394b12078f4..06178e0b401015 100644 --- a/packages/components/src/angle-picker-control/index.tsx +++ b/packages/components/src/angle-picker-control/index.tsx @@ -41,7 +41,6 @@ function UnforwardedAnglePickerControl( 'Bottom margin styles for wp.components.AnglePickerControl', { since: '6.1', - version: '6.4', hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.', } ); diff --git a/packages/components/src/box-control/stories/index.story.js b/packages/components/src/box-control/stories/index.story.js deleted file mode 100644 index adbd0e15f7c441..00000000000000 --- a/packages/components/src/box-control/stories/index.story.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import BoxControl from '../'; - -export default { - title: 'Components (Experimental)/BoxControl', - component: BoxControl, -}; - -export const _default = () => { - return ; -}; - -const defaultSideValues = { - top: '10px', - right: '10px', - bottom: '10px', - left: '10px', -}; - -function DemoExample( { - sides, - defaultValues = defaultSideValues, - splitOnAxis = false, -} ) { - const [ values, setValues ] = useState( defaultValues ); - - return ( - - ); -} - -export const ArbitrarySides = () => { - return ( - - ); -}; - -export const SingleSide = () => { - return ( - - ); -}; - -export const AxialControls = () => { - return ; -}; - -export const AxialControlsWithSingleSide = () => { - return ( - - ); -}; diff --git a/packages/components/src/box-control/stories/index.story.tsx b/packages/components/src/box-control/stories/index.story.tsx new file mode 100644 index 00000000000000..1b6604048f6d52 --- /dev/null +++ b/packages/components/src/box-control/stories/index.story.tsx @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import BoxControl from '../'; + +const meta: Meta< typeof BoxControl > = { + title: 'Components (Experimental)/BoxControl', + component: BoxControl, + argTypes: { + values: { control: { type: null } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + docs: { canvas: { sourceState: 'shown' } }, + }, +}; +export default meta; + +const TemplateUncontrolled: StoryFn< typeof BoxControl > = ( props ) => { + return ; +}; + +const TemplateControlled: StoryFn< typeof BoxControl > = ( props ) => { + const [ values, setValues ] = useState< ( typeof props )[ 'values' ] >(); + + return ( + { + setValues( nextValue ); + props.onChange?.( nextValue ); + } } + /> + ); +}; + +export const Default = TemplateUncontrolled.bind( {} ); +Default.args = { + label: 'Label', +}; + +export const Controlled = TemplateControlled.bind( {} ); +Controlled.args = { + ...Default.args, +}; + +export const ArbitrarySides = TemplateControlled.bind( {} ); +ArbitrarySides.args = { + ...Default.args, + sides: [ 'top', 'bottom' ], +}; + +export const SingleSide = TemplateControlled.bind( {} ); +SingleSide.args = { + ...Default.args, + sides: [ 'bottom' ], +}; + +export const AxialControls = TemplateControlled.bind( {} ); +AxialControls.args = { + ...Default.args, + splitOnAxis: true, +}; + +export const AxialControlsWithSingleSide = TemplateControlled.bind( {} ); +AxialControlsWithSingleSide.args = { + ...Default.args, + sides: [ 'horizontal' ], + splitOnAxis: true, +}; diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 03273056cfa179..0af5144d3d4b7f 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -129,8 +129,11 @@ background: transparent; transform: none; opacity: 1; - box-shadow: none; - outline: none; + + &:not(:focus) { + box-shadow: none; + outline: none; + } } } @@ -242,6 +245,11 @@ &.is-secondary.is-busy:disabled, &.is-secondary.is-busy[aria-disabled="true"] { animation: components-button__busy-animation 2500ms infinite linear; + // This should be refactored to use the reduce-motion("animation") mixin + // as soon as https://github.com/WordPress/gutenberg/issues/55566 is closed. + @media (prefers-reduced-motion: reduce) { + animation-duration: 0s; + } opacity: 1; background-size: 100px 100%; // Disable reason: This function call looks nicer when each argument is on its own line. diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index 30f1f47e653e87..cc15248678d275 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -47,9 +47,7 @@ function MyComboboxControl() { onFilterValueChange={ ( inputValue ) => setFilteredOptions( options.filter( ( option ) => - option.label - .toLowerCase() - .startsWith( inputValue.toLowerCase() ) + option.value === inputValue ) ) } diff --git a/packages/components/src/custom-select-control-v2/README.md b/packages/components/src/custom-select-control-v2/README.md new file mode 100644 index 00000000000000..3cd9c3f8534e76 --- /dev/null +++ b/packages/components/src/custom-select-control-v2/README.md @@ -0,0 +1,73 @@ +
    +This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
    + +### `CustomSelect` + +Used to render a customizable select control component. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The child elements. This should be composed of CustomSelect.Item components. + +- Required: yes + +##### `defaultValue`: `string` + +An optional default value for the control. If left `undefined`, the first non-disabled item will be used. + +- Required: no + +##### `label`: `string` + +Label for the control. + +- Required: yes + +##### `onChange`: `( newValue: string ) => void` + +A function that receives the new value of the input. + +- Required: no + +##### `renderSelectedValue`: `( selectValue: string ) => React.ReactNode` + +Can be used to render select UI with custom styled values. + +- Required: no + +##### `size`: `'default' | 'large'` + +The size of the control. + +- Required: no + +##### `value`: `string` + +Can be used to externally control the value of the control. + +- Required: no + +### `CustomSelectItem` + +Used to render a select item. + +#### Props + +The component accepts the following props: + +##### `value`: `string` + +The value of the select item. This will be used as the children if children are left `undefined`. + +- Required: yes + +##### `children`: `React.ReactNode` + +The children to display for each select item. The `value` will be used if left `undefined`. + +- Required: no diff --git a/packages/components/src/custom-select-control-v2/index.tsx b/packages/components/src/custom-select-control-v2/index.tsx new file mode 100644 index 00000000000000..88231078fa8d56 --- /dev/null +++ b/packages/components/src/custom-select-control-v2/index.tsx @@ -0,0 +1,99 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import * as Styled from './styles'; +import type { + CustomSelectProps, + CustomSelectItemProps, + CustomSelectContext as CustomSelectContextType, +} from './types'; + +export const CustomSelectContext = + createContext< CustomSelectContextType >( undefined ); + +function defaultRenderSelectedValue( value: CustomSelectProps[ 'value' ] ) { + const isValueEmpty = Array.isArray( value ) + ? value.length === 0 + : value === undefined || value === null; + + if ( isValueEmpty ) { + return __( 'Select an item' ); + } + + if ( Array.isArray( value ) ) { + return value.length === 1 + ? value[ 0 ] + : // translators: %s: number of items selected (it will always be 2 or more items) + sprintf( __( '%s items selected' ), value.length ); + } + + return value; +} + +export function CustomSelect( props: CustomSelectProps ) { + const { + children, + defaultValue, + label, + onChange, + size = 'default', + value, + renderSelectedValue = defaultRenderSelectedValue, + } = props; + + const store = Ariakit.useSelectStore( { + setValue: ( nextValue ) => onChange?.( nextValue ), + defaultValue, + value, + } ); + + const { value: currentValue } = store.useState(); + + return ( + <> + + { label } + + + { renderSelectedValue( currentValue ) } + + + + + { children } + + + + ); +} + +export function CustomSelectItem( { + children, + ...props +}: CustomSelectItemProps ) { + const customSelectContext = useContext( CustomSelectContext ); + return ( + + { children ?? props.value } + + + ); +} diff --git a/packages/components/src/custom-select-control-v2/stories/index.story.tsx b/packages/components/src/custom-select-control-v2/stories/index.story.tsx new file mode 100644 index 00000000000000..2c7ae3507046bc --- /dev/null +++ b/packages/components/src/custom-select-control-v2/stories/index.story.tsx @@ -0,0 +1,149 @@ +/** + * External dependencies + */ +import type { Meta, StoryFn } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { CustomSelect, CustomSelectItem } from '..'; + +const meta: Meta< typeof CustomSelect > = { + title: 'Components (Experimental)/CustomSelectControl v2', + component: CustomSelect, + subcomponents: { + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + CustomSelectItem, + }, + argTypes: { + children: { control: { type: null } }, + renderSelectedValue: { control: { type: null } }, + value: { control: { type: null } }, + }, + parameters: { + actions: { argTypesRegex: '^on.*' }, + controls: { expanded: true }, + docs: { + canvas: { sourceState: 'shown' }, + source: { excludeDecorators: true }, + }, + }, + decorators: [ + ( Story ) => ( +
    + +
    + ), + ], +}; +export default meta; + +const Template: StoryFn< typeof CustomSelect > = ( props ) => { + return ; +}; + +const ControlledTemplate: StoryFn< typeof CustomSelect > = ( props ) => { + const [ value, setValue ] = useState< string | string[] >(); + return ( + { + setValue( nextValue ); + props.onChange?.( nextValue ); + } } + value={ value } + /> + ); +}; + +export const Default = Template.bind( {} ); +Default.args = { + label: 'Label', + children: ( + <> + + Small + + + Something bigger + + + ), +}; + +/** + * Multiple selection can be enabled by using an array for the `value` and + * `defaultValue` props. The argument of the `onChange` function will also + * change accordingly. + */ +export const MultiSelect = Template.bind( {} ); +MultiSelect.args = { + defaultValue: [ 'lavender', 'tangerine' ], + label: 'Select Colors', + renderSelectedValue: ( currentValue: string | string[] ) => { + if ( ! Array.isArray( currentValue ) ) { + return currentValue; + } + if ( currentValue.length === 0 ) return 'No colors selected'; + if ( currentValue.length === 1 ) return currentValue[ 0 ]; + return `${ currentValue.length } colors selected`; + }, + children: ( + <> + { [ + 'amber', + 'aquamarine', + 'flamingo pink', + 'lavender', + 'maroon', + 'tangerine', + ].map( ( item ) => ( + + { item } + + ) ) } + + ), +}; + +const renderControlledValue = ( gravatar: string | string[] ) => { + const avatar = `https://gravatar.com/avatar?d=${ gravatar }`; + return ( +
    + + { gravatar } +
    + ); +}; + +export const Controlled = ControlledTemplate.bind( {} ); +Controlled.args = { + label: 'Default Gravatars', + renderSelectedValue: renderControlledValue, + children: ( + <> + { [ 'mystery-person', 'identicon', 'wavatar', 'retro' ].map( + ( option ) => ( + + { renderControlledValue( option ) } + + ) + ) } + + ), +}; diff --git a/packages/components/src/custom-select-control-v2/styles.ts b/packages/components/src/custom-select-control-v2/styles.ts new file mode 100644 index 00000000000000..c04f6ac32e5ffb --- /dev/null +++ b/packages/components/src/custom-select-control-v2/styles.ts @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import styled from '@emotion/styled'; +// eslint-disable-next-line no-restricted-imports +import * as Ariakit from '@ariakit/react'; + +/** + * Internal dependencies + */ +import { COLORS } from '../utils'; +import { space } from '../utils/space'; +import type { CustomSelectProps } from './types'; + +export const CustomSelectLabel = styled( Ariakit.SelectLabel )` + font-size: 11px; + font-weight: 500; + line-height: 1.4; + text-transform: uppercase; + margin-bottom: ${ space( 2 ) }; +`; + +const inputHeights = { + default: 40, + small: 24, +}; + +export const CustomSelectButton = styled( Ariakit.Select, { + // Do not forward `hasCustomRenderProp` to the underlying Ariakit.Select component + shouldForwardProp: ( prop ) => prop !== 'hasCustomRenderProp', +} )( ( { + size, + hasCustomRenderProp, +}: { + size: NonNullable< CustomSelectProps[ 'size' ] >; + hasCustomRenderProp: boolean; +} ) => { + const isSmallSize = size === 'small' && ! hasCustomRenderProp; + const heightProperty = hasCustomRenderProp ? 'minHeight' : 'height'; + + return { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: COLORS.white, + border: `1px solid ${ COLORS.gray[ 600 ] }`, + borderRadius: space( 0.5 ), + cursor: 'pointer', + width: '100%', + [ heightProperty ]: `${ inputHeights[ size ] }px`, + padding: isSmallSize ? space( 2 ) : space( 4 ), + fontSize: isSmallSize ? '11px' : '13px', + '&[data-focus-visible]': { + outlineStyle: 'solid', + }, + '&[aria-expanded="true"]': { + outlineStyle: `1.5px solid ${ COLORS.theme.accent }`, + }, + }; +} ); + +export const CustomSelectPopover = styled( Ariakit.SelectPopover )` + border-radius: ${ space( 0.5 ) }; + background: ${ COLORS.white }; + border: 1px solid ${ COLORS.gray[ 900 ] }; +`; + +export const CustomSelectItem = styled( Ariakit.SelectItem )` + display: flex; + align-items: center; + justify-content: space-between; + padding: ${ space( 2 ) }; + &[data-active-item] { + background-color: ${ COLORS.gray[ 300 ] }; + } +`; diff --git a/packages/components/src/custom-select-control-v2/types.ts b/packages/components/src/custom-select-control-v2/types.ts new file mode 100644 index 00000000000000..2aecc1d4746f5c --- /dev/null +++ b/packages/components/src/custom-select-control-v2/types.ts @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +// eslint-disable-next-line no-restricted-imports +import type * as Ariakit from '@ariakit/react'; + +export type CustomSelectContext = + | { + /** + * The store object returned by Ariakit's `useSelectStore` hook. + */ + store: Ariakit.SelectStore; + } + | undefined; + +export type CustomSelectProps = { + /** + * The child elements. This should be composed of CustomSelectItem components. + */ + children: React.ReactNode; + /** + * An optional default value for the control. If left `undefined`, the first + * non-disabled item will be used. + */ + defaultValue?: string | string[]; + /** + * Label for the control. + */ + label: string; + /** + * A function that receives the new value of the input. + */ + onChange?: ( newValue: string | string[] ) => void; + /** + * Can be used to render select UI with custom styled values. + */ + renderSelectedValue?: ( + selectedValue: string | string[] + ) => React.ReactNode; + /** + * The size of the control. + * + * @default 'default' + */ + size?: 'default' | 'small'; + /** + * Can be used to externally control the value of the control. + */ + value?: string | string[]; +}; + +export type CustomSelectItemProps = { + /** + * The value of the select item. This will be used as the children if + * children are left `undefined`. + */ + value: string; + /** + * The children to display for each select item. The `value` will be + * used if left `undefined`. + */ + children?: React.ReactNode; +}; diff --git a/packages/components/src/dropdown-menu-v2-ariakit/README.md b/packages/components/src/dropdown-menu-v2-ariakit/README.md index f74098efff4103..2902b541169766 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/README.md +++ b/packages/components/src/dropdown-menu-v2-ariakit/README.md @@ -284,9 +284,9 @@ Event handler called when the checked radio menu item changes. - Required: no -### `DropdownMenuGroup` +### `DropdownMenuItemLabel` -Used to group menu items. +Used to render the menu item's label. #### Props @@ -294,13 +294,27 @@ The component accepts the following props: ##### `children`: `React.ReactNode` -The contents of the group. +The label contents. + +- Required: yes + +### `DropdownMenuItemHelpText` + +Used to render the menu item's help text. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The help text contents. - Required: yes -### `DropdownMenuGroupLabel` +### `DropdownMenuGroup` -Used to render a group label. +Used to group menu items. #### Props diff --git a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx index 10b93d8c552c13..37d4a1f9cfcc5e 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/index.tsx +++ b/packages/components/src/dropdown-menu-v2-ariakit/index.tsx @@ -30,7 +30,6 @@ import type { DropdownMenuContext as DropdownMenuContextType, DropdownMenuProps, DropdownMenuGroupProps, - DropdownMenuGroupLabelProps, DropdownMenuItemProps, DropdownMenuCheckboxItemProps, DropdownMenuRadioItemProps, @@ -55,16 +54,23 @@ export const DropdownMenuItem = forwardRef< - { prefix && ( - { prefix } - ) } - { children } - { suffix && ( - { suffix } - ) } + { prefix } + + + + { children } + + + { suffix && ( + + { suffix } + + ) } + ); } ); @@ -82,20 +88,30 @@ export const DropdownMenuCheckboxItem = forwardRef< } + // Override some ariakit inline styles + style={ { width: 'auto', height: 'auto' } } > - { children } - { suffix && ( - { suffix } - ) } + + + { children } + + + { suffix && ( + + { suffix } + + ) } + ); } ); @@ -119,17 +135,30 @@ export const DropdownMenuRadioItem = forwardRef< } + // Override some ariakit inline styles + style={ { width: 'auto', height: 'auto' } } > - { children } - { suffix } + + + + { children } + + + { suffix && ( + + { suffix } + + ) } + ); } ); @@ -148,20 +177,6 @@ export const DropdownMenuGroup = forwardRef< ); } ); -export const DropdownMenuGroupLabel = forwardRef< - HTMLDivElement, - WordPressComponentProps< DropdownMenuGroupLabelProps, 'div', false > ->( function DropdownMenuGroupLabel( props, ref ) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - return ( - - ); -} ); - const UnconnectedDropdownMenu = ( props: WordPressComponentProps< DropdownMenuProps, 'div', false >, ref: React.ForwardedRef< HTMLDivElement > @@ -280,12 +295,16 @@ const UnconnectedDropdownMenu = ( dropdownMenuStore.parent ? cloneElement( trigger, { // Add submenu arrow, unless a `suffix` is explicitly specified - suffix: trigger.props.suffix ?? ( -
  • { row.getVisibleCells().map( ( cell ) => (
    { const { getEditedPostContext, @@ -99,17 +112,27 @@ export default function Editor( { listViewToggleElement, isLoading } ) { getCanvasMode, isInserterOpened, isListViewOpened, - hasPageContentFocus: _hasPageContentFocus, } = unlock( select( editSiteStore ) ); const { __unstableGetEditorMode } = select( blockEditorStore ); const { getActiveComplementaryArea } = select( interfaceStore ); + const { getEntityRecord } = select( coreDataStore ); + const { getRenderingMode } = select( editorStore ); + const _context = getEditedPostContext(); // The currently selected entity to display. // Typically template or template part in the site editor. return { - context: getEditedPostContext(), + context: _context, + contextPost: _context?.postId + ? getEntityRecord( + 'postType', + _context.postType, + _context.postId + ) + : undefined, editorMode: getEditorMode(), canvasMode: getCanvasMode(), + renderingMode: getRenderingMode(), blockEditorMode: __unstableGetEditorMode(), isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), @@ -124,9 +147,9 @@ export default function Editor( { listViewToggleElement, isLoading } ) { 'core/edit-site', 'showBlockBreadcrumbs' ), - hasPageContentFocus: _hasPageContentFocus(), }; }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); const isViewMode = canvasMode === 'view'; const isEditMode = canvasMode === 'edit'; @@ -141,18 +164,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) { const secondarySidebarLabel = isListViewOpen ? __( 'List View' ) : __( 'Block Library' ); - const blockContext = useMemo( () => { - const { postType, postId, ...nonPostFields } = context ?? {}; - - return { - ...( hasPageContentFocus ? context : nonPostFields ), - // Ideally this context should be removed. However, it is currently used by the Query Loop block. - templateSlug: - editedPostType === TEMPLATE_POST_TYPE - ? editedPost.slug - : undefined, - }; - }, [ editedPost.slug, editedPostType, hasPageContentFocus, context ] ); + const postWithTemplate = !! context?.postId; let title; if ( hasLoadedPost ) { @@ -174,110 +186,116 @@ export default function Editor( { listViewToggleElement, isLoading } ) { 'edit-site-editor__loading-progress' ); - const contentProps = isLoading - ? { - 'aria-busy': 'true', - 'aria-describedby': loadingProgressId, - } - : undefined; + const settings = useSpecificEditorSettings(); + const isReady = + ! isLoading && + ( ( postWithTemplate && !! contextPost && !! editedPost ) || + ( ! postWithTemplate && !! editedPost ) ); + + // This is the only reliable way I've found to reinitialize the rendering mode + // when the canvas mode or the edited entity changes. + useEffect( () => { + if ( canvasMode === 'edit' && postWithTemplate ) { + setRenderingMode( 'template-locked' ); + } else { + setRenderingMode( 'all' ); + } + }, [ canvasMode, postWithTemplate, setRenderingMode ] ); return ( <> - { isLoading ? : null } + { ! isReady ? : null } { isEditMode && } - - + { __( + "You attempted to edit an item that doesn't exist. Perhaps it was deleted?" + ) } + + ) } + { isReady && ( + - - - { isEditMode && } - } - content={ - <> - - { isEditMode && } - { showVisualEditor && editedPost && ( - <> - - - - - ) } - { editorMode === 'text' && - editedPost && - isEditMode && } - { hasLoadedPost && ! editedPost && ( - - { __( - "You attempted to edit an item that doesn't exist. Perhaps it was deleted?" - ) } - - ) } - { isEditMode && ( - - ) } - + + { isEditMode && } + - ) ) || - ( shouldShowListView && ( - - ) ) ) - } - sidebar={ - isEditMode && - isRightSidebarOpen && ( + ) } + notices={ } + content={ + <> + + { isEditMode && } + { showVisualEditor && ( <> - - + + + + + + + - ) - } - footer={ - shouldShowBlockBreadcrumbs && ( - + ) } + { isEditMode && } + + } + secondarySidebar={ + isEditMode && + ( ( shouldShowInserter && ) || + ( shouldShowListView && ( + - ) - } - labels={ { - ...interfaceLabels, - secondarySidebar: secondarySidebarLabel, - } } - /> - - - + ) ) ) + } + sidebar={ + isEditMode && + isRightSidebarOpen && ( + <> + + + + ) + } + footer={ + shouldShowBlockBreadcrumbs && ( + + ) + } + labels={ { + ...interfaceLabels, + secondarySidebar: secondarySidebarLabel, + } } + /> + + ) } ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js index a3ffd31db2288d..e21e72c58ed533 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/get-intersecting-font-faces.js @@ -47,7 +47,7 @@ export default function getIntersectingFontFaces( incoming, existing ) { } ); } ); - matches.push( { ...existingFont, fontFace: matchingFaces } ); + matches.push( { ...incomingFont, fontFace: matchingFaces } ); } else { matches.push( incomingFont ); } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index d0a57978bcce94..f5723f5814e983 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -1,8 +1,12 @@ +/** + * External dependencies + */ +import { paramCase as kebabCase } from 'change-case'; + /** * Internal dependencies */ import { FONT_WEIGHTS, FONT_STYLES } from './constants'; -import { formatFontFamily } from './preview-styles'; export function setUIValuesNeeded( font, extraValues = {} ) { if ( ! font.name && ( font.fontFamily || font.slug ) ) { @@ -85,14 +89,10 @@ export async function loadFontFaceInBrowser( fontFace, source, addTo = 'all' ) { } // eslint-disable-next-line no-undef - const newFont = new FontFace( - formatFontFamily( fontFace.fontFamily ), - dataSource, - { - style: fontFace.fontStyle, - weight: fontFace.fontWeight, - } - ); + const newFont = new FontFace( fontFace.fontFamily, dataSource, { + style: fontFace.fontStyle, + weight: fontFace.fontWeight, + } ); const loadedFace = await newFont.load(); @@ -129,9 +129,20 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } +// This function replicates one behavior of _wp_to_kebab_case(). +// Additional context: https://github.com/WordPress/gutenberg/issues/53695 +export function wpKebabCase( str ) { + // If a string contains a digit followed by a number, insert a dash between them. + return kebabCase( str ).replace( + /([a-zA-Z])(\d)|(\d)([a-zA-Z])/g, + '$1$3-$2$4' + ); +} + export function makeFormDataFromFontFamilies( fontFamilies ) { const formData = new FormData(); const newFontFamilies = fontFamilies.map( ( family, familyIndex ) => { + family.slug = wpKebabCase( family.slug ); if ( family?.fontFace ) { family.fontFace = family.fontFace.map( ( face, faceIndex ) => { if ( face.file ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js index 91ae5f45d66da6..9899005ad65b89 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/getIntersectingFontFaces.spec.js @@ -5,7 +5,7 @@ import getIntersectingFontFaces from '../get-intersecting-font-faces'; describe( 'getIntersectingFontFaces', () => { it( 'returns matching font faces for matching font family', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -30,15 +30,15 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); - expect( result ).toEqual( intendedFontsFamilies ); + expect( result ).toEqual( incomingFontFamilies ); } ); it( 'returns empty array when there is no match', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -63,7 +63,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -71,7 +71,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns matching font faces', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -129,7 +129,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -137,7 +137,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns empty array when the first list is empty', () => { - const intendedFontsFamilies = []; + const incomingFontFamilies = []; const existingFontFamilies = [ { @@ -152,7 +152,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -160,7 +160,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns empty array when the second list is empty', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'lobster', fontFace: [ @@ -175,7 +175,7 @@ describe( 'getIntersectingFontFaces', () => { const existingFontFamilies = []; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -183,7 +183,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns intersecting font family when there are no fonfaces', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'piazzolla', fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], @@ -200,7 +200,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); @@ -208,7 +208,7 @@ describe( 'getIntersectingFontFaces', () => { } ); it( 'returns intersecting if there is an intended font face and is not present in the returning it should not be returned', () => { - const intendedFontsFamilies = [ + const incomingFontFamilies = [ { slug: 'piazzolla', fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], @@ -226,7 +226,7 @@ describe( 'getIntersectingFontFaces', () => { ]; const result = getIntersectingFontFaces( - intendedFontsFamilies, + incomingFontFamilies, existingFontFamilies ); const expected = [ @@ -237,4 +237,35 @@ describe( 'getIntersectingFontFaces', () => { ]; expect( result ).toEqual( expected ); } ); + + it( 'updates font family definition using the incoming data', () => { + const incomingFontFamilies = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: "'Gothic A1', serif", + }, + ]; + + const existingFontFamilies = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: 'Gothic A1, serif', + }, + ]; + + const result = getIntersectingFontFaces( + incomingFontFamilies, + existingFontFamilies + ); + const expected = [ + { + slug: 'gothic-a1', + fontFace: [ { fontStyle: 'normal', fontWeight: '400' } ], + fontFamily: "'Gothic A1', serif", + }, + ]; + expect( result ).toEqual( expected ); + } ); } ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js new file mode 100644 index 00000000000000..d296117ff3a49b --- /dev/null +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import { wpKebabCase } from '../index'; + +describe( 'wpKebabCase', () => { + it( 'should insert a dash between a letter and a digit', () => { + const input = 'abc1def'; + const expectedOutput = 'abc-1def'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + + const input2 = 'abc1def2ghi'; + const expectedOutput2 = 'abc-1def-2ghi'; + expect( wpKebabCase( input2 ) ).toEqual( expectedOutput2 ); + } ); + + it( 'should not insert a dash between two letters', () => { + const input = 'abcdef'; + const expectedOutput = 'abcdef'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + } ); + + it( 'should not insert a dash between a digit and a hyphen', () => { + const input = 'abc1-def'; + const expectedOutput = 'abc-1-def'; + expect( wpKebabCase( input ) ).toEqual( expectedOutput ); + } ); +} ); diff --git a/packages/edit-site/src/components/global-styles/header.js b/packages/edit-site/src/components/global-styles/header.js index f62820653ff925..e6da4115217f57 100644 --- a/packages/edit-site/src/components/global-styles/header.js +++ b/packages/edit-site/src/components/global-styles/header.js @@ -12,7 +12,7 @@ import { import { isRTL, __ } from '@wordpress/i18n'; import { chevronRight, chevronLeft } from '@wordpress/icons'; -function ScreenHeader( { title, description } ) { +function ScreenHeader( { title, description, onBack } ) { return ( @@ -27,6 +27,7 @@ function ScreenHeader( { title, description } ) { icon={ isRTL() ? chevronRight : chevronLeft } isSmall aria-label={ __( 'Navigate to the previous view' ) } + onClick={ onBack } /> select( blocksStore ).isMatchingSearchTerm, - [] - ); - const filteredBlockTypes = useMemo( () => { - if ( ! filterValue ) { - return sortedBlockTypes; - } - return sortedBlockTypes.filter( ( blockType ) => - isMatchingSearchTerm( blockType, filterValue ) - ); - }, [ filterValue, sortedBlockTypes, isMatchingSearchTerm ] ); + const { isMatchingSearchTerm } = useSelect( blocksStore ); + + const filteredBlockTypes = ! filterValue + ? sortedBlockTypes + : sortedBlockTypes.filter( ( blockType ) => + isMatchingSearchTerm( blockType, filterValue ) + ); const blockTypesListRef = useRef(); @@ -140,6 +140,27 @@ function ScreenBlockList() { debouncedSpeak( resultsFoundMessage, count ); }, [ filterValue, debouncedSpeak ] ); + return ( +
    + { filteredBlockTypes.map( ( block ) => ( + + ) ) } +
    + ); +} + +const MemoizedBlockList = memo( BlockList ); + +function ScreenBlockList() { + const [ filterValue, setFilterValue ] = useState( '' ); + const deferredFilterValue = useDeferredValue( filterValue ); + return ( <> -
    - { filteredBlockTypes.map( ( block ) => ( - - ) ) } -
    + ); } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index d5c884f9d20cfe..90bf68e579cb7c 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Button, __experimentalUseNavigator as useNavigator, @@ -10,6 +10,7 @@ import { __experimentalSpacer as Spacer, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; import { useContext, useState, useEffect } from '@wordpress/element'; import { privateApis as blockEditorPrivateApis, @@ -35,14 +36,36 @@ function ScreenRevisions() { const { goTo } = useNavigator(); const { user: currentEditorGlobalStyles, setUserConfig } = useContext( GlobalStylesContext ); - const { blocks, editorCanvasContainerView } = useSelect( ( select ) => { - return { - editorCanvasContainerView: unlock( - select( editSiteStore ) - ).getEditorCanvasContainerView(), - blocks: select( blockEditorStore ).getBlocks(), - }; - }, [] ); + const { blocks, editorCanvasContainerView, revisionsCount } = useSelect( + ( select ) => { + const { + getEntityRecord, + __experimentalGetCurrentGlobalStylesId, + __experimentalGetDirtyEntityRecords, + } = select( coreStore ); + const isDirty = __experimentalGetDirtyEntityRecords().length > 0; + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + const globalStyles = globalStylesId + ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) + : undefined; + let _revisionsCount = + globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count || 0; + // one for the reset item. + _revisionsCount++; + // one for any dirty changes (unsaved). + if ( isDirty ) { + _revisionsCount++; + } + return { + editorCanvasContainerView: unlock( + select( editSiteStore ) + ).getEditorCanvasContainerView(), + blocks: select( blockEditorStore ).getBlocks(), + revisionsCount: _revisionsCount, + }; + }, + [] + ); const { revisions, isLoading, hasUnsavedChanges } = useGlobalStylesRevisions(); const [ currentlySelectedRevision, setCurrentlySelectedRevision ] = @@ -61,6 +84,7 @@ function ScreenRevisions() { const onCloseRevisions = () => { goTo( '/' ); // Return to global styles main panel. + setEditorCanvasContainerView( undefined ); }; const restoreRevision = ( revision ) => { @@ -119,10 +143,15 @@ function ScreenRevisions() { return ( <> { isLoading && ( diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index feec0f25ac8823..2786bf6d791212 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -12,20 +12,28 @@ import { dateI18n, getDate, humanTimeDiff, getSettings } from '@wordpress/date'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +const DAY_IN_MILLISECONDS = 60 * 60 * 1000 * 24; + /** * Returns a button label for the revision. * - * @param {Object} revision A revision object. + * @param {string|number} id A revision object. + * @param {boolean} isLatest Whether the revision is the most current. + * @param {string} authorDisplayName Author name. + * @param {string} formattedModifiedDate Revision modified date formatted. * @return {string} Translated label. */ -function getRevisionLabel( revision ) { - const authorDisplayName = revision?.author?.name || __( 'User' ); - - if ( 'parent' === revision?.id ) { +function getRevisionLabel( + id, + isLatest, + authorDisplayName, + formattedModifiedDate +) { + if ( 'parent' === id ) { return __( 'Reset the styles to the theme defaults' ); } - if ( 'unsaved' === revision?.id ) { + if ( 'unsaved' === id ) { return sprintf( /* translators: %s author display name */ __( 'Unsaved changes by %s' ), @@ -33,23 +41,18 @@ function getRevisionLabel( revision ) { ); } - const formattedDate = dateI18n( - getSettings().formats.datetimeAbbreviated, - getDate( revision?.modified ) - ); - - return revision?.isLatest + return isLatest ? sprintf( /* translators: %1$s author display name, %2$s: revision creation date */ __( 'Changes saved by %1$s on %2$s (current)' ), authorDisplayName, - formattedDate + formattedModifiedDate ) : sprintf( /* translators: %1$s author display name, %2$s: revision creation date */ __( 'Changes saved by %1$s on %2$s' ), authorDisplayName, - formattedDate + formattedModifiedDate ); } @@ -65,10 +68,18 @@ function getRevisionLabel( revision ) { * @return {JSX.Element} The modal component. */ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { - const currentTheme = useSelect( - ( select ) => select( coreStore ).getCurrentTheme(), - [] - ); + const { currentThemeName, currentUser } = useSelect( ( select ) => { + const { getCurrentTheme, getCurrentUser } = select( coreStore ); + const currentTheme = getCurrentTheme(); + return { + currentThemeName: + currentTheme?.name?.rendered || currentTheme?.stylesheet, + currentUser: getCurrentUser(), + }; + }, [] ); + const dateNowInMs = getDate().getTime(); + const { date: dateFormat, datetimeAbbreviated } = getSettings().formats; + return (
      { userRevisions.map( ( revision, index ) => { - const { id, author, modified } = revision; - const authorDisplayName = author?.name || __( 'User' ); - const authorAvatar = author?.avatar_urls?.[ '48' ]; - const isUnsaved = 'unsaved' === revision?.id; + const { id, isLatest, author, modified } = revision; + const isUnsaved = 'unsaved' === id; + // Unsaved changes are created by the current user. + const revisionAuthor = isUnsaved ? currentUser : author; + const authorDisplayName = revisionAuthor?.name || __( 'User' ); + const authorAvatar = revisionAuthor?.avatar_urls?.[ '48' ]; const isSelected = selectedRevisionId - ? selectedRevisionId === revision?.id + ? selectedRevisionId === id : index === 0; - const isReset = 'parent' === revision?.id; + const isReset = 'parent' === id; + const modifiedDate = getDate( modified ); + const displayDate = + modified && + dateNowInMs - modifiedDate.getTime() > DAY_IN_MILLISECONDS + ? dateI18n( dateFormat, modifiedDate ) + : humanTimeDiff( modified ); + const revisionLabel = getRevisionLabel( + id, + isLatest, + authorDisplayName, + dateI18n( datetimeAbbreviated, modifiedDate ) + ); return (
    1. { onChange( revision ); } } - label={ getRevisionLabel( revision ) } + label={ revisionLabel } > { isReset ? ( { __( 'Default styles' ) } - { currentTheme?.name?.rendered || - currentTheme?.stylesheet } + { currentThemeName } ) : ( - + { isUnsaved ? ( + + { __( '(Unsaved)' ) } + + ) : ( + + ) } - { isUnsaved - ? sprintf( - /* translators: %s author display name */ - __( - 'Unsaved changes by %s' - ), - authorDisplayName - ) - : sprintf( - /* translators: %s author display name */ - __( 'Changes saved by %s' ), - authorDisplayName - ) } - { + { authorDisplayName } ) } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index 238f3f7d116e19..6598fcb5ce1c74 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -8,62 +8,80 @@ margin: 0; li { margin-bottom: 0; - border-left: 1px solid $gray-300; } } .edit-site-global-styles-screen-revisions__revision-item { position: relative; - padding: $grid-unit-10 0 $grid-unit-10 $grid-unit-15; + padding-left: $grid-unit-20; + overflow: hidden; + cursor: pointer; - &:first-child { - padding-top: 0; + &:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + .edit-site-global-styles-screen-revisions__date { + color: var(--wp-admin-theme-color); + } } - &:last-child { - padding-bottom: 0; + &::before, + &::after { + position: absolute; + content: "\a"; + display: block; } &::before { background: $gray-300; border-radius: 50%; - content: "\a"; - display: inline-block; height: $grid-unit-10; width: $grid-unit-10; - position: absolute; - top: 50%; - left: 0; + top: $grid-unit-20 + 2; + left: $grid-unit-20 + 1; // So the circle is centered on the line. transform: translate(-50%, -50%); + z-index: 1; } &.is-selected::before { background: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); } -} -.edit-site-global-styles-screen-revisions__revision-button { - width: 100%; - height: auto; - display: block; - padding: $grid-unit-10 $grid-unit-15; + &::after { + height: 100%; + left: $grid-unit-20; + top: 0; + width: 0; + border: 0.5px solid $gray-300; + } - &:hover { - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + &:first-child::after { + top: $grid-unit-20 + 2; + } - .edit-site-global-styles-screen-revisions__date { - color: var(--wp-admin-theme-color); + &:last-child::after { + height: $grid-unit-20 + 2; + } + + // Nested to override specificity of .components-button. + .edit-site-global-styles-screen-revisions__revision-button { + width: 100%; + height: auto; + display: block; + padding: $grid-unit-15 $grid-unit-15 $grid-unit-15 $grid-unit-30; + &:focus, + &:active { + outline: 0; + box-shadow: none; } } } .is-selected { + color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); .edit-site-global-styles-screen-revisions__revision-button { - color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); opacity: 1; - background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } - - .edit-site-global-styles-screen-revisions__meta { + .edit-site-global-styles-screen-revisions__date { color: var(--wp-admin-theme-color); } } @@ -78,20 +96,26 @@ flex-direction: column; align-items: flex-start; gap: $grid-unit-10; + .edit-site-global-styles-screen-revisions__date { + text-transform: uppercase; + font-weight: 600; + font-size: 12px; + } } .edit-site-global-styles-screen-revisions__meta { - color: $gray-700; + color: $gray-600; display: flex; - justify-content: space-between; + justify-content: start; width: 100%; align-items: center; - text-align: left; + font-size: 12px; img { width: $grid-unit-20; height: $grid-unit-20; border-radius: 100%; + margin-right: $grid-unit-10; } } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js index 6e3573061a4214..bacc79a97cb6de 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js @@ -32,24 +32,33 @@ export default function useGlobalStylesRevisions() { __experimentalGetDirtyEntityRecords, getCurrentUser, getUsers, - getCurrentThemeGlobalStylesRevisions, + getRevisions, + __experimentalGetCurrentGlobalStylesId, isResolving, } = select( coreStore ); const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); const _currentUser = getCurrentUser(); const _isDirty = dirtyEntityRecords.length > 0; + const query = { + per_page: 100, + }; + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); const globalStylesRevisions = - getCurrentThemeGlobalStylesRevisions() || EMPTY_ARRAY; + getRevisions( 'root', 'globalStyles', globalStylesId, query ) || + EMPTY_ARRAY; const _authors = getUsers( SITE_EDITOR_AUTHORS_QUERY ) || EMPTY_ARRAY; - + const _isResolving = isResolving( 'getRevisions', [ + 'root', + 'globalStyles', + globalStylesId, + query, + ] ); return { authors: _authors, currentUser: _currentUser, isDirty: _isDirty, revisions: globalStylesRevisions, - isLoadingGlobalStylesRevisions: isResolving( - 'getCurrentThemeGlobalStylesRevisions' - ), + isLoadingGlobalStylesRevisions: _isResolving, }; }, [] ); return useMemo( () => { diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 3560ef139fa3fe..a899495cc332b7 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -191,14 +191,3 @@ .edit-site-global-styles-sidebar__panel .block-editor-block-icon svg { fill: currentColor; } - -[class][class].edit-site-global-styles-sidebar__revisions-count-badge { - align-items: center; - background: $gray-800; - border-radius: 2px; - color: $white; - display: inline-flex; - justify-content: center; - min-height: $icon-size; - min-width: $icon-size; -} diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 2e33d4b599b7b9..c8d72205c3bed8 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -55,6 +50,7 @@ const { Slot: GlobalStylesMenuSlot, Fill: GlobalStylesMenuFill } = createSlotFill( SLOT_FILL_NAME ); function GlobalStylesActionMenu() { + const [ canReset, onReset ] = useGlobalStylesReset(); const { toggle } = useDispatch( preferencesStore ); const { canEditCSS } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = @@ -69,49 +65,56 @@ function GlobalStylesActionMenu() { canEditCSS: !! globalStyles?._links?.[ 'wp:action-edit-css' ], }; }, [] ); + const { setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); const { goTo } = useNavigator(); - const loadCustomCSS = () => goTo( '/css' ); + const loadCustomCSS = () => { + setEditorCanvasContainerView( 'global-styles-css' ); + goTo( '/css' ); + }; return ( { ( { onClose } ) => ( - - { canEditCSS && ( - - { __( 'Additional CSS' ) } + <> + + { canEditCSS && ( + + { __( 'Additional CSS' ) } + + ) } + { + toggle( + 'core/edit-site', + 'welcomeGuideStyles' + ); + onClose(); + } } + > + { __( 'Welcome Guide' ) } + + + + { + onReset(); + onClose(); + } } + disabled={ ! canReset } + > + { __( 'Reset styles' ) } - ) } - { - toggle( - 'core/edit-site', - 'welcomeGuideStyles' - ); - onClose(); - } } - > - { __( 'Welcome Guide' ) } - - + + ) } ); } -function RevisionsCountBadge( { className, children } ) { - return ( - - { children } - - ); -} function GlobalStylesRevisionsMenu() { const { setIsListViewOpened } = useDispatch( editSiteStore ); const { revisionsCount } = useSelect( ( select ) => { @@ -128,56 +131,38 @@ function GlobalStylesRevisionsMenu() { globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, }; }, [] ); - const [ canReset, onReset ] = useGlobalStylesReset(); const { goTo } = useNavigator(); const { setEditorCanvasContainerView } = unlock( useDispatch( editSiteStore ) ); + const isRevisionsOpened = useSelect( + ( select ) => + 'global-styles-revisions' === + unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), + [] + ); const loadRevisions = () => { setIsListViewOpened( false ); - goTo( '/revisions' ); - setEditorCanvasContainerView( 'global-styles-revisions' ); + + if ( ! isRevisionsOpened ) { + goTo( '/revisions' ); + setEditorCanvasContainerView( 'global-styles-revisions' ); + } else { + goTo( '/' ); + setEditorCanvasContainerView( undefined ); + } }; const hasRevisions = revisionsCount > 0; return ( - { canReset || hasRevisions ? ( - - { ( { onClose } ) => ( - - { hasRevisions && ( - - { revisionsCount } - - } - > - { __( 'Revision history' ) } - - ) } - { - onReset(); - onClose(); - } } - disabled={ ! canReset } - > - { __( 'Reset to defaults' ) } - - - ) } - - ) : ( - - ) } - renderContent={ ( { onClose } ) => { - return ( - <> - - -
      - { - onSlugChange( newValue ); - // When we delete the field the permalink gets - // reverted to the original value. - // The forceEmptyField logic allows the user to have - // the field temporarily empty while typing. - if ( ! newValue ) { - if ( ! forceEmptyField ) { - setForceEmptyField( true ); - } - return; - } - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - onBlur={ ( event ) => { - onSlugChange( - cleanForSlug( - event.target.value || - savedSlug - ) - ); - if ( forceEmptyField ) { - setForceEmptyField( false ); - } - } } - /> - -
      - - ); - } } - /> - - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-status.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-status.js index f80ed4d3fe8204..1d74b09db773e6 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-status.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-status.js @@ -6,7 +6,6 @@ import { ToggleControl, Dropdown, __experimentalText as Text, - __experimentalHStack as HStack, __experimentalVStack as VStack, TextControl, RadioControl, @@ -19,11 +18,15 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as noticesStore } from '@wordpress/notices'; import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; import { useInstanceId } from '@wordpress/compose'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ import StatusLabel from '../../sidebar-navigation-screen-page/status-label'; +import { unlock } from '../../../lock-unlock'; + +const { PostPanelRow } = unlock( editorPrivateApis ); const STATUS_OPTIONS = [ { @@ -159,10 +162,7 @@ export default function PageStatus( { }; return ( - - - { __( 'Status' ) } - + ) } /> - + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js index 25b69985bcbd6e..0219b568e57c50 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js @@ -2,13 +2,17 @@ * WordPress dependencies */ import { __experimentalVStack as VStack } from '@wordpress/components'; +import { + PostAuthorPanel, + PostURLPanel, + PostSchedulePanel, +} from '@wordpress/editor'; + /** * Internal dependencies */ import PageStatus from './page-status'; -import PublishDate from './publish-date'; import EditTemplate from './edit-template'; -import PageSlug from './page-slug'; export default function PageSummary( { status, @@ -18,7 +22,7 @@ export default function PageSummary( { postType, } ) { return ( - + - + - + + ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/publish-date.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/publish-date.js deleted file mode 100644 index d000394f6816ba..00000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/publish-date.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * WordPress dependencies - */ -import { - Button, - Dropdown, - __experimentalText as Text, - __experimentalHStack as HStack, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; -import { useState, useMemo } from '@wordpress/element'; -import { store as coreStore } from '@wordpress/core-data'; -import { store as noticesStore } from '@wordpress/notices'; -import { __experimentalPublishDateTimePicker as PublishDateTimePicker } from '@wordpress/block-editor'; -import { humanTimeDiff } from '@wordpress/date'; - -export default function ChangeStatus( { postType, postId, status, date } ) { - const { editEntityRecord } = useDispatch( coreStore ); - const { createErrorNotice } = useDispatch( noticesStore ); - - const [ popoverAnchor, setPopoverAnchor ] = useState( null ); - // Memoize popoverProps to avoid returning a new object every time. - const popoverProps = useMemo( - () => ( { - // Anchor the popover to the middle of the entire row so that it doesn't - // move around when the label changes. - anchor: popoverAnchor, - 'aria-label': __( 'Change publish date' ), - placement: 'bottom-end', - } ), - [ popoverAnchor ] - ); - - const saveDate = async ( newDate ) => { - try { - let newStatus = status; - if ( status === 'future' && new Date( newDate ) < new Date() ) { - newStatus = 'publish'; - } else if ( - status === 'publish' && - new Date( newDate ) > new Date() - ) { - newStatus = 'future'; - } - await editEntityRecord( 'postType', postType, postId, { - status: newStatus, - date: newDate, - } ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while updating the status' ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } - }; - - const relateToNow = date ? humanTimeDiff( date ) : __( 'Immediately' ); - - return ( - - - { __( 'Publish' ) } - - ( - - ) } - renderContent={ ( { onClose } ) => ( - - ) } - /> - - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js index 0f29292274546b..795477cc8fc7c6 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js @@ -10,18 +10,18 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import { + useAllowSwitchingTemplates, useCurrentTemplateSlug, useEditedPostContext, - useIsPostsPage, } from './hooks'; export default function ResetDefaultTemplate( { onClick } ) { const currentTemplateSlug = useCurrentTemplateSlug(); - const isPostsPage = useIsPostsPage(); + const allowSwitchingTemplate = useAllowSwitchingTemplates(); const { postType, postId } = useEditedPostContext(); const { editEntityRecord } = useDispatch( coreStore ); // The default template in a post is indicated by an empty string. - if ( ! currentTemplateSlug || isPostsPage ) { + if ( ! currentTemplateSlug || ! allowSwitchingTemplate ) { return null; } return ( diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index 64d72db4e15fd7..f3da54c244bd1b 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -59,25 +59,15 @@ } } -.edit-site-summary-field { - .components-dropdown { - width: 70%; - } - - .edit-site-summary-field__trigger { - max-width: 100%; - - // Truncate - display: block; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .edit-site-summary-field__label { - width: 30%; - } +.edit-site-summary-field__trigger { + max-width: 100%; + + // Truncate + display: block; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .edit-site-page-panels-edit-template__dropdown { @@ -90,11 +80,3 @@ color: inherit; } } - -.edit-site-page-panels-edit-slug__dropdown { - .components-popover__content { - min-width: 320px; - padding: $grid-unit-20; - } -} - diff --git a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js index 569bad72ad7ef9..c8ceb089cf0f5d 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/settings-header/index.js @@ -10,6 +10,7 @@ import { Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -20,12 +21,12 @@ import { store as editSiteStore } from '../../../store'; import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../../utils/constants'; const SettingsHeader = ( { sidebarName } ) => { - const { hasPageContentFocus, entityType } = useSelect( ( select ) => { - const { getEditedPostType, hasPageContentFocus: _hasPageContentFocus } = - select( editSiteStore ); + const { isEditingPage, entityType } = useSelect( ( select ) => { + const { getEditedPostType, isPage } = select( editSiteStore ); + const { getRenderingMode } = select( editorStore ); return { - hasPageContentFocus: _hasPageContentFocus(), + isEditingPage: isPage() && getRenderingMode() !== 'template-only', entityType: getEditedPostType(), }; } ); @@ -41,7 +42,7 @@ const SettingsHeader = ( { sidebarName } ) => { enableComplementaryArea( STORE_NAME, SIDEBAR_BLOCK ); let templateAriaLabel; - if ( hasPageContentFocus ) { + if ( isEditingPage ) { templateAriaLabel = sidebarName === SIDEBAR_TEMPLATE ? // translators: ARIA label for the Template sidebar tab, selected. @@ -70,11 +71,9 @@ const SettingsHeader = ( { sidebarName } ) => { } ) } aria-label={ templateAriaLabel } - data-label={ - hasPageContentFocus ? __( 'Page' ) : entityLabel - } + data-label={ isEditingPage ? __( 'Page' ) : entityLabel } > - { hasPageContentFocus ? __( 'Page' ) : entityLabel } + { isEditingPage ? __( 'Page' ) : entityLabel }
    2. diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js index 7effca4510ede7..110bc920fb0a9f 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-page/index.js @@ -15,6 +15,8 @@ import { pencil } from '@wordpress/icons'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { escapeAttribute } from '@wordpress/escape-html'; import { safeDecodeURIComponent, filterURLForDisplay } from '@wordpress/url'; +import { useEffect } from '@wordpress/element'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -27,13 +29,20 @@ import PageDetails from './page-details'; import PageActions from '../page-actions'; import SidebarNavigationScreenDetailsFooter from '../sidebar-navigation-screen-details-footer'; +const { useHistory } = unlock( routerPrivateApis ); + export default function SidebarNavigationScreenPage() { - const navigator = useNavigator(); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); + const history = useHistory(); const { params: { postId }, + goTo, } = useNavigator(); - const { record } = useEntityRecord( 'postType', 'page', postId ); + const { record, hasResolved } = useEntityRecord( + 'postType', + 'page', + postId + ); const { featuredMediaAltText, featuredMediaSourceUrl } = useSelect( ( select ) => { @@ -61,6 +70,18 @@ export default function SidebarNavigationScreenPage() { [ record ] ); + // Redirect to the main pages navigation screen if the page is not found or has been deleted. + useEffect( () => { + if ( hasResolved && ! record ) { + history.push( { + path: '/page', + postId: undefined, + postType: undefined, + canvas: 'view', + } ); + } + }, [ hasResolved, history ] ); + const featureImageAltText = featuredMediaAltText ? decodeEntities( featuredMediaAltText ) : decodeEntities( record?.title?.rendered || __( 'Featured image' ) ); @@ -76,7 +97,7 @@ export default function SidebarNavigationScreenPage() { postId={ postId } toggleProps={ { as: SidebarButton } } onRemove={ () => { - navigator.goTo( '/page' ); + goTo( '/page' ); } } /> } /> diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index b6931b8e656653..19508f0a59f8ea 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -7,13 +7,10 @@ import classnames from 'classnames'; * WordPress dependencies */ import { - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, - __unstableCompositeItem as CompositeItem, Disabled, TabPanel, + privateApis as componentsPrivateApis, } from '@wordpress/components'; - import { __, sprintf } from '@wordpress/i18n'; import { getCategories, @@ -43,6 +40,12 @@ const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( blockEditorPrivateApis ); +const { + CompositeV2: Composite, + CompositeItemV2: CompositeItem, + useCompositeStoreV2: useCompositeStore, +} = unlock( componentsPrivateApis ); + // The content area of the Style Book is rendered within an iframe so that global styles // are applied to elements within the entire content area. To support elements that are // not part of the block previews, such as headings and layout for the block previews, @@ -66,6 +69,8 @@ const STYLE_BOOK_IFRAME_STYLES = ` padding: 16px; width: 100%; box-sizing: border-box; + scroll-margin-top: 32px; + scroll-margin-bottom: 32px; } .edit-site-style-book__example.is-selected { @@ -332,6 +337,7 @@ const StyleBookBody = ( { } isSelected={ isSelected } onSelect={ onSelect } + key={ category } /> ); @@ -339,12 +345,14 @@ const StyleBookBody = ( { const Examples = memo( ( { className, examples, category, label, isSelected, onSelect } ) => { - const composite = useCompositeState( { orientation: 'vertical' } ); + const compositeStore = useCompositeStore( { orientation: 'vertical' } ); + return ( { examples .filter( ( example ) => @@ -354,7 +362,6 @@ const Examples = memo( { +const Example = ( { id, title, blocks, isSelected, onClick } ) => { const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), [] @@ -385,35 +392,41 @@ const Example = ( { composite, id, title, blocks, isSelected, onClick } ) => { ); return ( - - - { title } - -
      - - +
      + } + role="button" + onClick={ onClick } + > + + { title } + +
      - - - + + + + + +
      +
      - +
      ); }; diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index 64eb3778a99c70..46079cbce8efd5 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -28,21 +28,46 @@ const postTypesWithoutParentTemplate = [ ]; function useResolveEditedEntityAndContext( { postId, postType } ) { - const { isRequestingSite, homepageId, url } = useSelect( ( select ) => { - const { getSite, getUnstableBase } = select( coreDataStore ); - const siteData = getSite(); - const base = getUnstableBase(); - - return { - isRequestingSite: ! base, - homepageId: - siteData?.show_on_front === 'page' - ? siteData.page_on_front - : null, - url: base?.home, - }; - }, [] ); + const { hasLoadedAllDependencies, homepageId, url, frontPageTemplateId } = + useSelect( ( select ) => { + const { getSite, getUnstableBase, getEntityRecords } = + select( coreDataStore ); + const siteData = getSite(); + const base = getUnstableBase(); + const templates = getEntityRecords( + 'postType', + TEMPLATE_POST_TYPE, + { + per_page: -1, + } + ); + let _frontPateTemplateId; + if ( templates ) { + const frontPageTemplate = templates.find( + ( t ) => t.slug === 'front-page' + ); + _frontPateTemplateId = frontPageTemplate + ? frontPageTemplate.id + : false; + } + return { + hasLoadedAllDependencies: !! base && !! siteData, + homepageId: + siteData?.show_on_front === 'page' + ? siteData.page_on_front.toString() + : null, + url: base?.home, + frontPageTemplateId: _frontPateTemplateId, + }; + }, [] ); + + /** + * This is a hook that recreates the logic to resolve a template for a given WordPress postID postTypeId + * in order to match the frontend as closely as possible in the site editor. + * + * It is not possible to rely on the server logic because there maybe unsaved changes that impact the template resolution. + */ const resolvedTemplateId = useSelect( ( select ) => { // If we're rendering a post type that doesn't have a template @@ -62,6 +87,22 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { postTypeToResolve, postIdToResolve ) { + // For the front page, we always use the front page template if existing. + if ( + postTypeToResolve === 'page' && + homepageId === postIdToResolve + ) { + // We're still checking whether the front page template exists. + // Don't resolve the template yet. + if ( frontPageTemplateId === undefined ) { + return undefined; + } + + if ( !! frontPageTemplateId ) { + return frontPageTemplateId; + } + } + const editedEntity = getEditedEntityRecord( 'postType', postTypeToResolve, @@ -91,6 +132,10 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { } ); } + if ( ! hasLoadedAllDependencies ) { + return undefined; + } + // If we're rendering a specific page, post... we need to resolve its template. if ( postType && postId ) { return resolveTemplateForPostTypeAndId( postType, postId ); @@ -102,12 +147,19 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { } // If we're not rendering a specific page, use the front page template. - if ( ! isRequestingSite && url ) { + if ( url ) { const template = __experimentalGetTemplateForLink( url ); return template?.id; } }, - [ homepageId, isRequestingSite, url, postId, postType ] + [ + homepageId, + hasLoadedAllDependencies, + url, + postId, + postType, + frontPageTemplateId, + ] ); const context = useMemo( () => { @@ -130,7 +182,7 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { return { isReady: true, postType, postId, context }; } - if ( ( postType && postId ) || homepageId || ! isRequestingSite ) { + if ( hasLoadedAllDependencies ) { return { isReady: resolvedTemplateId !== undefined, postType: TEMPLATE_POST_TYPE, diff --git a/packages/edit-site/src/components/welcome-guide/page.js b/packages/edit-site/src/components/welcome-guide/page.js index adb64a8033e999..db89d9b653ad58 100644 --- a/packages/edit-site/src/components/welcome-guide/page.js +++ b/packages/edit-site/src/components/welcome-guide/page.js @@ -23,8 +23,8 @@ export default function WelcomeGuidePage() { 'core/edit-site', 'welcomeGuide' ); - const { hasPageContentFocus } = select( editSiteStore ); - return isPageActive && ! isEditorActive && hasPageContentFocus(); + const { isPage } = select( editSiteStore ); + return isPageActive && ! isEditorActive && isPage(); }, [] ); if ( ! isVisible ) { diff --git a/packages/edit-site/src/components/welcome-guide/template.js b/packages/edit-site/src/components/welcome-guide/template.js index f0c02c09d1124a..073a19c2d6efdc 100644 --- a/packages/edit-site/src/components/welcome-guide/template.js +++ b/packages/edit-site/src/components/welcome-guide/template.js @@ -5,6 +5,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { Guide } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -23,12 +24,13 @@ export default function WelcomeGuideTemplate() { 'core/edit-site', 'welcomeGuide' ); - const { isPage, hasPageContentFocus } = select( editSiteStore ); + const { isPage } = select( editSiteStore ); + const { getRenderingMode } = select( editorStore ); return ( isTemplateActive && ! isEditorActive && isPage() && - ! hasPageContentFocus() + getRenderingMode() === 'template-only' ); }, [] ); diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index 37baef18dffd48..ece6d349db1e7f 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -24,6 +24,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as interfaceStore } from '@wordpress/interface'; import { store as noticesStore } from '@wordpress/notices'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -42,15 +43,18 @@ const { useHistory } = unlock( routerPrivateApis ); function usePageContentFocusCommands() { const { record: template } = useEditedEntityRecord(); - const { isPage, canvasMode, hasPageContentFocus } = useSelect( - ( select ) => ( { - isPage: select( editSiteStore ).isPage(), - canvasMode: unlock( select( editSiteStore ) ).getCanvasMode(), - hasPageContentFocus: select( editSiteStore ).hasPageContentFocus(), - } ), - [] - ); - const { setHasPageContentFocus } = useDispatch( editSiteStore ); + const { isPage, canvasMode, renderingMode } = useSelect( ( select ) => { + const { isPage: _isPage, getCanvasMode } = unlock( + select( editSiteStore ) + ); + const { getRenderingMode } = select( editorStore ); + return { + isPage: _isPage(), + canvasMode: getCanvasMode(), + renderingMode: getRenderingMode(), + }; + }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); if ( ! isPage || canvasMode !== 'edit' ) { return { isLoading: false, commands: [] }; @@ -58,7 +62,7 @@ function usePageContentFocusCommands() { const commands = []; - if ( hasPageContentFocus ) { + if ( renderingMode !== 'template-only' ) { commands.push( { name: 'core/switch-to-template-focus', /* translators: %1$s: template title */ @@ -68,7 +72,7 @@ function usePageContentFocusCommands() { ), icon: layout, callback: ( { close } ) => { - setHasPageContentFocus( false ); + setRenderingMode( 'template-only' ); close(); }, } ); @@ -78,7 +82,7 @@ function usePageContentFocusCommands() { label: __( 'Back to page' ), icon: page, callback: ( { close } ) => { - setHasPageContentFocus( true ); + setRenderingMode( 'template-locked' ); close(); }, } ); @@ -122,8 +126,10 @@ function useManipulateDocumentCommands() { const { isLoaded, record: template } = useEditedEntityRecord(); const { removeTemplate, revertTemplate } = useDispatch( editSiteStore ); const history = useHistory(); - const hasPageContentFocus = useSelect( - ( select ) => select( editSiteStore ).hasPageContentFocus(), + const isEditingPage = useSelect( + ( select ) => + select( editSiteStore ).isPage() && + select( editorStore ).getRenderingMode() !== 'template-only', [] ); @@ -133,7 +139,7 @@ function useManipulateDocumentCommands() { const commands = []; - if ( isTemplateRevertable( template ) && ! hasPageContentFocus ) { + if ( isTemplateRevertable( template ) && ! isEditingPage ) { const label = template.type === TEMPLATE_POST_TYPE ? /* translators: %1$s: template title */ @@ -157,7 +163,7 @@ function useManipulateDocumentCommands() { } ); } - if ( isTemplateRemovable( template ) && ! hasPageContentFocus ) { + if ( isTemplateRemovable( template ) && ! isEditingPage ) { const label = template.type === TEMPLATE_POST_TYPE ? /* translators: %1$s: template title */ diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 4de1f4ac2a61fd..2dd7aacd384014 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -575,6 +575,10 @@ export const switchEditorMode = export const setHasPageContentFocus = ( hasPageContentFocus ) => ( { dispatch, registry } ) => { + deprecated( `dispatch( 'core/edit-site' ).setHasPageContentFocus`, { + since: '6.5', + } ); + if ( hasPageContentFocus ) { registry.dispatch( blockEditorStore ).clearSelectedBlock(); } @@ -599,7 +603,7 @@ export const toggleDistractionFree = registry.batch( () => { registry .dispatch( preferencesStore ) - .set( 'core/edit-site', 'fixedToolbar', false ); + .set( 'core/edit-site', 'fixedToolbar', true ); dispatch.setIsInserterOpened( false ); dispatch.setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index 3e2bfe2ee47b24..2d858c15208991 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -11,7 +11,7 @@ import { store as preferencesStore } from '@wordpress/preferences'; */ export const setCanvasMode = ( mode ) => - ( { registry, dispatch, select } ) => { + ( { registry, dispatch } ) => { registry.dispatch( blockEditorStore ).__unstableSetEditorMode( 'edit' ); dispatch( { type: 'SET_CANVAS_MODE', @@ -30,10 +30,6 @@ export const setCanvasMode = ) { dispatch.setIsListViewOpened( true ); } - // Switch focus away from editing the template when switching to view mode. - if ( mode === 'view' && select.isPage() ) { - dispatch.setHasPageContentFocus( true ); - } }; /** @@ -49,22 +45,3 @@ export const setEditorCanvasContainerView = view, } ); }; - -/** - * Sets the type of page content focus. Can be one of: - * - * - `'disableTemplate'`: Disable the blocks belonging to the page's template. - * - `'hideTemplate'`: Hide the blocks belonging to the page's template. - * - * @param {'disableTemplate'|'hideTemplate'} pageContentFocusType The type of page content focus. - * - * @return {Object} Action object. - */ -export const setPageContentFocusType = - ( pageContentFocusType ) => - ( { dispatch } ) => { - dispatch( { - type: 'SET_PAGE_CONTENT_FOCUS_TYPE', - pageContentFocusType, - } ); - }; diff --git a/packages/edit-site/src/store/private-selectors.js b/packages/edit-site/src/store/private-selectors.js index 0d4cf2b3eefdaa..1f1f6e999fdb29 100644 --- a/packages/edit-site/src/store/private-selectors.js +++ b/packages/edit-site/src/store/private-selectors.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import { hasPageContentFocus } from './selectors'; - /** * Returns the current canvas mode. * @@ -24,20 +19,3 @@ export function getCanvasMode( state ) { export function getEditorCanvasContainerView( state ) { return state.editorCanvasContainerView; } - -/** - * Returns the type of the current page content focus, or null if there is no - * page content focus. - * - * Possible values are: - * - * - `'disableTemplate'`: Disable the blocks belonging to the page's template. - * - `'hideTemplate'`: Hide the blocks belonging to the page's template. - * - * @param {Object} state Global application state. - * - * @return {'disableTemplate'|'hideTemplate'|null} Type of the current page content focus. - */ -export function getPageContentFocusType( state ) { - return hasPageContentFocus( state ) ? state.pageContentFocusType : null; -} diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index e99c6dda1fc1d0..a46d215f905074 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -157,43 +157,6 @@ function editorCanvasContainerView( state = undefined, action ) { return state; } -/** - * Reducer used to track whether the editor allows only page content to be - * edited. - * - * @param {boolean} state Current state. - * @param {Object} action Dispatched action. - * - * @return {boolean} Updated state. - */ -export function hasPageContentFocus( state = false, action ) { - switch ( action.type ) { - case 'SET_EDITED_POST': - return !! action.context?.postId; - case 'SET_HAS_PAGE_CONTENT_FOCUS': - return action.hasPageContentFocus; - } - - return state; -} - -/** - * Reducer used to track the type of page content focus. - * - * @param {string} state Current state. - * @param {Object} action Dispatched action. - * - * @return {string} Updated state. - */ -export function pageContentFocusType( state = 'disableTemplate', action ) { - switch ( action.type ) { - case 'SET_PAGE_CONTENT_FOCUS_TYPE': - return action.pageContentFocusType; - } - - return state; -} - export default combineReducers( { deviceType, settings, @@ -203,6 +166,4 @@ export default combineReducers( { saveViewPanel, canvasMode, editorCanvasContainerView, - hasPageContentFocus, - pageContentFocusType, } ); diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index f9c2f7d65cfaf4..9d00e141270c40 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -7,6 +7,7 @@ import deprecated from '@wordpress/deprecated'; import { Platform } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -181,7 +182,10 @@ export const __experimentalGetInsertionPoint = createRegistrySelector( return { rootClientId, insertionIndex, filterValue }; } - if ( hasPageContentFocus( state ) ) { + if ( + isPage( state ) && + select( editorStore ).getRenderingMode() !== 'template-only' + ) { const [ postContentClientId ] = select( blockEditorStore ).__experimentalGetGlobalBlocksByName( 'core/post-content' @@ -310,10 +314,14 @@ export function isPage( state ) { /** * Whether or not the editor allows only page content to be edited. * - * @param {Object} state Global application state. + * @deprecated * * @return {boolean} Whether or not focus is on editing page content. */ -export function hasPageContentFocus( state ) { - return isPage( state ) ? state.hasPageContentFocus : false; +export function hasPageContentFocus() { + deprecated( `select( 'core/edit-site' ).hasPageContentFocus`, { + since: '6.5', + } ); + + return false; } diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 787809acda0890..6f0597fec12434 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -7,18 +7,19 @@ import { createRegistry } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ import { store as editSiteStore } from '..'; -import { setHasPageContentFocus } from '../actions'; function createRegistryWithStores() { // create a registry const registry = createRegistry(); // register stores + registry.register( editorStore ); registry.register( blockEditorStore ); registry.register( coreStore ); registry.register( editSiteStore ); @@ -158,7 +159,7 @@ describe( 'actions', () => { registry .select( preferencesStore ) .get( 'core/edit-site', 'fixedToolbar' ) - ).toBe( false ); + ).toBe( true ); expect( registry.select( editSiteStore ).isListViewOpened() ).toBe( false ); @@ -177,34 +178,4 @@ describe( 'actions', () => { ).toBe( true ); } ); } ); - - describe( 'setHasPageContentFocus', () => { - it( 'toggles the page content lock on', () => { - const dispatch = jest.fn(); - const clearSelectedBlock = jest.fn(); - const registry = { - dispatch: () => ( { clearSelectedBlock } ), - }; - setHasPageContentFocus( true )( { dispatch, registry } ); - expect( clearSelectedBlock ).toHaveBeenCalled(); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: true, - } ); - } ); - - it( 'toggles the page content lock off', () => { - const dispatch = jest.fn(); - const clearSelectedBlock = jest.fn(); - const registry = { - dispatch: () => ( { clearSelectedBlock } ), - }; - setHasPageContentFocus( false )( { dispatch, registry } ); - expect( clearSelectedBlock ).not.toHaveBeenCalled(); - expect( dispatch ).toHaveBeenCalledWith( { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: false, - } ); - } ); - } ); } ); diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js index a5e47ec5bbbaf3..f39261fea38802 100644 --- a/packages/edit-site/src/store/test/reducer.js +++ b/packages/edit-site/src/store/test/reducer.js @@ -11,8 +11,6 @@ import { editedPost, blockInserterPanel, listViewPanel, - hasPageContentFocus, - pageContentFocusType, } from '../reducer'; import { setIsInserterOpened } from '../actions'; @@ -149,64 +147,4 @@ describe( 'state', () => { ); } ); } ); - - describe( 'hasPageContentFocus()', () => { - it( 'defaults to false', () => { - expect( hasPageContentFocus( undefined, {} ) ).toBe( false ); - } ); - - it( 'becomes false when editing a template', () => { - expect( - hasPageContentFocus( true, { - type: 'SET_EDITED_POST', - postType: 'wp_template', - } ) - ).toBe( false ); - } ); - - it( 'becomes true when editing a page', () => { - expect( - hasPageContentFocus( false, { - type: 'SET_EDITED_POST', - postType: 'wp_template', - context: { - postType: 'page', - postId: 123, - }, - } ) - ).toBe( true ); - } ); - - it( 'can be set', () => { - expect( - hasPageContentFocus( false, { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: true, - } ) - ).toBe( true ); - expect( - hasPageContentFocus( true, { - type: 'SET_HAS_PAGE_CONTENT_FOCUS', - hasPageContentFocus: false, - } ) - ).toBe( false ); - } ); - } ); - - describe( 'pageContentFocusType', () => { - it( 'defaults to disableTemplate', () => { - expect( pageContentFocusType( undefined, {} ) ).toBe( - 'disableTemplate' - ); - } ); - - it( 'can be set', () => { - expect( - pageContentFocusType( 'disableTemplate', { - type: 'SET_PAGE_CONTENT_FOCUS_TYPE', - pageContentFocusType: 'enableTemplate', - } ) - ).toBe( 'enableTemplate' ); - } ); - } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 7e36d2f4b75f4d..07577e897b04ec 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -13,7 +13,6 @@ import { isInserterOpened, isListViewOpened, isPage, - hasPageContentFocus, } from '../selectors'; describe( 'selectors', () => { @@ -88,38 +87,4 @@ describe( 'selectors', () => { expect( isPage( state ) ).toBe( false ); } ); } ); - - describe( 'hasPageContentFocus', () => { - it( 'returns true if locked and the edited post type is a page', () => { - const state = { - editedPost: { - postType: 'wp_template', - context: { postType: 'page', postId: 123 }, - }, - hasPageContentFocus: true, - }; - expect( hasPageContentFocus( state ) ).toBe( true ); - } ); - - it( 'returns false if not locked and the edited post type is a page', () => { - const state = { - editedPost: { - postType: 'wp_template', - context: { postType: 'page', postId: 123 }, - }, - hasPageContentFocus: false, - }; - expect( hasPageContentFocus( state ) ).toBe( false ); - } ); - - it( 'returns false if locked and the edited post type is a template', () => { - const state = { - editedPost: { - postType: 'wp_template', - }, - hasPageContentFocus: true, - }; - expect( hasPageContentFocus( state ) ).toBe( false ); - } ); - } ); } ); diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 0b49f48a3e5845..30fbec3a94cc1b 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -13,6 +13,7 @@ @import "./components/page/style.scss"; @import "./components/page-pages/style.scss"; @import "./components/page-patterns/style.scss"; +@import "./components/page-templates/style.scss"; @import "./components/table/style.scss"; @import "./components/sidebar-edit-mode/style.scss"; @import "./components/sidebar-edit-mode/page-panels/style.scss"; diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index 2f00bc13f6de8d..0aae3e681a16e5 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -38,16 +38,6 @@ export const FOCUSABLE_ENTITIES = [ PATTERN_TYPES.user, ]; -/** - * Block types that are considered to be page content. These are the only blocks - * editable when hasPageContentFocus() is true. - */ -export const PAGE_CONTENT_BLOCK_TYPES = { - 'core/post-title': true, - 'core/post-featured-image': true, - 'core/post-content': true, -}; - export const POST_TYPE_LABELS = { [ TEMPLATE_POST_TYPE ]: __( 'Template' ), [ TEMPLATE_PART_POST_TYPE ]: __( 'Template part' ), diff --git a/packages/edit-widgets/CHANGELOG.md b/packages/edit-widgets/CHANGELOG.md index e1d1c915a2ad21..65b83144691eb1 100644 --- a/packages/edit-widgets/CHANGELOG.md +++ b/packages/edit-widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.23.0 (2023-11-16) + ## 5.22.0 (2023-11-02) ## 5.21.0 (2023-10-18) diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 754e83f54a5f20..33c7f9d75dd619 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.22.0", + "version": "5.23.0", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 0e99e83f464181..1012c6163ec292 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 13.23.0 (2023-11-16) + ## 13.22.0 (2023-11-02) ## 13.21.0 (2023-10-18) diff --git a/packages/editor/package.json b/packages/editor/package.json index b45a28c982ee4c..5a4bf3ba7bf216 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.22.0", + "version": "13.23.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/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index 07097969fffc8c..a47dd29fef036c 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -3,7 +3,7 @@ */ import { Button, Flex, FlexItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { useDispatch } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useCallback, useRef } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; @@ -85,6 +85,15 @@ export function EntitiesSavedStatesExtensible( { const saveEnabled = saveEnabledProp ?? isDirty; + const { homeUrl } = useSelect( ( select ) => { + const { + getUnstableBase, // Site index. + } = select( coreStore ); + return { + homeUrl: getUnstableBase()?.home, + }; + }, [] ); + const saveCheckedEntities = () => { const saveNoticeId = 'site-editor-save-success'; removeNotice( saveNoticeId ); @@ -149,6 +158,12 @@ export function EntitiesSavedStatesExtensible( { createSuccessNotice( __( 'Site updated.' ), { type: 'snackbar', id: saveNoticeId, + actions: [ + { + label: __( 'View site' ), + url: homeUrl, + }, + ], } ); } } ) diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 39b562806c109a..5fefc5506a02fc 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -26,6 +26,7 @@ export { default as PageAttributesParent } from './page-attributes/parent'; export { default as PageTemplate } from './post-template'; export { default as PostAuthor } from './post-author'; export { default as PostAuthorCheck } from './post-author/check'; +export { default as PostAuthorPanel } from './post-author/panel'; export { default as PostComments } from './post-comments'; export { default as PostExcerpt } from './post-excerpt'; export { default as PostExcerptCheck } from './post-excerpt/check'; @@ -50,6 +51,7 @@ export { default as PostScheduleLabel, usePostScheduleLabel, } from './post-schedule/label'; +export { default as PostSchedulePanel } from './post-schedule/panel'; export { default as PostSlug } from './post-slug'; export { default as PostSlugCheck } from './post-slug/check'; export { default as PostSticky } from './post-sticky'; @@ -65,12 +67,14 @@ export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } fr export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; +export { default as PostTitleRaw } from './post-title/post-title-raw'; export { default as PostTrash } from './post-trash'; export { default as PostTrashCheck } from './post-trash/check'; export { default as PostTypeSupportCheck } from './post-type-support-check'; export { default as PostURL } from './post-url'; export { default as PostURLCheck } from './post-url/check'; export { default as PostURLLabel, usePostURLLabel } from './post-url/label'; +export { default as PostURLPanel } from './post-url/panel'; export { default as PostVisibility } from './post-visibility'; export { default as PostVisibilityLabel, diff --git a/packages/editor/src/components/post-author/panel.js b/packages/editor/src/components/post-author/panel.js new file mode 100644 index 00000000000000..78f0e0a5f2cc89 --- /dev/null +++ b/packages/editor/src/components/post-author/panel.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import PostAuthorCheck from './check'; +import PostAuthorForm from './index'; +import PostPanelRow from '../post-panel-row'; + +export function PostAuthor() { + return ( + + + + + + ); +} + +export default PostAuthor; diff --git a/packages/editor/src/components/post-author/style.scss b/packages/editor/src/components/post-author/style.scss new file mode 100644 index 00000000000000..349ad712334c8d --- /dev/null +++ b/packages/editor/src/components/post-author/style.scss @@ -0,0 +1,7 @@ +.editor-post-author__panel { + padding-top: $grid-unit-10; +} + +.editor-post-author__panel .editor-post-panel__row-control > div { + width: 100%; +} diff --git a/packages/editor/src/components/post-panel-row/index.js b/packages/editor/src/components/post-panel-row/index.js new file mode 100644 index 00000000000000..f6f0c658cd7240 --- /dev/null +++ b/packages/editor/src/components/post-panel-row/index.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __experimentalHStack as HStack } from '@wordpress/components'; +import { forwardRef } from '@wordpress/element'; + +const PostPanelRow = forwardRef( ( { className, label, children }, ref ) => { + return ( + + { label && ( +
      { label }
      + ) } +
      { children }
      +
      + ); +} ); + +export default PostPanelRow; diff --git a/packages/editor/src/components/post-panel-row/style.scss b/packages/editor/src/components/post-panel-row/style.scss new file mode 100644 index 00000000000000..bc1c7fbd000c63 --- /dev/null +++ b/packages/editor/src/components/post-panel-row/style.scss @@ -0,0 +1,21 @@ +.editor-post-panel__row { + width: 100%; + min-height: $button-size; + justify-content: flex-start !important; + align-items: flex-start !important; +} + +.editor-post-panel__row-label { + width: 30%; + flex-shrink: 0; + min-height: $button-size; + display: flex; + align-items: center; +} + +.editor-post-panel__row-control { + flex-grow: 1; + min-height: $button-size; + display: flex; + align-items: center; +} diff --git a/packages/editor/src/components/post-saved-state/index.js b/packages/editor/src/components/post-saved-state/index.js index 9d6cb49d91e8e8..7b2c19d6eabe5d 100644 --- a/packages/editor/src/components/post-saved-state/index.js +++ b/packages/editor/src/components/post-saved-state/index.js @@ -9,6 +9,7 @@ import classnames from 'classnames'; import { __unstableGetAnimateClassName as getAnimateClassName, Button, + Tooltip, } from '@wordpress/components'; import { usePrevious, useViewportMatch } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -128,45 +129,53 @@ export default function PostSavedState( { text = shortLabel; } + const buttonAccessibleLabel = text || label; + + /** + * The tooltip needs to be enabled only if the button is not disabled. When + * relying on the internal Button tooltip functionality, this causes the + * resulting `button` element to be always removed and re-added to the DOM, + * causing focus loss. An alternative approach to circumvent the issue + * is not to use the `label` and `shortcut` props on `Button` (which would + * trigger the tooltip), and instead manually wrap the `Button` in a separate + * `Tooltip` component. + */ + const tooltipProps = isDisabled + ? undefined + : { + text: buttonAccessibleLabel, + shortcut: displayShortcut.primary( 's' ), + }; + // Use common Button instance for all saved states so that focus is not // lost. return ( - + + + ); } diff --git a/packages/editor/src/components/post-schedule/panel.js b/packages/editor/src/components/post-schedule/panel.js new file mode 100644 index 00000000000000..2e725a06bc9fd7 --- /dev/null +++ b/packages/editor/src/components/post-schedule/panel.js @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { Button, Dropdown } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import PostScheduleCheck from './check'; +import PostScheduleForm from './index'; +import { usePostScheduleLabel } from './label'; +import PostPanelRow from '../post-panel-row'; + +export default function PostSchedulePanel() { + const [ popoverAnchor, setPopoverAnchor ] = useState( null ); + // Memoize popoverProps to avoid returning a new object every time. + const popoverProps = useMemo( + () => ( { + // Anchor the popover to the middle of the entire row so that it doesn't + // move around when the label changes. + anchor: popoverAnchor, + 'aria-label': __( 'Change publish date' ), + placement: 'bottom-end', + } ), + [ popoverAnchor ] + ); + + const label = usePostScheduleLabel(); + const fullLabel = usePostScheduleLabel( { full: true } ); + + return ( + + + ( + + ) } + renderContent={ ( { onClose } ) => ( + + ) } + /> + + + ); +} diff --git a/packages/editor/src/components/post-schedule/style.scss b/packages/editor/src/components/post-schedule/style.scss new file mode 100644 index 00000000000000..23cb185167db56 --- /dev/null +++ b/packages/editor/src/components/post-schedule/style.scss @@ -0,0 +1,23 @@ +.editor-post-schedule__panel-dropdown { + width: 100%; +} + +.editor-post-schedule__dialog { + .components-popover__content { + min-width: 320px; + padding: $grid-unit-20; + } +} + +.editor-post-schedule__dialog-toggle.components-button { + display: block; + max-width: 100%; + overflow: hidden; + text-align: left; + white-space: unset; + height: auto; + + // The line height + the padding should be the same as the button size. + padding: math.div($button-size - $grid-unit-20, 2) 12px; + line-height: $grid-unit-20; +} diff --git a/packages/editor/src/components/post-sync-status/index.js b/packages/editor/src/components/post-sync-status/index.js index abc45146c36af1..9fa9f3181654e2 100644 --- a/packages/editor/src/components/post-sync-status/index.js +++ b/packages/editor/src/components/post-sync-status/index.js @@ -4,7 +4,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; import { - PanelRow, Modal, Button, __experimentalHStack as HStack, @@ -17,6 +16,7 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ +import PostPanelRow from '../post-panel-row'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; @@ -44,14 +44,13 @@ export default function PostSyncStatus() { } return ( - - { __( 'Sync status' ) } -
      + +
      { syncStatus === 'unsynced' ? __( 'Not synced' ) : __( 'Fully synced' ) }
      - +
      ); } @@ -114,7 +113,7 @@ export function PostSyncStatusModal() { 'Option that makes an individual pattern synchronized' ) } help={ __( - 'Editing the pattern will update it anywhere it is used.' + 'Sync this pattern across multiple locations.' ) } checked={ ! syncType } onChange={ () => { diff --git a/packages/editor/src/components/post-sync-status/style.scss b/packages/editor/src/components/post-sync-status/style.scss index 90a75c86bf466d..d5ee21cad8ee46 100644 --- a/packages/editor/src/components/post-sync-status/style.scss +++ b/packages/editor/src/components/post-sync-status/style.scss @@ -1,19 +1,4 @@ -.edit-post-sync-status { - width: 100%; - position: relative; - justify-content: flex-start; - align-items: flex-start; - - > span { - display: block; - width: 45%; - flex-shrink: 0; - padding: $grid-unit-15 * 0.5 0; - word-break: break-word; - } - - > div { - // Match padding on tertiary buttons for alignment. - padding: $grid-unit-15 * 0.5 0 $grid-unit-15 * 0.5 $grid-unit-15; - } +.editor-post-sync-status__value { + // Match padding on tertiary buttons for alignment. + padding: $grid-unit-15 * 0.5 0 $grid-unit-15 * 0.5 $grid-unit-15; } diff --git a/packages/editor/src/components/post-title/constants.js b/packages/editor/src/components/post-title/constants.js new file mode 100644 index 00000000000000..2b0ff197f2b9f1 --- /dev/null +++ b/packages/editor/src/components/post-title/constants.js @@ -0,0 +1,4 @@ +export const DEFAULT_CLASSNAMES = + 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text'; + +export const REGEXP_NEWLINES = /[\r\n]+/g; diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index 09f5f30c2a660c..0c3dbbf7349a17 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -7,18 +7,12 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from '@wordpress/element'; +import { forwardRef, useState } from '@wordpress/element'; import { decodeEntities } from '@wordpress/html-entities'; -import { ENTER } from '@wordpress/keycodes'; import { useSelect, useDispatch } from '@wordpress/data'; -import { pasteHandler } from '@wordpress/blocks'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { ENTER } from '@wordpress/keycodes'; +import { pasteHandler } from '@wordpress/blocks'; import { __unstableUseRichText as useRichText, create, @@ -31,78 +25,45 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies */ -import PostTypeSupportCheck from '../post-type-support-check'; import { store as editorStore } from '../../store'; - -/** - * Constants - */ -const REGEXP_NEWLINES = /[\r\n]+/g; +import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants'; +import usePostTitleFocus from './use-post-title-focus'; +import usePostTitle from './use-post-title'; +import PostTypeSupportCheck from '../post-type-support-check'; function PostTitle( _, forwardedRef ) { - const ref = useRef(); + const { placeholder, hasFixedToolbar } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + const { getSettings } = select( blockEditorStore ); + const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = + getSettings(); + + return { + title: getEditedPostAttribute( 'title' ), + placeholder: titlePlaceholder, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); + const [ isSelected, setIsSelected ] = useState( false ); - const { editPost } = useDispatch( editorStore ); - const { insertDefaultBlock, clearSelectedBlock, insertBlocks } = - useDispatch( blockEditorStore ); - const { isCleanNewPost, title, placeholder, hasFixedToolbar } = useSelect( - ( select ) => { - const { getEditedPostAttribute, isCleanNewPost: _isCleanNewPost } = - select( editorStore ); - const { getSettings } = select( blockEditorStore ); - const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = - getSettings(); - - return { - isCleanNewPost: _isCleanNewPost(), - title: getEditedPostAttribute( 'title' ), - placeholder: titlePlaceholder, - hasFixedToolbar: _hasFixedToolbar, - }; - }, - [] - ); - useImperativeHandle( forwardedRef, () => ( { - focus: () => { - ref?.current?.focus(); - }, - } ) ); + const { ref: focusRef } = usePostTitleFocus( forwardedRef ); - useEffect( () => { - if ( ! ref.current ) { - return; - } + const { title, setTitle: onUpdate } = usePostTitle(); - const { defaultView } = ref.current.ownerDocument; - const { name, parent } = defaultView; - const ownerDocument = - name === 'editor-canvas' ? parent.document : defaultView.document; - const { activeElement, body } = ownerDocument; - - // Only autofocus the title when the post is entirely empty. This should - // only happen for a new post, which means we focus the title on new - // post so the author can start typing right away, without needing to - // click anything. - if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) { - ref.current.focus(); - } - }, [ isCleanNewPost ] ); + const [ selection, setSelection ] = useState( {} ); - function onEnterPress() { - insertDefaultBlock( undefined, undefined, 0 ); + const { clearSelectedBlock, insertBlocks, insertDefaultBlock } = + useDispatch( blockEditorStore ); + + function onChange( value ) { + onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); } function onInsertBlockAfter( blocks ) { insertBlocks( blocks, 0 ); } - function onUpdate( newTitle ) { - editPost( { title: newTitle } ); - } - - const [ selection, setSelection ] = useState( {} ); - function onSelect() { setIsSelected( true ); clearSelectedBlock(); @@ -113,8 +74,8 @@ function PostTitle( _, forwardedRef ) { setSelection( {} ); } - function onChange( value ) { - onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); + function onEnterPress() { + insertDefaultBlock( undefined, undefined, 0 ); } function onKeyDown( event ) { @@ -170,7 +131,13 @@ function PostTitle( _, forwardedRef ) { ( firstBlock.name === 'core/heading' || firstBlock.name === 'core/paragraph' ) ) { - onUpdate( stripHTML( firstBlock.attributes.content ) ); + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( + firstBlock.attributes.content + ); + onUpdate( contentNoHTML ); onInsertBlockAfter( content.slice( 1 ) ); } else { onInsertBlockAfter( content ); @@ -180,10 +147,13 @@ function PostTitle( _, forwardedRef ) { ...create( { html: title } ), ...selection, }; - const newValue = insert( - value, - create( { html: stripHTML( content ) } ) - ); + + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( content ); + + const newValue = insert( value, create( { html: contentNoHTML } ) ); onUpdate( toHTMLString( { value: newValue } ) ); setSelection( { start: newValue.start, @@ -192,17 +162,9 @@ function PostTitle( _, forwardedRef ) { } } - // The wp-block className is important for editor styles. - // This same block is used in both the visual and the code editor. - const className = classnames( - 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text', - { - 'is-selected': isSelected, - 'has-fixed-toolbar': hasFixedToolbar, - } - ); const decodedPlaceholder = decodeEntities( placeholder ) || __( 'Add title' ); + const { ref: richTextRef } = useRichText( { value: title, onChange, @@ -221,14 +183,21 @@ function PostTitle( _, forwardedRef ) { }; } ); }, - __unstableDisableFormats: true, + __unstableDisableFormats: false, + } ); + + // The wp-block className is important for editor styles. + // This same block is used in both the visual and the code editor. + const className = classnames( DEFAULT_CLASSNAMES, { + 'is-selected': isSelected, + 'has-fixed-toolbar': hasFixedToolbar, } ); - /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ return ( + /* eslint-disable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */

      + /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ ); - /* eslint-enable jsx-a11y/heading-has-content, jsx-a11y/no-noninteractive-element-to-interactive-role */ } export default forwardRef( PostTitle ); diff --git a/packages/editor/src/components/post-title/post-title-raw.js b/packages/editor/src/components/post-title/post-title-raw.js new file mode 100644 index 00000000000000..f59ec40e872e45 --- /dev/null +++ b/packages/editor/src/components/post-title/post-title-raw.js @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { TextareaControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; +import { useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { useState, forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { DEFAULT_CLASSNAMES, REGEXP_NEWLINES } from './constants'; +import usePostTitleFocus from './use-post-title-focus'; +import usePostTitle from './use-post-title'; + +function PostTitleRaw( _, forwardedRef ) { + const { placeholder, hasFixedToolbar } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const { titlePlaceholder, hasFixedToolbar: _hasFixedToolbar } = + getSettings(); + + return { + placeholder: titlePlaceholder, + hasFixedToolbar: _hasFixedToolbar, + }; + }, [] ); + + const [ isSelected, setIsSelected ] = useState( false ); + + const { title, setTitle: onUpdate } = usePostTitle(); + const { ref: focusRef } = usePostTitleFocus( forwardedRef ); + + function onChange( value ) { + onUpdate( value.replace( REGEXP_NEWLINES, ' ' ) ); + } + + function onSelect() { + setIsSelected( true ); + } + + function onUnselect() { + setIsSelected( false ); + } + + // The wp-block className is important for editor styles. + // This same block is used in both the visual and the code editor. + const className = classnames( DEFAULT_CLASSNAMES, { + 'is-selected': isSelected, + 'has-fixed-toolbar': hasFixedToolbar, + 'is-raw-text': true, + } ); + + const decodedPlaceholder = + decodeEntities( placeholder ) || __( 'Add title' ); + + return ( + + ); +} + +export default forwardRef( PostTitleRaw ); diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss new file mode 100644 index 00000000000000..98bdfb9a2ebf3a --- /dev/null +++ b/packages/editor/src/components/post-title/style.scss @@ -0,0 +1,5 @@ +// Raw Text Variant +.edit-post-text-editor__body .editor-post-title.is-raw-text { + margin-bottom: $grid-unit-30; + margin-top: 2px; // space for focus outline to appear. +} diff --git a/packages/editor/src/components/post-title/use-post-title-focus.js b/packages/editor/src/components/post-title/use-post-title-focus.js new file mode 100644 index 00000000000000..effac53f2670a2 --- /dev/null +++ b/packages/editor/src/components/post-title/use-post-title-focus.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { useEffect, useImperativeHandle, useRef } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function usePostTitleFocus( forwardedRef ) { + const ref = useRef(); + + const { isCleanNewPost } = useSelect( ( select ) => { + const { isCleanNewPost: _isCleanNewPost } = select( editorStore ); + + return { + isCleanNewPost: _isCleanNewPost(), + }; + }, [] ); + + useImperativeHandle( forwardedRef, () => ( { + focus: () => { + ref?.current?.focus(); + }, + } ) ); + + useEffect( () => { + if ( ! ref.current ) { + return; + } + + const { defaultView } = ref.current.ownerDocument; + const { name, parent } = defaultView; + const ownerDocument = + name === 'editor-canvas' ? parent.document : defaultView.document; + const { activeElement, body } = ownerDocument; + + // Only autofocus the title when the post is entirely empty. This should + // only happen for a new post, which means we focus the title on new + // post so the author can start typing right away, without needing to + // click anything. + if ( isCleanNewPost && ( ! activeElement || body === activeElement ) ) { + ref.current.focus(); + } + }, [ isCleanNewPost ] ); + + return { ref }; +} diff --git a/packages/editor/src/components/post-title/use-post-title.js b/packages/editor/src/components/post-title/use-post-title.js new file mode 100644 index 00000000000000..65bd67af6fb4c8 --- /dev/null +++ b/packages/editor/src/components/post-title/use-post-title.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +export default function usePostTitle() { + const { editPost } = useDispatch( editorStore ); + const { title } = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + + return { + title: getEditedPostAttribute( 'title' ), + }; + }, [] ); + + function updateTitle( newTitle ) { + editPost( { title: newTitle } ); + } + + return { title, setTitle: updateTitle }; +} diff --git a/packages/edit-post/src/components/sidebar/post-url/index.js b/packages/editor/src/components/post-url/panel.js similarity index 66% rename from packages/edit-post/src/components/sidebar/post-url/index.js rename to packages/editor/src/components/post-url/panel.js index 1dc1b8d804cd77..4c4fc38d3e2df3 100644 --- a/packages/edit-post/src/components/sidebar/post-url/index.js +++ b/packages/editor/src/components/post-url/panel.js @@ -2,15 +2,18 @@ * WordPress dependencies */ import { useMemo, useState } from '@wordpress/element'; -import { PanelRow, Dropdown, Button } from '@wordpress/components'; +import { Dropdown, Button } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { - PostURLCheck, - PostURL as PostURLForm, - usePostURLLabel, -} from '@wordpress/editor'; -export default function PostURL() { +/** + * Internal dependencies + */ +import PostURLCheck from './check'; +import PostURL from './index'; +import { usePostURLLabel } from './label'; +import PostPanelRow from '../post-panel-row'; + +export default function PostURLPanel() { // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. const [ popoverAnchor, setPopoverAnchor ] = useState( null ); @@ -22,21 +25,20 @@ export default function PostURL() { return ( - - { __( 'URL' ) } + ( ) } renderContent={ ( { onClose } ) => ( - + ) } /> - + ); } @@ -45,7 +47,7 @@ function PostURLToggle( { isOpen, onClick } ) { const label = usePostURLLabel(); return ( - *

      - * ``` - * - * @param {StoreProps} properties Properties to be added to the global store. - * @param {StoreOptions} [options] Options passed to the `store` call. - */ -export const store = ( { state, ...block }, { afterLoad } = {} ) => { - deepMerge( rawStore, block ); - deepMerge( rawState, state ); - if ( afterLoad ) afterLoads.add( afterLoad ); -}; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts new file mode 100644 index 00000000000000..1e9ab7e1a8f46b --- /dev/null +++ b/packages/interactivity/src/store.ts @@ -0,0 +1,289 @@ +/** + * External dependencies + */ +import { deepSignal } from 'deepsignal'; +import { computed } from '@preact/signals'; + +/** + * Internal dependencies + */ +import { + getScope, + setScope, + resetScope, + setNamespace, + resetNamespace, +} from './hooks'; + +const isObject = ( item: unknown ): boolean => + !! item && typeof item === 'object' && ! Array.isArray( item ); + +const deepMerge = ( target: any, source: any ) => { + if ( isObject( target ) && isObject( source ) ) { + for ( const key in source ) { + const getter = Object.getOwnPropertyDescriptor( source, key )?.get; + if ( typeof getter === 'function' ) { + Object.defineProperty( target, key, { get: getter } ); + } else if ( isObject( source[ key ] ) ) { + if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); + deepMerge( target[ key ], source[ key ] ); + } else { + Object.assign( target, { [ key ]: source[ key ] } ); + } + } + } +}; + +const parseInitialState = () => { + const storeTag = document.querySelector( + `script[type="application/json"]#wp-interactivity-initial-state` + ); + if ( ! storeTag?.textContent ) return {}; + try { + const initialState = JSON.parse( storeTag.textContent ); + if ( isObject( initialState ) ) return initialState; + throw Error( 'Parsed state is not an object' ); + } catch ( e ) { + // eslint-disable-next-line no-console + console.log( e ); + } + return {}; +}; + +export const stores = new Map(); +const rawStores = new Map(); +const storeLocks = new Map(); + +const objToProxy = new WeakMap(); +const proxyToNs = new WeakMap(); +const scopeToGetters = new WeakMap(); + +const proxify = ( obj: any, ns: string ) => { + if ( ! objToProxy.has( obj ) ) { + const proxy = new Proxy( obj, handlers ); + objToProxy.set( obj, proxy ); + proxyToNs.set( proxy, ns ); + } + return objToProxy.get( obj ); +}; + +const handlers = { + get: ( target: any, key: string | symbol, receiver: any ) => { + const ns = proxyToNs.get( receiver ); + + // Check if the property is a getter and we are inside an scope. If that is + // the case, we clone the getter to avoid overwriting the scoped + // dependencies of the computed each time that getter runs. + const getter = Object.getOwnPropertyDescriptor( target, key )?.get; + if ( getter ) { + const scope = getScope(); + if ( scope ) { + const getters = + scopeToGetters.get( scope ) || + scopeToGetters.set( scope, new Map() ).get( scope ); + if ( ! getters.has( getter ) ) { + getters.set( + getter, + computed( () => { + setNamespace( ns ); + setScope( scope ); + try { + return getter.call( target ); + } finally { + resetScope(); + resetNamespace(); + } + } ) + ); + } + return getters.get( getter ).value; + } + } + + const result = Reflect.get( target, key, receiver ); + + // Check if the proxy is the store root and no key with that name exist. In + // that case, return an empty object for the requested key. + if ( typeof result === 'undefined' && receiver === stores.get( ns ) ) { + const obj = {}; + Reflect.set( target, key, obj, receiver ); + return proxify( obj, ns ); + } + + // Check if the property is a generator. If it is, we turn it into an + // asynchronous function where we restore the default namespace and scope + // each time it awaits/yields. + if ( result?.constructor?.name === 'GeneratorFunction' ) { + return async ( ...args: unknown[] ) => { + const scope = getScope(); + const gen: Generator< any > = result( ...args ); + + let value: any; + let it: IteratorResult< any >; + + while ( true ) { + setNamespace( ns ); + setScope( scope ); + try { + it = gen.next( value ); + } finally { + resetScope(); + resetNamespace(); + } + + try { + value = await it.value; + } catch ( e ) { + gen.throw( e ); + } + + if ( it.done ) break; + } + + return value; + }; + } + + // Check if the property is a synchronous function. If it is, set the + // default namespace. Synchronous functions always run in the proper scope, + // which is set by the Directives component. + if ( typeof result === 'function' ) { + return ( ...args: unknown[] ) => { + setNamespace( ns ); + try { + return result( ...args ); + } finally { + resetNamespace(); + } + }; + } + + // Check if the property is an object. If it is, proxyify it. + if ( isObject( result ) ) return proxify( result, ns ); + + return result; + }, +}; + +/** + * @typedef StoreProps Properties object passed to `store`. + * @property {Object} state State to be added to the global store. All the + * properties included here become reactive. + */ + +/** + * @typedef StoreOptions Options object. + */ + +/** + * Extends the Interactivity API global store with the passed properties. + * + * These props typically consist of `state`, which is reactive, and other + * properties like `selectors`, `actions`, `effects`, etc. which can store + * callbacks and derived state. These props can then be referenced by any + * directive to make the HTML interactive. + * + * @example + * ```js + * store({ + * state: { + * counter: { value: 0 }, + * }, + * actions: { + * counter: { + * increment: ({ state }) => { + * state.counter.value += 1; + * }, + * }, + * }, + * }); + * ``` + * + * The code from the example above allows blocks to subscribe and interact with + * the store by using directives in the HTML, e.g.: + * + * ```html + *
      + * + *
      + * ``` + * + * @param {StoreProps} properties Properties to be added to the global store. + * @param {StoreOptions} [options] Options passed to the `store` call. + */ + +interface StoreOptions { + lock?: boolean | string; +} + +const universalUnlock = + 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; + +export function store< S extends object = {} >( + namespace: string, + storePart?: S, + options?: StoreOptions +): S; +export function store< T extends object >( + namespace: string, + storePart?: T, + options?: StoreOptions +): T; + +export function store( + namespace: string, + { state = {}, ...block }: any = {}, + { lock = false }: StoreOptions = {} +) { + if ( ! stores.has( namespace ) ) { + // Lock the store if the passed lock is different from the universal + // unlock. Once the lock is set (either false, true, or a given string), + // it cannot change. + if ( lock !== universalUnlock ) { + storeLocks.set( namespace, lock ); + } + const rawStore = { state: deepSignal( state ), ...block }; + const proxiedStore = new Proxy( rawStore, handlers ); + rawStores.set( namespace, rawStore ); + stores.set( namespace, proxiedStore ); + proxyToNs.set( proxiedStore, namespace ); + } else { + // Lock the store if it wasn't locked yet and the passed lock is + // different from the universal unlock. If no lock is given, the store + // will be public and won't accept any lock from now on. + if ( lock !== universalUnlock && ! storeLocks.has( namespace ) ) { + storeLocks.set( namespace, lock ); + } else { + const storeLock = storeLocks.get( namespace ); + const isLockValid = + lock === universalUnlock || + ( lock !== true && lock === storeLock ); + + if ( ! isLockValid ) { + if ( ! storeLock ) { + throw Error( 'Cannot lock a public store' ); + } else { + throw Error( + 'Cannot unlock a private store with an invalid lock code' + ); + } + } + } + + const target = rawStores.get( namespace ); + deepMerge( target, block ); + deepMerge( target.state, state ); + } + + return stores.get( namespace ); +} + +// Parse and populate the initial state. +Object.entries( parseInitialState() ).forEach( ( [ namespace, state ] ) => { + store( namespace, { state } ); +} ); diff --git a/packages/interactivity/src/vdom.js b/packages/interactivity/src/vdom.js index 1cf4a91ec1ead5..b1342ac271a8e2 100644 --- a/packages/interactivity/src/vdom.js +++ b/packages/interactivity/src/vdom.js @@ -10,6 +10,7 @@ import { directivePrefix as p } from './constants'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; +let namespace = null; // Regular expression for directive parsing. const directiveParser = new RegExp( @@ -25,6 +26,12 @@ const directiveParser = new RegExp( 'i' // Case insensitive. ); +// Regular expression for reference parsing. It can contain a namespace before +// the reference, separated by `::`, like `some-namespace::state.somePath`. +// Namespaces can contain any alphanumeric characters, hyphens, underscores or +// forward slashes. References don't have any restrictions. +const nsPathRegExp = /^([\w-_\/]+)::(.+)$/; + export const hydratedIslands = new WeakSet(); // Recursive function that transforms a DOM tree into vDOM. @@ -51,8 +58,7 @@ export function toVdom( root ) { const props = {}; const children = []; - const directives = {}; - let hasDirectives = false; + const directives = []; let ignore = false; let island = false; @@ -64,17 +70,19 @@ export function toVdom( root ) { ) { if ( n === ignoreAttr ) { ignore = true; - } else if ( n === islandAttr ) { - island = true; } else { - hasDirectives = true; - let val = attributes[ i ].value; + let [ ns, value ] = nsPathRegExp + .exec( attributes[ i ].value ) + ?.slice( 1 ) ?? [ null, attributes[ i ].value ]; try { - val = JSON.parse( val ); + value = JSON.parse( value ); } catch ( e ) {} - const [ , prefix, suffix ] = directiveParser.exec( n ); - directives[ prefix ] = directives[ prefix ] || {}; - directives[ prefix ][ suffix || 'default' ] = val; + if ( n === islandAttr ) { + island = true; + namespace = value?.namespace ?? null; + } else { + directives.push( [ n, ns, value ] ); + } } } else if ( n === 'ref' ) { continue; @@ -92,7 +100,22 @@ export function toVdom( root ) { ]; if ( island ) hydratedIslands.add( node ); - if ( hasDirectives ) props.__directives = directives; + if ( directives.length ) { + props.__directives = directives.reduce( + ( obj, [ name, ns, value ] ) => { + const [ , prefix, suffix = 'default' ] = + directiveParser.exec( name ); + if ( ! obj[ prefix ] ) obj[ prefix ] = []; + obj[ prefix ].push( { + namespace: ns ?? namespace, + value, + suffix, + } ); + return obj; + }, + {} + ); + } let child = treeWalker.firstChild(); if ( child ) { diff --git a/packages/interactivity/tsconfig.json b/packages/interactivity/tsconfig.json new file mode 100644 index 00000000000000..bcb26904e1d09d --- /dev/null +++ b/packages/interactivity/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "checkJs": false, + "strict": false + }, + "include": [ "src/**/*" ] +} diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index 73737f3ed99e93..9c7370291eef60 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.23.0 (2023-11-16) + ## 5.22.0 (2023-11-02) ## 5.21.0 (2023-10-18) diff --git a/packages/interface/package.json b/packages/interface/package.json index 28b11d1cbee5f1..429d2ad59d0fdb 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.22.0", + "version": "5.23.0", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js index 58684ebaddd7e8..baf98d153ed870 100644 --- a/packages/interface/src/components/interface-skeleton/index.js +++ b/packages/interface/src/components/interface-skeleton/index.js @@ -52,7 +52,6 @@ function InterfaceSkeleton( secondarySidebar, notices, content, - contentProps, actions, labels, className, @@ -151,7 +150,6 @@ function InterfaceSkeleton( { content } diff --git a/packages/is-shallow-equal/CHANGELOG.md b/packages/is-shallow-equal/CHANGELOG.md index 9eef05af9f1d68..9f5db485bb5405 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.46.0 (2023-11-16) + ## 4.45.0 (2023-11-02) ## 4.44.0 (2023-10-18) diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index e6b553baaceb70..ffc4e97590bc97 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/is-shallow-equal", - "version": "4.45.0", + "version": "4.46.0", "description": "Test for shallow equality between two objects or arrays.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-console/CHANGELOG.md b/packages/jest-console/CHANGELOG.md index 463bfd52db437b..4032ec50a5ebfd 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.17.0 (2023-11-16) + ## 7.16.0 (2023-11-02) ## 7.15.0 (2023-10-18) diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 40981febe941f6..1dfb50d5bd59c2 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "7.16.0", + "version": "7.17.0", "description": "Custom Jest matchers for the Console object.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index a56d34dddad385..30db9b59cca7c3 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 11.17.0 (2023-11-16) + ## 11.16.0 (2023-11-02) ## 11.15.0 (2023-10-18) diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index ecc07690603568..7e3b0812e3d067 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "11.16.0", + "version": "11.17.0", "description": "Default Jest preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-puppeteer-axe/CHANGELOG.md b/packages/jest-puppeteer-axe/CHANGELOG.md index acf19522a59bc9..6755f2d3f41365 100644 --- a/packages/jest-puppeteer-axe/CHANGELOG.md +++ b/packages/jest-puppeteer-axe/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.17.0 (2023-11-16) + ## 6.16.0 (2023-11-02) ## 6.15.0 (2023-10-18) diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index 68af3143838f38..eed831e07d801f 100644 --- a/packages/jest-puppeteer-axe/package.json +++ b/packages/jest-puppeteer-axe/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.16.0", + "version": "6.17.0", "description": "Axe API integration with Jest and Puppeteer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keyboard-shortcuts/CHANGELOG.md b/packages/keyboard-shortcuts/CHANGELOG.md index b5988c7102d039..f6fb39c60931c0 100644 --- a/packages/keyboard-shortcuts/CHANGELOG.md +++ b/packages/keyboard-shortcuts/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.23.0 (2023-11-16) + ## 4.22.0 (2023-11-02) ## 4.21.0 (2023-10-18) diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index 03100a616bc4ad..dfe02e60773679 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "4.22.0", + "version": "4.23.0", "description": "Handling keyboard shortcuts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index 3ab5dc90aa8581..382510e52e3676 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.46.0 (2023-11-16) + ## 3.45.0 (2023-11-02) ## 3.44.0 (2023-10-18) diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index f3705c6e523c17..4ca561d68b83d1 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "3.45.0", + "version": "3.46.0", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/lazy-import/CHANGELOG.md b/packages/lazy-import/CHANGELOG.md index e96828769051f3..a092b101c7fe43 100644 --- a/packages/lazy-import/CHANGELOG.md +++ b/packages/lazy-import/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.33.0 (2023-11-16) + ## 1.32.0 (2023-11-02) ## 1.31.0 (2023-10-18) diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index f697a2c2595980..62320482ae15bf 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "1.32.0", + "version": "1.33.0", "description": "Lazily import a module, installing it automatically if missing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index bb86ca45b59654..3c16e7ca341d0a 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.23.0 (2023-11-16) + ## 4.22.0 (2023-11-02) ## 4.21.0 (2023-10-18) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 9cc27c6911a91c..0feaddf684b5fc 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "4.22.0", + "version": "4.23.0", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/media-utils/CHANGELOG.md b/packages/media-utils/CHANGELOG.md index bc1ea49c74c866..791752726da0e9 100644 --- a/packages/media-utils/CHANGELOG.md +++ b/packages/media-utils/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.37.0 (2023-11-16) + ## 4.36.0 (2023-11-02) ## 4.35.0 (2023-10-18) diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index 16477d6546ad7c..93ed96c45246a9 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/media-utils", - "version": "4.36.0", + "version": "4.37.0", "description": "WordPress Media Upload Utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index f22d214e00037b..0bb727268bc07b 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.14.0 (2023-11-16) + ## 4.13.0 (2023-11-02) ## 4.12.0 (2023-10-18) diff --git a/packages/notices/package.json b/packages/notices/package.json index 798f2120b94df6..9250d196365c46 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "4.13.0", + "version": "4.14.0", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/npm-package-json-lint-config/CHANGELOG.md b/packages/npm-package-json-lint-config/CHANGELOG.md index 1242e51b295a68..c4c811e7396d7e 100644 --- a/packages/npm-package-json-lint-config/CHANGELOG.md +++ b/packages/npm-package-json-lint-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.31.0 (2023-11-16) + ## 4.30.0 (2023-11-02) ## 4.29.0 (2023-10-18) diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index a3f4a0dfcc9305..a62009f5d88a03 100644 --- a/packages/npm-package-json-lint-config/package.json +++ b/packages/npm-package-json-lint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.30.0", + "version": "4.31.0", "description": "WordPress npm-package-json-lint shareable configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index fa995b4dbcf54c..d3fbfa4a703809 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.8.0 (2023-11-16) + ## 8.7.0 (2023-11-02) ## 8.6.0 (2023-10-18) diff --git a/packages/nux/package.json b/packages/nux/package.json index f03f91dcf4c8b0..12f659accc9503 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.7.0", + "version": "8.8.0", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/CHANGELOG.md b/packages/patterns/CHANGELOG.md index 3a9c684d01b04e..30df46641ff4cd 100644 --- a/packages/patterns/CHANGELOG.md +++ b/packages/patterns/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.7.0 (2023-11-16) + ## 1.6.0 (2023-11-02) ## 1.5.0 (2023-10-18) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 41846c1047d931..bab11059bf92c9 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.6.0", + "version": "1.7.0", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/src/components/category-selector.js b/packages/patterns/src/components/category-selector.js index 7f00350e278ecf..fdd56f159b0921 100644 --- a/packages/patterns/src/components/category-selector.js +++ b/packages/patterns/src/components/category-selector.js @@ -61,6 +61,7 @@ export default function CategorySelector( { tokenizeOnBlur __experimentalExpandOnFocus __next40pxDefaultSize + __nextHasNoMarginBottom /> ); } diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index f5e6e85b8602d2..b12e4c9d21bedf 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -166,12 +166,13 @@ export default function CreatePatternModal( { > { @@ -197,6 +198,7 @@ export default function CreatePatternModal( { />