diff --git a/.eslintrc.js b/.eslintrc.js index 13362e76c4e473..caf01b7cbd71d4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -323,11 +323,14 @@ module.exports = { 'FontSizePicker', 'FormTokenField', 'InputControl', + 'LetterSpacingControl', 'LineHeightControl', 'NumberControl', 'RangeControl', + 'SelectControl', 'TextControl', 'ToggleGroupControl', + 'UnitControl', ].map( ( componentName ) => ( { // Falsy `__next40pxDefaultSize` without a non-default `size` prop. selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="size"][value.value!="default"]))`, @@ -343,7 +346,7 @@ module.exports = { 'FormFileUpload should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', }, // Temporary rules until all existing components have the `__next40pxDefaultSize` prop. - ...[ 'SelectControl' ].map( ( componentName ) => ( { + ...[ 'Button' ].map( ( componentName ) => ( { // Not strict. Allows pre-existing __next40pxDefaultSize={ false } usage until they are all manually updated. selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"])):not(:has(JSXAttribute[name.name="size"]))`, message: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e5f958eb9e9d85..2ec03cba722c6b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # Documentation -/docs @ajitbohra @ryanwelcher @juanmaguitar @fabiankaegy @ndiego +/docs @ajitbohra @juanmaguitar @fabiankaegy @ndiego /packages/interactivity/docs @juanmaguitar # Schemas @@ -119,9 +119,9 @@ /packages/plugins @gziolo @adamsilverstein # Rich Text -/packages/format-library @ellatrix @dcalhoun -/packages/rich-text @ellatrix @dcalhoun -/packages/block-editor/src/components/rich-text @ellatrix @dcalhoun +/packages/format-library @ellatrix +/packages/rich-text @ellatrix +/packages/block-editor/src/components/rich-text @ellatrix # Project Management /.github @desrosj diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 245b136ee22c18..0de1b9ee6566ae 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -14,17 +14,13 @@ concurrency: jobs: checks: - name: Checks w/Node.js ${{ matrix.node.name }} on ${{ matrix.os }} + name: Checks w/Node.js ${{ matrix.node }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: - - name: 20 - version: 20 - - name: 22 - version: 22.4 + node: ['20', '22'] os: ['macos-latest', 'ubuntu-latest', 'windows-latest'] steps: @@ -35,7 +31,7 @@ jobs: - name: Setup Node.js and install dependencies uses: ./.github/setup-node with: - node-version: ${{ matrix.node.version }} + node-version: ${{ matrix.node }} - name: Create block shell: bash diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 23c1134c1417a0..3efd7d79ee2276 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -28,7 +28,7 @@ jobs: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - name: Use desired version of Java - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + uses: actions/setup-java@6a0805fcefea3d4657a47ac4c165951e33482018 # v4.2.2 with: distribution: 'corretto' java-version: '17' @@ -47,7 +47,7 @@ jobs: run: npm run native test:e2e:setup - name: Gradle cache - uses: gradle/actions/setup-gradle@dbbdc275be76ac10734476cc723d82dfe7ec6eda # v3.4.2 + uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 - name: AVD cache uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 @@ -60,7 +60,7 @@ jobs: - name: Create AVD and generate snapshot for caching if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@77986be26589807b8ebab3fde7bbf5c60dabec32 # v2.31.0 + uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # v2.32.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false @@ -71,7 +71,7 @@ jobs: script: echo "Generated AVD snapshot for caching." - name: Run tests - uses: reactivecircus/android-emulator-runner@77986be26589807b8ebab3fde7bbf5c60dabec32 # v2.31.0 + uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 # v2.32.0 with: api-level: ${{ matrix.api-level }} force-avd-creation: false diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 2f515ce5148f8b..2926e494b09f89 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -27,7 +27,7 @@ jobs: with: show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - uses: ruby/setup-ruby@50ba3386b050ad5b97a41fcb81240cbee1d1821f # v1.188.0 + - uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 with: # `.ruby-version` file location working-directory: packages/react-native-editor/ios diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 98764848ecd809..c0f70070908c1c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -21,17 +21,13 @@ concurrency: jobs: unit-js: - name: JavaScript (Node.js ${{ matrix.node.name }}) ${{ matrix.shard }} + name: JavaScript (Node.js ${{ matrix.node }}) ${{ matrix.shard }} runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: - - name: 20 - version: 20 - - name: 22 - version: 22.4 + node: ['20', '22'] shard: ['1/4', '2/4', '3/4', '4/4'] steps: @@ -43,7 +39,7 @@ jobs: - name: Setup Node.js and install dependencies uses: ./.github/setup-node with: - node-version: ${{ matrix.node.version }} + node-version: ${{ matrix.node }} - name: Determine the number of CPU cores uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0 @@ -64,17 +60,13 @@ jobs: --cacheDirectory="$HOME/.jest-cache" unit-js-date: - name: JavaScript Date Tests (Node.js ${{ matrix.node.name }}) + name: JavaScript Date Tests (Node.js ${{ matrix.node }}) runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: - - name: 20 - version: 20 - - name: 22 - version: 22.4 + node: ['20', '22'] steps: - name: Checkout repository @@ -85,7 +77,7 @@ jobs: - name: Setup Node.js and install dependencies uses: ./.github/setup-node with: - node-version: ${{ matrix.node.version }} + node-version: ${{ matrix.node }} - name: Determine the number of CPU cores uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0 diff --git a/backport-changelog/6.7/7125.md b/backport-changelog/6.7/7125.md index ce208decd2d145..341e0415cc61a2 100644 --- a/backport-changelog/6.7/7125.md +++ b/backport-changelog/6.7/7125.md @@ -1,3 +1,4 @@ https://github.com/WordPress/wordpress-develop/pull/7125 * https://github.com/WordPress/gutenberg/pull/61577 +* https://github.com/WordPress/gutenberg/pull/64610 diff --git a/backport-changelog/6.7/7139.md b/backport-changelog/6.7/7139.md new file mode 100644 index 00000000000000..9023695102a919 --- /dev/null +++ b/backport-changelog/6.7/7139.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7139 + +* https://github.com/WordPress/gutenberg/pull/64504 diff --git a/backport-changelog/6.7/7200.md b/backport-changelog/6.7/7200.md new file mode 100644 index 00000000000000..520b3d6054cc18 --- /dev/null +++ b/backport-changelog/6.7/7200.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7200 + +* https://github.com/WordPress/gutenberg/pull/64511 diff --git a/backport-changelog/6.7/7247.md b/backport-changelog/6.7/7247.md new file mode 100644 index 00000000000000..d0b1de25872344 --- /dev/null +++ b/backport-changelog/6.7/7247.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7247 + +* https://github.com/WordPress/gutenberg/pull/64790 diff --git a/backport-changelog/6.7/7258.md b/backport-changelog/6.7/7258.md new file mode 100644 index 00000000000000..6714b13b70b8d2 --- /dev/null +++ b/backport-changelog/6.7/7258.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7258 + +* https://github.com/WordPress/gutenberg/pull/64570 \ No newline at end of file diff --git a/backport-changelog/6.7/7270.md b/backport-changelog/6.7/7270.md new file mode 100644 index 00000000000000..358b0d7c9a9674 --- /dev/null +++ b/backport-changelog/6.7/7270.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7270 + +* https://github.com/WordPress/gutenberg/pull/64890 diff --git a/backport-changelog/6.7/6910.md b/backport-changelog/6.8/6910.md similarity index 100% rename from backport-changelog/6.7/6910.md rename to backport-changelog/6.8/6910.md diff --git a/changelog.txt b/changelog.txt index 748df8da3484c7..0fb2b93056b9bc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,322 @@ == Changelog == += 19.1.0 = + +## Changelog + +### Enhancements + +#### Components +- Allow `style` prop on `Popover`. ([64489](https://github.com/WordPress/gutenberg/pull/64489)) +- Add elevation scale. ([64108](https://github.com/WordPress/gutenberg/pull/64108)) +- Apply elevation scale to: Modal, Popover, and Snackbar components. ([64655](https://github.com/WordPress/gutenberg/pull/64655)) +- Ariakit: Update to v0.4.10. ([64637](https://github.com/WordPress/gutenberg/pull/64637)) +- DimensionControl: Add flag to remove bottom margin. ([64346](https://github.com/WordPress/gutenberg/pull/64346)) +- DropdownMenu V2: Use themed color variables. ([64647](https://github.com/WordPress/gutenberg/pull/64647)) +- Placeholders: Update radius temporarily. ([64672](https://github.com/WordPress/gutenberg/pull/64672)) +- Reduce gap between steps in SpacingSizesControl, add animation, remove first/last marks. ([63803](https://github.com/WordPress/gutenberg/pull/63803)) +- Textarea Control: Update styles. ([64586](https://github.com/WordPress/gutenberg/pull/64586)) +- Tools Panel: Sets column-gap to 16px for grid. ([64497](https://github.com/WordPress/gutenberg/pull/64497)) +- Update DropdownMenuV2 elevation, remove unused configuration value. ([64432](https://github.com/WordPress/gutenberg/pull/64432)) +- Update components radius. ([64368](https://github.com/WordPress/gutenberg/pull/64368)) +- Use `useStoreState()` instead of `store.useState()`. ([64648](https://github.com/WordPress/gutenberg/pull/64648)) +- Composite: Use internal context to consume composite store. ([64493](https://github.com/WordPress/gutenberg/pull/64493)) +- Default to new 40px size in the following: + - FocalPointPicker: ([64456](https://github.com/WordPress/gutenberg/pull/64456)) + - QueryControls: ([64457](https://github.com/WordPress/gutenberg/pull/64457)) + +#### Data Views +- Do not display element descriptions in filters. ([64674](https://github.com/WordPress/gutenberg/pull/64674)) +- Apply minimal variant to pagination dropdown. ([63815](https://github.com/WordPress/gutenberg/pull/63815)) +- Update the style of the datetime fields to match the other types. ([64438](https://github.com/WordPress/gutenberg/pull/64438)) +- Use the fields array to define the order of the fields. ([64335](https://github.com/WordPress/gutenberg/pull/64335)) +- Make the move left/right controls in table header always available. ([64646](https://github.com/WordPress/gutenberg/pull/64646)) +- Support defining field headers/names as React elements. ([64642](https://github.com/WordPress/gutenberg/pull/64642)) +- Add marks to preview size control. ([64546](https://github.com/WordPress/gutenberg/pull/64546)) +- Move item size control to the new view configuration UI. ([64380](https://github.com/WordPress/gutenberg/pull/64380)) +- Update search appearance in narrow containers. ([64681](https://github.com/WordPress/gutenberg/pull/64681)) +- Quick edit additions: + - `comment_status` field. ([64370](https://github.com/WordPress/gutenberg/pull/64370)) + - `status` field. ([64398](https://github.com/WordPress/gutenberg/pull/64398)) + - 'Date' as field and `datetime` as field type. ([64267](https://github.com/WordPress/gutenberg/pull/64267)) +- Extensibility - allow unregistering of the following: + - Duplicate post action ([64441](https://github.com/WordPress/gutenberg/pull/64441)) + - Duplicate pattern action ([64373](https://github.com/WordPress/gutenberg/pull/64373)) + - Duplicate template part action ([64388](https://github.com/WordPress/gutenberg/pull/64388)) + - Rename post action ([64366](https://github.com/WordPress/gutenberg/pull/64366)) + - Reorder-page action ([64199](https://github.com/WordPress/gutenberg/pull/64199)) + - View post action ([64467](https://github.com/WordPress/gutenberg/pull/64467)) + - View post revisions action ([64464](https://github.com/WordPress/gutenberg/pull/64464)) +- Add missing styles and remove opinionated ones for generic usage. ([64711](https://github.com/WordPress/gutenberg/pull/64711)) + +#### Block Library +- Embed Block: Replace native input element with InputControl component. ([64668](https://github.com/WordPress/gutenberg/pull/64668)) +- Grid: Prevent highlight of cells when dragging a block if block type can't be dropped into grid. ([64290](https://github.com/WordPress/gutenberg/pull/64290)) +- Image block: Add reset button. ([64669](https://github.com/WordPress/gutenberg/pull/64669)) +- Overlay caption w. text-shadow. ([63471](https://github.com/WordPress/gutenberg/pull/63471)) + +#### Design Tools +- Background image: Add uploading state and restrict drag to one image. ([64565](https://github.com/WordPress/gutenberg/pull/64565)) +- Quote Block: Add align support. ([64188](https://github.com/WordPress/gutenberg/pull/64188)) +- Add border support to the following: + - Comment Author Name ([64550](https://github.com/WordPress/gutenberg/pull/64550)) + - Comment Content ([64230](https://github.com/WordPress/gutenberg/pull/64230)) + - Comment Date ([64210](https://github.com/WordPress/gutenberg/pull/64210)) + - Post Author Biography ([64615](https://github.com/WordPress/gutenberg/pull/64615)) + - Post Author Name ([64530](https://github.com/WordPress/gutenberg/pull/64530)) + - Post Author ([64599](https://github.com/WordPress/gutenberg/pull/64599)) + - Query Title ([64581](https://github.com/WordPress/gutenberg/pull/64581)) + - File: ([64509](https://github.com/WordPress/gutenberg/pull/64509)) + - List Item: ([63541](https://github.com/WordPress/gutenberg/pull/63541)) + - List: ([63540](https://github.com/WordPress/gutenberg/pull/63540)) + - Preformatted: ([64302](https://github.com/WordPress/gutenberg/pull/64302)) + - Tag Cloud: ([63579](https://github.com/WordPress/gutenberg/pull/63579)) + +#### Zoom Out +- Add private `isZoomOutMode` selector. ([64503](https://github.com/WordPress/gutenberg/pull/64503)) +- Block Insertion: Clear the insertion point when selecting a different block or clearing block selection. ([64048](https://github.com/WordPress/gutenberg/pull/64048)) +- Default the inserter to the patterns tab when in zoom out. ([64193](https://github.com/WordPress/gutenberg/pull/64193)) +- Focus pattern inserter search when activating zoom out inserter. ([64396](https://github.com/WordPress/gutenberg/pull/64396)) +- Stop unwanted drag and drop operations within section Patterns in Zoom Out mode. ([64331](https://github.com/WordPress/gutenberg/pull/64331)) + +#### Block Editor +- Button groups in Typography tools should use ToggleGroupControl. ([64529](https://github.com/WordPress/gutenberg/pull/64529)) +- Hyphenate long block names in the inserter. ([64667](https://github.com/WordPress/gutenberg/pull/64667)) + +#### Global Styles +- Additional CSS: Localize the link if it exists. ([64603](https://github.com/WordPress/gutenberg/pull/64603)) +- Background images: Add support for theme.json ref value resolution. ([64128](https://github.com/WordPress/gutenberg/pull/64128)) + + +### New APIs + +#### Components +- Composite + - Add Hover and Typeahead subcomponents. ([64399](https://github.com/WordPress/gutenberg/pull/64399)) + - Stabilize new ariakit implementation. ([63564](https://github.com/WordPress/gutenberg/pull/63564)) + - Export `useCompositeStore`, add more focus-related props. ([64450](https://github.com/WordPress/gutenberg/pull/64450)) + +#### Synced Patterns +- Block Bindings: Create utils to update or remove bindings. ([64102](https://github.com/WordPress/gutenberg/pull/64102)) + +#### Extensibility +- Add plugin template registration API. ([61577](https://github.com/WordPress/gutenberg/pull/61577)) + + +### Bug Fixes + +#### Components +- CustomSelectControl: Improve props type inferring. ([64412](https://github.com/WordPress/gutenberg/pull/64412)) +- ColorPalette: Partial support of `color-mix()` CSS colors. ([64224](https://github.com/WordPress/gutenberg/pull/64224)) +- RangeControl: Disable reset button consistently. ([64579](https://github.com/WordPress/gutenberg/pull/64579)) +- RangeControl: Tweak mark and label absolute positioning. ([64487](https://github.com/WordPress/gutenberg/pull/64487)) + +#### Data Views +- Load the filter toggle as open if there are primary filters. ([64651](https://github.com/WordPress/gutenberg/pull/64651)) +- Sort descending button may be wrongly pressed. ([64547](https://github.com/WordPress/gutenberg/pull/64547)) +- Filter icon is displayed even when no filter capabilities are given to any field. ([64640](https://github.com/WordPress/gutenberg/pull/64640)) +- Hide sort direction control if there are no sortable fields. ([64817](https://github.com/WordPress/gutenberg/pull/64817)) + +#### Zoom Out +- Disallow dropping outside section root in Zoom Out mode. ([64500](https://github.com/WordPress/gutenberg/pull/64500)) +- Don't hide the insertion point when hovering patterns. ([64392](https://github.com/WordPress/gutenberg/pull/64392)) +- Use previous device width for scale calculations. ([64478](https://github.com/WordPress/gutenberg/pull/64478)) + +#### Block Library +- Embed blocks: Adding captions via toolbar - #64385. ([64394](https://github.com/WordPress/gutenberg/pull/64394)) +- Paste: Fix blob uploading. ([64479](https://github.com/WordPress/gutenberg/pull/64479)) +- Table Block: Hide caption toolbar button on multiple selection. ([64462](https://github.com/WordPress/gutenberg/pull/64462)) + +#### Post Editor +- Fix user pattern preloading filter. ([64477](https://github.com/WordPress/gutenberg/pull/64477)) +- Fix preloaded REST API paths. ([64459](https://github.com/WordPress/gutenberg/pull/64459)) +- Force iframe editor when zoom-out mode. ([64316](https://github.com/WordPress/gutenberg/pull/64316)) + +#### Block Editor +- Don't hide the toolbar for an empty default block in HTML mode. ([64374](https://github.com/WordPress/gutenberg/pull/64374)) +- In-between Inserter: Show inserter when it doesn't conflict with block toolbar. ([64229](https://github.com/WordPress/gutenberg/pull/64229)) +- Slash Inserter: Restrict block list to allowed blocks only. ([64413](https://github.com/WordPress/gutenberg/pull/64413)) + +#### Site Editor +- Don't allow duplicating template parts in non-block-based themes. ([64379](https://github.com/WordPress/gutenberg/pull/64379)) +- Fix Template Parts post type preload path. ([64401](https://github.com/WordPress/gutenberg/pull/64401)) +- Cancel button in duplicate template part modal doesn't work. ([64377](https://github.com/WordPress/gutenberg/pull/64377)) +- Fix empty content sidebar panel. ([64569](https://github.com/WordPress/gutenberg/pull/64569)) + +#### Block bindings +- Fix long keys overflow in bindings panel. ([64465](https://github.com/WordPress/gutenberg/pull/64465)) +- Hide keys starting with underscore. ([64618](https://github.com/WordPress/gutenberg/pull/64618)) +- Refactor utils file. ([64740](https://github.com/WordPress/gutenberg/pull/64740)) + +#### CSS & Styling +- Remove inconsistent dark theme focus style on block selection. ([64549](https://github.com/WordPress/gutenberg/pull/64549)) +- Update postcss-prefixwrap dependency to 1.51.0 to fix prefixing in `:Where` selectors. ([64458](https://github.com/WordPress/gutenberg/pull/64458)) + +#### Interactivity API +- Fix context inheritance from namespaces different than the current one. ([64677](https://github.com/WordPress/gutenberg/pull/64677)) +- Fix computeds without scope in Firefox. ([64825](https://github.com/WordPress/gutenberg/pull/64825)) + +#### Document Settings +- Post Featured Image: Disable the media modal while uploading an image. ([64566](https://github.com/WordPress/gutenberg/pull/64566)) + +#### Patterns +- Changing sorting direction on patterns does nothing. ([64508](https://github.com/WordPress/gutenberg/pull/64508)) + +#### Design Tools +- Background image: Ensure consistency with defaults and fix reset/remove functionality. ([64328](https://github.com/WordPress/gutenberg/pull/64328)) + +#### Global Styles +- Fix bumped specificity for layout styles in non-iframed editor. ([64076](https://github.com/WordPress/gutenberg/pull/64076)) + + +### Accessibility + +- Site Editor: Always use auto-cursor style for editable text. ([64627](https://github.com/WordPress/gutenberg/pull/64627)) +- Post Editor: Update textControl to searchControl in taxonomy search. ([64605](https://github.com/WordPress/gutenberg/pull/64605)) +- RadioControl: Label radio group using fieldset and legend. ([64582](https://github.com/WordPress/gutenberg/pull/64582)) +- Fix labeling in Typography font size presets panel. ([64428](https://github.com/WordPress/gutenberg/pull/64428)) +- Latests Posts: Used ToggleGroupControl instead for Image alignment. ([64352](https://github.com/WordPress/gutenberg/pull/64352)) + + +### Performance + +- Fetch permissions for visible patterns only. ([64606](https://github.com/WordPress/gutenberg/pull/64606)) +- Background Image: Remove unnecessary 'block-editor' store subscription. ([64568](https://github.com/WordPress/gutenberg/pull/64568)) +- Edit Post: Avoid unnecessary post-template ID lookup. ([64431](https://github.com/WordPress/gutenberg/pull/64431)) +- GridVisualizer: Avoid over-selecting by using a new getBlockStyles private selector. ([64386](https://github.com/WordPress/gutenberg/pull/64386)) + + +### Experiments + +#### Data Views +- DataViews Quick Edit + - Add Post Card to the quick edit panel. ([64365](https://github.com/WordPress/gutenberg/pull/64365)) + - Add the PostActions dropdown menu. ([64393](https://github.com/WordPress/gutenberg/pull/64393)) + - Rely on the global save flow instead of a custom save button. ([64389](https://github.com/WordPress/gutenberg/pull/64389)) +- Update the copy of quick edit tooltip. ([64475](https://github.com/WordPress/gutenberg/pull/64475)) + +#### Components +- Composite v2: Undo stabilizing new version. ([64510](https://github.com/WordPress/gutenberg/pull/64510)) + + +### Documentation + +- Add clarification about importing css/scss files. ([61252](https://github.com/WordPress/gutenberg/pull/61252)) +- Components + - Add "Naming conventions" section. ([63714](https://github.com/WordPress/gutenberg/pull/63714)) + - Add 40px size prop to readmes. ([64592](https://github.com/WordPress/gutenberg/pull/64592)) +- Composite: Improve Storybook examples and clean up prop documentation. ([64397](https://github.com/WordPress/gutenberg/pull/64397)) +- Dataviews + - Added missing properties for actions object and link to storybook example. ([64442](https://github.com/WordPress/gutenberg/pull/64442)) + - Fixed tip link for block editor view. ([64469](https://github.com/WordPress/gutenberg/pull/64469)) + - Update README with missing properties and recent changes. ([64435](https://github.com/WordPress/gutenberg/pull/64435)) + - Better explanation of the "elements" property and its connection to the "filterBy" property. ([64633](https://github.com/WordPress/gutenberg/pull/64633)) +- Interactivity API + - The first three Core Concepts guides. ([63759](https://github.com/WordPress/gutenberg/pull/63759)) + - Fix internal links core-concepts. ([64609](https://github.com/WordPress/gutenberg/pull/64609)) + - Remove typed function from API reference. ([64429](https://github.com/WordPress/gutenberg/pull/64429)) + - Add code concepts to Navigating the Interactivity API documentation. ([64608](https://github.com/WordPress/gutenberg/pull/64608)) + - Interactivity API: Add wp_interactivity_state() clarification. ([64356](https://github.com/WordPress/gutenberg/pull/64356)) +- Fix typos in the Block Filters documentation.. ([64426](https://github.com/WordPress/gutenberg/pull/64426)) +- Fix example of useBlockProps hook. ([64363](https://github.com/WordPress/gutenberg/pull/64363)) +- Fix typo and link in static-dynamic-rendering.md. ([64449](https://github.com/WordPress/gutenberg/pull/64449)) +- Fix typo in block-filters.md. ([64452](https://github.com/WordPress/gutenberg/pull/64452)) +- Fix typo in block-wrapper.md. ([64447](https://github.com/WordPress/gutenberg/pull/64447)) +- Note about image sizes in MediaUpload::OnSelect. ([64616](https://github.com/WordPress/gutenberg/pull/64616)) +- Small typo correction in doc file. ([64596](https://github.com/WordPress/gutenberg/pull/64596)) +- TextDecorationControl, TextTransformControl: Remove size prop in Storybook. ([64583](https://github.com/WordPress/gutenberg/pull/64583)) +- Updated `@since` order in Inline document in client-assets.php file. ([64653](https://github.com/WordPress/gutenberg/pull/64653)) +- Updated small typo in compat.php file. ([64535](https://github.com/WordPress/gutenberg/pull/64535)) +- Updated small typo in modularity.md. ([64518](https://github.com/WordPress/gutenberg/pull/64518)) + + +### Code Quality + +- Add lint rule for 40px size prop usage in the following: + - BorderBoxControl, BorderControl, DimensionControl, FontSizePicker: ([64410](https://github.com/WordPress/gutenberg/pull/64410)) + - Block Editor typography components ([64591](https://github.com/WordPress/gutenberg/pull/64591)) + - FormFileUpload: ([64585](https://github.com/WordPress/gutenberg/pull/64585)) + - FormTokenField: ([64590](https://github.com/WordPress/gutenberg/pull/64590)) + - InputControl: ([64589](https://github.com/WordPress/gutenberg/pull/64589)) + - NumberControl: ([64561](https://github.com/WordPress/gutenberg/pull/64561)) + - RangeControl: ([64558](https://github.com/WordPress/gutenberg/pull/64558)) + - SelectControl: ([64486](https://github.com/WordPress/gutenberg/pull/64486)) + - TextControl: ([64455](https://github.com/WordPress/gutenberg/pull/64455)) + - ToggleGroupControl: ([64524](https://github.com/WordPress/gutenberg/pull/64524)) + - ComboboxControl: ([64560](https://github.com/WordPress/gutenberg/pull/64560)) + - CustomSelectControl: ([64559](https://github.com/WordPress/gutenberg/pull/64559)) +- Add margin-bottom lint rules for BaseControl. ([64355](https://github.com/WordPress/gutenberg/pull/64355)) +- Add missing changes to the changelog for the PR #62734. ([64507](https://github.com/WordPress/gutenberg/pull/64507)) +- Base Styles: Restore deprecated `$dark-theme-focus` variable. ([64563](https://github.com/WordPress/gutenberg/pull/64563)) +- ESLint: Enable and enforce remaining i18n rules for the plugin (e.g. no trailing spaces). ([60196](https://github.com/WordPress/gutenberg/pull/60196)) +- Remove unnecessary className. ([64403](https://github.com/WordPress/gutenberg/pull/64403)) +- Replace instances of deprecated elevation variables. ([64656](https://github.com/WordPress/gutenberg/pull/64656)) +- Style engine: Export util to compile CSS custom var from preset string. ([64490](https://github.com/WordPress/gutenberg/pull/64490)) +- Style engine: Update type for getCSSValueFromRawStyle. ([64528](https://github.com/WordPress/gutenberg/pull/64528)) +- TextControl: Fix remaining 40px size violations. ([64594](https://github.com/WordPress/gutenberg/pull/64594)) +- Border: 1px → $border-width. ([64680](https://github.com/WordPress/gutenberg/pull/64680)) + +#### Block Library +- Gallery: Remove 'withNotices' HoC. ([64384](https://github.com/WordPress/gutenberg/pull/64384)) +- Missing Block: Use hooks instead of HoC. ([64657](https://github.com/WordPress/gutenberg/pull/64657)) + +#### Block Editor +- Use hooks instead of HoC in: + - 'BlockModeToggle'. ([64460](https://github.com/WordPress/gutenberg/pull/64460)) + - 'MultiSelectionInspector'. ([64634](https://github.com/WordPress/gutenberg/pull/64634)) + +#### Components +- Deprecate bottom margin on BaseControl-based components. ([64408](https://github.com/WordPress/gutenberg/pull/64408)) +- Navigator: Simplify backwards navigation APIs. ([63317](https://github.com/WordPress/gutenberg/pull/63317)) + +#### Data Views +- Refactor the edit function to be based on discrete controls. ([64404](https://github.com/WordPress/gutenberg/pull/64404)) +- Update `renderFormElements` to make sure the value respects the type. ([64391](https://github.com/WordPress/gutenberg/pull/64391)) +- Abandon the ItemRecord type. ([64367](https://github.com/WordPress/gutenberg/pull/64367)) + +#### Block hooks +- Navigation Block: Remove now-obsolete function_exists guards. ([64673](https://github.com/WordPress/gutenberg/pull/64673)) + +#### Nested / Inner Blocks +- Block Editor: Refactor inner blocks appender components. ([64470](https://github.com/WordPress/gutenberg/pull/64470)) + +#### Plugin +- Script Modules: Move data passing to 6.7 compat file. ([64006](https://github.com/WordPress/gutenberg/pull/64006)) + + +### Tools + +- Make wp-env compatible with WordPress versions older than 5.4 by fixing wp-config anchors. ([55864](https://github.com/WordPress/gutenberg/pull/55864)) + +#### Testing +- Background block supports: Remove unused properties in unit tests. ([64564](https://github.com/WordPress/gutenberg/pull/64564)) +- Fix flaky block template registration end-to-end test. ([64541](https://github.com/WordPress/gutenberg/pull/64541)) +- Improve Image block end-to-end tests. ([64537](https://github.com/WordPress/gutenberg/pull/64537)) +- Upgrade Playwright to v1.46. ([64372](https://github.com/WordPress/gutenberg/pull/64372)) + +#### Build Tooling +- Fix gutenberg/gutenberg-coding-standards licensing issues. ([61913](https://github.com/WordPress/gutenberg/pull/61913)) +- Props Bot: Update to correct event type. ([64557](https://github.com/WordPress/gutenberg/pull/64557)) + + +## First-time contributors + +The following PRs were merged by first-time contributors: + +- @cweiske: Note about image sizes in MediaUpload::OnSelect. ([64616](https://github.com/WordPress/gutenberg/pull/64616)) +- @imrraaj: Dataviews: Filter icon is displayed even when no filter capabilities are given to any field. ([64640](https://github.com/WordPress/gutenberg/pull/64640)) +- @janpfeil: Fix typo in block-filters.md. ([64452](https://github.com/WordPress/gutenberg/pull/64452)) +- @Rishit30G: `ColorPalette`: Partial support of `color-mix()` CSS colors. ([64224](https://github.com/WordPress/gutenberg/pull/64224)) +- @ssang: Slash Inserter: Restrict block list to allowed blocks only. ([64413](https://github.com/WordPress/gutenberg/pull/64413)) + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @akasunil @Aljullu @amitraj2203 @anton-vlasenko @arthur791004 @cbravobernal @ciampo @colorful-tones @cweiske @DAreRodz @ellatrix @felixarntz @getdave @hbhalodia @imrraaj @jameskoster @janpfeil @jasmussen @jeherve @jorgefilipecosta @jsnajdr @juanmaguitar @luisherranz @Mamaduka @meteorlxy @mirka @ndiego @noisysocks @ntsekouras @oandregal @ockham @ramonjd @richtabor @Rishit30G @SantosGuillamot @scruffian @shail-mehta @shreya0204 @sirreal @ssang @swissspidy @t-hamano @talldan @tyxla @vipul0425 @youknowriad + + = 19.0.0 = ## Changelog diff --git a/docs/explanations/user-interface/design-resources.md b/docs/explanations/user-interface/design-resources.md index 235951e4839e98..4a2a78f6822a5d 100644 --- a/docs/explanations/user-interface/design-resources.md +++ b/docs/explanations/user-interface/design-resources.md @@ -2,7 +2,7 @@ ## Figma -The [WordPress Design team](https://make.wordpress.org/design/) uses [Figma](https://www.figma.com/) to collaborate and share work. If you'd like to contribute, join the [#design channel](https://app.slack.com/client/T024MFP4J/C02S78ZAL) in [Slack](https://make.wordpress.org/chat/) and ask the team to set you up with a free Figma account. This will give you access to a helpful library of components used in WordPress. They are stable, fully supported, up to date, and ready for use in designs and prototypes. +The [WordPress Design team](https://make.wordpress.org/design/) uses [Figma](https://www.figma.com/) to collaborate and share work. If you'd like to contribute, you can use [the WordPress Figma design library](https://make.wordpress.org/design/handbook/get-involved/tools-figma/) to make mockups. You can also join the [#design channel](https://app.slack.com/client/T024MFP4J/C02S78ZAL) in [Slack](https://make.wordpress.org/chat/) and if you'd like to ask for advice or otherwise. Figma accounts are free, and with one you can use components from the shared libraries, or duplicate files to your draft if you need to make edits. Full edit access to the WordPress libraries is paid and reserved for the design team. ### How to contribute diff --git a/docs/getting-started/fundamentals/block-in-the-editor.md b/docs/getting-started/fundamentals/block-in-the-editor.md index f7357def5ec2df..d1f2a25063e6c6 100644 --- a/docs/getting-started/fundamentals/block-in-the-editor.md +++ b/docs/getting-started/fundamentals/block-in-the-editor.md @@ -124,7 +124,7 @@ export default function Edit( { attributes, setAttributes } ) { { __( 'Background color', 'block-development-examples' ) } - diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index 52a325ff9253de..544f35593106f1 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -1,6 +1,6 @@ # Attributes -Block attributes provide information about the data stored by a block. For example, rich content, a list of image URLs, a background colour, or a button title. +Block attributes provide information about the data stored by a block. For example, rich content, a list of image URLs, a background color, or a button title. A block can contain any number of attributes, and these are specified by the `attributes` field - an object where each key is the name of the attribute, and the value is the attribute definition. diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 040a10f8f506c2..c6552ef431cef8 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -84,7 +84,7 @@ _Parameters_ _Returns_ -- `boolean | undefined`: Whether the given block is allowed to be moved. +- `boolean`: Whether the given block is allowed to be moved. ### canMoveBlocks diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index f08fbc960b8b28..474207aa20460f 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -772,7 +772,7 @@ _Parameters_ - _kind_ `string`: Kind of the deleted entity. - _name_ `string`: Name of the deleted entity. -- _recordId_ `string`: Record ID of the deleted entity. +- _recordId_ `number|string`: Record ID of the deleted entity. - _query_ `?Object`: Special query parameters for the DELETE API call. - _options_ `[Object]`: Delete options. - _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md index 46bd20bece0bda..25498d10bde2ef 100644 --- a/docs/reference-guides/interactivity-api/api-reference.md +++ b/docs/reference-guides/interactivity-api/api-reference.md @@ -84,7 +84,7 @@ It provides a **local** state available to a specific HTML node and its children The `wp-context` directive accepts a stringified JSON as a value. ```php -//render.php +// render.php
+ + + + ); +}; + +registerPlugin( 'plugin-sidebar-more-menu-item-example', { render: PluginSidebarMoreMenuItemTest, } ); ``` ## Location -![Interaction](https://github.com/raw/WordPress/gutenberg/HEAD/docs/assets/plugin-sidebar-more-menu-item.gif?raw=true) +![Interaction](https://developer.wordpress.org/files/2024/08/pluginsidebar-more-menu-item-1.gif) diff --git a/docs/reference-guides/slotfills/plugin-sidebar.md b/docs/reference-guides/slotfills/plugin-sidebar.md index dbf1c5643d3aab..9bf911b3bb13f2 100644 --- a/docs/reference-guides/slotfills/plugin-sidebar.md +++ b/docs/reference-guides/slotfills/plugin-sidebar.md @@ -2,6 +2,7 @@ This slot allows adding items to the tool bar of either the Post or Site editor screens. Using this slot will add an icon to the toolbar that, when clicked, opens a panel with containing the items wrapped in the `` component. +Additionally, it will also create a `` that will allow opening the panel from Options panel when clicked. ## Example @@ -63,6 +64,4 @@ registerPlugin( 'plugin-sidebar-example', { render: PluginSidebarExample } ); ## Location -### Open State - -![Open State](https://github.com/raw/WordPress/gutenberg/HEAD/docs/assets/plugin-sidebar-open-state.png?raw=true) +![PluginSidebar example expanded](https://developer.wordpress.org/files/2024/08/plugin-sidebar-example.png) diff --git a/gutenberg.php b/gutenberg.php index 2cd33a570f5857..117f4168524d8a 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 19.1.0-rc.1 + * Version: 19.2.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/block-style-variations.php b/lib/block-supports/block-style-variations.php index 73e4f9fce25843..3942fed24b98a8 100644 --- a/lib/block-supports/block-style-variations.php +++ b/lib/block-supports/block-style-variations.php @@ -11,12 +11,15 @@ * * @since 6.6.0 * + * @deprecated 6.7.0 + * * @param array $block Block object. * @param string $variation Slug for the block style variation. * * @return string The unique variation name. */ function gutenberg_create_block_style_variation_instance_name( $block, $variation ) { + _deprecated_function( __FUNCTION__, '6.7.0' ); return $variation . '--' . md5( serialize( $block ) ); } @@ -119,7 +122,7 @@ function gutenberg_render_block_style_variation_support_styles( $parsed_block ) // theme_json data. gutenberg_resolve_block_style_variation_ref_values( $variation_data, $theme_json ); - $variation_instance = gutenberg_create_block_style_variation_instance_name( $parsed_block, $variation ); + $variation_instance = wp_unique_id( $variation . '--' ); $class_name = "is-style-$variation_instance"; $updated_class_name = $parsed_block['attrs']['className'] . " $class_name"; @@ -224,11 +227,9 @@ function gutenberg_render_block_style_variation_class_name( $block_content, $blo /* * Matches a class prefixed by `is-style`, followed by the - * variation slug, then `--`, and finally a hash. - * - * See `gutenberg_create_block_style_variation_instance_name` for class generation. + * variation slug, then `--`, and finally an instance number. */ - preg_match( '/\bis-style-(\S+?--\w+)\b/', $block['attrs']['className'], $matches ); + preg_match( '/\bis-style-(\S+?--\d+)\b/', $block['attrs']['className'], $matches ); if ( empty( $matches ) ) { return $block_content; diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index fa2a7b81e94e21..a4719b7bdd4099 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -449,6 +449,7 @@ function gutenberg_get_computed_fluid_typography_value( $args = array() ) { * @since 6.3.0 Using layout.wideSize as max viewport width, and logarithmic scale factor to calculate minimum font scale. * @since 6.4.0 Added configurable min and max viewport width values to the typography.fluid theme.json schema. * @since 6.6.0 Deprecated bool argument $should_use_fluid_typography. + * @since 6.7.0 Font size presets can enable fluid typography individually, even if it’s disabled globally. * * @param array $preset { * Required. fontSizes preset value as seen in theme.json. @@ -468,10 +469,11 @@ function gutenberg_get_typography_font_size_value( $preset, $settings = array() } /* - * Catches empty values and 0/'0'. - * Fluid calculations cannot be performed on 0. + * Catch falsy values and 0/'0'. Fluid calculations cannot be performed on `0`. + * Also return early when a preset font size explicitly disables fluid typography with `false`. */ - if ( empty( $preset['size'] ) ) { + $fluid_font_size_settings = $preset['fluid'] ?? null; + if ( false === $fluid_font_size_settings || empty( $preset['size'] ) ) { return $preset['size']; } @@ -489,20 +491,25 @@ function gutenberg_get_typography_font_size_value( $preset, $settings = array() } // Fallback to global settings as default. - $global_settings = gutenberg_get_global_settings(); - $settings = wp_parse_args( + $global_settings = gutenberg_get_global_settings(); + $settings = wp_parse_args( $settings, $global_settings ); - $typography_settings = isset( $settings['typography'] ) ? $settings['typography'] : array(); - $should_use_fluid_typography = ! empty( $typography_settings['fluid'] ); + $typography_settings = $settings['typography'] ?? array(); - if ( ! $should_use_fluid_typography ) { + /* + * Return early when fluid typography is disabled in the settings, and there + * are no local settings to enable it for the individual preset. + * + * If this condition isn't met, either the settings or individual preset settings + * have enabled fluid typography. + */ + if ( empty( $typography_settings['fluid'] ) && empty( $fluid_font_size_settings ) ) { return $preset['size']; } - // $typography_settings['fluid'] can be a bool or an array. Normalize to array. - $fluid_settings = is_array( $typography_settings['fluid'] ) ? $typography_settings['fluid'] : array(); + $fluid_settings = isset( $typography_settings['fluid'] ) ? $typography_settings['fluid'] : array(); $layout_settings = isset( $settings['layout'] ) ? $settings['layout'] : array(); // Defaults. @@ -522,14 +529,6 @@ function gutenberg_get_typography_font_size_value( $preset, $settings = array() $has_min_font_size = isset( $fluid_settings['minFontSize'] ) && ! empty( gutenberg_get_typography_value_and_unit( $fluid_settings['minFontSize'] ) ); $minimum_font_size_limit = $has_min_font_size ? $fluid_settings['minFontSize'] : $default_minimum_font_size_limit; - // Font sizes. - $fluid_font_size_settings = isset( $preset['fluid'] ) ? $preset['fluid'] : null; - - // A font size has explicitly bypassed fluid calculations. - if ( false === $fluid_font_size_settings ) { - return $preset['size']; - } - // Try to grab explicit min and max fluid font sizes. $minimum_font_size_raw = isset( $fluid_font_size_settings['min'] ) ? $fluid_font_size_settings['min'] : null; $maximum_font_size_raw = isset( $fluid_font_size_settings['max'] ) ? $fluid_font_size_settings['max'] : null; diff --git a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php index 2999921be52259..d0b9f18bf5b29b 100644 --- a/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php +++ b/lib/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php @@ -196,6 +196,8 @@ public static function decode( $context, $text ) { * * @since 6.6.0 * + * @global WP_Token_Map $html5_named_character_references + * * @param string $context `attribute` for decoding attribute values, `data` otherwise. * @param string $text Text document containing span of text to decode. * @param int $at Optional. Byte offset into text where span begins, defaults to the beginning (0). diff --git a/lib/compat/wordpress-6.7/block-bindings.php b/lib/compat/wordpress-6.7/block-bindings.php index 398b53b340673b..9e82c1843f35a0 100644 --- a/lib/compat/wordpress-6.7/block-bindings.php +++ b/lib/compat/wordpress-6.7/block-bindings.php @@ -38,3 +38,18 @@ function gutenberg_add_server_block_bindings_sources_to_editor_settings( $editor } add_filter( 'block_editor_settings_all', 'gutenberg_add_server_block_bindings_sources_to_editor_settings', 10 ); + +/** + * Initialize `canUpdateBlockBindings` editor setting if it doesn't exist. By default, it is `true` only for admin users. + * + * @param array $settings The block editor settings from the `block_editor_settings_all` filter. + * @return array The editor settings including `canUpdateBlockBindings`. + */ +function gutenberg_add_can_update_block_bindings_editor_setting( $editor_settings ) { + if ( empty( $editor_settings['canUpdateBlockBindings'] ) ) { + $editor_settings['canUpdateBlockBindings'] = current_user_can( 'manage_options' ); + } + return $editor_settings; +} + +add_filter( 'block_editor_settings_all', 'gutenberg_add_can_update_block_bindings_editor_setting', 10 ); diff --git a/lib/compat/wordpress-6.7/class-gutenberg-rest-server.php b/lib/compat/wordpress-6.7/class-gutenberg-rest-server.php new file mode 100644 index 00000000000000..8374e8dc1fa23f --- /dev/null +++ b/lib/compat/wordpress-6.7/class-gutenberg-rest-server.php @@ -0,0 +1,169 @@ +get_data(); + $links = static::get_compact_response_links( $response ); + + if ( ! empty( $links ) ) { + // Convert links to part of the data. + $data['_links'] = $links; + } + + if ( $embed ) { + $this->embed_cache = array(); + // Determine if this is a numeric array. + if ( wp_is_numeric_array( $data ) ) { + foreach ( $data as $key => $item ) { + $data[ $key ] = $this->embed_links( $item, $embed ); + } + } else { + $data = $this->embed_links( $data, $embed ); + } + $this->embed_cache = array(); + } + + return $data; + } + + /** + * Retrieves links from a response. + * + * Extracts the links from a response into a structured hash, suitable for + * direct output. + * + * @since 4.4.0 + * @since 6.7.0 The `targetHints` property to the `self` link object was added. + * + * @param WP_REST_Response $response Response to extract links from. + * @return array Map of link relation to list of link hashes. + */ + public static function get_response_links( $response ) { + $links = $response->get_links(); + + if ( empty( $links ) ) { + return array(); + } + + $server = rest_get_server(); + + // Convert links to part of the data. + $data = array(); + foreach ( $links as $rel => $items ) { + $data[ $rel ] = array(); + + foreach ( $items as $item ) { + $attributes = $item['attributes']; + $attributes['href'] = $item['href']; + + if ( 'self' !== $rel ) { + $data[ $rel ][] = $attributes; + continue; + } + + // Prefer targetHints that were specifically designated by the developer. + if ( isset( $attributes['targetHints']['allow'] ) ) { + $data[ $rel ][] = $attributes; + continue; + } + + $request = WP_REST_Request::from_url( $item['href'] ); + if ( ! $request ) { + $data[ $rel ][] = $attributes; + continue; + } + + $match = $server->match_request_to_handler( $request ); + if ( ! is_wp_error( $match ) ) { + $response = new WP_REST_Response(); + $response->set_matched_route( $match[0] ); + $response->set_matched_handler( $match[1] ); + $headers = rest_send_allow_header( $response, $server, $request )->get_headers(); + + foreach ( $headers as $name => $value ) { + $name = WP_REST_Request::canonicalize_header_name( $name ); + $attributes['targetHints'][ $name ] = array_map( 'trim', explode( ',', $value ) ); + } + } + + $data[ $rel ][] = $attributes; + } + } + + return $data; + } + + /** + * Retrieves the CURIEs (compact URIs) used for relations. + * + * Extracts the links from a response into a structured hash, suitable for + * direct output. + * + * @since 4.5.0 + * + * @param WP_REST_Response $response Response to extract links from. + * @return array Map of link relation to list of link hashes. + */ + // @core-merge: Do not merge. The method is copied here to fix the inheritance issue. + public static function get_compact_response_links( $response ) { + $links = static::get_response_links( $response ); + + if ( empty( $links ) ) { + return array(); + } + + $curies = $response->get_curies(); + $used_curies = array(); + + foreach ( $links as $rel => $items ) { + + // Convert $rel URIs to their compact versions if they exist. + foreach ( $curies as $curie ) { + $href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) ); + if ( ! str_starts_with( $rel, $href_prefix ) ) { + continue; + } + + // Relation now changes from '$uri' to '$curie:$relation'. + $rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) ); + preg_match( '!' . $rel_regex . '!', $rel, $matches ); + if ( $matches ) { + $new_rel = $curie['name'] . ':' . $matches[1]; + $used_curies[ $curie['name'] ] = $curie; + $links[ $new_rel ] = $items; + unset( $links[ $rel ] ); + break; + } + } + } + + // Push the curies onto the start of the links array. + if ( $used_curies ) { + $links['curies'] = array_values( $used_curies ); + } + + return $links; + } +} diff --git a/lib/compat/wordpress-6.7/compat.php b/lib/compat/wordpress-6.7/compat.php index edc8e3fa5fb03f..cd533a42cc528e 100644 --- a/lib/compat/wordpress-6.7/compat.php +++ b/lib/compat/wordpress-6.7/compat.php @@ -27,11 +27,15 @@ function _gutenberg_add_block_templates_from_registry( $query_result, $query, $t foreach ( $query_result as $key => $value ) { $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $query_result[ $key ]->slug ); if ( $registered_template ) { - $query_result[ $key ]->plugin = $registered_template->plugin; - $query_result[ $key ]->origin = + $query_result[ $key ]->plugin = $registered_template->plugin; + $query_result[ $key ]->origin = 'theme' !== $query_result[ $key ]->origin && 'theme' !== $query_result[ $key ]->source ? 'plugin' : $query_result[ $key ]->origin; + $query_result[ $key ]->title = + empty( $query_result[ $key ]->title ) || $query_result[ $key ]->title === $query_result[ $key ]->slug ? + $registered_template->title : $query_result[ $key ]->title; + $query_result[ $key ]->description = empty( $query_result[ $key ]->description ) ? $registered_template->description : $query_result[ $key ]->description; } } @@ -70,11 +74,13 @@ function _gutenberg_add_block_template_plugin_attribute( $block_template ) { if ( $block_template ) { $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug ); if ( $registered_template ) { - $block_template->plugin = $registered_template->plugin; - $block_template->origin = + $block_template->plugin = $registered_template->plugin; + $block_template->origin = 'theme' !== $block_template->origin && 'theme' !== $block_template->source ? 'plugin' : $block_template->origin; + $block_template->title = empty( $block_template->title ) || $block_template->title === $block_template->slug ? $registered_template->title : $block_template->title; + $block_template->description = empty( $block_template->description ) ? $registered_template->description : $block_template->description; } } diff --git a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php index 70f51151d86478..c745904865b1f8 100644 --- a/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php +++ b/lib/compat/wordpress-6.7/html-api/class-gutenberg-html-decoder-6-7.php @@ -196,6 +196,8 @@ public static function decode( $context, $text ): string { * * @since 6.6.0 * + * @global WP_Token_Map $html5_named_character_references + * * @param string $context `attribute` for decoding attribute values, `data` otherwise. * @param string $text Text document containing span of text to decode. * @param int $at Optional. Byte offset into text where span begins, defaults to the beginning (0). diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 081c22c8102914..c5e2927198da0c 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -24,6 +24,12 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { if ( false !== $parts_key ) { $paths[ $parts_key ] = '/wp/v2/types/wp_template_part?context=edit'; } + + $page_options_path = array( rest_get_route_for_post_type_items( 'page' ), 'OPTIONS' ); + $page_options_key = array_search( $page_options_path, $paths, true ); + if ( false === $page_options_key ) { + $paths[] = $page_options_path; + } } if ( 'core/edit-post' === $context->name ) { @@ -98,3 +104,13 @@ function gutenberg_register_wp_rest_templates_controller_plugin_field() { ); } add_action( 'rest_api_init', 'gutenberg_register_wp_rest_templates_controller_plugin_field' ); + +/** + * Overrides the default 'WP_REST_Server' class. + * + * @return string The name of the custom server class. + */ +function gutenberg_override_default_rest_server() { + return 'Gutenberg_REST_Server'; +} +add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 ); diff --git a/lib/experimental/block-editor-settings-mobile.php b/lib/experimental/block-editor-settings-mobile.php index 91e3694c199f83..62c03db222afce 100644 --- a/lib/experimental/block-editor-settings-mobile.php +++ b/lib/experimental/block-editor-settings-mobile.php @@ -16,24 +16,15 @@ * @return array New block editor settings. */ function gutenberg_get_block_editor_settings_mobile( $settings ) { - if ( - defined( 'REST_REQUEST' ) && - REST_REQUEST && - isset( $_GET['context'] ) && - 'mobile' === $_GET['context'] - ) { - if ( wp_theme_has_theme_json() ) { - $settings['__experimentalStyles'] = gutenberg_get_global_styles(); - } - - // To tell mobile that the site uses quote v2 (inner blocks). - // See https://github.com/WordPress/gutenberg/pull/25892. - $settings['__experimentalEnableQuoteBlockV2'] = true; - // To tell mobile that the site uses list v2 (inner blocks). - $settings['__experimentalEnableListBlockV2'] = true; + if ( wp_theme_has_theme_json() ) { + $settings['__experimentalStyles'] = gutenberg_get_global_styles(); } + // To tell mobile that the site uses quote v2 (inner blocks). + // See https://github.com/WordPress/gutenberg/pull/25892. + $settings['__experimentalEnableQuoteBlockV2'] = true; + // To tell mobile that the site uses list v2 (inner blocks). + $settings['__experimentalEnableListBlockV2'] = true; + return $settings; } - -add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_mobile', PHP_INT_MAX ); diff --git a/lib/experimental/class-wp-rest-block-editor-settings-controller.php b/lib/experimental/class-wp-rest-block-editor-settings-controller.php index 2c4bf29bc21a73..ed2dcfe8584a6c 100644 --- a/lib/experimental/class-wp-rest-block-editor-settings-controller.php +++ b/lib/experimental/class-wp-rest-block-editor-settings-controller.php @@ -93,9 +93,13 @@ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAna break; } + add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_mobile', PHP_INT_MAX ); + $editor_context = new WP_Block_Editor_Context( array( 'name' => $editor_context_name ) ); $settings = get_block_editor_settings( array(), $editor_context ); + remove_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings_mobile', PHP_INT_MAX ); + return rest_ensure_response( $settings ); } diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index c34984baa0a619..919be2e6e34a45 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -28,14 +28,14 @@ function gutenberg_enable_experiments() { if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalFullPageClientSideNavigation = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-zoomed-out-patterns-tab', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableZoomedOutPatternsTab = true', 'before' ); - } if ( $gutenberg_experiments && array_key_exists( 'gutenberg-quick-edit-dataviews', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalQuickEditDataViews = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings-ui', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindingsUI = true', 'before' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-media-processing', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalMediaProcessing = true', 'before' ); + } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-zoom-out-experiment', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableZoomOutExperiment = true', 'before' ); } } diff --git a/lib/experimental/media/class-gutenberg-rest-attachments-controller.php b/lib/experimental/media/class-gutenberg-rest-attachments-controller.php new file mode 100644 index 00000000000000..71bf7b7a958351 --- /dev/null +++ b/lib/experimental/media/class-gutenberg-rest-attachments-controller.php @@ -0,0 +1,367 @@ +namespace, + '/' . $this->rest_base . '/(?P[\d]+)/sideload', + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'sideload_item' ), + 'permission_callback' => array( $this, 'sideload_item_permissions_check' ), + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the attachment.', 'gutenberg' ), + 'type' => 'integer', + ), + 'image_size' => array( + 'description' => __( 'Image size.', 'gutenberg' ), + 'type' => 'string', + 'enum' => $valid_image_sizes, + 'required' => true, + ), + ), + ), + 'allow_batch' => $this->allow_batch, + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves an array of endpoint arguments from the item schema for the controller. + * + * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are + * checked for required values and may fall-back to a given default, this is not done + * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE. + * @return array Endpoint arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + $args = rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method ); + + if ( WP_REST_Server::CREATABLE === $method ) { + $args['generate_sub_sizes'] = array( + 'type' => 'boolean', + 'default' => true, + 'description' => __( 'Whether to generate image sub sizes.', 'gutenberg' ), + ); + $args['convert_format'] = array( + 'type' => 'boolean', + 'default' => true, + 'description' => __( 'Whether to convert image formats.', 'gutenberg' ), + ); + } + + return $args; + } + + /** + * Prepares a single attachment output for response. + * + * Ensures 'missing_image_sizes' is set for PDFs and not just images. + * + * @param WP_Post $item Attachment object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ): WP_REST_Response { + $response = parent::prepare_item_for_response( $item, $request ); + + $data = $response->get_data(); + + // Handle missing image sizes for PDFs. + + $fields = $this->get_fields_for_response( $request ); + + if ( + rest_is_field_included( 'missing_image_sizes', $fields ) && + empty( $data['missing_image_sizes'] ) + ) { + $mime_type = get_post_mime_type( $item ); + + if ( 'application/pdf' === $mime_type ) { + $metadata = wp_get_attachment_metadata( $item->ID, true ); + + if ( ! is_array( $metadata ) ) { + $metadata = array(); + } + + $metadata['sizes'] = $metadata['sizes'] ?? array(); + + $fallback_sizes = array( + 'thumbnail', + 'medium', + 'large', + ); + + // The filter might have been added by ::create_item(). + remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + + /** This filter is documented in wp-admin/includes/image.php */ + $fallback_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + $registered_sizes = wp_get_registered_image_subsizes(); + $merged_sizes = array_keys( array_intersect_key( $registered_sizes, array_flip( $fallback_sizes ) ) ); + + $missing_image_sizes = array_diff( $merged_sizes, array_keys( $metadata['sizes'] ) ); + $data['missing_image_sizes'] = $missing_image_sizes; + } + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $links = $response->get_links(); + + $response = rest_ensure_response( $data ); + + foreach ( $links as $rel => $rel_links ) { + foreach ( $rel_links as $link ) { + $response->add_link( $rel, $link['href'], $link['attributes'] ); + } + } + + return $response; + } + + /** + * Creates a single attachment. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. + */ + public function create_item( $request ) { + if ( ! $request['generate_sub_sizes'] ) { + add_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); + add_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + + } + + if ( ! $request['convert_format'] ) { + add_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + } + + $response = parent::create_item( $request ); + + remove_filter( 'intermediate_image_sizes_advanced', '__return_empty_array', 100 ); + remove_filter( 'fallback_intermediate_image_sizes', '__return_empty_array', 100 ); + remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + + return $response; + } + + + /** + * Checks if a given request has access to sideload a file. + * + * Sideloading a file for an existing attachment + * requires both update and create permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise. + */ + public function sideload_item_permissions_check( $request ) { + return $this->edit_media_item_permissions_check( $request ); + } + + /** + * Filters {@see 'wp_unique_filename'} during sideloads. + * + * {@see wp_unique_filename()} will always add numeric suffix if the name looks like a sub-size to avoid conflicts. + * + * Adding this closure to the filter helps work around this safeguard. + * + * Example: when uploading myphoto.jpeg, WordPress normally creates myphoto-150x150.jpeg, + * and when uploading myphoto-150x150.jpeg, it will be renamed to myphoto-150x150-1.jpeg + * However, here it is desired not to add the suffix in order to maintain the same + * naming convention as if the file was uploaded regularly. + * + * @link https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 + * + * @param string $filename Unique file name. + * @param string $ext File extension. Example: ".png". + * @param string $dir Directory path. + * @param callable|null $unique_filename_callback Callback function that generates the unique file name. + * @param string[] $alt_filenames Array of alternate file names that were checked for collisions. + * @param int|string $number The highest number that was used to make the file name unique + * or an empty string if unused. + * @return string Filtered file name. + */ + private function filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ) { + if ( empty( $number ) || ! $attachment_filename ) { + return $filename; + } + + $ext = pathinfo( $filename, PATHINFO_EXTENSION ); + $name = pathinfo( $filename, PATHINFO_FILENAME ); + $orig_name = pathinfo( $attachment_filename, PATHINFO_FILENAME ); + + if ( ! $ext || ! $name ) { + return $filename; + } + + $matches = array(); + if ( preg_match( '/(.*)(-\d+x\d+)-' . $number . '$/', $name, $matches ) ) { + $filename_without_suffix = $matches[1] . $matches[2] . ".$ext"; + if ( $matches[1] === $orig_name && ! file_exists( "$dir/$filename_without_suffix" ) ) { + return $filename_without_suffix; + } + } + + return $filename; + } + + /** + * Side-loads a media file without creating an attachment. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure. + */ + public function sideload_item( WP_REST_Request $request ) { + $attachment_id = $request['id']; + + $post = $this->get_post( $attachment_id ); + + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( + ! wp_attachment_is_image( $post ) && + ! wp_attachment_is( 'pdf', $post ) + ) { + return new WP_Error( + 'rest_post_invalid_id', + __( 'Invalid post ID, only images and PDFs can be sideloaded.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + if ( ! $request['convert_format'] ) { + // Prevent image conversion as that is done client-side. + add_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + } + + // Get the file via $_FILES or raw data. + $files = $request->get_file_params(); + $headers = $request->get_headers(); + + /* + * wp_unique_filename() will always add numeric suffix if the name looks like a sub-size to avoid conflicts. + * See https://github.com/WordPress/wordpress-develop/blob/30954f7ac0840cfdad464928021d7f380940c347/src/wp-includes/functions.php#L2576-L2582 + * With the following filter we can work around this safeguard. + */ + + $attachment_filename = get_attached_file( $attachment_id, true ); + $attachment_filename = $attachment_filename ? wp_basename( $attachment_filename ) : null; + + /** + * @param string $filename Unique file name. + * @param string $ext File extension. Example: ".png". + * @param string $dir Directory path. + * @param callable|null $unique_filename_callback Callback function that generates the unique file name. + * @param string[] $alt_filenames Array of alternate file names that were checked for collisions. + * @param int|string $number The highest number that was used to make the file name unique + * or an empty string if unused. + * @return string Filtered file name. + */ + $filter_filename = function ( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number ) use ( $attachment_filename ) { + return $this->filter_wp_unique_filename( $filename, $ext, $dir, $unique_filename_callback, $alt_filenames, $number, $attachment_filename ); + }; + + add_filter( 'wp_unique_filename', $filter_filename, 10, 6 ); + + $parent_post = get_post_parent( $attachment_id ); + + $time = null; + + // Matches logic in media_handle_upload(). + // The post date doesn't usually matter for pages, so don't backdate this upload. + if ( $parent_post && 'page' !== $parent_post->post_type && substr( $parent_post->post_date, 0, 4 ) > 0 ) { + $time = $parent_post->post_date; + } + + if ( ! empty( $files ) ) { + $file = $this->upload_from_file( $files, $headers, $time ); + } else { + $file = $this->upload_from_data( $request->get_body(), $headers, $time ); + } + + remove_filter( 'wp_unique_filename', $filter_filename ); + remove_filter( 'image_editor_output_format', '__return_empty_array', 100 ); + + if ( is_wp_error( $file ) ) { + return $file; + } + + $type = $file['type']; + $path = $file['file']; + + $image_size = $request['image_size']; + + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + + if ( ! $metadata ) { + $metadata = array(); + } + + if ( 'original' === $image_size ) { + $metadata['original_image'] = wp_basename( $path ); + } else { + $metadata['sizes'] = $metadata['sizes'] ?? array(); + + $size = wp_getimagesize( $path ); + + $metadata['sizes'][ $image_size ] = array( + 'width' => $size ? $size[0] : 0, + 'height' => $size ? $size[1] : 0, + 'file' => wp_basename( $path ), + 'mime-type' => $type, + 'filesize' => wp_filesize( $path ), + ); + } + + wp_update_attachment_metadata( $attachment_id, $metadata ); + + $response_request = new WP_REST_Request( + WP_REST_Server::READABLE, + rest_get_route_for_post( $attachment_id ) + ); + + $response_request['context'] = 'edit'; + + if ( isset( $request['_fields'] ) ) { + $response_request['_fields'] = $request['_fields']; + } + + $response = $this->prepare_item_for_response( get_post( $attachment_id ), $response_request ); + + $response->header( 'Location', rest_url( rest_get_route_for_post( $attachment_id ) ) ); + + return $response; + } +} diff --git a/lib/experimental/media/load.php b/lib/experimental/media/load.php new file mode 100644 index 00000000000000..5cb16d84e1d8d9 --- /dev/null +++ b/lib/experimental/media/load.php @@ -0,0 +1,347 @@ + &$size ) { + $size['height'] = (int) $size['height']; + $size['width'] = (int) $size['width']; + $size['name'] = $name; + } + unset( $size ); + + return $sizes; +} + +/** + * Returns the default output format mapping for the supported image formats. + * + * @return array Map of input formats to output formats. + */ +function gutenberg_get_default_image_output_formats() { + $input_formats = array( + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'image/avif', + 'image/heic', + ); + + $output_formats = array(); + + foreach ( $input_formats as $mime_type ) { + /** This filter is documented in wp-includes/media.php */ + $output_formats = apply_filters( + 'image_editor_output_format', // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $output_formats, + '', + $mime_type + ); + } + + return $output_formats; +} + +/** + * Filters the REST API root index data to add custom settings. + * + * @param WP_REST_Response $response Response data. + */ +function gutenberg_media_processing_filter_rest_index( WP_REST_Response $response ) { + /** This filter is documented in wp-admin/includes/images.php */ + $image_size_threshold = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + $default_image_output_formats = gutenberg_get_default_image_output_formats(); + + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + $jpeg_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + $png_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/png' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ + $gif_interlaced = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + + if ( current_user_can( 'upload_files' ) ) { + $response->data['image_sizes'] = gutenberg_get_all_image_sizes(); + $response->data['image_size_threshold'] = $image_size_threshold; + $response->data['image_output_formats'] = (object) $default_image_output_formats; + $response->data['jpeg_interlaced'] = $jpeg_interlaced; + $response->data['png_interlaced'] = $png_interlaced; + $response->data['gif_interlaced'] = $gif_interlaced; + } + + return $response; +} + +add_filter( 'rest_index', 'gutenberg_media_processing_filter_rest_index' ); + + +/** + * Overrides the REST controller for the attachment post type. + * + * @param array $args Array of arguments for registering a post type. + * See the register_post_type() function for accepted arguments. + * @param string $post_type Post type key. + */ +function gutenberg_filter_attachment_post_type_args( array $args, string $post_type ): array { + if ( 'attachment' === $post_type ) { + require_once __DIR__ . '/class-gutenberg-rest-attachments-controller.php'; + + $args['rest_controller_class'] = Gutenberg_REST_Attachments_Controller::class; + } + + return $args; +} + +add_filter( 'register_post_type_args', 'gutenberg_filter_attachment_post_type_args', 10, 2 ); + + +/** + * Registers additional REST fields for attachments. + */ +function gutenberg_media_processing_register_rest_fields(): void { + register_rest_field( + 'attachment', + 'filename', + array( + 'schema' => array( + 'description' => __( 'Original attachment file name', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'get_callback' => 'gutenberg_rest_get_attachment_filename', + ) + ); + + register_rest_field( + 'attachment', + 'filesize', + array( + 'schema' => array( + 'description' => __( 'Attachment file size', 'gutenberg' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + ), + 'get_callback' => 'gutenberg_rest_get_attachment_filesize', + ) + ); +} + +add_action( 'rest_api_init', 'gutenberg_media_processing_register_rest_fields' ); + +/** + * Returns the attachment's original file name. + * + * @param array $post Post data. + * @return string|null Attachment file name. + */ +function gutenberg_rest_get_attachment_filename( array $post ): ?string { + $path = wp_get_original_image_path( $post['id'] ); + + if ( $path ) { + return basename( $path ); + } + + $path = get_attached_file( $post['id'] ); + + if ( $path ) { + return basename( $path ); + } + + return null; +} + +/** + * Returns the attachment's file size in bytes. + * + * @param array $post Post data. + * @return int|null Attachment file size. + */ +function gutenberg_rest_get_attachment_filesize( array $post ): ?int { + $attachment_id = $post['id']; + + $meta = wp_get_attachment_metadata( $attachment_id ); + + if ( isset( $meta['filesize'] ) ) { + return $meta['filesize']; + } + + $original_path = wp_get_original_image_path( $attachment_id ); + $attached_file = $original_path ? $original_path : get_attached_file( $attachment_id ); + + if ( is_string( $attached_file ) && file_exists( $attached_file ) ) { + return wp_filesize( $attached_file ); + } + + return null; +} + +/** + * Enables cross-origin isolation in the block editor. + * + * Required for enabling SharedArrayBuffer for WebAssembly-based + * media processing in the editor. + * + * @link https://web.dev/coop-coep/ + */ +function gutenberg_set_up_cross_origin_isolation() { + $screen = get_current_screen(); + + if ( ! $screen ) { + return; + } + + if ( ! $screen->is_block_editor() && 'site-editor' !== $screen->id && ! ( 'widgets' === $screen->id && wp_use_widgets_block_editor() ) ) { + return; + } + + $user_id = get_current_user_id(); + if ( ! $user_id ) { + return; + } + + // Cross-origin isolation is not needed if users can't upload files anyway. + if ( ! user_can( $user_id, 'upload_files' ) ) { + return; + } + + gutenberg_start_cross_origin_isolation_output_buffer(); +} + +add_action( 'load-post.php', 'gutenberg_set_up_cross_origin_isolation' ); +add_action( 'load-post-new.php', 'gutenberg_set_up_cross_origin_isolation' ); +add_action( 'load-site-editor.php', 'gutenberg_set_up_cross_origin_isolation' ); +add_action( 'load-widgets.php', 'gutenberg_set_up_cross_origin_isolation' ); + +/** + * Sends headers for cross-origin isolation. + * + * Uses an output buffer to add crossorigin="anonymous" where needed. + * + * @link https://web.dev/coop-coep/ + */ +function gutenberg_start_cross_origin_isolation_output_buffer(): void { + global $is_safari; + + $coep = $is_safari ? 'require-corp' : 'credentialless'; + + ob_start( + function ( string $output, ?int $phase ) use ( $coep ): string { + // Only send the header when the buffer is not being cleaned. + if ( ( $phase & PHP_OUTPUT_HANDLER_CLEAN ) === 0 ) { + header( 'Cross-Origin-Opener-Policy: same-origin' ); + header( "Cross-Origin-Embedder-Policy: $coep" ); + + $output = gutenberg_add_crossorigin_attributes( $output ); + } + + return $output; + } + ); +} + +/** + * Adds crossorigin="anonymous" to relevant tags in the given HTML string. + * + * @param string $html HTML input. + * + * @return string Modified HTML. + */ +function gutenberg_add_crossorigin_attributes( string $html ): string { + $site_url = site_url(); + + $processor = new WP_HTML_Tag_Processor( $html ); + + // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin. + $tags = array( + 'AUDIO' => 'src', + 'IMG' => 'src', + 'LINK' => 'href', + 'SCRIPT' => 'src', + 'VIDEO' => 'src', + 'SOURCE' => 'src', + ); + + $tag_names = array_keys( $tags ); + + while ( $processor->next_tag() ) { + $tag = $processor->get_tag(); + + if ( ! in_array( $tag, $tag_names, true ) ) { + continue; + } + + if ( 'AUDIO' === $tag || 'VIDEO' === $tag ) { + $processor->set_bookmark( 'audio-video-parent' ); + } + + $processor->set_bookmark( 'resume' ); + + $seeked = false; + + $crossorigin = $processor->get_attribute( 'crossorigin' ); + + $url = $processor->get_attribute( $tags[ $tag ] ); + + if ( is_string( $url ) && ! str_starts_with( $url, $site_url ) && ! str_starts_with( $url, '/' ) && ! is_string( $crossorigin ) ) { + if ( 'SOURCE' === $tag ) { + $seeked = $processor->seek( 'audio-video-parent' ); + + if ( $seeked ) { + $processor->set_attribute( 'crossorigin', 'anonymous' ); + } + } else { + $processor->set_attribute( 'crossorigin', 'anonymous' ); + } + + if ( $seeked ) { + $processor->seek( 'resume' ); + $processor->release_bookmark( 'audio-video-parent' ); + } + } + } + + return $processor->get_updated_html(); +} + +/** + * Overrides templates from wp_print_media_templates with custom ones. + * + * Adds `crossorigin` attribute to all tags that + * could have assets loaded from a different domain. + */ +function gutenberg_override_media_templates(): void { + remove_action( 'admin_footer', 'wp_print_media_templates' ); + add_action( + 'admin_footer', + static function (): void { + ob_start(); + wp_print_media_templates(); + $html = (string) ob_get_clean(); + + $tags = array( + 'audio', + 'img', + 'video', + ); + + foreach ( $tags as $tag ) { + $html = (string) str_replace( "<$tag", "<$tag crossorigin=\"anonymous\"", $html ); + } + + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + ); +} + +add_action( 'wp_enqueue_media', 'gutenberg_override_media_templates' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index fa95923061daff..5acd5f0f192364 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -139,18 +139,6 @@ function gutenberg_initialize_experiments_settings() { ) ); - add_settings_field( - 'gutenberg-zoomed-out-patterns-tab', - __( 'Enable zoomed out view when patterns are browsed in the inserter', 'gutenberg' ), - 'gutenberg_display_experiment_field', - 'gutenberg-experiments', - 'gutenberg_experiments_section', - array( - 'label' => __( 'Enable zoomed out view when selecting a pattern category in the main inserter.', 'gutenberg' ), - 'id' => 'gutenberg-zoomed-out-patterns-tab', - ) - ); - add_settings_field( 'gutenberg-new-posts-dashboard', __( 'Redesigned posts dashboard', 'gutenberg' ), @@ -176,17 +164,28 @@ function gutenberg_initialize_experiments_settings() { ); add_settings_field( - 'gutenberg-block-bindings-ui', - __( 'UI to create block bindings', 'gutenberg' ), + 'gutenberg-media-processing', + __( 'Client-side media processing', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Add UI to create and update block bindings in block inspector controls.', 'gutenberg' ), - 'id' => 'gutenberg-block-bindings-ui', + 'label' => __( 'Enable client-side media processing.', 'gutenberg' ), + 'id' => 'gutenberg-media-processing', ) ); + add_settings_field( + 'gutenberg-zoom-out-experiment', + __( 'Zoom out experiments', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Enable zoom out experiments; shows zoom out in the device preview and other zoom out experiments.', 'gutenberg' ), + 'id' => 'gutenberg-zoom-out-experiment', + ) + ); register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/lib/load.php b/lib/load.php index b501f0abd1c978..6ac2bd61f1de49 100644 --- a/lib/load.php +++ b/lib/load.php @@ -42,6 +42,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.7 compat. require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php'; + require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-server.php'; require __DIR__ . '/compat/wordpress-6.7/rest-api.php'; // Plugin specific code. @@ -184,3 +185,8 @@ function gutenberg_is_experiment_enabled( $name ) { // Data views. require_once __DIR__ . '/experimental/data-views.php'; + +// Client-side media processing. +if ( gutenberg_is_experiment_enabled( 'gutenberg-media-processing' ) ) { + require_once __DIR__ . '/experimental/media/load.php'; +} diff --git a/package-lock.json b/package-lock.json index fe6c1dfa150dba..10ebadc05aa36b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.1.0-rc.1", + "version": "19.2.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.1.0-rc.1", + "version": "19.2.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -216,10 +216,12 @@ "npm-run-all": "4.1.5", "patch-package": "8.0.0", "postcss": "8.4.38", + "postcss-import": "16.1.0", "postcss-loader": "6.2.1", "postcss-local-keyframes": "^0.0.2", "prettier": "npm:wp-prettier@3.0.3", "progress": "2.0.3", + "puppeteer-core": "23.1.0", "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.73.3", @@ -22568,15 +22570,6 @@ "node": ">=4.0" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "dependencies": { - "node-fetch": "2.6.7" - } - }, "node_modules/cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -24110,10 +24103,11 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.981744", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", - "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", - "dev": true + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "4.0.2", @@ -40549,6 +40543,12 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parsel-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz", + "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==", + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -41322,6 +41322,29 @@ "postcss": "^8.2.15" } }, + "node_modules/postcss-import": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz", + "integrity": "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/postcss-loader": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", @@ -41752,6 +41775,15 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postcss-prefix-selector": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.1.tgz", + "integrity": "sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==", + "license": "MIT", + "peerDependencies": { + "postcss": ">4 <9" + } + }, "node_modules/postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -42286,33 +42318,120 @@ } }, "node_modules/puppeteer-core": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-13.7.0.tgz", - "integrity": "sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==", + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.1.0.tgz", + "integrity": "sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.981744", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.5.0" + "@puppeteer/browsers": "2.3.1", + "chromium-bidi": "0.6.4", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" }, "engines": { - "node": ">=10.18.1" + "node": ">=18" } }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/chromium-bidi": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz", + "integrity": "sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/puppeteer-core/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/puppeteer-core/node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -42328,99 +42447,199 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/puppeteer-core/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/puppeteer-core/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=8" + "node": ">= 14" } }, - "node_modules/puppeteer-core/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, + "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "agent-base": "^7.0.2", + "debug": "4" }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/puppeteer-core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/puppeteer-core/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/puppeteer-core/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/puppeteer-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/puppeteer-core/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 14" } }, - "node_modules/puppeteer-core/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/puppeteer-core/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer-core/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/puppeteer-core/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/puppeteer-core/node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, "engines": { - "node": ">=6" + "node": ">= 14" } }, - "node_modules/puppeteer-core/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/puppeteer-core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { "node": ">=8" } }, - "node_modules/puppeteer-core/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/puppeteer-core/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dev": true, + "license": "MIT", "dependencies": { - "find-up": "^4.0.0" + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/puppeteer-core/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/puppeteer-core/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/puppeteer-core/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -42431,6 +42650,45 @@ } } }, + "node_modules/puppeteer-core/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer-core/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/pure-rand": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", @@ -43395,6 +43653,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, "node_modules/read-cmd-shim": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", @@ -48585,6 +48852,12 @@ "node": ">= 10" } }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -49270,6 +49543,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -52193,6 +52472,7 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", + "@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser", "@wordpress/blocks": "file:../blocks", "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", @@ -52226,8 +52506,9 @@ "diff": "^4.0.2", "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", + "parsel-js": "^1.1.2", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.51.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", @@ -52242,15 +52523,6 @@ "react-dom": "^18.0.0" } }, - "packages/block-editor/node_modules/postcss-prefixwrap": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", - "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==", - "license": "MIT", - "peerDependencies": { - "postcss": "*" - } - }, "packages/block-editor/node_modules/postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -54547,9 +54819,10 @@ "npm-package-json-lint": "^6.4.0", "npm-packlist": "^3.0.0", "postcss": "^8.4.5", + "postcss-import": "^16.1.0", "postcss-loader": "^6.2.1", "prettier": "npm:wp-prettier@3.0.3", - "puppeteer-core": "^13.2.0", + "puppeteer-core": "^23.1.0", "react-refresh": "^0.14.0", "read-pkg-up": "^7.0.1", "resolve-bin": "^0.4.0", @@ -67207,6 +67480,7 @@ "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", + "@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser", "@wordpress/blocks": "file:../blocks", "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", @@ -67240,19 +67514,15 @@ "diff": "^4.0.2", "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", + "parsel-js": "^1.1.2", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.51.0", + "postcss-prefix-selector": "^1.16.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "dependencies": { - "postcss-prefixwrap": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", - "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==" - }, "postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -68728,9 +68998,10 @@ "npm-package-json-lint": "^6.4.0", "npm-packlist": "^3.0.0", "postcss": "^8.4.5", + "postcss-import": "^16.1.0", "postcss-loader": "^6.2.1", "prettier": "npm:wp-prettier@3.0.3", - "puppeteer-core": "^13.2.0", + "puppeteer-core": "^23.1.0", "react-refresh": "^0.14.0", "read-pkg-up": "^7.0.1", "resolve-bin": "^0.4.0", @@ -73279,15 +73550,6 @@ "is-windows": "^1.0.0" } }, - "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "requires": { - "node-fetch": "2.6.7" - } - }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", @@ -74411,9 +74673,9 @@ } }, "devtools-protocol": { - "version": "0.0.981744", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.981744.tgz", - "integrity": "sha512-0cuGS8+jhR67Fy7qG3i3Pc7Aw494sb9yG9QgpG97SFVWwolgYjlhJg7n+UaHxOQT30d1TYu/EYe9k01ivLErIg==", + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", "dev": true }, "diff": { @@ -86925,6 +87187,11 @@ } } }, + "parsel-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parsel-js/-/parsel-js-1.1.2.tgz", + "integrity": "sha512-D66DG2nKx4Yoq66TMEyCUHlR2STGqO7vsBrX7tgyS9cfQyO6XD5JyzOiflwmWN6a4wbUAqpmHqmrxlTQVGZcbA==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -87480,6 +87747,25 @@ "integrity": "sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==", "dev": true }, + "postcss-import": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz", + "integrity": "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + } + } + }, "postcss-loader": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", @@ -87801,6 +88087,11 @@ } } }, + "postcss-prefix-selector": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/postcss-prefix-selector/-/postcss-prefix-selector-1.16.1.tgz", + "integrity": "sha512-Umxu+FvKMwlY6TyDzGFoSUnzW+NOfMBLyC1tAkIjgX+Z/qGspJeRjVC903D7mx7TuBpJlwti2ibXtWuA7fKMeQ==" + }, "postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -88215,25 +88506,81 @@ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "puppeteer-core": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-13.7.0.tgz", - "integrity": "sha512-rXja4vcnAzFAP1OVLq/5dWNfwBGuzcOARJ6qGV7oAZhnLmVRU8G5MsdeQEAOy332ZhkIOnn9jp15R89LKHyp2Q==", + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.1.0.tgz", + "integrity": "sha512-SvAsu+xnLN2FMXE/59bp3s3WXp8ewqUGzVV4AQtml/2xmsciZnU/bXcCW+eETHPWQ6Agg2vTI7QzWXPpEARK2g==", "dev": true, "requires": { - "cross-fetch": "3.1.5", - "debug": "4.3.4", - "devtools-protocol": "0.0.981744", - "extract-zip": "2.0.1", - "https-proxy-agent": "5.0.1", - "pkg-dir": "4.2.0", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "rimraf": "3.0.2", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "ws": "8.5.0" + "@puppeteer/browsers": "2.3.1", + "chromium-bidi": "0.6.4", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" }, "dependencies": { + "@puppeteer/browsers": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz", + "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==", + "dev": true, + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + } + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "requires": { + "debug": "^4.3.4" + } + }, + "chromium-bidi": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.4.tgz", + "integrity": "sha512-8zoq6ogmhQQkAKZVKO2ObFTl4uOkqoX1PlKQX3hZQ5E9cbUotcAb7h4pTNVAGGv8Z36PF3CtdOriEp/Rz82JqQ==", + "dev": true, + "requires": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -88246,68 +88593,169 @@ "yauzl": "^2.10.0" } }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "agent-base": "^7.1.0", + "debug": "^4.3.4" } }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "requires": { - "p-locate": "^4.1.0" + "agent-base": "^7.0.2", + "debug": "4" } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true + }, + "mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" } }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "requires": { - "p-limit": "^2.2.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "requires": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + } }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "find-up": "^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true } } @@ -89027,6 +89475,15 @@ } } }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, "read-cmd-shim": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", @@ -93026,6 +93483,12 @@ "integrity": "sha512-bctQIOqx2iVbWGDGPWwIm18QScpu2XRmkC19D8rQGFsjKSgteq/o1hTZvIG/wuDq8fanpBDrLkLq+aEN/6y5XQ==", "dev": true }, + "typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "dev": true + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -93553,6 +94016,12 @@ "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "dev": true }, + "urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index 7349e60eb4c210..a67b376f3dbdc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.1.0-rc.1", + "version": "19.2.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -228,10 +228,12 @@ "npm-run-all": "4.1.5", "patch-package": "8.0.0", "postcss": "8.4.38", + "postcss-import": "16.1.0", "postcss-loader": "6.2.1", "postcss-local-keyframes": "^0.0.2", "prettier": "npm:wp-prettier@3.0.3", "progress": "2.0.3", + "puppeteer-core": "23.1.0", "react": "18.3.1", "react-dom": "18.3.1", "react-native": "0.73.3", diff --git a/packages/api-fetch/src/middlewares/media-upload.js b/packages/api-fetch/src/middlewares/media-upload.js index 417abf775db636..ddd0be4e4ab436 100644 --- a/packages/api-fetch/src/middlewares/media-upload.js +++ b/packages/api-fetch/src/middlewares/media-upload.js @@ -63,6 +63,11 @@ const mediaUploadMiddleware = ( options, next ) => { return next( { ...options, parse: false } ) .catch( ( response ) => { + // `response` could actually be an error thrown by `defaultFetchHandler`. + if ( ! response.headers ) { + return Promise.reject( response ); + } + const attachmentId = response.headers.get( 'x-wp-upload-attachment-id' ); diff --git a/packages/base-styles/_animations.scss b/packages/base-styles/_animations.scss index bdfd7595da8e44..ce1f935b7d4d5b 100644 --- a/packages/base-styles/_animations.scss +++ b/packages/base-styles/_animations.scss @@ -1,5 +1,5 @@ -@mixin edit-post__fade-in-animation($speed: 0.2s, $delay: 0s) { - animation: edit-post__fade-in-animation $speed ease-out $delay; +@mixin edit-post__fade-in-animation($speed: 0.08s, $delay: 0s) { + animation: edit-post__fade-in-animation $speed linear $delay; animation-fill-mode: forwards; @include reduce-motion("animation"); } diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index 69735d75aac71a..ebccbe0e5e8aee 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -74,7 +74,7 @@ @mixin input-style__neutral() { box-shadow: 0 0 0 transparent; transition: box-shadow 0.1s linear; - border-radius: $radius-block-ui; + border-radius: $radius-small; border: $border-width solid $gray-600; @include reduce-motion("transition"); } @@ -227,7 +227,7 @@ border: $border-width solid $gray-900; margin-right: $grid-unit-15; transition: none; - border-radius: $radius-block-ui; + border-radius: $radius-small; &:focus { box-shadow: 0 0 0 ($border-width * 2) $white, 0 0 0 ($border-width * 2 + $border-width-focus-fallback) var(--wp-admin-theme-color); @@ -363,7 +363,7 @@ &:focus { color: var(--wp-admin-theme-color--rgb); box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color, #007cba); - border-radius: $radius-block-ui; + border-radius: $radius-small; } } @@ -375,7 +375,7 @@ padding: $grid-unit-15 !important; border: $border-width solid $gray-900 !important; box-shadow: none !important; - border-radius: $radius-block-ui !important; + border-radius: $radius-small !important; // Fonts smaller than 16px causes mobile safari to zoom. font-size: $mobile-text-min-font-size !important; diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 4d5f22e02fa7d1..cc99df6dbeaafc 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -131,7 +131,6 @@ $z-layers: ( ".block-editor-template-part__selection-modal": 1000001, ".block-editor-block-rename-modal": 1000001, ".edit-site-list__rename-modal": 1000001, - ".dataviews-bulk-actions__modal": 1000001, ".dataviews-action-modal": 1000001, ".editor-action-modal": 1000001, ".editor-post-template__swap-template-modal": 1000001, @@ -194,7 +193,6 @@ $z-layers: ( // Site editor layout ".edit-site-page-header": 2, ".edit-site-page-content": 1, - ".edit-site-patterns__dataviews-list-pagination": 2, ".edit-site-templates__dataviews-list-pagination": 2, ".edit-site-layout__canvas-container": 2, ".edit-site-layout__sidebar": 1, @@ -211,8 +209,8 @@ $z-layers: ( // Ensure selection checkbox stays above the preview field. ".dataviews-view-grid__card .dataviews-selection-checkbox": 1, - // Ensure quick actions toolbar appear above pagination - ".dataviews-bulk-actions-toolbar": 2, + // Ensure footer stays above the preview field. + ".dataviews-footer": 2, ); @function z-index( $key ) { diff --git a/packages/block-directory/src/components/downloadable-block-list-item/index.js b/packages/block-directory/src/components/downloadable-block-list-item/index.js index b1ef34ccdba596..45a2930ad656e3 100644 --- a/packages/block-directory/src/components/downloadable-block-list-item/index.js +++ b/packages/block-directory/src/components/downloadable-block-list-item/index.js @@ -65,7 +65,7 @@ function getDownloadableBlockLabel( ); } -function DownloadableBlockListItem( { composite, item, onClick } ) { +function DownloadableBlockListItem( { item, onClick } ) { const { author, description, icon, rating, title } = item; // getBlockType returns a block object if this block exists, or null if not. const isInstalled = !! getBlockType( item.name ); @@ -96,6 +96,8 @@ function DownloadableBlockListItem( { composite, item, onClick } ) { } - store={ composite } disabled={ isInstalling || ! isInstallable } >
diff --git a/packages/block-directory/src/components/downloadable-blocks-list/index.js b/packages/block-directory/src/components/downloadable-blocks-list/index.js index 09f509c4ed49c9..3911c3297376db 100644 --- a/packages/block-directory/src/components/downloadable-blocks-list/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-list/index.js @@ -13,12 +13,10 @@ import DownloadableBlockListItem from '../downloadable-block-list-item'; import { store as blockDirectoryStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { CompositeV2: Composite, useCompositeStoreV2: useCompositeStore } = - unlock( componentsPrivateApis ); +const { CompositeV2: Composite } = unlock( componentsPrivateApis ); const noop = () => {}; function DownloadableBlocksList( { items, onHover = noop, onSelect } ) { - const composite = useCompositeStore(); const { installBlockType } = useDispatch( blockDirectoryStore ); if ( ! items.length ) { @@ -27,7 +25,6 @@ function DownloadableBlocksList( { items, onHover = noop, onSelect } ) { return ( { // Check if the block is registered (`getBlockType` // will return an object). If so, insert the block. diff --git a/packages/block-directory/src/plugins/get-install-missing/index.js b/packages/block-directory/src/plugins/get-install-missing/index.js index 58d6cdb1a188cb..8b192cbe8fdc49 100644 --- a/packages/block-directory/src/plugins/get-install-missing/index.js +++ b/packages/block-directory/src/plugins/get-install-missing/index.js @@ -100,7 +100,13 @@ const ModifiedWarning = ( { originalBlock, ...props } ) => { originalBlock.title || originalName ); actions.push( - ); diff --git a/packages/block-directory/src/plugins/get-install-missing/install-button.js b/packages/block-directory/src/plugins/get-install-missing/install-button.js index 9dcd45f67760f4..3b05b53a380b9a 100644 --- a/packages/block-directory/src/plugins/get-install-missing/install-button.js +++ b/packages/block-directory/src/plugins/get-install-missing/install-button.js @@ -22,6 +22,8 @@ export default function InstallButton( { attributes, block, clientId } ) { return (
diff --git a/packages/block-editor/src/components/block-draggable/content.scss b/packages/block-editor/src/components/block-draggable/content.scss index f1318daebd5a04..102230168e2133 100644 --- a/packages/block-editor/src/components/block-draggable/content.scss +++ b/packages/block-editor/src/components/block-draggable/content.scss @@ -3,7 +3,7 @@ .block-editor-block-list__layout .is-dragging { background-color: currentColor !important; opacity: 0.05 !important; - border-radius: $radius-block-ui !important; + border-radius: $radius-small !important; // Disabling pointer events during the drag event is necessary, // lest the block might affect your drag operation. diff --git a/packages/block-editor/src/components/block-draggable/index.js b/packages/block-editor/src/components/block-draggable/index.js index e1afc1f2513841..5bb328f7cf9f58 100644 --- a/packages/block-editor/src/components/block-draggable/index.js +++ b/packages/block-editor/src/components/block-draggable/index.js @@ -62,7 +62,7 @@ const BlockDraggable = ( { [ clientIds ] ); - const isDragging = useRef( false ); + const isDraggingRef = useRef( false ); const [ startScrolling, scrollOnDragOver, stopScrolling ] = useScrollWhenDragging(); @@ -75,7 +75,7 @@ const BlockDraggable = ( { // Stop dragging blocks if the block draggable is unmounted. useEffect( () => { return () => { - if ( isDragging.current ) { + if ( isDraggingRef.current ) { stopDraggingBlocks(); } }; @@ -193,7 +193,7 @@ const BlockDraggable = ( { // frame to enable dragging. window.requestAnimationFrame( () => { startDraggingBlocks( clientIds ); - isDragging.current = true; + isDraggingRef.current = true; startScrolling( event ); @@ -205,7 +205,7 @@ const BlockDraggable = ( { onDragOver={ scrollOnDragOver } onDragEnd={ () => { stopDraggingBlocks(); - isDragging.current = false; + isDraggingRef.current = false; stopScrolling(); diff --git a/packages/block-editor/src/components/block-draggable/style.scss b/packages/block-editor/src/components/block-draggable/style.scss index afbf77319f7200..349afa2c3563c0 100644 --- a/packages/block-editor/src/components/block-draggable/style.scss +++ b/packages/block-editor/src/components/block-draggable/style.scss @@ -7,7 +7,7 @@ .block-editor-block-draggable-chip { background-color: $gray-900; - border-radius: $radius-block-ui; + border-radius: $radius-small; box-shadow: 0 6px 8px rgba($black, 0.3); color: $white; cursor: grabbing; diff --git a/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.js b/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.js index a01db4927b6525..515f3002de9344 100644 --- a/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.js +++ b/packages/block-editor/src/components/block-draggable/use-scroll-when-dragging.js @@ -18,36 +18,36 @@ const VELOCITY_MULTIPLIER = * and `onDragEnd` events respectively. */ export default function useScrollWhenDragging() { - const dragStartY = useRef( null ); - const velocityY = useRef( null ); - const scrollParentY = useRef( null ); - const scrollEditorInterval = useRef( null ); + const dragStartYRef = useRef( null ); + const velocityYRef = useRef( null ); + const scrollParentYRef = useRef( null ); + const scrollEditorIntervalRef = useRef( null ); // Clear interval when unmounting. useEffect( () => () => { - if ( scrollEditorInterval.current ) { - clearInterval( scrollEditorInterval.current ); - scrollEditorInterval.current = null; + if ( scrollEditorIntervalRef.current ) { + clearInterval( scrollEditorIntervalRef.current ); + scrollEditorIntervalRef.current = null; } }, [] ); const startScrolling = useCallback( ( event ) => { - dragStartY.current = event.clientY; + dragStartYRef.current = event.clientY; // Find nearest parent(s) to scroll. - scrollParentY.current = getScrollContainer( event.target ); + scrollParentYRef.current = getScrollContainer( event.target ); - scrollEditorInterval.current = setInterval( () => { - if ( scrollParentY.current && velocityY.current ) { + scrollEditorIntervalRef.current = setInterval( () => { + if ( scrollParentYRef.current && velocityYRef.current ) { const newTop = - scrollParentY.current.scrollTop + velocityY.current; + scrollParentYRef.current.scrollTop + velocityYRef.current; // Setting `behavior: 'smooth'` as a scroll property seems to hurt performance. // Better to use a small scroll interval. - scrollParentY.current.scroll( { + scrollParentYRef.current.scroll( { top: newTop, } ); } @@ -55,14 +55,14 @@ export default function useScrollWhenDragging() { }, [] ); const scrollOnDragOver = useCallback( ( event ) => { - if ( ! scrollParentY.current ) { + if ( ! scrollParentYRef.current ) { return; } - const scrollParentHeight = scrollParentY.current.offsetHeight; + const scrollParentHeight = scrollParentYRef.current.offsetHeight; const offsetDragStartPosition = - dragStartY.current - scrollParentY.current.offsetTop; + dragStartYRef.current - scrollParentYRef.current.offsetTop; const offsetDragPosition = - event.clientY - scrollParentY.current.offsetTop; + event.clientY - scrollParentYRef.current.offsetTop; if ( event.clientY > offsetDragStartPosition ) { // User is dragging downwards. @@ -82,7 +82,7 @@ export default function useScrollWhenDragging() { moveableDistance === 0 || dragDistance === 0 ? 0 : dragDistance / moveableDistance; - velocityY.current = VELOCITY_MULTIPLIER * distancePercentage; + velocityYRef.current = VELOCITY_MULTIPLIER * distancePercentage; } else if ( event.clientY < offsetDragStartPosition ) { // User is dragging upwards. const moveableDistance = Math.max( @@ -99,19 +99,19 @@ export default function useScrollWhenDragging() { moveableDistance === 0 || dragDistance === 0 ? 0 : dragDistance / moveableDistance; - velocityY.current = -VELOCITY_MULTIPLIER * distancePercentage; + velocityYRef.current = -VELOCITY_MULTIPLIER * distancePercentage; } else { - velocityY.current = 0; + velocityYRef.current = 0; } }, [] ); const stopScrolling = () => { - dragStartY.current = null; - scrollParentY.current = null; + dragStartYRef.current = null; + scrollParentYRef.current = null; - if ( scrollEditorInterval.current ) { - clearInterval( scrollEditorInterval.current ); - scrollEditorInterval.current = null; + if ( scrollEditorIntervalRef.current ) { + clearInterval( scrollEditorIntervalRef.current ); + scrollEditorIntervalRef.current = null; } }; diff --git a/packages/block-editor/src/components/block-edit/multiple-usage-warning.js b/packages/block-editor/src/components/block-edit/multiple-usage-warning.js index 4acd4d1f349dd0..bff12342904537 100644 --- a/packages/block-editor/src/components/block-edit/multiple-usage-warning.js +++ b/packages/block-editor/src/components/block-edit/multiple-usage-warning.js @@ -24,6 +24,8 @@ export function MultipleUsageWarning( { selectBlock( originalBlockClientId ) } @@ -31,6 +33,8 @@ export function MultipleUsageWarning( { { __( 'Find original' ) } , , ] } secondaryActions={ secondaryActions } > - { __( 'This block contains unexpected or invalid content.' ) } + { __( 'Block contains unexpected or invalid content.' ) } { compare && ( { + if ( editorMode !== 'zoom-out' ) { + return; + } + + function onDoubleClick( event ) { + if ( ! event.defaultPrevented ) { + event.preventDefault(); + __unstableSetEditorMode( 'edit' ); + } + } + + node.addEventListener( 'dblclick', onDoubleClick ); + + return () => { + node.removeEventListener( 'dblclick', onDoubleClick ); + }; + }, + [ editorMode, __unstableSetEditorMode ] + ); +} diff --git a/packages/block-editor/src/components/block-lock/style.scss b/packages/block-editor/src/components/block-lock/style.scss index 8dc6bfb2021f08..ad59030a8f4405 100644 --- a/packages/block-editor/src/components/block-lock/style.scss +++ b/packages/block-editor/src/components/block-lock/style.scss @@ -41,7 +41,7 @@ &:hover { background-color: $gray-100; - border-radius: $radius-block-ui; + border-radius: $radius-small; } } diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index 29bf0cdd6a60c6..12376a7b56a5ee 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -20,7 +20,7 @@ export default function BlockLockToolbar( { clientId } ) { false ); - const hasLockButtonShown = useRef( false ); + const hasLockButtonShownRef = useRef( false ); // If the block lock button has been shown, we don't want to remove it // from the toolbar until the toolbar is rendered again without it. @@ -29,11 +29,11 @@ export default function BlockLockToolbar( { clientId } ) { // whence it came, and to do that, we need to leave the button in the toolbar. useEffect( () => { if ( isLocked ) { - hasLockButtonShown.current = true; + hasLockButtonShownRef.current = true; } }, [ isLocked ] ); - if ( ! isLocked && ! hasLockButtonShown.current ) { + if ( ! isLocked && ! hasLockButtonShownRef.current ) { return null; } diff --git a/packages/block-editor/src/components/block-mover/button.js b/packages/block-editor/src/components/block-mover/button.js index 7e01ee4a359db9..98c0aff79eff38 100644 --- a/packages/block-editor/src/components/block-mover/button.js +++ b/packages/block-editor/src/components/block-mover/button.js @@ -129,6 +129,8 @@ const BlockMoverButton = forwardRef( return ( <>
@@ -31,14 +36,18 @@ const CarouselNavigation = ( { } ) => (
diff --git a/packages/block-editor/src/components/block-variation-transforms/index.js b/packages/block-editor/src/components/block-variation-transforms/index.js index b7ecaad635e44c..97a3f980541842 100644 --- a/packages/block-editor/src/components/block-variation-transforms/index.js +++ b/packages/block-editor/src/components/block-variation-transforms/index.js @@ -35,6 +35,8 @@ function VariationsButtons( { { variations.map( ( variation ) => ( ; + return ( + + ); } } /> ); diff --git a/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-explorer-sidebar.js b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-explorer-sidebar.js index bc5e4d37ab2c28..95de775e48eef8 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-explorer-sidebar.js +++ b/packages/block-editor/src/components/inserter/block-patterns-explorer/pattern-explorer-sidebar.js @@ -15,6 +15,8 @@ function PatternCategoriesList( { { patternCategories.map( ( { name, label } ) => { return ( - diff --git a/packages/block-editor/src/components/inserter/media-tab/media-tab.js b/packages/block-editor/src/components/inserter/media-tab/media-tab.js index 2f3985aef569cb..07fce9ee6d80b0 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-tab.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-tab.js @@ -71,6 +71,8 @@ function MediaTab( { allowedTypes={ ALLOWED_MEDIA_TYPES } render={ ( { open } ) => ( + { isVisible && ( {
- ) } @@ -299,6 +286,7 @@ export default function CoverInspectorControls( { ) } !! minHeight } label={ __( 'Minimum height' ) } onDeselect={ () => diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index 189d658e955c98..b92c401311beee 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -1,9 +1,4 @@ .wp-block-cover { - /* Extra specificity needed because the reset.css applied in the editor context is overriding this rule. */ - .editor-styles-wrapper & { - box-sizing: border-box; - } - // Override default cover styles // because we're not ready yet to show the cover block. &.is-placeholder { diff --git a/packages/block-library/src/cover/test/edit.js b/packages/block-library/src/cover/test/edit.js index ab99d3c555e3bc..97c52a47ec4d72 100644 --- a/packages/block-library/src/cover/test/edit.js +++ b/packages/block-library/src/cover/test/edit.js @@ -155,6 +155,34 @@ describe( 'Cover block', () => { 'is-position-top-left' ); } ); + + test( 'clears media when clear media button clicked', async () => { + await setup( { + url: 'http://localhost/my-image.jpg', + } ); + + await selectBlock( 'Block: Cover' ); + expect( + within( screen.getByLabelText( 'Block: Cover' ) ).getByRole( + 'img' + ) + ).toBeInTheDocument(); + + await userEvent.click( + screen.getByRole( 'button', { name: 'Replace' } ) + ); + await userEvent.click( + screen.getByRole( 'menuitem', { + name: 'Reset', + } ) + ); + + expect( + within( screen.getByLabelText( 'Block: Cover' ) ).queryByRole( + 'img' + ) + ).not.toBeInTheDocument(); + } ); } ); describe( 'Inspector controls', () => { @@ -242,30 +270,6 @@ describe( 'Cover block', () => { expect( screen.getByAltText( 'Me' ) ).toBeInTheDocument(); } ); - test( 'clears media when clear media button clicked', async () => { - await setup( { - url: 'http://localhost/my-image.jpg', - } ); - - await selectBlock( 'Block: Cover' ); - expect( - within( screen.getByLabelText( 'Block: Cover' ) ).getByRole( - 'img' - ) - ).toBeInTheDocument(); - - await userEvent.click( - screen.getByRole( 'button', { - name: 'Clear Media', - } ) - ); - expect( - within( screen.getByLabelText( 'Block: Cover' ) ).queryByRole( - 'img' - ) - ).not.toBeInTheDocument(); - } ); - describe( 'Color panel', () => { test( 'applies selected opacity to block when number control value changed', async () => { const { container } = await setup(); @@ -372,10 +376,10 @@ describe( 'Cover block', () => { } ) ); await userEvent.clear( - screen.getByLabelText( 'Minimum height of cover' ) + screen.getByLabelText( 'Minimum height' ) ); await userEvent.type( - screen.getByLabelText( 'Minimum height of cover' ), + screen.getByLabelText( 'Minimum height' ), '300' ); diff --git a/packages/block-library/src/details/index.js b/packages/block-library/src/details/index.js index e30d1a8e04974f..3ba5efb04e27d2 100644 --- a/packages/block-library/src/details/index.js +++ b/packages/block-library/src/details/index.js @@ -11,6 +11,7 @@ import initBlock from '../utils/init-block'; import metadata from './block.json'; import edit from './edit'; import save from './save'; +import transforms from './transforms'; const { name } = metadata; export { metadata, name }; @@ -35,6 +36,7 @@ export const settings = { }, save, edit, + transforms, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/details/transforms.js b/packages/block-library/src/details/transforms.js new file mode 100644 index 00000000000000..e8a30dbe34759c --- /dev/null +++ b/packages/block-library/src/details/transforms.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { createBlock, cloneBlock } from '@wordpress/blocks'; + +export default { + from: [ + { + type: 'block', + isMultiBlock: true, + blocks: [ '*' ], + isMatch( {}, blocks ) { + return ! ( + blocks.length === 1 && blocks[ 0 ].name === 'core/details' + ); + }, + __experimentalConvert( blocks ) { + return createBlock( + 'core/details', + {}, + blocks.map( ( block ) => cloneBlock( block ) ) + ); + }, + }, + ], +}; diff --git a/packages/block-library/src/embed/embed-placeholder.js b/packages/block-library/src/embed/embed-placeholder.js index 57aa0645ee3d94..9a26060a294120 100644 --- a/packages/block-library/src/embed/embed-placeholder.js +++ b/packages/block-library/src/embed/embed-placeholder.js @@ -65,10 +65,20 @@ const EmbedPlaceholder = ( { spacing={ 3 } justify="flex-start" > - { ' ' } - diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index 448b6f5ec73d15..be061d357bffd1 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -109,6 +109,16 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { function onSelectFile( newMedia ) { if ( ! newMedia || ! newMedia.url ) { + // Reset attributes. + setAttributes( { + href: undefined, + fileName: undefined, + textLinkHref: undefined, + id: undefined, + fileId: undefined, + displayPreview: undefined, + previewHeight: undefined, + } ); setTemporaryURL(); return; } @@ -230,6 +240,7 @@ function FileEdit( { attributes, isSelected, setAttributes, clientId } ) { accept="*" onSelect={ onSelectFile } onError={ onUploadError } + onReset={ () => onSelectFile( undefined ) } /> { - if ( ! didMount.current ) { + if ( ! didMountRef.current ) { return; } @@ -96,7 +96,7 @@ function ClassicEdit( { useEffect( () => { const { baseURL, suffix } = window.wpEditorL10n.tinymce; - didMount.current = true; + didMountRef.current = true; window.tinymce.EditorManager.overrideDefaults( { base_url: baseURL, @@ -230,7 +230,7 @@ function ClassicEdit( { onReadyStateChange ); wp.oldEditor.remove( `editor-${ clientId }` ); - didMount.current = false; + didMountRef.current = false; }; }, [] ); diff --git a/packages/block-library/src/freeform/editor.scss b/packages/block-library/src/freeform/editor.scss index 7329eb6e5fb064..c2256ecd7a795f 100644 --- a/packages/block-library/src/freeform/editor.scss +++ b/packages/block-library/src/freeform/editor.scss @@ -299,7 +299,7 @@ div[data-type="core/freeform"] { top: 0; border: $border-width solid $gray-300; border-bottom: none; - border-radius: $radius-block-ui; + border-radius: $radius-small; margin-bottom: $grid-unit-10; // On mobile, toolbars go edge to edge. diff --git a/packages/block-library/src/freeform/modal.js b/packages/block-library/src/freeform/modal.js index c1b10a61de808d..1768022377a432 100644 --- a/packages/block-library/src/freeform/modal.js +++ b/packages/block-library/src/freeform/modal.js @@ -25,6 +25,8 @@ function ModalAuxiliaryActions( { onClick, isModalFullScreen } ) { return ( ); @@ -69,7 +75,7 @@ export default function MissingEdit( { attributes, clientId } ) { messageHTML = sprintf( /* translators: %s: block name */ __( - 'Your site doesn’t include support for the "%s" block. You can leave this block intact, convert its content to a Custom HTML block, or remove it entirely.' + 'Your site doesn’t include support for the "%s" block. You can leave it as-is, convert it to custom HTML, or remove it.' ), originalName ); @@ -78,7 +84,7 @@ export default function MissingEdit( { attributes, clientId } ) { messageHTML = sprintf( /* translators: %s: block name */ __( - 'Your site doesn’t include support for the "%s" block. You can leave this block intact or remove it entirely.' + 'Your site doesn’t include support for the "%s" block. You can leave it as-is or remove it.' ), originalName ); diff --git a/packages/block-library/src/navigation-link/link-ui.js b/packages/block-library/src/navigation-link/link-ui.js index deed35145d6dea..1d303843644474 100644 --- a/packages/block-library/src/navigation-link/link-ui.js +++ b/packages/block-library/src/navigation-link/link-ui.js @@ -8,7 +8,7 @@ import { VisuallyHidden, __experimentalVStack as VStack, } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, sprintf, isRTL } from '@wordpress/i18n'; import { __experimentalLinkControl as LinkControl, store as blockEditorStore, @@ -28,7 +28,7 @@ import { } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { useSelect, useDispatch } from '@wordpress/data'; -import { chevronLeftSmall, plus } from '@wordpress/icons'; +import { chevronLeftSmall, chevronRightSmall, plus } from '@wordpress/icons'; import { useInstanceId, useFocusOnMount } from '@wordpress/compose'; /** @@ -123,7 +123,7 @@ function LinkUIBlockInserter( { clientId, onBack, onSelectBlock } ) { ' ), { - button: diff --git a/packages/block-library/src/query/edit/query-placeholder.js b/packages/block-library/src/query/edit/query-placeholder.js index eec982c9750cf8..098c29f0ec512b 100644 --- a/packages/block-library/src/query/edit/query-placeholder.js +++ b/packages/block-library/src/query/edit/query-placeholder.js @@ -79,6 +79,8 @@ export default function QueryPlaceholder( { > { !! hasPatterns && ( ) } { ! isResolving && isBlockBasedTheme && canCreateTemplatePart && ( - - + + + ) } diff --git a/packages/components/src/date-time/time/time-input/test/index.tsx b/packages/components/src/date-time/time/time-input/test/index.tsx index c107cd2c9d79b6..eae082a12bc1c9 100644 --- a/packages/components/src/date-time/time/time-input/test/index.tsx +++ b/packages/components/src/date-time/time/time-input/test/index.tsx @@ -80,12 +80,11 @@ describe( 'TimeInput', () => { const minutesInput = screen.getByRole( 'spinbutton', { name: 'Minutes', } ); - const amButton = screen.getByRole( 'button', { name: 'AM' } ); - const pmButton = screen.getByRole( 'button', { name: 'PM' } ); + const amButton = screen.getByRole( 'radio', { name: 'AM' } ); + const pmButton = screen.getByRole( 'radio', { name: 'PM' } ); - // TODO: Update assert these states through the accessibility tree rather than through styles, see: https://github.com/WordPress/gutenberg/issues/61163 - expect( amButton ).toHaveClass( 'is-primary' ); - expect( pmButton ).not.toHaveClass( 'is-primary' ); + expect( amButton ).toBeChecked(); + expect( pmButton ).not.toBeChecked(); expect( hoursInput ).not.toHaveValue( 0 ); expect( hoursInput ).toHaveValue( 12 ); @@ -94,7 +93,7 @@ describe( 'TimeInput', () => { await user.keyboard( '{Tab}' ); expect( onChangeSpy ).toHaveBeenCalledWith( { hours: 0, minutes: 35 } ); - expect( amButton ).toHaveClass( 'is-primary' ); + expect( amButton ).toBeChecked(); await user.clear( hoursInput ); await user.type( hoursInput, '12' ); @@ -107,7 +106,7 @@ describe( 'TimeInput', () => { hours: 12, minutes: 35, } ); - expect( pmButton ).toHaveClass( 'is-primary' ); + expect( pmButton ).toBeChecked(); } ); it( 'should call onChange with defined minutes steps', async () => { diff --git a/packages/components/src/dimension-control/README.md b/packages/components/src/dimension-control/README.md index 3cd0191a046068..78c1a60275c13a 100644 --- a/packages/components/src/dimension-control/README.md +++ b/packages/components/src/dimension-control/README.md @@ -1,5 +1,9 @@ # DimensionControl +
+This component is deprecated. +
+
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
diff --git a/packages/components/src/dimension-control/index.tsx b/packages/components/src/dimension-control/index.tsx index 52662f31c3f24c..25880f9b4fdb38 100644 --- a/packages/components/src/dimension-control/index.tsx +++ b/packages/components/src/dimension-control/index.tsx @@ -17,6 +17,7 @@ import sizesTable, { findSizeBySlug } from './sizes'; import type { DimensionControlProps, Size } from './types'; import type { SelectControlSingleSelectionProps } from '../select-control/types'; import { ContextSystemProvider } from '../context'; +import deprecated from '@wordpress/deprecated'; const CONTEXT_VALUE = { BaseControl: { @@ -29,7 +30,7 @@ const CONTEXT_VALUE = { /** * `DimensionControl` is a component designed to provide a UI to control spacing and/or dimensions. * - * This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. + * @deprecated * * ```jsx * import { __experimentalDimensionControl as DimensionControl } from '@wordpress/components'; @@ -62,6 +63,11 @@ export function DimensionControl( props: DimensionControlProps ) { className = '', } = props; + deprecated( 'wp.components.DimensionControl', { + since: '6.7', + version: '7.0', + } ); + const onChangeSpacingSize: SelectControlSingleSelectionProps[ 'onChange' ] = ( val ) => { const theSize = findSizeBySlug( sizes, val ); diff --git a/packages/components/src/dimension-control/stories/index.story.tsx b/packages/components/src/dimension-control/stories/index.story.tsx index 3a6da44f461164..15a63fcf6ccf6c 100644 --- a/packages/components/src/dimension-control/stories/index.story.tsx +++ b/packages/components/src/dimension-control/stories/index.story.tsx @@ -13,9 +13,15 @@ import sizes from '../sizes'; */ import { desktop, tablet, mobile } from '@wordpress/icons'; +/** + * `DimensionControl` is a component designed to provide a UI to control spacing and/or dimensions. + * + * This component is deprecated. + */ const meta: Meta< typeof DimensionControl > = { component: DimensionControl, - title: 'Components (Experimental)/DimensionControl', + title: 'Components (Deprecated)/DimensionControl', + id: 'components-dimensioncontrol', argTypes: { onChange: { action: 'onChange' }, value: { control: { type: null } }, @@ -42,7 +48,6 @@ const Template: StoryFn< typeof DimensionControl > = ( args ) => ( ); export const Default = Template.bind( {} ); - Default.args = { __nextHasNoMarginBottom: true, label: 'Please select a size', diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index 658fe7febc02bc..bd2c26d641fe72 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -45,7 +45,7 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` min-height: 0; } -.emotion-6:focus-within:not( :has( :is( .em5sgkm7, .emotion-19 ):focus-within ) ) .emotion-26 { +.emotion-6:focus-within:not( :has( :is( .em5sgkm8, .emotion-19 ):focus-within ) ) .emotion-26 { border-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); outline: 2px solid transparent; @@ -157,8 +157,8 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` } .emotion-21 { - margin-bottom: 0; - padding-right: calc(4px * 2); + -webkit-padding-end: 8px; + padding-inline-end: 8px; position: absolute; pointer-events: none; right: 0; @@ -249,7 +249,7 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` class="components-input-control__suffix emotion-18 emotion-19" >
@@ -327,7 +327,7 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` min-height: 0; } -.emotion-6:focus-within:not( :has( :is( .em5sgkm7, .emotion-19 ):focus-within ) ) .emotion-26 { +.emotion-6:focus-within:not( :has( :is( .em5sgkm8, .emotion-19 ):focus-within ) ) .emotion-26 { border-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); outline: 2px solid transparent; @@ -439,8 +439,8 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` } .emotion-21 { - margin-bottom: 0; - padding-right: calc(4px * 2); + -webkit-padding-end: 8px; + padding-inline-end: 8px; position: absolute; pointer-events: none; right: 0; @@ -541,7 +541,7 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` class="components-input-control__suffix emotion-18 emotion-19" >
@@ -619,7 +619,7 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] min-height: 0; } -.emotion-6:focus-within:not( :has( :is( .em5sgkm7, .emotion-19 ):focus-within ) ) .emotion-26 { +.emotion-6:focus-within:not( :has( :is( .em5sgkm8, .emotion-19 ):focus-within ) ) .emotion-26 { border-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); outline: 2px solid transparent; @@ -731,8 +731,8 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] } .emotion-21 { - margin-bottom: 0; - padding-right: calc(4px * 2); + -webkit-padding-end: 8px; + padding-inline-end: 8px; position: absolute; pointer-events: none; right: 0; @@ -845,7 +845,7 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] class="components-input-control__suffix emotion-18 emotion-19" >
@@ -923,7 +923,7 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] min-height: 0; } -.emotion-6:focus-within:not( :has( :is( .em5sgkm7, .emotion-19 ):focus-within ) ) .emotion-26 { +.emotion-6:focus-within:not( :has( :is( .em5sgkm8, .emotion-19 ):focus-within ) ) .emotion-26 { border-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #3858e9)); outline: 2px solid transparent; @@ -1035,8 +1035,8 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] } .emotion-21 { - margin-bottom: 0; - padding-right: calc(4px * 2); + -webkit-padding-end: 8px; + padding-inline-end: 8px; position: absolute; pointer-events: none; right: 0; @@ -1149,7 +1149,7 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] class="components-input-control__suffix emotion-18 emotion-19" >
diff --git a/packages/components/src/dimension-control/test/index.test.js b/packages/components/src/dimension-control/test/index.test.js index 1b34d2983ad0f1..14f1c509f70cf9 100644 --- a/packages/components/src/dimension-control/test/index.test.js +++ b/packages/components/src/dimension-control/test/index.test.js @@ -31,6 +31,7 @@ describe( 'DimensionControl', () => { const { container } = render( ); + expect( console ).toHaveWarned(); expect( container ).toMatchSnapshot(); } ); diff --git a/packages/components/src/draggable/index.tsx b/packages/components/src/draggable/index.tsx index 0a3000538dbf24..b9dea8fed43061 100644 --- a/packages/components/src/draggable/index.tsx +++ b/packages/components/src/draggable/index.tsx @@ -71,7 +71,7 @@ export function Draggable( { __experimentalDragComponent: dragComponent, }: DraggableProps ) { const dragComponentRef = useRef< HTMLDivElement >( null ); - const cleanup = useRef( () => {} ); + const cleanupRef = useRef( () => {} ); /** * Removes the element clone, resets cursor, and removes drag listener. @@ -80,7 +80,7 @@ export function Draggable( { */ function end( event: DragEvent ) { event.preventDefault(); - cleanup.current(); + cleanupRef.current(); if ( onDragEnd ) { onDragEnd( event ); @@ -216,7 +216,7 @@ export function Draggable( { onDragStart( event ); } - cleanup.current = () => { + cleanupRef.current = () => { // Remove drag clone. if ( cloneWrapper && cloneWrapper.parentNode ) { cloneWrapper.parentNode.removeChild( cloneWrapper ); @@ -235,7 +235,7 @@ export function Draggable( { useEffect( () => () => { - cleanup.current(); + cleanupRef.current(); }, [] ); diff --git a/packages/components/src/dropdown-menu-v2/README.md b/packages/components/src/dropdown-menu-v2/README.md index 2902b541169766..771b5b74b24bf7 100644 --- a/packages/components/src/dropdown-menu-v2/README.md +++ b/packages/components/src/dropdown-menu-v2/README.md @@ -1,11 +1,10 @@ -# `DropdownMenu` (v2) +# `DropdownMenuV2`
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
-`DropdownMenu` displays a menu to the user (such as a set of actions or functions) triggered by a button. - +`DropdownMenuV2` displays a menu to the user (such as a set of actions or functions) triggered by a button. ## Design guidelines @@ -46,7 +45,7 @@ This component is still highly experimental, and it's not normally accessible to The component exposes a set of components that are meant to be used in combination with each other in order to implement a `DropdownMenu` correctly. -### `DropdownMenu` +### `DropdownMenuV2` The root component, used to specify the menu's trigger and its contents. @@ -58,62 +57,62 @@ The component accepts the following props: The trigger button -- Required: yes +- Required: yes ##### `children`: `React.ReactNode` The contents of the dropdown -- Required: yes +- Required: yes ##### `defaultOpen`: `boolean` The open state of the dropdown menu when it is initially rendered. Use when not wanting to control its open state. -- Required: no -- Default: `false` +- Required: no +- Default: `false` ##### `open`: `boolean` The controlled open state of the dropdown menu. Must be used in conjunction with `onOpenChange`. -- Required: no +- Required: no ##### `onOpenChange`: `(open: boolean) => void` Event handler called when the open state of the dropdown menu changes. -- Required: no +- Required: no ##### `modal`: `boolean` The modality of the dropdown menu. When set to true, interaction with outside elements will be disabled and only menu content will be visible to screen readers. -- Required: no -- Default: `true` +- Required: no +- Default: `true` ##### `placement`: ``'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'` The placement of the dropdown menu popover. -- Required: no -- Default: `'bottom-start'` for root-level menus, `'right-start'` for nested menus +- Required: no +- Default: `'bottom-start'` for root-level menus, `'right-start'` for nested menus ##### `gutter`: `number` The distance in pixels from the trigger. -- Required: no -- Default: `8` for root-level menus, `16` for nested menus +- Required: no +- Default: `8` for root-level menus, `16` for nested menus ##### `shift`: `number` The skidding of the popover along the anchor element. Can be set to negative values to make the popover shift to the opposite side. -- Required: no -- Default: `0` for root-level menus, `-8` for nested menus +- Required: no +- Default: `0` for root-level menus, `-8` for nested menus -### `DropdownMenuItem` +### `DropdownMenuV2.Item` Used to render a menu item. @@ -125,35 +124,35 @@ The component accepts the following props: The contents of the item -- Required: yes +- Required: yes ##### `prefix`: `React.ReactNode` The contents of the item's prefix. -- Required: no +- Required: no ##### `suffix`: `React.ReactNode` The contents of the item's suffix. -- Required: no +- Required: no ##### `hideOnClick`: `boolean` Whether to hide the dropdown menu when the menu item is clicked. -- Required: no -- Default: `true` +- Required: no +- Default: `true` ##### `disabled`: `boolean` Determines if the element is disabled. -- Required: no -- Default: `false` +- Required: no +- Default: `false` -### `DropdownMenuCheckboxItem` +### `DropdownMenuV2.CheckboxItem` Used to render a checkbox item. @@ -165,61 +164,61 @@ The component accepts the following props: The contents of the item -- Required: yes +- Required: yes ##### `suffix`: `React.ReactNode` The contents of the item's suffix. -- Required: no +- Required: no ##### `hideOnClick`: `boolean` Whether to hide the dropdown menu when the menu item is clicked. -- Required: no -- Default: `false` +- Required: no +- Default: `false` ##### `disabled`: `boolean` Determines if the element is disabled. -- Required: no -- Default: `false` +- Required: no +- Default: `false` ##### `name`: `string` The checkbox item's name. -- Required: yes +- Required: yes ##### `value`: `string` The checkbox item's value, useful when using multiple checkbox items - associated to the same `name`. +associated to the same `name`. -- Required: no +- Required: no ##### `checked`: `boolean` The checkbox item's value, useful when using multiple checkbox items - associated to the same `name`. +associated to the same `name`. -- Required: no +- Required: no ##### `defaultChecked`: `boolean` The checked state of the checkbox menu item when it is initially rendered. Use when not wanting to control its checked state. -- Required: no +- Required: no ##### `onChange`: `( event: React.ChangeEvent< HTMLInputElement > ) => void;` Event handler called when the checked state of the checkbox menu item changes. -- Required: no +- Required: no -### `DropdownMenuRadioItem` +### `DropdownMenuV2.RadioItem` Used to render a radio item. @@ -231,60 +230,60 @@ The component accepts the following props: The contents of the item -- Required: yes +- Required: yes ##### `suffix`: `React.ReactNode` The contents of the item's suffix. -- Required: no +- Required: no ##### `hideOnClick`: `boolean` Whether to hide the dropdown menu when the menu item is clicked. -- Required: no -- Default: `false` +- Required: no +- Default: `false` ##### `disabled`: `boolean` Determines if the element is disabled. -- Required: no -- Default: `false` +- Required: no +- Default: `false` ##### `name`: `string` The radio item's name. -- Required: yes +- Required: yes ##### `value`: `string | number` The radio item's value. -- Required: yes +- Required: yes ##### `checked`: `boolean` The checkbox item's value, useful when using multiple checkbox items - associated to the same `name`. +associated to the same `name`. -- Required: no +- Required: no ##### `defaultChecked`: `boolean` The checked state of the radio menu item when it is initially rendered. Use when not wanting to control its checked state. -- Required: no +- Required: no ##### `onChange`: `( event: React.ChangeEvent< HTMLInputElement > ) => void;` Event handler called when the checked radio menu item changes. -- Required: no +- Required: no -### `DropdownMenuItemLabel` +### `DropdownMenuV2.ItemLabel` Used to render the menu item's label. @@ -296,9 +295,9 @@ The component accepts the following props: The label contents. -- Required: yes +- Required: yes -### `DropdownMenuItemHelpText` +### `DropdownMenuV2.ItemHelpText` Used to render the menu item's help text. @@ -310,9 +309,9 @@ The component accepts the following props: The help text contents. -- Required: yes +- Required: yes -### `DropdownMenuGroup` +### `DropdownMenuV2.Group` Used to group menu items. @@ -324,8 +323,22 @@ The component accepts the following props: The contents of the group. -- Required: yes +- Required: yes + +### `DropdownMenuV2.GroupLabel` + +Used to render a group label. The label text should be kept as short as possible. + +#### Props + +The component accepts the following props: + +##### `children`: `React.ReactNode` + +The contents of the group label. + +- Required: yes -### `DropdownMenuSeparatorProps` +### `DropdownMenuV2.Separator` Used to render a visual separator. diff --git a/packages/components/src/dropdown-menu-v2/checkbox-item.tsx b/packages/components/src/dropdown-menu-v2/checkbox-item.tsx new file mode 100644 index 00000000000000..bcbc920cbb7205 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/checkbox-item.tsx @@ -0,0 +1,63 @@ +/** + * External dependencies + */ +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; +import { Icon, check } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import { DropdownMenuContext } from './context'; +import type { DropdownMenuCheckboxItemProps } from './types'; +import * as Styled from './styles'; +import { useTemporaryFocusVisibleFix } from './use-temporary-focus-visible-fix'; + +export const DropdownMenuCheckboxItem = forwardRef< + HTMLDivElement, + WordPressComponentProps< DropdownMenuCheckboxItemProps, 'div', false > +>( function DropdownMenuCheckboxItem( + { suffix, children, onBlur, hideOnClick = false, ...props }, + ref +) { + // TODO: Remove when https://github.com/ariakit/ariakit/issues/4083 is fixed + const focusVisibleFixProps = useTemporaryFocusVisibleFix( { onBlur } ); + const dropdownMenuContext = useContext( DropdownMenuContext ); + + return ( + + } + // Override some ariakit inline styles + style={ { width: 'auto', height: 'auto' } } + > + + + + + + { children } + + + { suffix && ( + + { suffix } + + ) } + + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/context.tsx b/packages/components/src/dropdown-menu-v2/context.tsx new file mode 100644 index 00000000000000..b285843327267f --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/context.tsx @@ -0,0 +1,13 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DropdownMenuContext as DropdownMenuContextType } from './types'; + +export const DropdownMenuContext = createContext< + DropdownMenuContextType | undefined +>( undefined ); diff --git a/packages/components/src/dropdown-menu-v2/group-label.tsx b/packages/components/src/dropdown-menu-v2/group-label.tsx new file mode 100644 index 00000000000000..7d838ef9fa620a --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/group-label.tsx @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import { DropdownMenuContext } from './context'; +import { Text } from '../text'; +import type { DropdownMenuGroupLabelProps } from './types'; +import * as Styled from './styles'; + +export const DropdownMenuGroupLabel = forwardRef< + HTMLDivElement, + WordPressComponentProps< DropdownMenuGroupLabelProps, 'div', false > +>( function DropdownMenuGroup( props, ref ) { + const dropdownMenuContext = useContext( DropdownMenuContext ); + return ( + + } + { ...props } + store={ dropdownMenuContext?.store } + /> + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/group.tsx b/packages/components/src/dropdown-menu-v2/group.tsx new file mode 100644 index 00000000000000..f2bf79015bc691 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/group.tsx @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import { DropdownMenuContext } from './context'; +import type { DropdownMenuGroupProps } from './types'; +import * as Styled from './styles'; + +export const DropdownMenuGroup = forwardRef< + HTMLDivElement, + WordPressComponentProps< DropdownMenuGroupProps, 'div', false > +>( function DropdownMenuGroup( props, ref ) { + const dropdownMenuContext = useContext( DropdownMenuContext ); + return ( + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/index.tsx b/packages/components/src/dropdown-menu-v2/index.tsx index b6ac9e45e37758..50c4f3069d51b5 100644 --- a/packages/components/src/dropdown-menu-v2/index.tsx +++ b/packages/components/src/dropdown-menu-v2/index.tsx @@ -8,8 +8,6 @@ import { useStoreState } from '@ariakit/react'; * WordPress dependencies */ import { - forwardRef, - createContext, useContext, useMemo, cloneElement, @@ -17,165 +15,27 @@ import { useCallback, } from '@wordpress/element'; import { isRTL } from '@wordpress/i18n'; -import { check, chevronRightSmall } from '@wordpress/icons'; -import { SVG, Circle } from '@wordpress/primitives'; +import { chevronRightSmall } from '@wordpress/icons'; /** * Internal dependencies */ import { useContextSystem, contextConnect } from '../context'; import type { WordPressComponentProps } from '../context'; -import Icon from '../icon'; import type { DropdownMenuContext as DropdownMenuContextType, DropdownMenuProps, - DropdownMenuGroupProps, - DropdownMenuItemProps, - DropdownMenuCheckboxItemProps, - DropdownMenuRadioItemProps, - DropdownMenuSeparatorProps, } from './types'; import * as Styled from './styles'; - -export const DropdownMenuContext = createContext< - DropdownMenuContextType | undefined ->( undefined ); - -export const DropdownMenuItem = forwardRef< - HTMLDivElement, - WordPressComponentProps< DropdownMenuItemProps, 'div', false > ->( function DropdownMenuItem( - { prefix, suffix, children, hideOnClick = true, ...props }, - ref -) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - - return ( - - { prefix } - - - - { children } - - - { suffix && ( - - { suffix } - - ) } - - - ); -} ); - -export const DropdownMenuCheckboxItem = forwardRef< - HTMLDivElement, - WordPressComponentProps< DropdownMenuCheckboxItemProps, 'div', false > ->( function DropdownMenuCheckboxItem( - { suffix, children, hideOnClick = false, ...props }, - ref -) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - - return ( - - } - // Override some ariakit inline styles - style={ { width: 'auto', height: 'auto' } } - > - - - - - - { children } - - - { suffix && ( - - { suffix } - - ) } - - - ); -} ); - -const radioCheck = ( - - - -); - -export const DropdownMenuRadioItem = forwardRef< - HTMLDivElement, - WordPressComponentProps< DropdownMenuRadioItemProps, 'div', false > ->( function DropdownMenuRadioItem( - { suffix, children, hideOnClick = false, ...props }, - ref -) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - - return ( - - } - // Override some ariakit inline styles - style={ { width: 'auto', height: 'auto' } } - > - - - - - - { children } - - - { suffix && ( - - { suffix } - - ) } - - - ); -} ); - -export const DropdownMenuGroup = forwardRef< - HTMLDivElement, - WordPressComponentProps< DropdownMenuGroupProps, 'div', false > ->( function DropdownMenuGroup( props, ref ) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - return ( - - ); -} ); +import { DropdownMenuContext } from './context'; +import { DropdownMenuItem } from './item'; +import { DropdownMenuCheckboxItem } from './checkbox-item'; +import { DropdownMenuRadioItem } from './radio-item'; +import { DropdownMenuGroup } from './group'; +import { DropdownMenuGroupLabel } from './group-label'; +import { DropdownMenuSeparator } from './separator'; +import { DropdownMenuItemLabel } from './item-label'; +import { DropdownMenuItemHelpText } from './item-help-text'; const UnconnectedDropdownMenu = ( props: WordPressComponentProps< DropdownMenuProps, 'div', false >, @@ -249,9 +109,11 @@ const UnconnectedDropdownMenu = ( ); // Extract the side from the applied placement — useful for animations. + // Using `currentPlacement` instead of `placement` to make sure that we + // use the final computed placement (including "flips" etc). const appliedPlacementSide = useStoreState( dropdownMenuStore, - 'placement' + 'currentPlacement' ).split( '-' )[ 0 ]; if ( @@ -313,7 +175,7 @@ const UnconnectedDropdownMenu = ( /> { /* Menu popover */ } - ( + // Two wrappers are needed for the entry animation, where the menu + // container scales with a different factor than its contents. + // The {...renderProps} are passed to the inner wrapper, so that the + // menu element is the direct parent of the menu item elements. + + + + ) } > { children } - + ); }; -export const DropdownMenu = contextConnect( - UnconnectedDropdownMenu, - 'DropdownMenu' -); - -export const DropdownMenuSeparator = forwardRef< - HTMLHRElement, - WordPressComponentProps< DropdownMenuSeparatorProps, 'hr', false > ->( function DropdownMenuSeparator( props, ref ) { - const dropdownMenuContext = useContext( DropdownMenuContext ); - return ( - - ); -} ); -export const DropdownMenuItemLabel = forwardRef< - HTMLSpanElement, - WordPressComponentProps< { children: React.ReactNode }, 'span', true > ->( function DropdownMenuItemLabel( props, ref ) { - return ( - - ); -} ); +export const DropdownMenuV2 = Object.assign( + contextConnect( UnconnectedDropdownMenu, 'DropdownMenu' ), + { + Context: Object.assign( DropdownMenuContext, { + displayName: 'DropdownMenuV2.Context', + } ), + Item: Object.assign( DropdownMenuItem, { + displayName: 'DropdownMenuV2.Item', + } ), + RadioItem: Object.assign( DropdownMenuRadioItem, { + displayName: 'DropdownMenuV2.RadioItem', + } ), + CheckboxItem: Object.assign( DropdownMenuCheckboxItem, { + displayName: 'DropdownMenuV2.CheckboxItem', + } ), + Group: Object.assign( DropdownMenuGroup, { + displayName: 'DropdownMenuV2.Group', + } ), + GroupLabel: Object.assign( DropdownMenuGroupLabel, { + displayName: 'DropdownMenuV2.GroupLabel', + } ), + Separator: Object.assign( DropdownMenuSeparator, { + displayName: 'DropdownMenuV2.Separator', + } ), + ItemLabel: Object.assign( DropdownMenuItemLabel, { + displayName: 'DropdownMenuV2.ItemLabel', + } ), + ItemHelpText: Object.assign( DropdownMenuItemHelpText, { + displayName: 'DropdownMenuV2.ItemHelpText', + } ), + } +); -export const DropdownMenuItemHelpText = forwardRef< - HTMLSpanElement, - WordPressComponentProps< { children: React.ReactNode }, 'span', true > ->( function DropdownMenuItemHelpText( props, ref ) { - return ( - - ); -} ); +export default DropdownMenuV2; diff --git a/packages/components/src/dropdown-menu-v2/item-help-text.tsx b/packages/components/src/dropdown-menu-v2/item-help-text.tsx new file mode 100644 index 00000000000000..0408d20dfbd40d --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/item-help-text.tsx @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import * as Styled from './styles'; + +export const DropdownMenuItemHelpText = forwardRef< + HTMLSpanElement, + WordPressComponentProps< { children: React.ReactNode }, 'span', true > +>( function DropdownMenuItemHelpText( props, ref ) { + return ( + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/item-label.tsx b/packages/components/src/dropdown-menu-v2/item-label.tsx new file mode 100644 index 00000000000000..a1f9391af2f92d --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/item-label.tsx @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import * as Styled from './styles'; + +export const DropdownMenuItemLabel = forwardRef< + HTMLSpanElement, + WordPressComponentProps< { children: React.ReactNode }, 'span', true > +>( function DropdownMenuItemLabel( props, ref ) { + return ( + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/item.tsx b/packages/components/src/dropdown-menu-v2/item.tsx new file mode 100644 index 00000000000000..2680603db22aa7 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/item.tsx @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import type { DropdownMenuItemProps } from './types'; +import * as Styled from './styles'; +import { DropdownMenuContext } from './context'; +import { useTemporaryFocusVisibleFix } from './use-temporary-focus-visible-fix'; + +export const DropdownMenuItem = forwardRef< + HTMLDivElement, + WordPressComponentProps< DropdownMenuItemProps, 'div', false > +>( function DropdownMenuItem( + { prefix, suffix, children, onBlur, hideOnClick = true, ...props }, + ref +) { + // TODO: Remove when https://github.com/ariakit/ariakit/issues/4083 is fixed + const focusVisibleFixProps = useTemporaryFocusVisibleFix( { onBlur } ); + const dropdownMenuContext = useContext( DropdownMenuContext ); + + return ( + + { prefix } + + + + { children } + + + { suffix && ( + + { suffix } + + ) } + + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/radio-item.tsx b/packages/components/src/dropdown-menu-v2/radio-item.tsx new file mode 100644 index 00000000000000..547d8f257cdf4b --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/radio-item.tsx @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import * as Ariakit from '@ariakit/react'; + +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; +import { Icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import { DropdownMenuContext } from './context'; +import type { DropdownMenuRadioItemProps } from './types'; +import * as Styled from './styles'; +import { SVG, Circle } from '@wordpress/primitives'; +import { useTemporaryFocusVisibleFix } from './use-temporary-focus-visible-fix'; + +const radioCheck = ( + + + +); + +export const DropdownMenuRadioItem = forwardRef< + HTMLDivElement, + WordPressComponentProps< DropdownMenuRadioItemProps, 'div', false > +>( function DropdownMenuRadioItem( + { suffix, children, onBlur, hideOnClick = false, ...props }, + ref +) { + // TODO: Remove when https://github.com/ariakit/ariakit/issues/4083 is fixed + const focusVisibleFixProps = useTemporaryFocusVisibleFix( { onBlur } ); + const dropdownMenuContext = useContext( DropdownMenuContext ); + + return ( + + } + // Override some ariakit inline styles + style={ { width: 'auto', height: 'auto' } } + > + + + + + + { children } + + + { suffix && ( + + { suffix } + + ) } + + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/separator.tsx b/packages/components/src/dropdown-menu-v2/separator.tsx new file mode 100644 index 00000000000000..bc5aff7ae26118 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/separator.tsx @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { WordPressComponentProps } from '../context'; +import { DropdownMenuContext } from './context'; +import type { DropdownMenuSeparatorProps } from './types'; +import * as Styled from './styles'; + +export const DropdownMenuSeparator = forwardRef< + HTMLHRElement, + WordPressComponentProps< DropdownMenuSeparatorProps, 'hr', false > +>( function DropdownMenuSeparator( props, ref ) { + const dropdownMenuContext = useContext( DropdownMenuContext ); + return ( + + ); +} ); diff --git a/packages/components/src/dropdown-menu-v2/stories/index.story.tsx b/packages/components/src/dropdown-menu-v2/stories/index.story.tsx index a2fc476cd8ac07..90d15ca2ea6c18 100644 --- a/packages/components/src/dropdown-menu-v2/stories/index.story.tsx +++ b/packages/components/src/dropdown-menu-v2/stories/index.story.tsx @@ -14,43 +14,35 @@ import { useState, useMemo, useContext } from '@wordpress/element'; * Internal dependencies */ import { useCx } from '../../utils'; -import { - DropdownMenu, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuGroup, - DropdownMenuSeparator, - DropdownMenuContext, - DropdownMenuRadioItem, - DropdownMenuItemLabel, - DropdownMenuItemHelpText, -} from '..'; +import DropdownMenuV2 from '..'; import Icon from '../../icon'; import Button from '../../button'; import Modal from '../../modal'; import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill'; import { ContextSystemProvider } from '../../context'; -const meta: Meta< typeof DropdownMenu > = { +const meta: Meta< typeof DropdownMenuV2 > = { title: 'Components (Experimental)/DropdownMenu V2', - component: DropdownMenu, + component: DropdownMenuV2, subcomponents: { // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuItem, + Item: DropdownMenuV2.Item, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuCheckboxItem, + CheckboxItem: DropdownMenuV2.CheckboxItem, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuGroup, + Group: DropdownMenuV2.Group, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuSeparator, + GroupLabel: DropdownMenuV2.GroupLabel, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuContext, + Separator: DropdownMenuV2.Separator, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuRadioItem, + Context: DropdownMenuV2.Context, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuItemLabel, + RadioItem: DropdownMenuV2.RadioItem, // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - DropdownMenuItemHelpText, + ItemLabel: DropdownMenuV2.ItemLabel, + // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 + ItemHelpText: DropdownMenuV2.ItemHelpText, }, argTypes: { children: { control: { type: null } }, @@ -68,51 +60,52 @@ const meta: Meta< typeof DropdownMenu > = { }; export default meta; -export const Default: StoryFn< typeof DropdownMenu > = ( props ) => ( - - - Label - - - Label - Help text - - - Label - +export const Default: StoryFn< typeof DropdownMenuV2 > = ( props ) => ( + + + Label + + + Label + Help text + + + Label + The menu item help text is automatically truncated when there are more than two lines of text - - - - Label - + + + + Label + This item doesn't close the menu on click - - - Disabled item - - - + + Disabled item + + + Group label + } > - With prefix - - With suffix - With prefix + + With suffix + } suffix="⌥⌘T" > - + Disabled with prefix and suffix - - + + And help text - - - - + + + + ); Default.args = { trigger: ( @@ -122,48 +115,56 @@ Default.args = { ), }; -export const WithSubmenu: StoryFn< typeof DropdownMenu > = ( props ) => ( - - Level 1 item - = ( props ) => ( + + Level 1 item + - + + Submenu trigger item with a long label - - + + } > - - Level 2 item - - - Level 2 item - - + + Level 2 item + + + + + Level 2 item + + + - + + Submenu trigger - - + + } > - - Level 3 item - - - Level 3 item - - - - + + + Level 3 item + + + + + Level 3 item + + + + + ); WithSubmenu.args = { ...Default.args, }; -export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { +export const WithCheckboxes: StoryFn< typeof DropdownMenuV2 > = ( props ) => { const [ isAChecked, setAChecked ] = useState( false ); const [ isBChecked, setBChecked ] = useState( true ); const [ multipleCheckboxesValue, setMultipleCheckboxesValue ] = useState< @@ -171,7 +172,7 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { >( [ 'b' ] ); const onMultipleCheckboxesCheckedChange: React.ComponentProps< - typeof DropdownMenuCheckboxItem + typeof DropdownMenuV2.CheckboxItem >[ 'onChange' ] = ( e ) => { setMultipleCheckboxesValue( ( prevValues ) => { if ( prevValues.includes( e.target.value ) ) { @@ -182,176 +183,202 @@ export const WithCheckboxes: StoryFn< typeof DropdownMenu > = ( props ) => { }; return ( - - - + + + Single selection, uncontrolled + + - + Checkbox item A - - - Uncontrolled - - - + + Initially unchecked + + + - + Checkbox item B - - - Uncontrolled, initially checked - - - - - - + + Initially checked + + + + + + + Single selection, controlled + + setAChecked( e.target.checked ) } > - + Checkbox item A - - - Controlled - - - + + Initially unchecked + + + setBChecked( e.target.checked ) } > - + Checkbox item B - - - Controlled, initially checked - - - - - - + + Initially checked + + + + + + + Multiple selection, uncontrolled + + - + Checkbox item A - - - Uncontrolled, multiple selection - - - + + Initially unchecked + + + - + Checkbox item B - - - Uncontrolled, multiple selection, initially checked - - - - - - + + Initially checked + + + + + + + Multiple selection, controlled + + - + Checkbox item A - - - Controlled, multiple selection - - - + + Initially unchecked + + + - + Checkbox item B - - - Controlled, multiple selection, initially checked - - - - + + + Initially checked + + + + ); }; WithCheckboxes.args = { ...Default.args, }; -export const WithRadios: StoryFn< typeof DropdownMenu > = ( props ) => { +export const WithRadios: StoryFn< typeof DropdownMenuV2 > = ( props ) => { const [ radioValue, setRadioValue ] = useState( 'two' ); const onRadioChange: React.ComponentProps< - typeof DropdownMenuRadioItem + typeof DropdownMenuV2.RadioItem >[ 'onChange' ] = ( e ) => setRadioValue( e.target.value ); return ( - - - - Radio item 1 - - Uncontrolled - - - + + + Uncontrolled + + + + Radio item 1 + + + Initially unchecked + + + - Radio item 2 - - Uncontrolled, initially checked - - - - - - + Radio item 2 + + + Initially checked + + + + + + + Controlled + + - Radio item 1 - - Controlled - - - + Radio item 1 + + + Initially unchecked + + + - Radio item 2 - - Controlled, initially checked - - - - + + Radio item 2 + + + Initially checked + + + + ); }; WithRadios.args = { @@ -365,7 +392,7 @@ const modalOnTopOfDropdown = css` `; // For more examples with `Modal`, check https://ariakit.org/examples/menu-wordpress-modal -export const WithModals: StoryFn< typeof DropdownMenu > = ( props ) => { +export const WithModals: StoryFn< typeof DropdownMenuV2 > = ( props ) => { const [ isOuterModalOpen, setOuterModalOpen ] = useState( false ); const [ isInnerModalOpen, setInnerModalOpen ] = useState( false ); @@ -374,23 +401,23 @@ export const WithModals: StoryFn< typeof DropdownMenu > = ( props ) => { return ( <> - - + setOuterModalOpen( true ) } hideOnClick={ false } > - + Open outer modal - - - + + setInnerModalOpen( true ) } hideOnClick={ false } > - + Open inner modal - - + + { isInnerModalOpen && ( setInnerModalOpen( false ) } @@ -402,7 +429,7 @@ export const WithModals: StoryFn< typeof DropdownMenu > = ( props ) => { ) } - + { isOuterModalOpen && ( setOuterModalOpen( false ) } @@ -424,14 +451,14 @@ WithModals.args = { const ExampleSlotFill = createSlotFill( 'Example' ); const Slot = () => { - const dropdownMenuContext = useContext( DropdownMenuContext ); + const dropdownMenuContext = useContext( DropdownMenuV2.Context ); // Forwarding the content of the slot so that it can be used by the fill const fillProps = useMemo( () => ( { forwardedContext: [ [ - DropdownMenuContext.Provider, + DropdownMenuV2.Context.Provider, { value: dropdownMenuContext }, ], ], @@ -472,37 +499,37 @@ const Fill = ( { children }: { children: React.ReactNode } ) => { ); }; -export const WithSlotFill: StoryFn< typeof DropdownMenu > = ( props ) => { +export const WithSlotFill: StoryFn< typeof DropdownMenuV2 > = ( props ) => { return ( - - - Item - + + + Item + - + - - + + Item from fill - - - + + - + + Submenu from fill - - + + } > - - + + Submenu item from fill - - - + + + ); @@ -512,42 +539,48 @@ WithSlotFill.args = { }; const toolbarVariantContextValue = { - DropdownMenu: { + DropdownMenuV2: { variant: 'toolbar', }, }; -export const ToolbarVariant: StoryFn< typeof DropdownMenu > = ( props ) => ( +export const ToolbarVariant: StoryFn< typeof DropdownMenuV2 > = ( props ) => ( // TODO: add toolbar - - - Level 1 item - - - Level 1 item - - - + + + Level 1 item + + + + + Level 1 item + + + + - + + Submenu trigger - - + + } > - - Level 2 item - - - + + + Level 2 item + + + + ); ToolbarVariant.args = { ...Default.args, }; -export const InsideModal: StoryFn< typeof DropdownMenu > = ( props ) => { +export const InsideModal: StoryFn< typeof DropdownMenuV2 > = ( props ) => { const [ isModalOpen, setModalOpen ] = useState( false ); return ( <> @@ -560,34 +593,34 @@ export const InsideModal: StoryFn< typeof DropdownMenu > = ( props ) => { { isModalOpen && ( setModalOpen( false ) }> - - - + + + Level 1 item - - - - + + + + Level 1 item - - - - + + + - + + Submenu trigger - - + + } > - - + + Level 2 item - - - - + + + + diff --git a/packages/components/src/dropdown-menu-v2/styles.ts b/packages/components/src/dropdown-menu-v2/styles.ts index 9e71316b6b9dc5..55020fbcd41354 100644 --- a/packages/components/src/dropdown-menu-v2/styles.ts +++ b/packages/components/src/dropdown-menu-v2/styles.ts @@ -2,7 +2,7 @@ * External dependencies */ import * as Ariakit from '@ariakit/react'; -import { css, keyframes } from '@emotion/react'; +import { css } from '@emotion/react'; import styled from '@emotion/styled'; /** @@ -15,9 +15,13 @@ import { Truncate } from '../truncate'; import type { DropdownMenuContext } from './types'; const ANIMATION_PARAMS = { - SLIDE_AMOUNT: '2px', - DURATION: '400ms', - EASING: 'cubic-bezier( 0.16, 1, 0.3, 1 )', + SCALE_AMOUNT_OUTER: 0.82, + SCALE_AMOUNT_CONTENT: 0.9, + DURATION: { + IN: '400ms', + OUT: '200ms', + }, + EASING: 'cubic-bezier(0.33, 0, 0, 1)', }; const CONTENT_WRAPPER_PADDING = space( 1 ); @@ -38,41 +42,60 @@ const TOOLBAR_VARIANT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ TOOLBAR_VAR const GRID_TEMPLATE_COLS = 'minmax( 0, max-content ) 1fr'; -const slideUpAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateY(${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateY(0)' }, -} ); +export const MenuPopoverOuterWrapper = styled.div< + Pick< DropdownMenuContext, 'variant' > +>` + position: relative; -const slideRightAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateX(-${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateX(0)' }, -} ); + background-color: ${ COLORS.ui.background }; + border-radius: ${ CONFIG.radiusMedium }; + ${ ( props ) => css` + box-shadow: ${ props.variant === 'toolbar' + ? TOOLBAR_VARIANT_BOX_SHADOW + : DEFAULT_BOX_SHADOW }; + ` } -const slideDownAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateY(-${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateY(0)' }, -} ); + overflow: hidden; -const slideLeftAndFade = keyframes( { - '0%': { - opacity: 0, - transform: `translateX(${ ANIMATION_PARAMS.SLIDE_AMOUNT })`, - }, - '100%': { opacity: 1, transform: 'translateX(0)' }, -} ); + /* Open/close animation (outer wrapper) */ + @media not ( prefers-reduced-motion ) { + transition-property: transform, opacity; + transition-timing-function: ${ ANIMATION_PARAMS.EASING }; + transition-duration: ${ ANIMATION_PARAMS.DURATION.IN }; + will-change: transform, opacity; -export const DropdownMenu = styled( Ariakit.Menu )< - Pick< DropdownMenuContext, 'variant' > ->` + /* Regardless of the side, fade in and out. */ + opacity: 0; + &:has( [data-enter] ) { + opacity: 1; + } + + &:has( [data-leave] ) { + transition-duration: ${ ANIMATION_PARAMS.DURATION.OUT }; + } + + /* For menus opening on top and bottom side, animate the scale Y too. */ + &:has( [data-side='bottom'] ), + &:has( [data-side='top'] ) { + transform: scaleY( ${ ANIMATION_PARAMS.SCALE_AMOUNT_OUTER } ); + } + &:has( [data-side='bottom'] ) { + transform-origin: top; + } + &:has( [data-side='top'] ) { + transform-origin: bottom; + } + &:has( [data-enter][data-side='bottom'] ), + &:has( [data-enter][data-side='top'] ), + /* Do not animate the scaleY when closing the menu */ + &:has( [data-leave][data-side='bottom'] ), + &:has( [data-leave][data-side='top'] ) { + transform: scaleY( 1 ); + } + } +`; + +export const MenuPopoverInnerWrapper = styled.div` position: relative; /* Same as popover component */ /* TODO: is there a way to read the sass variable? */ @@ -86,15 +109,8 @@ export const DropdownMenu = styled( Ariakit.Menu )< min-width: 160px; max-width: 320px; max-height: var( --popover-available-height ); - padding: ${ CONTENT_WRAPPER_PADDING }; - background-color: ${ COLORS.ui.background }; - border-radius: ${ CONFIG.radiusMedium }; - ${ ( props ) => css` - box-shadow: ${ props.variant === 'toolbar' - ? TOOLBAR_VARIANT_BOX_SHADOW - : DEFAULT_BOX_SHADOW }; - ` } + padding: ${ CONTENT_WRAPPER_PADDING }; overscroll-behavior: contain; overflow: auto; @@ -102,23 +118,32 @@ export const DropdownMenu = styled( Ariakit.Menu )< /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent !important; - /* Animation */ - &[data-open] { - @media not ( prefers-reduced-motion ) { - animation-duration: ${ ANIMATION_PARAMS.DURATION }; - animation-timing-function: ${ ANIMATION_PARAMS.EASING }; - will-change: transform, opacity; - /* Default animation.*/ - animation-name: ${ slideDownAndFade }; - &[data-side='left'] { - animation-name: ${ slideLeftAndFade }; - } - &[data-side='up'] { - animation-name: ${ slideUpAndFade }; - } - &[data-side='right'] { - animation-name: ${ slideRightAndFade }; - } + /* Open/close animation (inner content wrapper) */ + @media not ( prefers-reduced-motion ) { + transition: inherit; + transform-origin: inherit; + + /* + * For menus opening on top and bottom side, animate the scale Y too. + * The content scales at a different rate than the outer container: + * - first, counter the outer scale factor by doing "1 / scaleAmountOuter" + * - then, apply the content scale factor. + */ + &[data-side='bottom'], + &[data-side='top'] { + transform: scaleY( + calc( + 1 / ${ ANIMATION_PARAMS.SCALE_AMOUNT_OUTER } * + ${ ANIMATION_PARAMS.SCALE_AMOUNT_CONTENT } + ) + ); + } + &[data-enter][data-side='bottom'], + &[data-enter][data-side='top'], + /* Do not animate the scaleY when closing the menu */ + &[data-leave][data-side='bottom'], + &[data-leave][data-side='top'] { + transform: scaleY( 1 ); } } `; @@ -171,7 +196,7 @@ const baseItem = css` cursor: not-allowed; } - /* Hover */ + /* Active item (including hover) */ &[data-active-item]:not( [data-focus-visible] ):not( [aria-disabled='true'] ) { @@ -194,7 +219,7 @@ const baseItem = css` } /* When the item is the trigger of an open submenu */ - ${ DropdownMenu }:not(:focus) &:not(:focus)[aria-expanded="true"] { + ${ MenuPopoverInnerWrapper }:not(:focus) &:not(:focus)[aria-expanded="true"] { background-color: ${ LIGHT_BACKGROUND_COLOR }; color: ${ COLORS.theme.foreground }; } @@ -292,9 +317,9 @@ export const ItemSuffixWrapper = styled.span` * When the parent menu item is active, except when it's a non-focused/hovered * submenu trigger (in that case, color should not be inherited) */ - [data-active-item]:not( [data-focus-visible] ) *:not(${ DropdownMenu }) &, + [data-active-item]:not( [data-focus-visible] ) *:not(${ MenuPopoverInnerWrapper }) &, /* When the parent menu item is disabled */ - [aria-disabled='true'] *:not(${ DropdownMenu }) & { + [aria-disabled='true'] *:not(${ MenuPopoverInnerWrapper }) & { color: inherit; } `; @@ -304,6 +329,15 @@ export const DropdownMenuGroup = styled( Ariakit.MenuGroup )` display: contents; `; +export const DropdownMenuGroupLabel = styled( Ariakit.MenuGroupLabel )` + /* Occupy the width of all grid columns (ie. full width) */ + grid-column: 1 / -1; + + padding-block-start: ${ space( 3 ) }; + padding-block-end: ${ space( 2 ) }; + padding-inline: ${ ITEM_PADDING_INLINE }; +`; + export const DropdownMenuSeparator = styled( Ariakit.MenuSeparator )< Pick< DropdownMenuContext, 'variant' > >` @@ -348,8 +382,10 @@ export const DropdownMenuItemHelpText = styled( Truncate )` color: ${ LIGHTER_TEXT_COLOR }; word-break: break-all; - [data-active-item]:not( [data-focus-visible] ) *:not( ${ DropdownMenu } ) &, - [aria-disabled='true'] *:not( ${ DropdownMenu } ) & { + [data-active-item]:not( [data-focus-visible] ) + *:not( ${ MenuPopoverInnerWrapper } ) + &, + [aria-disabled='true'] *:not( ${ MenuPopoverInnerWrapper } ) & { color: inherit; } `; diff --git a/packages/components/src/dropdown-menu-v2/test/index.tsx b/packages/components/src/dropdown-menu-v2/test/index.tsx index 5457f5e73e23c3..cb674f27edaacf 100644 --- a/packages/components/src/dropdown-menu-v2/test/index.tsx +++ b/packages/components/src/dropdown-menu-v2/test/index.tsx @@ -12,14 +12,7 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuItem, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuGroup, -} from '..'; +import { DropdownMenuV2 } from '..'; const delay = ( delayInMs: number ) => { return new Promise( ( resolve ) => setTimeout( resolve, delayInMs ) ); @@ -29,18 +22,24 @@ describe( 'DropdownMenu', () => { // See https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ it( 'should follow the WAI-ARIA spec', async () => { render( - Open dropdown }> - Dropdown menu item - - Open dropdown }> + Dropdown menu item + + Dropdown submenu + + Dropdown submenu + } > - Dropdown submenu item 1 - Dropdown submenu item 2 - - + + Dropdown submenu item 1 + + + Dropdown submenu item 2 + + + ); const toggleButton = screen.getByRole( 'button', { @@ -95,9 +94,11 @@ describe( 'DropdownMenu', () => { describe( 'pointer and keyboard interactions', () => { it( 'should open and focus the menu when clicking the trigger', async () => { render( - Open dropdown }> - Dropdown menu item - + Open dropdown }> + + Dropdown menu item + + ); const toggleButton = screen.getByRole( 'button', { @@ -116,11 +117,13 @@ describe( 'DropdownMenu', () => { it( 'should open and focus the first item when pressing the arrow down key on the trigger', async () => { render( - Open dropdown }> - First item - Second item - Third item - + Open dropdown }> + + First item + + Second item + Third item + ); const toggleButton = screen.getByRole( 'button', { @@ -146,11 +149,13 @@ describe( 'DropdownMenu', () => { it( 'should open and focus the first item when pressing the space key on the trigger', async () => { render( - Open dropdown }> - First item - Second item - Third item - + Open dropdown }> + + First item + + Second item + Third item + ); const toggleButton = screen.getByRole( 'button', { @@ -176,9 +181,11 @@ describe( 'DropdownMenu', () => { it( 'should close when pressing the escape key', async () => { render( - Open dropdown }> - Dropdown menu item - + Open dropdown }> + + Dropdown menu item + + ); const trigger = screen.getByRole( 'button', { @@ -205,12 +212,14 @@ describe( 'DropdownMenu', () => { it( 'should close when clicking outside of the content', async () => { render( - Open dropdown } > - Dropdown menu item - + + Dropdown menu item + + ); expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); @@ -223,12 +232,14 @@ describe( 'DropdownMenu', () => { it( 'should close when clicking on a menu item', async () => { render( - Open dropdown } > - Dropdown menu item - + + Dropdown menu item + + ); expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); @@ -241,14 +252,14 @@ describe( 'DropdownMenu', () => { it( 'should not close when clicking on a menu item when the `hideOnClick` prop is set to `false`', async () => { render( - Open dropdown } > - + Dropdown menu item - - + + ); expect( screen.getByRole( 'menu' ) ).toBeVisible(); @@ -261,14 +272,14 @@ describe( 'DropdownMenu', () => { it( 'should not close when clicking on a disabled menu item', async () => { render( - Open dropdown } > - + Dropdown menu item - - + + ); expect( screen.getByRole( 'menu' ) ).toBeInTheDocument(); @@ -281,28 +292,34 @@ describe( 'DropdownMenu', () => { it( 'should reveal submenu content when hovering over the submenu trigger', async () => { render( - Open dropdown } > - Dropdown menu item 1 - Dropdown menu item 2 - + Dropdown menu item 1 + + + Dropdown menu item 2 + + + Dropdown submenu - + } > - + Dropdown submenu item 1 - - + + Dropdown submenu item 2 - - - Dropdown menu item 3 - + + + + Dropdown menu item 3 + + ); // Before hover, submenu items are not rendered @@ -326,28 +343,34 @@ describe( 'DropdownMenu', () => { it( 'should navigate menu items and subitems using the arrow, spacebar and enter keys', async () => { render( - Open dropdown } > - Dropdown menu item 1 - Dropdown menu item 2 - + Dropdown menu item 1 + + + Dropdown menu item 2 + + + Dropdown submenu - + } > - + Dropdown submenu item 1 - - + + Dropdown submenu item 2 - - - Dropdown menu item 3 - + + + + Dropdown menu item 3 + + ); // The menu is focused automatically when `defaultOpen` is set. @@ -450,32 +473,32 @@ describe( 'DropdownMenu', () => { const ControlledRadioGroup = () => { const [ radioValue, setRadioValue ] = useState( 'two' ); const onRadioChange: React.ComponentProps< - typeof DropdownMenuRadioItem + typeof DropdownMenuV2.RadioItem >[ 'onChange' ] = ( e ) => { onRadioValueChangeSpy( e.target.value ); setRadioValue( e.target.value ); }; return ( - Open dropdown }> - - Open dropdown }> + + Radio item one - - + Radio item two - - - + + + ); }; @@ -533,9 +556,9 @@ describe( 'DropdownMenu', () => { it( 'should check radio items and keep the menu open when clicking (uncontrolled)', async () => { const onRadioValueChangeSpy = jest.fn(); render( - Open dropdown }> - - Open dropdown }> + + @@ -543,8 +566,8 @@ describe( 'DropdownMenu', () => { } > Radio item one - - + { } > Radio item two - - - + + + ); // Open dropdown @@ -617,8 +640,8 @@ describe( 'DropdownMenu', () => { useState< boolean >(); return ( - Open dropdown }> - Open dropdown }> + { } } > Checkbox item one - + - { } } > Checkbox item two - - + + ); }; @@ -740,8 +763,8 @@ describe( 'DropdownMenu', () => { const onCheckboxValueChangeSpy = jest.fn(); render( - Open dropdown }> - Open dropdown }> + { @@ -753,9 +776,9 @@ describe( 'DropdownMenu', () => { } } > Checkbox item one - + - { } } > Checkbox item two - - + + ); // Open dropdown @@ -858,9 +881,11 @@ describe( 'DropdownMenu', () => { it( 'should be modal by default', async () => { render( <> - Open dropdown }> - Dropdown menu item - + Open dropdown }> + + Dropdown menu item + + ); @@ -885,12 +910,14 @@ describe( 'DropdownMenu', () => { it( 'should not be modal when the `modal` prop is set to `false`', async () => { render( <> - Open dropdown } modal={ false } > - Dropdown menu item - + + Dropdown menu item + + ); @@ -922,11 +949,11 @@ describe( 'DropdownMenu', () => { describe( 'items prefix and suffix', () => { it( 'should display a prefix on regular items', async () => { render( - Open dropdown }> - Item prefix }> + Open dropdown }> + Item prefix }> Dropdown menu item - - + + ); // Click to open the menu @@ -946,11 +973,11 @@ describe( 'DropdownMenu', () => { it( 'should display a suffix on regular items', async () => { render( - Open dropdown }> - Item suffix }> + Open dropdown }> + Item suffix }> Dropdown menu item - - + + ); // Click to open the menu @@ -970,15 +997,15 @@ describe( 'DropdownMenu', () => { it( 'should display a suffix on radio items', async () => { render( - Open dropdown }> - Open dropdown }> + Radio item one - - + + ); // Click to open the menu @@ -998,15 +1025,15 @@ describe( 'DropdownMenu', () => { it( 'should display a suffix on checkbox items', async () => { render( - Open dropdown }> - Open dropdown }> + Checkbox item one - - + + ); // Click to open the menu @@ -1028,10 +1055,10 @@ describe( 'DropdownMenu', () => { describe( 'typeahead', () => { it( 'should highlight matching item', async () => { render( - Open dropdown }> - One - Two - + Open dropdown }> + One + Two + ); // Click to open the menu @@ -1061,10 +1088,10 @@ describe( 'DropdownMenu', () => { it( 'should keep previous focus when no matches are found', async () => { render( - Open dropdown }> - One - Two - + Open dropdown }> + One + Two + ); // Click to open the menu diff --git a/packages/components/src/dropdown-menu-v2/types.ts b/packages/components/src/dropdown-menu-v2/types.ts index f49f026b787933..795cd9ac76ff58 100644 --- a/packages/components/src/dropdown-menu-v2/types.ts +++ b/packages/components/src/dropdown-menu-v2/types.ts @@ -87,6 +87,13 @@ export interface DropdownMenuGroupProps { children: React.ReactNode; } +export interface DropdownMenuGroupLabelProps { + /** + * The contents of the dropdown menu group. + */ + children: React.ReactNode; +} + export interface DropdownMenuItemProps { /** * The contents of the menu item. diff --git a/packages/components/src/dropdown-menu-v2/use-temporary-focus-visible-fix.ts b/packages/components/src/dropdown-menu-v2/use-temporary-focus-visible-fix.ts new file mode 100644 index 00000000000000..0df13133739609 --- /dev/null +++ b/packages/components/src/dropdown-menu-v2/use-temporary-focus-visible-fix.ts @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { useState, flushSync } from '@wordpress/element'; + +export function useTemporaryFocusVisibleFix( { + onBlur: onBlurProp, +}: { + onBlur?: React.FocusEventHandler< HTMLDivElement >; +} ) { + const [ focusVisible, setFocusVisible ] = useState( false ); + return { + 'data-focus-visible': focusVisible || undefined, + onFocusVisible: () => { + flushSync( () => setFocusVisible( true ) ); + }, + onBlur: ( ( event ) => { + onBlurProp?.( event ); + setFocusVisible( false ); + } ) as React.FocusEventHandler< HTMLDivElement >, + }; +} diff --git a/packages/components/src/focal-point-picker/styles/focal-point-style.ts b/packages/components/src/focal-point-picker/styles/focal-point-style.ts index 6f95978256f72e..ad1e8eeb286779 100644 --- a/packages/components/src/focal-point-picker/styles/focal-point-style.ts +++ b/packages/components/src/focal-point-picker/styles/focal-point-style.ts @@ -3,6 +3,11 @@ */ import styled from '@emotion/styled'; +/** + * Internal dependencies + */ +import { CONFIG } from '../../utils'; + export const PointerCircle = styled.div` background-color: transparent; cursor: grab; @@ -15,7 +20,7 @@ export const PointerCircle = styled.div` z-index: 10000; background: rgba( 255, 255, 255, 0.4 ); border: 1px solid rgba( 255, 255, 255, 0.4 ); - border-radius: 50%; + border-radius: ${ CONFIG.radiusRound }; backdrop-filter: blur( 16px ) saturate( 180% ); box-shadow: rgb( 0 0 0 / 10% ) 0px 0px 8px; diff --git a/packages/components/src/form-toggle/style.scss b/packages/components/src/form-toggle/style.scss index 6fd25590b56e41..dd7888403b34d4 100644 --- a/packages/components/src/form-toggle/style.scss +++ b/packages/components/src/form-toggle/style.scss @@ -54,7 +54,7 @@ $transition-duration: 0.2s; left: math.div($toggle-thumb-size, 6); width: $toggle-thumb-size; height: $toggle-thumb-size; - border-radius: 50%; + border-radius: $radius-round; transition: $transition-duration transform ease, $transition-duration background-color ease-out; diff --git a/packages/components/src/form-token-field/style.scss b/packages/components/src/form-token-field/style.scss index 57435a3f62ad26..cf5cc5b63e2227 100644 --- a/packages/components/src/form-token-field/style.scss +++ b/packages/components/src/form-token-field/style.scss @@ -102,7 +102,6 @@ &.is-error { .components-form-token-field__token-text { color: $alert-red; - border-radius: 4px 0 0 4px; padding: 0 4px 0 6px; } } @@ -133,7 +132,7 @@ } .components-form-token-field__token-text { - border-radius: 2px 0 0 2px; + border-radius: $radius-x-small 0 0 $radius-x-small; padding: 0 0 0 8px; white-space: nowrap; overflow: hidden; @@ -142,7 +141,7 @@ .components-form-token-field__remove-token.components-button { cursor: pointer; - border-radius: 0 2px 2px 0; + border-radius: 0 $radius-x-small $radius-x-small 0; padding: 0 2px; color: $gray-900; line-height: 10px; diff --git a/packages/components/src/form-token-field/types.ts b/packages/components/src/form-token-field/types.ts index db4549a7f0779c..051f00a6cdeb06 100644 --- a/packages/components/src/form-token-field/types.ts +++ b/packages/components/src/form-token-field/types.ts @@ -157,6 +157,7 @@ export interface FormTokenFieldProps * * @default false * @deprecated + * @ignore */ __next36pxDefaultSize?: boolean; /** diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 6483e34dc222a8..3ddfbd05cd6581 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -12,7 +12,11 @@ export { } from '@wordpress/primitives'; // Components. -export { default as __experimentalAlignmentMatrixControl } from './alignment-matrix-control'; +export { + /** @deprecated Import `AlignmentMatrixControl` instead. */ + default as __experimentalAlignmentMatrixControl, + default as AlignmentMatrixControl, +} from './alignment-matrix-control'; export { default as Animate, getAnimateClassName as __unstableGetAnimateClassName, diff --git a/packages/components/src/input-control/index.tsx b/packages/components/src/input-control/index.tsx index a5a9e054bc37d8..fd0fc0a5c45536 100644 --- a/packages/components/src/input-control/index.tsx +++ b/packages/components/src/input-control/index.tsx @@ -100,8 +100,8 @@ export function UnforwardedInputControl( isPressEnterToChange={ isPressEnterToChange } onKeyDown={ onKeyDown } onValidate={ onValidate } - paddingInlineStart={ prefix ? space( 2 ) : undefined } - paddingInlineEnd={ suffix ? space( 2 ) : undefined } + paddingInlineStart={ prefix ? space( 1 ) : undefined } + paddingInlineEnd={ suffix ? space( 1 ) : undefined } ref={ ref } size={ size } stateReducer={ stateReducer } diff --git a/packages/components/src/input-control/input-base.tsx b/packages/components/src/input-control/input-base.tsx index 58396f3ab98591..f1b8227563cfdc 100644 --- a/packages/components/src/input-control/input-base.tsx +++ b/packages/components/src/input-control/input-base.tsx @@ -14,13 +14,7 @@ import { useMemo } from '@wordpress/element'; */ import Backdrop from './backdrop'; import Label from './label'; -import { - Container, - Root, - Prefix, - Suffix, - getSizeConfig, -} from './styles/input-control-styles'; +import { Container, Root, Prefix, Suffix } from './styles/input-control-styles'; import type { InputBaseProps, LabelPosition } from './types'; import type { WordPressComponentProps } from '../context'; import { @@ -90,16 +84,12 @@ function InputBase( const id = useUniqueId( idProp ); const hideLabel = hideLabelFromVision || ! label; - const { paddingLeft, paddingRight } = getSizeConfig( { - inputSize: size, - __next40pxDefaultSize, - } ); const prefixSuffixContextValue = useMemo( () => { return { - InputControlPrefixWrapper: { paddingLeft }, - InputControlSuffixWrapper: { paddingRight }, + InputControlPrefixWrapper: { __next40pxDefaultSize, size }, + InputControlSuffixWrapper: { __next40pxDefaultSize, size }, }; - }, [ paddingLeft, paddingRight ] ); + }, [ __next40pxDefaultSize, size ] ); return ( // @ts-expect-error The `direction` prop from Flex (FlexDirection) conflicts with legacy SVGAttributes `direction` (string) that come from React intrinsic prop definitions. diff --git a/packages/components/src/input-control/input-prefix-wrapper.tsx b/packages/components/src/input-control/input-prefix-wrapper.tsx index 23d9e823da932d..c5232e4d9e6bbc 100644 --- a/packages/components/src/input-control/input-prefix-wrapper.tsx +++ b/packages/components/src/input-control/input-prefix-wrapper.tsx @@ -6,19 +6,23 @@ import type { ForwardedRef } from 'react'; /** * Internal dependencies */ -import { Spacer } from '../spacer'; import type { WordPressComponentProps } from '../context'; import { contextConnect, useContextSystem } from '../context'; -import type { InputControlPrefixWrapperProps } from './types'; +import type { PrefixSuffixWrapperProps } from './types'; +import { PrefixSuffixWrapper } from './styles/input-control-styles'; function UnconnectedInputControlPrefixWrapper( - props: WordPressComponentProps< InputControlPrefixWrapperProps, 'div' >, + props: WordPressComponentProps< PrefixSuffixWrapperProps, 'div' >, forwardedRef: ForwardedRef< any > ) { const derivedProps = useContextSystem( props, 'InputControlPrefixWrapper' ); return ( - + ); } diff --git a/packages/components/src/input-control/input-suffix-wrapper.tsx b/packages/components/src/input-control/input-suffix-wrapper.tsx index 1be352f562e369..fb3ec4c6ae9be9 100644 --- a/packages/components/src/input-control/input-suffix-wrapper.tsx +++ b/packages/components/src/input-control/input-suffix-wrapper.tsx @@ -6,20 +6,18 @@ import type { ForwardedRef } from 'react'; /** * Internal dependencies */ -import { Spacer } from '../spacer'; import type { WordPressComponentProps } from '../context'; import { contextConnect, useContextSystem } from '../context'; -import type { InputControlSuffixWrapperProps } from './types'; +import type { PrefixSuffixWrapperProps } from './types'; +import { PrefixSuffixWrapper } from './styles/input-control-styles'; function UnconnectedInputControlSuffixWrapper( - props: WordPressComponentProps< InputControlSuffixWrapperProps, 'div' >, + props: WordPressComponentProps< PrefixSuffixWrapperProps, 'div' >, forwardedRef: ForwardedRef< any > ) { const derivedProps = useContextSystem( props, 'InputControlSuffixWrapper' ); - return ( - - ); + return ; } /** diff --git a/packages/components/src/input-control/reducer/reducer.ts b/packages/components/src/input-control/reducer/reducer.ts index 8e3584d3910d74..51e54d6492f5c5 100644 --- a/packages/components/src/input-control/reducer/reducer.ts +++ b/packages/components/src/input-control/reducer/reducer.ts @@ -192,25 +192,28 @@ export function useInputControlStateReducer( const pressDown = createKeyEvent( actions.PRESS_DOWN ); const pressEnter = createKeyEvent( actions.PRESS_ENTER ); - const currentState = useRef( state ); - const refProps = useRef( { value: initialState.value, onChangeHandler } ); + const currentStateRef = useRef( state ); + const refPropsRef = useRef( { + value: initialState.value, + onChangeHandler, + } ); // Freshens refs to props and state so that subsequent effects have access // to their latest values without their changes causing effect runs. useLayoutEffect( () => { - currentState.current = state; - refProps.current = { value: initialState.value, onChangeHandler }; + currentStateRef.current = state; + refPropsRef.current = { value: initialState.value, onChangeHandler }; } ); // Propagates the latest state through onChange. useLayoutEffect( () => { if ( - currentState.current._event !== undefined && - state.value !== refProps.current.value && + currentStateRef.current._event !== undefined && + state.value !== refPropsRef.current.value && ! state.isDirty ) { - refProps.current.onChangeHandler( state.value ?? '', { - event: currentState.current._event as + refPropsRef.current.onChangeHandler( state.value ?? '', { + event: currentStateRef.current._event as | ChangeEvent< HTMLInputElement > | PointerEvent< HTMLInputElement >, } ); @@ -220,8 +223,8 @@ export function useInputControlStateReducer( // Updates the state from props. useLayoutEffect( () => { if ( - initialState.value !== currentState.current.value && - ! currentState.current.isDirty + initialState.value !== currentStateRef.current.value && + ! currentStateRef.current.isDirty ) { dispatch( { type: actions.CONTROL, diff --git a/packages/components/src/input-control/stories/index.story.tsx b/packages/components/src/input-control/stories/index.story.tsx index 6067c467f9fe68..1a9290e8e856ea 100644 --- a/packages/components/src/input-control/stories/index.story.tsx +++ b/packages/components/src/input-control/stories/index.story.tsx @@ -5,7 +5,7 @@ import type { Meta, StoryFn } from '@storybook/react'; /** * WordPress dependencies */ -import { seen, unseen } from '@wordpress/icons'; +import { closeSmall, Icon, link, seen, unseen } from '@wordpress/icons'; import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -75,6 +75,29 @@ WithSuffix.args = { suffix: %, }; +/** + * `` and `` have a `variant` prop that can be used to + * adjust the wrapper based on the prefix or suffix content. + * + * - `'default'`: Standard padding for text content. + * - `'icon'`: For icons. + * - `'control'`: For controls, like buttons or selects. + */ +export const WithIconOrControl = Template.bind( {} ); +WithIconOrControl.args = { + ...Default.args, + prefix: ( + + + + ), + suffix: ( + +
+ + - ) } - renderContent={ () => ( - - Go - Stuff - - ) } - /> - - - + + Go to dynamic path screen with id 2. + + - - -

This is the child screen.

- - Go back - -
-
-
- - - - - - Go back - -
- - ¯\_(ツ)_/¯ - -
-
-
+

This is the child screen.

+ + + Go back + + + + Go to grand child screen. + +
- - - - - Go back - - - -
-

A wild sticky element appears

-
- -
- -
-

Another wild sticky element appears

-
- -
- - - -
+ +

This is the grand child screen.

+ + Go back +
- + ), }, }; -function getStickyStyles( { - bottom = 0, - bgColor = 'whitesmoke', - top = 0, - zIndex = 1, -} ): React.CSSProperties { - return { - display: 'flex', - position: 'sticky', - top, - bottom, - zIndex, - backgroundColor: bgColor, - }; -} +function DynamicScreen() { + const { params } = useNavigator(); -function MetaphorIpsum( { quantity }: { quantity: number } ) { - const list = [ - 'A loopy clarinet’s year comes with it the thought that the fenny step-son is an ophthalmologist. The literature would have us believe that a glabrate country is not but a rhythm. A beech is a rub from the right perspective. In ancient times few can name an unglossed walrus that isn’t an unspilt trial.', - 'Authors often misinterpret the afterthought as a roseless mother-in-law, when in actuality it feels more like an uncapped thunderstorm. In recent years, some posit the tarry bottle to be less than acerb. They were lost without the unkissed timbale that composed their customer. A donna is a springtime breath.', - 'It’s an undeniable fact, really; their museum was, in this moment, a snotty beef. The swordfishes could be said to resemble prowessed lasagnas. However, the rainier authority comes from a cureless soup. Unfortunately, that is wrong; on the contrary, the cover is a powder.', - ]; - quantity = Math.min( list.length, quantity ); return ( <> - { list.slice( 0, quantity ).map( ( text, key ) => ( -

- { text } -

- ) ) } +

This is the dynamic screen

+

+ This screen can parse params dynamically. The current id is:{ ' ' } + { params.id } +

+ + Go back + ); } -function ProductDetails() { - const { params } = useNavigator(); - - return ( - - - - Go back - -

This is the screen for the product with id: { params.id }

-
-
- ); -} - -export const NestedNavigator: StoryObj< typeof NavigatorProvider > = { +export const WithNestedInitialPath: StoryObj< typeof NavigatorProvider > = { ...Default, args: { ...Default.args, - initialPath: '/child2/grandchild', - children: ( - <> - - - - - Go to first child. - - - Go to second child. - - - - - - - - This is the first child - - Go back to parent - - - - - - - - This is the second child - - Go back to parent - - - Go to grand child. - - - - - - - - This is the grand child - - Go back to parent - - - - - - ), + initialPath: '/child/grandchild', }, }; @@ -308,6 +144,10 @@ const NavigatorButtonWithSkipFocus = ( { return ( ); diff --git a/packages/customize-widgets/src/components/header/style.scss b/packages/customize-widgets/src/components/header/style.scss index 27460a82e0ad10..5c3f37a0bf0d42 100644 --- a/packages/customize-widgets/src/components/header/style.scss +++ b/packages/customize-widgets/src/components/header/style.scss @@ -30,7 +30,7 @@ align-items: center; .customize-widgets-header-toolbar__inserter-toggle.components-button.has-icon { - border-radius: 2px; + border-radius: $radius-small; color: $white; padding: 0; min-width: $grid-unit-30; diff --git a/packages/customize-widgets/src/components/inserter/index.js b/packages/customize-widgets/src/components/inserter/index.js index c0156cf774ddc6..41fc037cf673c9 100644 --- a/packages/customize-widgets/src/components/inserter/index.js +++ b/packages/customize-widgets/src/components/inserter/index.js @@ -37,6 +37,8 @@ function Inserter( { setIsOpened } ) { { __( 'Add a block' ) } - } - > - - - { - onChangeSelection( - selectableItems.map( ( item ) => - getItemId( item ) - ) - ); - } } - suffix={ numberSelectableItems } - > - { __( 'Select all' ) } - - { - onChangeSelection( [] ); - } } - > - { __( 'Deselect' ) } - - - - { actionWithModal && ( - - ) } - + const actionsToShow = useMemo( + () => + actions.filter( ( action ) => { + return ( + action.supportsBulk && + action.icon && + selectedItems.some( + ( item ) => + ! action.isEligible || action.isEligible( item ) + ) + ); + } ), + [ actions, selectedItems ] ); + if ( ! actionInProgress ) { + if ( footerContent.current ) { + footerContent.current = null; + } + return renderFooterContent( + data, + actions, + getItemId, + selection, + actionsToShow, + selectedItems, + actionInProgress, + setActionInProgress, + onChangeSelection + ); + } else if ( ! footerContent.current ) { + footerContent.current = renderFooterContent( + data, + actions, + getItemId, + selection, + actionsToShow, + selectedItems, + actionInProgress, + setActionInProgress, + onChangeSelection + ); + } + return footerContent.current; } -export default function BulkActions() { - const { data, actions = [], view } = useContext( DataViewsContext ); - const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction( - actions, - data +export function BulkActionsFooter() { + const { + data, + selection, + actions = EMPTY_ARRAY, + onChangeSelection, + getItemId, + } = useContext( DataViewsContext ); + return ( + ); - if ( - ! [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) || - ! hasPossibleBulkAction - ) { - return null; - } - - return <_BulkActions />; } diff --git a/packages/dataviews/src/components/dataviews-bulk-actions/style.scss b/packages/dataviews/src/components/dataviews-bulk-actions/style.scss index 71f76ce9a6c16f..b2e946aa202abe 100644 --- a/packages/dataviews/src/components/dataviews-bulk-actions/style.scss +++ b/packages/dataviews/src/components/dataviews-bulk-actions/style.scss @@ -1,7 +1,12 @@ -.dataviews-bulk-actions__modal { - z-index: z-index(".dataviews-bulk-actions__modal"); + +.dataviews-bulk-actions-footer__item-count { + color: $gray-900; + font-weight: 500; + font-size: 11px; + text-transform: uppercase; } -.dataviews-bulk-actions__edit-button.components-button { - flex-shrink: 0; +.dataviews-bulk-actions-footer__container { + margin-right: auto; + min-height: $grid-unit-40; } diff --git a/packages/dataviews/src/components/dataviews-filters/add-filter.tsx b/packages/dataviews/src/components/dataviews-filters/add-filter.tsx index ca1043d6e99fdb..c8b6b5fda38fcc 100644 --- a/packages/dataviews/src/components/dataviews-filters/add-filter.tsx +++ b/packages/dataviews/src/components/dataviews-filters/add-filter.tsx @@ -19,11 +19,7 @@ import { forwardRef } from '@wordpress/element'; import { unlock } from '../../lock-unlock'; import type { NormalizedFilter, View } from '../../types'; -const { - DropdownMenuV2: DropdownMenu, - DropdownMenuItemV2: DropdownMenuItem, - DropdownMenuItemLabelV2: DropdownMenuItemLabel, -} = unlock( componentsPrivateApis ); +const { DropdownMenuV2 } = unlock( componentsPrivateApis ); interface AddFilterProps { filters: NormalizedFilter[]; @@ -43,10 +39,10 @@ export function AddFilterDropdownMenu( { } ) { const inactiveFilters = filters.filter( ( filter ) => ! filter.isVisible ); return ( - + { inactiveFilters.map( ( filter ) => { return ( - { setOpenedFilter( filter.field ); @@ -64,13 +60,13 @@ export function AddFilterDropdownMenu( { } ); } } > - + { filter.name } - - + + ); } ) } - + ); } diff --git a/packages/dataviews/src/components/dataviews-filters/search-widget.tsx b/packages/dataviews/src/components/dataviews-filters/search-widget.tsx index ca24de59b1ea73..24ef3b5594b413 100644 --- a/packages/dataviews/src/components/dataviews-filters/search-widget.tsx +++ b/packages/dataviews/src/components/dataviews-filters/search-widget.tsx @@ -8,6 +8,7 @@ import removeAccents from 'remove-accents'; /** * WordPress dependencies */ +import { useInstanceId } from '@wordpress/compose'; import { __, sprintf } from '@wordpress/i18n'; import { useState, useMemo, useDeferredValue } from '@wordpress/element'; import { @@ -27,7 +28,8 @@ import type { Filter, NormalizedFilter, View } from '../../types'; const { CompositeV2: Composite, CompositeItemV2: CompositeItem, - useCompositeStoreV2: useCompositeStore, + CompositeHoverV2: CompositeHover, + CompositeTypeaheadV2: CompositeTypeahead, } = unlock( componentsPrivateApis ); interface SearchWidgetProps { @@ -84,22 +86,37 @@ const getNewValue = ( return [ value ]; }; +function generateFilterElementCompositeItemId( + prefix: string, + filterElementValue: string +) { + return `${ prefix }-${ filterElementValue }`; +} + function ListBox( { view, filter, onChangeView }: SearchWidgetProps ) { - const compositeStore = useCompositeStore( { - virtualFocus: true, - focusLoop: true, - // When we have no or just one operator, we can set the first item as active. - // We do that by passing `undefined` to `defaultActiveId`. Otherwise, we set it to `null`, - // so the first item is not selected, since the focus is on the operators control. - defaultActiveId: filter.operators?.length === 1 ? undefined : null, - } ); + const baseId = useInstanceId( ListBox, 'dataviews-filter-list-box' ); + + const [ activeCompositeId, setActiveCompositeId ] = useState< + string | null | undefined + >( + // When there are one or less operators, the first item is set as active + // (by setting the initial `activeId` to `undefined`). + // With 2 or more operators, the focus is moved on the operators control + // (by setting the initial `activeId` to `null`), meaning that there won't + // be an active item initially. Focus is then managed via the + // `onFocusVisible` callback. + filter.operators?.length === 1 ? undefined : null + ); const currentFilter = view.filters?.find( ( f ) => f.field === filter.field ); const currentValue = getCurrentValue( filter, currentFilter ); return ( { - if ( ! compositeStore.getState().activeId ) { - compositeStore.move( compositeStore.first() ); + // `onFocusVisible` needs the `Composite` component to be focusable, + // which is implicitly achieved via the `virtualFocus: true` option + // in the `useCompositeStore` hook. + if ( ! activeCompositeId && filter.elements.length ) { + setActiveCompositeId( + generateFilterElementCompositeItemId( + baseId, + filter.elements[ 0 ].value + ) + ); } } } - render={ } + render={ } > { filter.elements.map( ( element ) => ( - { element.label } - + ) ) } ); @@ -206,7 +234,6 @@ function ComboboxList( { view, filter, onChangeView }: SearchWidgetProps ) { }, [ filter.elements, deferredSearchValue ] ); return ( { const newFilters = currentFilter @@ -266,6 +293,7 @@ function ComboboxList( { view, filter, onChangeView }: SearchWidgetProps ) { { matches.map( ( element ) => { return ( + { hasBulkActions && } + + + ) + ); +} diff --git a/packages/dataviews/src/components/dataviews-footer/style.scss b/packages/dataviews/src/components/dataviews-footer/style.scss new file mode 100644 index 00000000000000..cdb1359ccee393 --- /dev/null +++ b/packages/dataviews/src/components/dataviews-footer/style.scss @@ -0,0 +1,40 @@ +.dataviews-footer { + position: sticky; + bottom: 0; + left: 0; + background-color: $white; + padding: $grid-unit-15 $grid-unit-60; + border-top: $border-width solid $gray-100; + flex-shrink: 0; + transition: padding ease-out 0.1s; + @include reduce-motion("transition"); + z-index: z-index(".dataviews-footer"); +} + + +/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ +@container (max-width: 430px) { + .dataviews-footer { + padding: $grid-unit-15 $grid-unit-30; + } +} + +/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ +@container (max-width: 560px) { + .dataviews-footer { + flex-direction: column !important; + + .dataviews-bulk-actions-footer__container { + width: 100%; + } + + .dataviews-bulk-actions-footer__item-count { + flex-grow: 1; + } + + .dataviews-pagination { + width: 100%; + justify-content: space-between; + } + } +} diff --git a/packages/dataviews/src/components/dataviews-item-actions/index.tsx b/packages/dataviews/src/components/dataviews-item-actions/index.tsx index 53b468c1ab7d3e..e33cb0fe56d0ed 100644 --- a/packages/dataviews/src/components/dataviews-item-actions/index.tsx +++ b/packages/dataviews/src/components/dataviews-item-actions/index.tsx @@ -23,13 +23,7 @@ import { useRegistry } from '@wordpress/data'; import { unlock } from '../../lock-unlock'; import type { Action, ActionModal as ActionModalType } from '../../types'; -const { - DropdownMenuV2: DropdownMenu, - DropdownMenuGroupV2: DropdownMenuGroup, - DropdownMenuItemV2: DropdownMenuItem, - DropdownMenuItemLabelV2: DropdownMenuItemLabel, - kebabCase, -} = unlock( componentsPrivateApis ); +const { DropdownMenuV2, kebabCase } = unlock( componentsPrivateApis ); export interface ActionTriggerProps< Item > { action: Action< Item >; @@ -91,12 +85,12 @@ function DropdownMenuItemTrigger< Item >( { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - - { label } - + { label } + ); } @@ -158,7 +152,7 @@ export function ActionsDropdownMenuGroup< Item >( { }: ActionsDropdownMenuGroupProps< Item > ) { const registry = useRegistry(); return ( - + { actions.map( ( action ) => { if ( 'RenderModal' in action ) { return ( @@ -181,7 +175,7 @@ export function ActionsDropdownMenuGroup< Item >( { /> ); } ) } - + ); } @@ -251,7 +245,7 @@ function CompactItemActions< Item >( { actions, }: CompactItemActionsProps< Item > ) { return ( - ( { placement="bottom-end" > - + ); } diff --git a/packages/dataviews/src/components/dataviews-pagination/index.tsx b/packages/dataviews/src/components/dataviews-pagination/index.tsx index f022b382cdb70d..08a25ea01224e0 100644 --- a/packages/dataviews/src/components/dataviews-pagination/index.tsx +++ b/packages/dataviews/src/components/dataviews-pagination/index.tsx @@ -7,7 +7,7 @@ import { SelectControl, } from '@wordpress/components'; import { createInterpolateElement, memo, useContext } from '@wordpress/element'; -import { sprintf, __, _x } from '@wordpress/i18n'; +import { sprintf, __, _x, isRTL } from '@wordpress/i18n'; import { next, previous } from '@wordpress/icons'; /** @@ -51,9 +51,9 @@ function DataViewsPagination() { totalPages !== 1 && ( = totalPages } accessibleWhenDisabled label={ __( 'Next page' ) } - icon={ next } + icon={ isRTL() ? previous : next } showTooltip size="compact" tooltipPosition="top" diff --git a/packages/dataviews/src/components/dataviews-pagination/style.scss b/packages/dataviews/src/components/dataviews-pagination/style.scss index 16f064cc3a5178..c8079c6b3ee828 100644 --- a/packages/dataviews/src/components/dataviews-pagination/style.scss +++ b/packages/dataviews/src/components/dataviews-pagination/style.scss @@ -1,15 +1,3 @@ -.dataviews-pagination { - position: sticky; - bottom: 0; - left: 0; - background-color: $white; - padding: $grid-unit-15 $grid-unit-60; - border-top: $border-width solid $gray-100; - flex-shrink: 0; - transition: padding ease-out 0.1s; - @include reduce-motion("transition"); -} - .dataviews-pagination__page-select { font-size: 11px; font-weight: 500; @@ -22,10 +10,3 @@ } } } - -/* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ -@container (max-width: 430px) { - .dataviews-pagination { - padding: $grid-unit-15 $grid-unit-30; - } -} diff --git a/packages/dataviews/src/components/dataviews-view-config/index.tsx b/packages/dataviews/src/components/dataviews-view-config/index.tsx index e396b1c68203cc..c01c72d2ebc699 100644 --- a/packages/dataviews/src/components/dataviews-view-config/index.tsx +++ b/packages/dataviews/src/components/dataviews-view-config/index.tsx @@ -21,10 +21,11 @@ import { __experimentalHeading as Heading, __experimentalText as Text, privateApis as componentsPrivateApis, + BaseControl, } from '@wordpress/components'; -import { __, _x } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { memo, useContext, useState, useMemo } from '@wordpress/element'; -import { cog, seen, unseen } from '@wordpress/icons'; +import { chevronDown, chevronUp, cog, seen, unseen } from '@wordpress/icons'; import warning from '@wordpress/warning'; /** @@ -33,20 +34,22 @@ import warning from '@wordpress/warning'; import { SORTING_DIRECTIONS, LAYOUT_GRID, + LAYOUT_TABLE, sortIcons, sortLabels, } from '../../constants'; -import { VIEW_LAYOUTS, getMandatoryFields } from '../../dataviews-layouts'; -import type { SupportedLayouts } from '../../types'; +import { + VIEW_LAYOUTS, + getNotHidableFieldIds, + getVisibleFieldIds, + getHiddenFieldIds, +} from '../../dataviews-layouts'; +import type { SupportedLayouts, View, Field } from '../../types'; import DataViewsContext from '../dataviews-context'; import { unlock } from '../../lock-unlock'; import DensityPicker from '../../dataviews-layouts/grid/density-picker'; -const { - DropdownMenuV2: DropdownMenu, - DropdownMenuRadioItemV2: DropdownMenuRadioItem, - DropdownMenuItemLabelV2: DropdownMenuItemLabel, -} = unlock( componentsPrivateApis ); +const { DropdownMenuV2 } = unlock( componentsPrivateApis ); interface ViewTypeMenuProps { defaultLayouts?: SupportedLayouts; @@ -62,7 +65,7 @@ function ViewTypeMenu( { } const activeView = VIEW_LAYOUTS.find( ( v ) => view.type === v.type ); return ( - - + { config.label } - - + + ); } ) } - + ); } @@ -143,6 +146,14 @@ function SortFieldControl() { function SortDirectionControl() { const { view, fields, onChangeView } = useContext( DataViewsContext ); + + const sortableFields = fields.filter( + ( field ) => field.enableSorting !== false + ); + if ( sortableFields.length === 0 ) { + return null; + } + let value = view.sort?.direction; if ( ! value && view.sort?.field ) { value = 'desc'; @@ -226,50 +237,238 @@ function ItemsPerPageControl() { ); } -function FieldControl() { - const { view, fields, onChangeView } = useContext( DataViewsContext ); - const mandatoryFields = getMandatoryFields( view ); - const hidableFields = fields.filter( - ( field ) => - field.enableHiding !== false && - ! mandatoryFields.includes( field.id ) - ); - const viewFields = view.fields || fields.map( ( field ) => field.id ); - if ( ! hidableFields?.length ) { - return null; - } +interface FieldItemProps { + id: any; + label: string; + index: number; + isVisible: boolean; + isHidable: boolean; +} + +function FieldItem( { + field: { id, label, index, isVisible, isHidable }, + fields, + view, + onChangeView, +}: { + field: FieldItemProps; + fields: Field< any >[]; + view: View; + onChangeView: ( view: View ) => void; +} ) { + const visibleFieldIds = getVisibleFieldIds( view, fields ); + return ( - - { hidableFields?.map( ( field ) => { - const isVisible = viewFields.includes( field.id ); - return ( - - - { field.label } + + + { label } + + { view.type === LAYOUT_TABLE && isVisible && ( + <>
); diff --git a/packages/dataviews/src/components/dataviews/stories/fixtures.js b/packages/dataviews/src/components/dataviews/stories/fixtures.js deleted file mode 100644 index 50401d97b026b5..00000000000000 --- a/packages/dataviews/src/components/dataviews/stories/fixtures.js +++ /dev/null @@ -1,250 +0,0 @@ -/** - * WordPress dependencies - */ -import { trash, image, Icon, category } from '@wordpress/icons'; -import { - Button, - __experimentalText as Text, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { LAYOUT_TABLE } from '../../../constants'; - -export const data = [ - { - id: 1, - title: 'Apollo', - description: 'Apollo description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Not a planet', - categories: [ 'Space', 'NASA' ], - satellites: 0, - date: '2021-01-01T00:00:00Z', - }, - { - id: 2, - title: 'Space', - description: 'Space description', - image: 'https://live.staticflickr.com/5678/21911065441_92e2d44708_b.jpg', - type: 'Not a planet', - categories: [ 'Space' ], - satellites: 0, - date: '2019-01-02T00:00:00Z', - }, - { - id: 3, - title: 'NASA', - description: 'NASA photo', - image: 'https://live.staticflickr.com/742/21712365770_8f70a2c91e_b.jpg', - type: 'Not a planet', - categories: [ 'NASA' ], - satellites: 0, - date: '2025-01-03T00:00:00Z', - }, - { - id: 4, - title: 'Neptune', - description: 'Neptune description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Ice giant', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 14, - date: '2020-01-01T00:00:00Z', - }, - { - id: 5, - title: 'Mercury', - description: 'Mercury description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Terrestrial', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 0, - date: '2020-01-02T01:00:00Z', - }, - { - id: 6, - title: 'Venus', - description: 'La planète Vénus', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Terrestrial', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 0, - date: '2020-01-02T00:00:00Z', - }, - { - id: 7, - title: 'Earth', - description: 'Earth description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Terrestrial', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 1, - date: '2023-01-03T00:00:00Z', - }, - { - id: 8, - title: 'Mars', - description: 'Mars description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Terrestrial', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 2, - date: '2020-01-01T00:00:00Z', - }, - { - id: 9, - title: 'Jupiter', - description: 'Jupiter description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Gas giant', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 95, - date: '2017-01-01T00:01:00Z', - }, - { - id: 10, - title: 'Saturn', - description: 'Saturn description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Gas giant', - categories: [ 'Space', 'Planet', 'Solar system' ], - satellites: 146, - date: '2020-02-01T00:02:00Z', - }, - { - id: 11, - title: 'Uranus', - description: 'Uranus description', - image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', - type: 'Ice giant', - categories: [ 'Space', 'Ice giant', 'Solar system' ], - satellites: 28, - date: '2020-03-01T00:00:00Z', - }, -]; - -export const DEFAULT_VIEW = { - type: LAYOUT_TABLE, - search: '', - page: 1, - perPage: 10, - fields: [ 'title', 'description', 'categories' ], - layout: {}, - filters: [], -}; - -export const actions = [ - { - id: 'delete', - label: 'Delete item', - isPrimary: true, - icon: trash, - hideModalHeader: true, - RenderModal: ( { item, closeModal } ) => { - return ( - - - { `Are you sure you want to delete "${ item.title }"?` } - - - - - - - ); - }, - }, - { - id: 'secondary', - label: 'Secondary action', - callback() {}, - }, -]; - -export const fields = [ - { - label: 'Image', - id: 'image', - header: ( - - - Image - - ), - render: ( { item } ) => { - return ( - - ); - }, - enableSorting: false, - }, - { - label: 'Title', - id: 'title', - enableHiding: false, - enableGlobalSearch: true, - }, - { - id: 'date', - label: 'Date', - type: 'datetime', - }, - { - label: 'Type', - id: 'type', - enableHiding: false, - elements: [ - { value: 'Not a planet', label: 'Not a planet' }, - { value: 'Ice giant', label: 'Ice giant' }, - { value: 'Terrestrial', label: 'Terrestrial' }, - { value: 'Gas giant', label: 'Gas giant' }, - ], - }, - { - label: 'Satellites', - id: 'satellites', - type: 'integer', - enableSorting: true, - }, - { - label: 'Description', - id: 'description', - enableSorting: false, - enableGlobalSearch: true, - }, - { - label: 'Categories', - id: 'categories', - header: ( - - - Categories - - ), - elements: [ - { value: 'Space', label: 'Space' }, - { value: 'NASA', label: 'NASA' }, - { value: 'Planet', label: 'Planet' }, - { value: 'Solar system', label: 'Solar system' }, - { value: 'Ice giant', label: 'Ice giant' }, - ], - filterBy: { - operators: [ 'isAny', 'isNone', 'isAll', 'isNotAll' ], - }, - getValue: ( { item } ) => { - return item.categories; - }, - render: ( { item } ) => { - return item.categories.join( ',' ); - }, - enableSorting: false, - }, -]; diff --git a/packages/dataviews/src/components/dataviews/stories/fixtures.tsx b/packages/dataviews/src/components/dataviews/stories/fixtures.tsx new file mode 100644 index 00000000000000..2ab02ec728e5e0 --- /dev/null +++ b/packages/dataviews/src/components/dataviews/stories/fixtures.tsx @@ -0,0 +1,690 @@ +/** + * WordPress dependencies + */ +import { trash, image, Icon, category } from '@wordpress/icons'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import type { Field, Action } from '../../../types'; + +export type Theme = { + slug: string; + name: string; + description: string; + requires: string; + tested: string; + tags: string[]; +}; + +export type SpaceObject = { + id: number; + title: string; + description: string; + image: string; + type: string; + categories: string[]; + satellites: number; + date: string; +}; + +export const data: SpaceObject[] = [ + { + id: 1, + title: 'Apollo', + description: 'Apollo description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Not a planet', + categories: [ 'Space', 'NASA' ], + satellites: 0, + date: '2021-01-01T00:00:00Z', + }, + { + id: 2, + title: 'Space', + description: 'Space description', + image: 'https://live.staticflickr.com/5678/21911065441_92e2d44708_b.jpg', + type: 'Not a planet', + categories: [ 'Space' ], + satellites: 0, + date: '2019-01-02T00:00:00Z', + }, + { + id: 3, + title: 'NASA', + description: 'NASA photo', + image: 'https://live.staticflickr.com/742/21712365770_8f70a2c91e_b.jpg', + type: 'Not a planet', + categories: [ 'NASA' ], + satellites: 0, + date: '2025-01-03T00:00:00Z', + }, + { + id: 4, + title: 'Neptune', + description: 'Neptune description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Ice giant', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 14, + date: '2020-01-01T00:00:00Z', + }, + { + id: 5, + title: 'Mercury', + description: 'Mercury description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 0, + date: '2020-01-02T01:00:00Z', + }, + { + id: 6, + title: 'Venus', + description: 'La planète Vénus', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 0, + date: '2020-01-02T00:00:00Z', + }, + { + id: 7, + title: 'Earth', + description: 'Earth description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 1, + date: '2023-01-03T00:00:00Z', + }, + { + id: 8, + title: 'Mars', + description: 'Mars description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Terrestrial', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 2, + date: '2020-01-01T00:00:00Z', + }, + { + id: 9, + title: 'Jupiter', + description: 'Jupiter description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Gas giant', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 95, + date: '2017-01-01T00:01:00Z', + }, + { + id: 10, + title: 'Saturn', + description: 'Saturn description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Gas giant', + categories: [ 'Space', 'Planet', 'Solar system' ], + satellites: 146, + date: '2020-02-01T00:02:00Z', + }, + { + id: 11, + title: 'Uranus', + description: 'Uranus description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + type: 'Ice giant', + categories: [ 'Space', 'Ice giant', 'Solar system' ], + satellites: 28, + date: '2020-03-01T00:00:00Z', + }, +]; + +export const themeData: Theme[] = [ + { + slug: 'twentyeleven', + name: 'Twenty Eleven', + description: + 'The 2011 theme for WordPress is sophisticated, lightweight, and adaptable. Make it yours with a custom menu, header image, and background -- then go further with available theme options for light or dark color scheme, custom link colors, and three layout choices. Twenty Eleven comes equipped with a Showcase page template that transforms your front page into a showcase to show off your best content, widget support galore (sidebar, three footer areas, and a Showcase page widget area), and a custom "Ephemera" widget to display your Aside, Link, Quote, or Status posts. Included are styles for print and for the admin editor, support for featured images (as custom header images on posts and pages and as large images on featured "sticky" posts), and special styles for six different post formats.', + requires: '3.2', + tested: '6.6', + tags: [ + 'blog', + 'one-column', + 'two-columns', + 'left-sidebar', + 'right-sidebar', + 'custom-background', + 'custom-colors', + 'custom-header', + 'custom-menu', + 'editor-style', + 'featured-image-header', + 'featured-images', + 'flexible-header', + 'footer-widgets', + 'full-width-template', + 'microformats', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'theme-options', + 'translation-ready', + 'block-patterns', + ], + }, + { + slug: 'twentyfifteen', + name: 'Twenty Fifteen', + description: + "Our 2015 default theme is clean, blog-focused, and designed for clarity. Twenty Fifteen's simple, straightforward typography is readable on a wide variety of screen sizes, and suitable for multiple languages. We designed it using a mobile-first approach, meaning your content takes center-stage, regardless of whether your visitors arrive by smartphone, tablet, laptop, or desktop computer.", + requires: '4.1', + tested: '6.6', + tags: [ + 'blog', + 'two-columns', + 'left-sidebar', + 'accessibility-ready', + 'custom-background', + 'custom-colors', + 'custom-header', + 'custom-logo', + 'custom-menu', + 'editor-style', + 'featured-images', + 'microformats', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'translation-ready', + 'block-patterns', + ], + }, + { + slug: 'twentyfourteen', + name: 'Twenty Fourteen', + description: + "In 2014, our default theme lets you create a responsive magazine website with a sleek, modern design. Feature your favorite homepage content in either a grid or a slider. Use the three widget areas to customize your website, and change your content's layout with a full-width page template and a contributor page to show off your authors. Creating a magazine website with WordPress has never been easier.", + requires: '3.6', + tested: '6.6', + tags: [ + 'blog', + 'news', + 'two-columns', + 'three-columns', + 'left-sidebar', + 'right-sidebar', + 'custom-background', + 'custom-header', + 'custom-menu', + 'editor-style', + 'featured-images', + 'flexible-header', + 'footer-widgets', + 'full-width-template', + 'microformats', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'theme-options', + 'translation-ready', + 'accessibility-ready', + 'block-patterns', + ], + }, + { + slug: 'twentynineteen', + name: 'Twenty Nineteen', + description: + "Our 2019 default theme is designed to show off the power of the block editor. It features custom styles for all the default blocks, and is built so that what you see in the editor looks like what you'll see on your website. Twenty Nineteen is designed to be adaptable to a wide range of websites, whether you’re running a photo blog, launching a new business, or supporting a non-profit. Featuring ample whitespace and modern sans-serif headlines paired with classic serif body text, it's built to be beautiful on all screen sizes.", + requires: '4.7', + tested: '6.6', + tags: [ + 'one-column', + 'accessibility-ready', + 'custom-colors', + 'custom-menu', + 'custom-logo', + 'editor-style', + 'featured-images', + 'footer-widgets', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'translation-ready', + 'block-patterns', + ], + }, + { + slug: 'twentyseventeen', + name: 'Twenty Seventeen', + description: + 'Twenty Seventeen brings your site to life with header video and immersive featured images. With a focus on business sites, it features multiple sections on the front page as well as widgets, navigation and social menus, a logo, and more. Personalize its asymmetrical grid with a custom color scheme and showcase your multimedia content with post formats. Our default theme for 2017 works great in many languages, for any abilities, and on any device.', + requires: '4.7', + tested: '6.6', + tags: [ + 'one-column', + 'two-columns', + 'right-sidebar', + 'flexible-header', + 'accessibility-ready', + 'custom-colors', + 'custom-header', + 'custom-menu', + 'custom-logo', + 'editor-style', + 'featured-images', + 'footer-widgets', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'theme-options', + 'threaded-comments', + 'translation-ready', + 'block-patterns', + ], + }, + { + slug: 'twentysixteen', + name: 'Twenty Sixteen', + description: + 'Twenty Sixteen is a modernized take on an ever-popular WordPress layout — the horizontal masthead with an optional right sidebar that works perfectly for blogs and websites. It has custom color options with beautiful default color schemes, a harmonious fluid grid using a mobile-first approach, and impeccable polish in every detail. Twenty Sixteen will make your WordPress look beautiful everywhere.', + requires: '4.4', + tested: '6.6', + tags: [ + 'one-column', + 'two-columns', + 'right-sidebar', + 'accessibility-ready', + 'custom-background', + 'custom-colors', + 'custom-header', + 'custom-menu', + 'editor-style', + 'featured-images', + 'flexible-header', + 'microformats', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'translation-ready', + 'blog', + 'block-patterns', + ], + }, + { + slug: 'twentyten', + name: 'Twenty Ten', + description: + 'The 2010 theme for WordPress is stylish, customizable, simple, and readable -- make it yours with a custom menu, header image, and background. Twenty Ten supports six widgetized areas (two in the sidebar, four in the footer) and featured images (thumbnails for gallery posts and custom header images for posts and pages). It includes stylesheets for print and the admin Visual Editor, special styles for posts in the "Asides" and "Gallery" categories, and has an optional one-column page template that removes the sidebar.', + requires: '5.6', + tested: '6.6', + tags: [ + 'blog', + 'two-columns', + 'custom-header', + 'custom-background', + 'threaded-comments', + 'sticky-post', + 'translation-ready', + 'microformats', + 'rtl-language-support', + 'editor-style', + 'custom-menu', + 'flexible-header', + 'featured-images', + 'footer-widgets', + 'featured-image-header', + 'block-patterns', + ], + }, + { + slug: 'twentythirteen', + name: 'Twenty Thirteen', + description: + 'The 2013 theme for WordPress takes us back to the blog, featuring a full range of post formats, each displayed beautifully in their own unique way. Design details abound, starting with a vibrant color scheme and matching header images, beautiful typography and icons, and a flexible layout that looks great on any device, big or small.', + requires: '3.6', + tested: '6.6', + tags: [ + 'blog', + 'one-column', + 'two-columns', + 'right-sidebar', + 'custom-header', + 'custom-menu', + 'editor-style', + 'featured-images', + 'footer-widgets', + 'microformats', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'translation-ready', + 'accessibility-ready', + 'block-patterns', + ], + }, + { + slug: 'twentytwelve', + name: 'Twenty Twelve', + description: + 'The 2012 theme for WordPress is a fully responsive theme that looks great on any device. Features include a front page template with its own widgets, an optional display font, styling for post formats on both index and single views, and an optional no-sidebar page template. Make it yours with a custom menu, header image, and background.', + requires: '3.5', + tested: '6.6', + tags: [ + 'blog', + 'one-column', + 'two-columns', + 'right-sidebar', + 'custom-background', + 'custom-header', + 'custom-menu', + 'editor-style', + 'featured-images', + 'flexible-header', + 'footer-widgets', + 'full-width-template', + 'microformats', + 'post-formats', + 'rtl-language-support', + 'sticky-post', + 'theme-options', + 'translation-ready', + 'block-patterns', + ], + }, + { + slug: 'twentytwenty', + name: 'Twenty Twenty', + description: + 'Our default theme for 2020 is designed to take full advantage of the flexibility of the block editor. Organizations and businesses have the ability to create dynamic landing pages with endless layouts using the group and column blocks. The centered content column and fine-tuned typography also makes it perfect for traditional blogs. Complete editor styles give you a good idea of what your content will look like, even before you publish. You can give your site a personal touch by changing the background colors and the accent color in the Customizer. The colors of all elements on your site are automatically calculated based on the colors you pick, ensuring a high, accessible color contrast for your visitors.', + requires: '4.7', + tested: '6.6', + tags: [ + 'blog', + 'one-column', + 'custom-background', + 'custom-colors', + 'custom-logo', + 'custom-menu', + 'editor-style', + 'featured-images', + 'footer-widgets', + 'full-width-template', + 'rtl-language-support', + 'sticky-post', + 'theme-options', + 'threaded-comments', + 'translation-ready', + 'block-patterns', + 'block-styles', + 'wide-blocks', + 'accessibility-ready', + ], + }, + { + slug: 'twentytwentyfour', + name: 'Twenty Twenty-Four', + description: + 'Twenty Twenty-Four is designed to be flexible, versatile and applicable to any website. Its collection of templates and patterns tailor to different needs, such as presenting a business, blogging and writing or showcasing work. A multitude of possibilities open up with just a few adjustments to color and typography. Twenty Twenty-Four comes with style variations and full page designs to help speed up the site building process, is fully compatible with the site editor, and takes advantage of new design tools introduced in WordPress 6.4.', + requires: '6.4', + tested: '6.6', + tags: [ + 'one-column', + 'custom-colors', + 'custom-menu', + 'custom-logo', + 'editor-style', + 'featured-images', + 'full-site-editing', + 'block-patterns', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'translation-ready', + 'wide-blocks', + 'block-styles', + 'style-variations', + 'accessibility-ready', + 'blog', + 'portfolio', + 'news', + ], + }, + { + slug: 'twentytwentyone', + name: 'Twenty Twenty-One', + description: + 'Twenty Twenty-One is a blank canvas for your ideas and it makes the block editor your best brush. With new block patterns, which allow you to create a beautiful layout in a matter of seconds, this theme’s soft colors and eye-catching — yet timeless — design will let your work shine. Take it for a spin! See how Twenty Twenty-One elevates your portfolio, business website, or personal blog.', + requires: '5.3', + tested: '6.6', + tags: [ + 'one-column', + 'accessibility-ready', + 'custom-colors', + 'custom-menu', + 'custom-logo', + 'editor-style', + 'featured-images', + 'footer-widgets', + 'block-patterns', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'translation-ready', + 'blog', + 'portfolio', + ], + }, + { + slug: 'twentytwentythree', + name: 'Twenty Twenty-Three', + description: + 'Twenty Twenty-Three is designed to take advantage of the new design tools introduced in WordPress 6.1. With a clean, blank base as a starting point, this default theme includes ten diverse style variations created by members of the WordPress community. Whether you want to build a complex or incredibly simple website, you can do it quickly and intuitively through the bundled styles or dive into creation and full customization yourself.', + requires: '6.1', + tested: '6.6', + tags: [ + 'one-column', + 'custom-colors', + 'custom-menu', + 'custom-logo', + 'editor-style', + 'featured-images', + 'full-site-editing', + 'block-patterns', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'translation-ready', + 'wide-blocks', + 'block-styles', + 'style-variations', + 'accessibility-ready', + 'blog', + 'portfolio', + 'news', + ], + }, + { + slug: 'twentytwentytwo', + name: 'Twenty Twenty-Two', + description: + 'Built on a solidly designed foundation, Twenty Twenty-Two embraces the idea that everyone deserves a truly unique website. The theme’s subtle styles are inspired by the diversity and versatility of birds: its typography is lightweight yet strong, its color palette is drawn from nature, and its layout elements sit gently on the page. The true richness of Twenty Twenty-Two lies in its opportunity for customization. The theme is built to take advantage of the Site Editor features introduced in WordPress 5.9, which means that colors, typography, and the layout of every single page on your site can be customized to suit your vision. It also includes dozens of block patterns, opening the door to a wide range of professionally designed layouts in just a few clicks. Whether you’re building a single-page website, a blog, a business website, or a portfolio, Twenty Twenty-Two will help you create a site that is uniquely yours.', + requires: '5.9', + tested: '6.6', + tags: [ + 'one-column', + 'custom-colors', + 'custom-menu', + 'custom-logo', + 'editor-style', + 'featured-images', + 'full-site-editing', + 'block-patterns', + 'rtl-language-support', + 'sticky-post', + 'threaded-comments', + 'style-variations', + 'wide-blocks', + 'block-styles', + 'accessibility-ready', + 'blog', + 'portfolio', + 'news', + ], + }, +]; + +export const themeFields: Field< Theme >[] = [ + { id: 'slug', label: 'Slug' }, + { id: 'name', label: 'Name' }, + { id: 'description', label: 'Description' }, + { id: 'requires', label: 'Requires at least' }, + { id: 'tested', label: 'Tested up to' }, + { + id: 'tags', + label: 'Tags', + render: ( { item } ) => item.tags.join( ', ' ), + }, +]; + +export const DEFAULT_VIEW = { + type: 'table' as const, + search: '', + page: 1, + perPage: 10, + layout: {}, + filters: [], +}; + +export const actions: Action< SpaceObject >[] = [ + { + id: 'delete', + label: 'Delete item', + isPrimary: true, + icon: trash, + hideModalHeader: true, + RenderModal: ( { items, closeModal } ) => { + return ( + + + { `Are you sure you want to delete "${ items[ 0 ].title }"?` } + + + + + + + ); + }, + }, + { + id: 'secondary', + label: 'Secondary action', + callback() {}, + }, +]; + +export const fields: Field< SpaceObject >[] = [ + { + label: 'Image', + id: 'image', + header: ( + + + Image + + ), + render: ( { item } ) => { + return ( + + ); + }, + enableSorting: false, + }, + { + label: 'Title', + id: 'title', + enableHiding: false, + enableGlobalSearch: true, + render: ( { item } ) => { + return { item.title }; + }, + }, + { + id: 'date', + label: 'Date', + type: 'datetime', + }, + { + label: 'Type', + id: 'type', + enableHiding: false, + elements: [ + { value: 'Not a planet', label: 'Not a planet' }, + { value: 'Ice giant', label: 'Ice giant' }, + { value: 'Terrestrial', label: 'Terrestrial' }, + { value: 'Gas giant', label: 'Gas giant' }, + ], + }, + { + label: 'Satellites', + id: 'satellites', + type: 'integer', + enableSorting: true, + }, + { + label: 'Description', + id: 'description', + enableSorting: false, + enableGlobalSearch: true, + }, + { + label: 'Categories', + id: 'categories', + header: ( + + + Categories + + ), + elements: [ + { value: 'Space', label: 'Space' }, + { value: 'NASA', label: 'NASA' }, + { value: 'Planet', label: 'Planet' }, + { value: 'Solar system', label: 'Solar system' }, + { value: 'Ice giant', label: 'Ice giant' }, + ], + filterBy: { + operators: [ 'isAny', 'isNone', 'isAll', 'isNotAll' ], + }, + getValue: ( { item } ) => { + return item.categories; + }, + render: ( { item } ) => { + return item.categories.join( ',' ); + }, + enableSorting: false, + }, +]; diff --git a/packages/dataviews/src/components/dataviews/stories/index.story.js b/packages/dataviews/src/components/dataviews/stories/index.story.js deleted file mode 100644 index 376b14a464666c..00000000000000 --- a/packages/dataviews/src/components/dataviews/stories/index.story.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useMemo } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import DataViews from '../index'; -import { DEFAULT_VIEW, actions, data, fields } from './fixtures'; -import { LAYOUT_GRID, LAYOUT_LIST, LAYOUT_TABLE } from '../../../constants'; -import { filterSortAndPaginate } from '../../../filter-and-sort-data-view'; - -const meta = { - title: 'DataViews/DataViews', - component: DataViews, -}; -export default meta; - -export const Default = ( props ) => { - const [ view, setView ] = useState( DEFAULT_VIEW ); - const { data: shownData, paginationInfo } = useMemo( () => { - return filterSortAndPaginate( data, view, fields ); - }, [ view ] ); - return ( - - ); -}; -Default.args = { - actions, - defaultLayouts: { - [ LAYOUT_TABLE ]: { - layout: { - primaryField: 'title', - styles: { - image: { - width: 50, - }, - title: { - maxWidth: 400, - }, - type: { - maxWidth: 400, - }, - description: { - maxWidth: 200, - }, - }, - }, - }, - [ LAYOUT_GRID ]: { - layout: { - mediaField: 'image', - primaryField: 'title', - }, - }, - [ LAYOUT_LIST ]: { - layout: { - mediaField: 'image', - primaryField: 'title', - }, - }, - }, -}; diff --git a/packages/dataviews/src/components/dataviews/stories/index.story.tsx b/packages/dataviews/src/components/dataviews/stories/index.story.tsx new file mode 100644 index 00000000000000..645c6d7ddcd922 --- /dev/null +++ b/packages/dataviews/src/components/dataviews/stories/index.story.tsx @@ -0,0 +1,164 @@ +/** + * WordPress dependencies + */ +import { useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DataViews from '../index'; +import { + DEFAULT_VIEW, + actions, + data, + fields, + themeData, + themeFields, +} from './fixtures'; +import { LAYOUT_GRID, LAYOUT_LIST, LAYOUT_TABLE } from '../../../constants'; +import { filterSortAndPaginate } from '../../../filter-and-sort-data-view'; +import type { View } from '../../../types'; + +const meta = { + title: 'DataViews/DataViews', + component: DataViews, +}; +export default meta; + +const defaultLayouts = { + [ LAYOUT_TABLE ]: { + layout: { + primaryField: 'title', + styles: { + image: { + width: 50, + }, + title: { + maxWidth: 400, + }, + type: { + maxWidth: 400, + }, + description: { + maxWidth: 200, + }, + }, + }, + }, + [ LAYOUT_GRID ]: { + layout: { + mediaField: 'image', + primaryField: 'title', + }, + }, + [ LAYOUT_LIST ]: { + layout: { + mediaField: 'image', + primaryField: 'title', + }, + }, +}; + +export const Default = () => { + const [ view, setView ] = useState< View >( { + ...DEFAULT_VIEW, + fields: [ 'title', 'description', 'categories' ], + } ); + const { data: shownData, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( data, view, fields ); + }, [ view ] ); + return ( + item.id.toString() } + paginationInfo={ paginationInfo } + data={ shownData } + view={ view } + fields={ fields } + onChangeView={ setView } + actions={ actions } + defaultLayouts={ defaultLayouts } + /> + ); +}; + +export const Empty = () => { + const [ view, setView ] = useState< View >( { + ...DEFAULT_VIEW, + fields: [ 'title', 'description', 'categories' ], + } ); + + return ( + item.id.toString() } + paginationInfo={ { totalItems: 0, totalPages: 0 } } + data={ [] } + view={ view } + fields={ fields } + onChangeView={ setView } + actions={ actions } + defaultLayouts={ defaultLayouts } + /> + ); +}; + +export const FieldsNoSortableNoHidable = () => { + const [ view, setView ] = useState< View >( { + ...DEFAULT_VIEW, + fields: [ 'title', 'description', 'categories' ], + } ); + const { data: shownData, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( data, view, fields ); + }, [ view ] ); + + const _fields = fields.map( ( field ) => ( { + ...field, + enableSorting: false, + enableHiding: false, + } ) ); + + return ( + item.id.toString() } + paginationInfo={ paginationInfo } + data={ shownData } + view={ view } + fields={ _fields } + onChangeView={ setView } + defaultLayouts={ { + table: {}, + } } + /> + ); +}; + +export const CombinedFields = () => { + const [ view, setView ] = useState< View >( { + ...DEFAULT_VIEW, + fields: [ 'theme', 'requires', 'tested' ], + layout: { + combinedFields: [ + { + id: 'theme', + label: 'Theme', + children: [ 'name', 'description' ], + direction: 'vertical', + }, + ], + }, + } ); + const { data: shownData, paginationInfo } = useMemo( () => { + return filterSortAndPaginate( themeData, view, themeFields ); + }, [ view ] ); + + return ( + item.name } + paginationInfo={ paginationInfo } + data={ shownData } + view={ view } + fields={ themeFields } + onChangeView={ setView } + defaultLayouts={ { table: {} } } + /> + ); +}; diff --git a/packages/dataviews/src/components/dataviews/style.scss b/packages/dataviews/src/components/dataviews/style.scss index 1ce75d36020a78..8909c7cf1c7cfd 100644 --- a/packages/dataviews/src/components/dataviews/style.scss +++ b/packages/dataviews/src/components/dataviews/style.scss @@ -7,6 +7,8 @@ container: dataviews-wrapper / inline-size; display: flex; flex-direction: column; + font-size: $default-font-size; + line-height: $default-line-height; } .dataviews__view-actions, diff --git a/packages/dataviews/src/dataforms-layouts/panel/index.tsx b/packages/dataviews/src/dataforms-layouts/panel/index.tsx index 151aefed12c24b..9f118584998bd3 100644 --- a/packages/dataviews/src/dataforms-layouts/panel/index.tsx +++ b/packages/dataviews/src/dataforms-layouts/panel/index.tsx @@ -44,6 +44,8 @@ function DropdownHeader( { { onClose && ( ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js index 8728e5d61b22a0..735179588d0723 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-card.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { _n, sprintf } from '@wordpress/i18n'; +import { _n, sprintf, isRTL } from '@wordpress/i18n'; import { __experimentalUseNavigator as useNavigator, __experimentalText as Text, @@ -15,7 +15,7 @@ import { * Internal dependencies */ import FontDemo from './font-demo'; -import { chevronRight } from '@wordpress/icons'; +import { chevronLeft, chevronRight } from '@wordpress/icons'; function FontCard( { font, onClick, variantsText, navigatorPath } ) { const variantsCount = font.fontFace?.length || 1; @@ -28,6 +28,8 @@ function FontCard( { font, onClick, variantsText, navigatorPath } ) { return (
- { setSelectedFont( null ); @@ -425,26 +427,40 @@ function FontCollection( { slug } ) { __nextHasNoMarginBottom /> - - { getSortedFontFaces( selectedFont ).map( - ( face, i ) => ( - - ) - ) } + { /* + * Disable reason: The `list` ARIA role is redundant but + * Safari+VoiceOver won't announce the list otherwise. + */ + /* eslint-disable jsx-a11y/no-redundant-roles */ } +
    + { getSortedFontFaces( selectedFont ).map( + ( face, i ) => ( +
  • + +
  • + ) + ) } +
+ { /* eslint-enable jsx-a11y/no-redundant-roles */ }
@@ -456,6 +472,8 @@ function FontCollection( { slug } ) { className="font-library-modal__footer" > - @@ -377,7 +380,12 @@ function ShadowItem( { shadow, onChange, canRemove, onRemove } ) { return ( - ); diff --git a/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js index 2374b35ad2d699..085ca38a3e182a 100644 --- a/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js +++ b/packages/edit-widgets/src/components/secondary-sidebar/inserter-sidebar.js @@ -44,6 +44,8 @@ export default function InserterSidebar() { > ); diff --git a/packages/editor/src/components/header/style.scss b/packages/editor/src/components/header/style.scss index de690d89a66ae4..99c9cc70e166e4 100644 --- a/packages/editor/src/components/header/style.scss +++ b/packages/editor/src/components/header/style.scss @@ -228,7 +228,7 @@ } .editor-header__post-preview-button { - @include break-small { + @include break-mobile { display: none; } } @@ -240,10 +240,13 @@ .editor-header { background-color: $white; - border-bottom: 1px solid #e0e0e0; - position: absolute; width: 100%; + @include break-medium { + border-bottom: 1px solid #e0e0e0; + position: absolute; + } + // hide some parts & > .edit-post-header__settings > .edit-post-header__post-preview-button { diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 91dcc883d661b2..b42566aac653be 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -32,6 +32,7 @@ export { default as PluginMoreMenuItem } from './plugin-more-menu-item'; export { default as PluginPostPublishPanel } from './plugin-post-publish-panel'; export { default as PluginPostStatusInfo } from './plugin-post-status-info'; export { default as PluginPrePublishPanel } from './plugin-pre-publish-panel'; +export { default as PluginPreviewMenuItem } from './plugin-preview-menu-item'; export { default as PluginSidebar } from './plugin-sidebar'; export { default as PluginSidebarMoreMenuItem } from './plugin-sidebar-more-menu-item'; export { default as PostTemplatePanel } from './post-template/panel'; diff --git a/packages/editor/src/components/inserter-sidebar/index.js b/packages/editor/src/components/inserter-sidebar/index.js index bf613b5c8c001a..fef5e54e52f68f 100644 --- a/packages/editor/src/components/inserter-sidebar/index.js +++ b/packages/editor/src/components/inserter-sidebar/index.js @@ -38,13 +38,14 @@ export default function InserterSidebar() { getBlockInsertionPoint, getBlockRootClientId, __unstableGetEditorMode, - getSettings, - } = select( blockEditorStore ); + getSectionRootClientId, + } = unlock( select( blockEditorStore ) ); const { get } = select( preferencesStore ); const { getActiveComplementaryArea } = select( interfaceStore ); const getBlockSectionRootClientId = () => { if ( __unstableGetEditorMode() === 'zoom-out' ) { - const { sectionRootClientId } = unlock( getSettings() ); + const sectionRootClientId = getSectionRootClientId(); + if ( sectionRootClientId ) { return sectionRootClientId; } diff --git a/packages/editor/src/components/local-autosave-monitor/index.js b/packages/editor/src/components/local-autosave-monitor/index.js index f999cf9ef85340..ad4e40d15d5c58 100644 --- a/packages/editor/src/components/local-autosave-monitor/index.js +++ b/packages/editor/src/components/local-autosave-monitor/index.js @@ -144,20 +144,20 @@ function useAutosavePurge() { [] ); - const lastIsDirty = useRef( isDirty ); - const lastIsAutosaving = useRef( isAutosaving ); + const lastIsDirtyRef = useRef( isDirty ); + const lastIsAutosavingRef = useRef( isAutosaving ); useEffect( () => { if ( ! didError && - ( ( lastIsAutosaving.current && ! isAutosaving ) || - ( lastIsDirty.current && ! isDirty ) ) + ( ( lastIsAutosavingRef.current && ! isAutosaving ) || + ( lastIsDirtyRef.current && ! isDirty ) ) ) { localAutosaveClear( postId, isEditedPostNew ); } - lastIsDirty.current = isDirty; - lastIsAutosaving.current = isAutosaving; + lastIsDirtyRef.current = isDirty; + lastIsAutosavingRef.current = isAutosaving; }, [ isDirty, isAutosaving, didError ] ); // Once the isEditedPostNew changes from true to false, let's clear the auto-draft autosave. diff --git a/packages/editor/src/components/page-attributes/parent.js b/packages/editor/src/components/page-attributes/parent.js index 1018834d1b9fca..0f19ffcdd5daa4 100644 --- a/packages/editor/src/components/page-attributes/parent.js +++ b/packages/editor/src/components/page-attributes/parent.js @@ -222,10 +222,11 @@ function PostParentToggle( { isOpen, onClick } ) { } export function ParentRow() { - const homeUrl = useSelect( - ( select ) => select( coreStore ).getUnstableBase()?.home, - [] - ); + const homeUrl = useSelect( ( select ) => { + // Site index. + return select( coreStore ).getEntityRecord( 'root', '__unstableBase' ) + ?.home; + }, [] ); // 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 ); diff --git a/packages/editor/src/components/plugin-preview-menu-item/index.js b/packages/editor/src/components/plugin-preview-menu-item/index.js new file mode 100644 index 00000000000000..422248e17b88e1 --- /dev/null +++ b/packages/editor/src/components/plugin-preview-menu-item/index.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { MenuItem } from '@wordpress/components'; +import { withPluginContext } from '@wordpress/plugins'; +import { ActionItem } from '@wordpress/interface'; + +/** + * Renders a menu item in the Preview dropdown, which can be used as a button or link depending on the props provided. + * The text within the component appears as the menu item label. + * + * @param {Object} props Component properties. + * @param {string} [props.href] When `href` is provided, the menu item is rendered as an anchor instead of a button. It corresponds to the `href` attribute of the anchor. + * @param {WPBlockTypeIconRender} [props.icon=inherits from the plugin] The icon to be rendered to the left of the menu item label. Can be a Dashicon slug or an SVG WP element. + * @param {Function} [props.onClick] The callback function to be executed when the user clicks the menu item. + * @param {...*} [props.other] Any additional props are passed through to the underlying MenuItem component. + * + * @example + * ```jsx + * import { __ } from '@wordpress/i18n'; + * import { PluginPreviewMenuItem } from '@wordpress/editor'; + * import { external } from '@wordpress/icons'; + * + * function onPreviewClick() { + * // Handle preview action + * } + * + * const ExternalPreviewMenuItem = () => ( + * + * { __( 'Preview in new tab' ) } + * + * ); + * registerPlugin( 'external-preview-menu-item', { + * render: ExternalPreviewMenuItem, + * } ); + * ``` + * + * @return {Component} The rendered menu item component. + */ +export default compose( + withPluginContext( ( context, ownProps ) => { + return { + as: ownProps.as ?? MenuItem, + icon: ownProps.icon || context.icon, + name: 'core/plugin-preview-menu', + }; + } ) +)( ActionItem ); diff --git a/packages/editor/src/components/post-actions/index.js b/packages/editor/src/components/post-actions/index.js index 9cc594233c8814..8a3850e8f547c1 100644 --- a/packages/editor/src/components/post-actions/index.js +++ b/packages/editor/src/components/post-actions/index.js @@ -18,13 +18,7 @@ import { store as coreStore } from '@wordpress/core-data'; import { unlock } from '../../lock-unlock'; import { usePostActions } from './actions'; -const { - DropdownMenuV2: DropdownMenu, - DropdownMenuGroupV2: DropdownMenuGroup, - DropdownMenuItemV2: DropdownMenuItem, - DropdownMenuItemLabelV2: DropdownMenuItemLabel, - kebabCase, -} = unlock( componentsPrivateApis ); +const { DropdownMenuV2, kebabCase } = unlock( componentsPrivateApis ); export default function PostActions( { postType, postId, onActionPerformed } ) { const [ isActionsMenuOpen, setIsActionsMenuOpen ] = useState( false ); @@ -60,7 +54,7 @@ export default function PostActions( { postType, postId, onActionPerformed } ) { }, [ allActions, itemWithPermissions ] ); return ( - - + ); } @@ -99,12 +93,12 @@ function DropdownMenuItemTrigger( { action, onClick, items } ) { const label = typeof action.label === 'string' ? action.label : action.label( items ); return ( - - { label } - + { label } + ); } @@ -151,7 +145,7 @@ function ActionWithModal( { action, item, ActionTrigger, onClose } ) { // With an added onClose prop. function ActionsDropdownMenuGroup( { actions, item, onClose } ) { return ( - + { actions.map( ( action ) => { if ( action.RenderModal ) { return ( @@ -173,6 +167,6 @@ function ActionsDropdownMenuGroup( { actions, item, onClose } ) { /> ); } ) } - + ); } diff --git a/packages/editor/src/components/post-card-panel/style.scss b/packages/editor/src/components/post-card-panel/style.scss index 3547b0ab104936..73b638673f3e91 100644 --- a/packages/editor/src/components/post-card-panel/style.scss +++ b/packages/editor/src/components/post-card-panel/style.scss @@ -46,7 +46,7 @@ background: $gray-100; color: $gray-700; padding: 0 $grid-unit-05; - border-radius: $radius-block-ui; + border-radius: $radius-small; font-size: 12px; font-weight: 400; flex-shrink: 0; diff --git a/packages/editor/src/components/post-excerpt/panel.js b/packages/editor/src/components/post-excerpt/panel.js index 0c800c1a360b9b..9a37e9134775aa 100644 --- a/packages/editor/src/components/post-excerpt/panel.js +++ b/packages/editor/src/components/post-excerpt/panel.js @@ -198,6 +198,8 @@ function PrivateExcerpt() { ref={ setPopoverAnchor } renderToggle={ ( { onToggle } ) => ( ) } - diff --git a/packages/editor/src/components/post-locked-modal/style.scss b/packages/editor/src/components/post-locked-modal/style.scss index 03e86642493df3..7f680022344664 100644 --- a/packages/editor/src/components/post-locked-modal/style.scss +++ b/packages/editor/src/components/post-locked-modal/style.scss @@ -3,7 +3,7 @@ } .editor-post-locked-modal__avatar { - border-radius: $radius-block-ui; + border-radius: $radius-round; margin-top: $grid-unit-20; min-width: initial !important; } diff --git a/packages/editor/src/components/post-publish-panel/index.js b/packages/editor/src/components/post-publish-panel/index.js index 28567c60780219..3e411e91db4724 100644 --- a/packages/editor/src/components/post-publish-panel/index.js +++ b/packages/editor/src/components/post-publish-panel/index.js @@ -78,6 +78,8 @@ export class PostPublishPanel extends Component {
{ isPostPublish ? ( ) }
+ { hadUploadError &&

{ __( 'Upload failed, try again.' ) }

} ); } diff --git a/packages/editor/src/components/post-publish-panel/style.scss b/packages/editor/src/components/post-publish-panel/style.scss index c319924086b0e4..9892cf5430f9a2 100644 --- a/packages/editor/src/components/post-publish-panel/style.scss +++ b/packages/editor/src/components/post-publish-panel/style.scss @@ -37,8 +37,9 @@ .components-site-icon { border: none; - border-radius: $radius-block-ui; + border-radius: $radius-small; margin-right: $grid-unit-15; + flex-shrink: 0; // Same size as in the Site menu. height: 36px; @@ -54,6 +55,7 @@ display: block; color: $gray-700; font-size: $helptext-font-size; + word-break: break-word; } .editor-post-publish-panel__header-publish-button, diff --git a/packages/editor/src/components/post-status/index.js b/packages/editor/src/components/post-status/index.js index ca89e40366b238..1d3050e7e3dd6b 100644 --- a/packages/editor/src/components/post-status/index.js +++ b/packages/editor/src/components/post-status/index.js @@ -171,7 +171,7 @@ export default function PostStatus() { contentClassName="editor-change-status__content" popoverProps={ popoverProps } focusOnMount - renderToggle={ ( { onToggle } ) => ( + renderToggle={ ( { onToggle, isOpen } ) => ( diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js index 112bfaabd0c8ec..4913cef9b4c86f 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -428,6 +428,8 @@ export function HierarchicalTermSelector( { slug } ) { { ! loading && hasCreateAction && ( diff --git a/packages/editor/src/components/table-of-contents/index.js b/packages/editor/src/components/table-of-contents/index.js index 74a535240833d6..f2c79fb0347446 100644 --- a/packages/editor/src/components/table-of-contents/index.js +++ b/packages/editor/src/components/table-of-contents/index.js @@ -30,6 +30,8 @@ function TableOfContents( contentClassName="table-of-contents__popover" renderToggle={ ( { isOpen, onToggle } ) => (

- diff --git a/packages/rich-text/src/component/event-listeners/copy-handler.js b/packages/rich-text/src/component/event-listeners/copy-handler.js index 0cc1594c3ab914..1a92237bb4c5b4 100644 --- a/packages/rich-text/src/component/event-listeners/copy-handler.js +++ b/packages/rich-text/src/component/event-listeners/copy-handler.js @@ -2,18 +2,21 @@ * Internal dependencies */ import { toHTMLString } from '../../to-html-string'; -import { isCollapsed } from '../../is-collapsed'; import { slice } from '../../slice'; +import { remove } from '../../remove'; import { getTextContent } from '../../get-text-content'; export default ( props ) => ( element ) => { function onCopy( event ) { - const { record } = props.current; + const { record, createRecord, handleChange } = props.current; const { ownerDocument } = element; - if ( - isCollapsed( record.current ) || - ! element.contains( ownerDocument.activeElement ) - ) { + const { defaultView } = ownerDocument; + const { anchorNode, focusNode, isCollapsed } = + defaultView.getSelection(); + const containsSelection = + element.contains( anchorNode ) && element.contains( focusNode ); + + if ( isCollapsed || ! containsSelection ) { return; } @@ -26,7 +29,7 @@ export default ( props ) => ( element ) => { event.preventDefault(); if ( event.type === 'cut' ) { - ownerDocument.execCommand( 'delete' ); + handleChange( remove( createRecord() ) ); } } diff --git a/packages/rich-text/src/component/event-listeners/input-and-selection.js b/packages/rich-text/src/component/event-listeners/input-and-selection.js index 621f1c59fab04e..11dcdb0d8ff9ab 100644 --- a/packages/rich-text/src/component/event-listeners/input-and-selection.js +++ b/packages/rich-text/src/component/event-listeners/input-and-selection.js @@ -114,14 +114,13 @@ export default ( props ) => ( element ) => { return; } - // Ensure the active element is the rich text element. - if ( ownerDocument.activeElement !== element ) { - // If it is not, we can stop listening for selection changes. We - // resume listening when the element is focused. - ownerDocument.removeEventListener( - 'selectionchange', - handleSelectionChange - ); + const { anchorNode, focusNode } = defaultView.getSelection(); + const containsSelection = + element.contains( anchorNode ) && + element.contains( focusNode ) && + ownerDocument.activeElement.contains( element ); + + if ( ! containsSelection ) { return; } @@ -255,5 +254,9 @@ export default ( props ) => ( element ) => { element.removeEventListener( 'compositionstart', onCompositionStart ); element.removeEventListener( 'compositionend', onCompositionEnd ); element.removeEventListener( 'focus', onFocus ); + ownerDocument.removeEventListener( + 'selectionchange', + handleSelectionChange + ); }; }; diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 2606ae9f902fab..600fc0faff5209 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -60,46 +60,48 @@ export function useRichText( { } // Internal values are updated synchronously, unlike props and state. - const _value = useRef( value ); - const record = useRef(); + const _valueRef = useRef( value ); + const recordRef = useRef(); function setRecordFromProps() { - _value.current = value; - record.current = value; + _valueRef.current = value; + recordRef.current = value; if ( ! ( value instanceof RichTextData ) ) { - record.current = value + recordRef.current = value ? RichTextData.fromHTMLString( value, { preserveWhiteSpace } ) : RichTextData.empty(); } // To do: make rich text internally work with RichTextData. - record.current = { - text: record.current.text, - formats: record.current.formats, - replacements: record.current.replacements, + recordRef.current = { + text: recordRef.current.text, + formats: recordRef.current.formats, + replacements: recordRef.current.replacements, }; if ( disableFormats ) { - record.current.formats = Array( value.length ); - record.current.replacements = Array( value.length ); + recordRef.current.formats = Array( value.length ); + recordRef.current.replacements = Array( value.length ); } if ( __unstableAfterParse ) { - record.current.formats = __unstableAfterParse( record.current ); + recordRef.current.formats = __unstableAfterParse( + recordRef.current + ); } - record.current.start = selectionStart; - record.current.end = selectionEnd; + recordRef.current.start = selectionStart; + recordRef.current.end = selectionEnd; } - const hadSelectionUpdate = useRef( false ); + const hadSelectionUpdateRef = useRef( false ); - if ( ! record.current ) { - hadSelectionUpdate.current = isSelected; + if ( ! recordRef.current ) { + hadSelectionUpdateRef.current = isSelected; setRecordFromProps(); } else if ( - selectionStart !== record.current.start || - selectionEnd !== record.current.end + selectionStart !== recordRef.current.start || + selectionEnd !== recordRef.current.end ) { - hadSelectionUpdate.current = isSelected; - record.current = { - ...record.current, + hadSelectionUpdateRef.current = isSelected; + recordRef.current = { + ...recordRef.current, start: selectionStart, end: selectionEnd, activeFormats: undefined, @@ -113,34 +115,34 @@ export function useRichText( { * @param {Object} newRecord The record to sync and apply. */ function handleChange( newRecord ) { - record.current = newRecord; + recordRef.current = newRecord; applyRecord( newRecord ); if ( disableFormats ) { - _value.current = newRecord.text; + _valueRef.current = newRecord.text; } else { const newFormats = __unstableBeforeSerialize ? __unstableBeforeSerialize( newRecord ) : newRecord.formats; newRecord = { ...newRecord, formats: newFormats }; if ( typeof value === 'string' ) { - _value.current = toHTMLString( { + _valueRef.current = toHTMLString( { value: newRecord, preserveWhiteSpace, } ); } else { - _value.current = new RichTextData( newRecord ); + _valueRef.current = new RichTextData( newRecord ); } } - const { start, end, formats, text } = record.current; + const { start, end, formats, text } = recordRef.current; // Selection must be updated first, so it is recorded in history when // the content change happens. // We batch both calls to only attempt to rerender once. registry.batch( () => { onSelectionChange( start, end ); - onChange( _value.current, { + onChange( _valueRef.current, { __unstableFormats: formats, __unstableText: text, } ); @@ -150,14 +152,14 @@ export function useRichText( { function applyFromProps() { setRecordFromProps(); - applyRecord( record.current ); + applyRecord( recordRef.current ); } - const didMount = useRef( false ); + const didMountRef = useRef( false ); // Value updates must happen synchonously to avoid overwriting newer values. useLayoutEffect( () => { - if ( didMount.current && value !== _value.current ) { + if ( didMountRef.current && value !== _valueRef.current ) { applyFromProps(); forceRender(); } @@ -165,7 +167,7 @@ export function useRichText( { // Value updates must happen synchonously to avoid overwriting newer values. useLayoutEffect( () => { - if ( ! hadSelectionUpdate.current ) { + if ( ! hadSelectionUpdateRef.current ) { return; } @@ -173,16 +175,16 @@ export function useRichText( { ref.current.focus(); } - applyRecord( record.current ); - hadSelectionUpdate.current = false; - }, [ hadSelectionUpdate.current ] ); + applyRecord( recordRef.current ); + hadSelectionUpdateRef.current = false; + }, [ hadSelectionUpdateRef.current ] ); const mergedRefs = useMergeRefs( [ ref, useDefaultStyle(), - useBoundaryStyle( { record } ), + useBoundaryStyle( { record: recordRef } ), useEventListeners( { - record, + record: recordRef, handleChange, applyRecord, createRecord, @@ -192,18 +194,18 @@ export function useRichText( { } ), useRefEffect( () => { applyFromProps(); - didMount.current = true; + didMountRef.current = true; }, [ placeholder, ...__unstableDependencies ] ), ] ); return { - value: record.current, + value: recordRef.current, // A function to get the most recent value so event handlers in // useRichText implementations have access to it. For example when // listening to input events, we internally update the state, but this // state is not yet available to the input event handler because React // may re-render asynchronously. - getValue: () => record.current, + getValue: () => recordRef.current, onChange: handleChange, ref: mergedRefs, }; diff --git a/packages/rich-text/src/to-tree.js b/packages/rich-text/src/to-tree.js index 131174460a74bc..46671c951bc09d 100644 --- a/packages/rich-text/src/to-tree.js +++ b/packages/rich-text/src/to-tree.js @@ -299,6 +299,8 @@ export function toTree( { if ( shouldInsertPadding && i === text.length ) { append( getParent( pointer ), ZWNBSP ); + // We CANNOT use CSS to add a placeholder with pseudo elements on + // the main block wrappers because that could clash with theme CSS. if ( placeholder && text.length === 0 ) { append( getParent( pointer ), { type: 'span', diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 178b1c4fae36da..9d666313f76abc 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### Breaking Changes + +- Fixed the issue with having 5 high severity vulnerabilities by upgrading the `puppeteer-core` package to the latest major version `^23.1.0` ([#64597](https://github.com/WordPress/gutenberg/pull/64597)). + +### Enhancements + +- Inlines CSS files imported from other CSS files before optimization in the `build` command ([#61121](https://github.com/WordPress/gutenberg/pull/61121)). + +### Bug Fixes + +- Added chunk filename in webpack config to avoid reading stale files ([#58176](https://github.com/WordPress/gutenberg/pull/58176)). + ## 28.6.0 (2024-08-21) ## 28.5.0 (2024-08-07) diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 508d4d262942d1..4c60e3859207de 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -119,6 +119,7 @@ const cssLoaders = [ plugins: isProduction ? [ ...postcssPlugins, + require( 'postcss-import' ), require( 'cssnano' )( { // Provide a fallback configuration if there's not // one explicitly available in the project. @@ -147,6 +148,7 @@ const baseConfig = { target, output: { filename: '[name].js', + chunkFilename: '[name].js?v=[chunkhash]', path: resolve( process.cwd(), 'build' ), }, resolve: { diff --git a/packages/scripts/package.json b/packages/scripts/package.json index dcc17a2926a654..291b8a58bca408 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -73,9 +73,10 @@ "npm-package-json-lint": "^6.4.0", "npm-packlist": "^3.0.0", "postcss": "^8.4.5", + "postcss-import": "^16.1.0", "postcss-loader": "^6.2.1", "prettier": "npm:wp-prettier@3.0.3", - "puppeteer-core": "^13.2.0", + "puppeteer-core": "^23.1.0", "react-refresh": "^0.14.0", "read-pkg-up": "^7.0.1", "resolve-bin": "^0.4.0", diff --git a/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js b/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js index cc7c0830a319bf..d543b7b4b08d80 100644 --- a/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js +++ b/packages/widgets/src/blocks/legacy-widget/edit/widget-type-selector.js @@ -27,8 +27,7 @@ export default function WidgetTypeSelector( { selectedId, onSelect } ) { return ( array( 'size' => null, ), - 'settings' => null, + 'settings' => array( + 'typography' => array( + 'fluid' => true, + ), + ), 'expected_output' => null, ), @@ -425,8 +429,7 @@ public function data_generate_font_size_preset_fixtures() { 'returns already clamped value' => array( 'font_size' => array( - 'size' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', - 'fluid' => false, + 'size' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', ), 'settings' => array( 'typography' => array( @@ -438,8 +441,7 @@ public function data_generate_font_size_preset_fixtures() { 'returns value with unsupported unit' => array( 'font_size' => array( - 'size' => '1000%', - 'fluid' => false, + 'size' => '1000%', ), 'settings' => array( 'typography' => array( @@ -769,6 +771,33 @@ public function data_generate_font_size_preset_fixtures() { ), 'expected_output' => 'clamp(100px, 6.25rem + ((1vw - 3.2px) * 7.813), 200px)', ), + + // Individual preset settings override global settings. + 'should convert individual preset size to fluid if fluid is disabled in global settings' => array( + 'font_size' => array( + 'size' => '17px', + 'fluid' => true, + ), + 'settings' => array( + 'typography' => array(), + ), + 'expected_output' => 'clamp(14px, 0.875rem + ((1vw - 3.2px) * 0.234), 17px)', + ), + 'should use individual preset settings if fluid is disabled in global settings' => array( + 'font_size' => array( + 'size' => '17px', + 'fluid' => array( + 'min' => '16px', + 'max' => '26px', + ), + ), + 'settings' => array( + 'typography' => array( + 'fluid' => false, + ), + ), + 'expected_output' => 'clamp(16px, 1rem + ((1vw - 3.2px) * 0.781), 26px)', + ), ); } diff --git a/phpunit/blocks/render-block-media-text-test.php b/phpunit/blocks/render-block-media-text-test.php index 94c184408c89fb..f5b67f0a8dc9b7 100644 --- a/phpunit/blocks/render-block-media-text-test.php +++ b/phpunit/blocks/render-block-media-text-test.php @@ -81,15 +81,14 @@ public function test_render_block_core_media_text_featured_image() { $rendered = gutenberg_render_block_core_media_text( $attributes, $content ); $this->assertStringContainsString( ' true, 'imageFill' => true, ); $rendered = gutenberg_render_block_core_media_text( $attributes, $content ); - $this->assertStringContainsString( 'background-image:url(' . wp_get_attachment_image_url( self::$attachment_id, 'full' ) . ')', $rendered ); - $this->assertStringNotContainsString( 'assertStringContainsString( 'assertStringContainsString( ' true, 'imageFill' => true, ); $rendered = gutenberg_render_block_core_media_text( $attributes, $content ); - $this->assertStringContainsString( 'background-image:url(' . wp_get_attachment_image_url( self::$attachment_id, 'full' ) . ')', $rendered ); - $this->assertStringNotContainsString( 'assertStringContainsString( 'assertStringContainsString( ' true, 'mediaPosition' => 'right', 'imageFill' => true, ); $rendered = gutenberg_render_block_core_media_text( $attributes, $content ); - $this->assertStringContainsString( 'background-image:url(' . wp_get_attachment_image_url( self::$attachment_id, 'full' ) . ')', $rendered ); - $this->assertStringNotContainsString( 'assertStringContainsString( 'assertStringContainsString( ' true, 'mediaPosition' => 'right', @@ -173,7 +170,6 @@ public function test_render_block_core_media_text_featured_image_media_on_right_ ); $rendered = gutenberg_render_block_core_media_text( $attributes, $content ); - $this->assertStringContainsString( 'background-image:url(' . wp_get_attachment_image_url( self::$attachment_id, 'full' ) . ')', $rendered ); - $this->assertStringNotContainsString( 'assertStringContainsString( ' 1, 'gutenberg-form-blocks' => 1, 'gutenberg-block-experiments' => 1, + 'gutenberg-media-processing' => 1, ), ); diff --git a/phpunit/class-gutenberg-rest-server-test.php b/phpunit/class-gutenberg-rest-server-test.php new file mode 100644 index 00000000000000..943d5b34b999ec --- /dev/null +++ b/phpunit/class-gutenberg-rest-server-test.php @@ -0,0 +1,88 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + + self::$post_id = $factory->post->create(); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + wp_delete_post( self::$post_id ); + } + + public function set_up() { + parent::set_up(); + add_filter( + 'wp_rest_server_class', + function () { + return 'Gutenberg_REST_Server'; + } + ); + } + + public function test_populates_target_hints_for_administrator() { + wp_set_current_user( self::$admin_id ); + $response = rest_do_request( '/wp/v2/posts' ); + $post = $response->get_data()[0]; + + $link = $post['_links']['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), $link['targetHints']['allow'] ); + } + + public function test_populates_target_hints_for_logged_out_user() { + $response = rest_do_request( '/wp/v2/posts' ); + $post = $response->get_data()[0]; + + $link = $post['_links']['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET' ), $link['targetHints']['allow'] ); + } + + public function test_does_not_error_on_invalid_urls() { + $response = new WP_REST_Response(); + $response->add_link( 'self', 'this is not a real URL' ); + + $links = rest_get_server()::get_response_links( $response ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + public function test_does_not_error_on_bad_rest_api_routes() { + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( '/this/is/not/a/real/route' ) ); + + $links = rest_get_server()::get_response_links( $response ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + public function test_prefers_developer_defined_target_hints() { + $response = new WP_REST_Response(); + $response->add_link( + 'self', + '/wp/v2/posts/' . self::$post_id, + array( + 'targetHints' => array( + 'allow' => array( 'GET', 'PUT' ), + ), + ) + ); + + $links = rest_get_server()::get_response_links( $response ); + $link = $links['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); + } +} diff --git a/phpunit/experimental/block-editor-settings-mobile-test.php b/phpunit/experimental/block-editor-settings-mobile-test.php index 907b750d8c8638..967efebc346fc8 100644 --- a/phpunit/experimental/block-editor-settings-mobile-test.php +++ b/phpunit/experimental/block-editor-settings-mobile-test.php @@ -10,11 +10,6 @@ * * @covers WP_REST_Block_Editor_Settings_Controller */ - -if ( ! defined( 'REST_REQUEST' ) ) { - define( 'REST_REQUEST', true ); -} - class Gutenberg_REST_Block_Editor_Settings_Controller_Test extends WP_Test_REST_Controller_Testcase { /** * @var int @@ -56,10 +51,9 @@ public function test_register_routes() { public function test_get_items() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp-block-editor/v1/settings' ); - // Set context for mobile settings. - $_GET['context'] = 'mobile'; - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); + $request->set_param( 'context', 'mobile' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); $this->assertArrayHasKey( '__experimentalStyles', $data, '__experimentalStyles should be in the returned data' ); $this->assertArrayHasKey( '__experimentalFeatures', $data, '__experimentalFeatures should be in the returned data' ); diff --git a/phpunit/experimental/media/class-gutenberg-rest-attachments-controller-test.php b/phpunit/experimental/media/class-gutenberg-rest-attachments-controller-test.php new file mode 100644 index 00000000000000..56286f76ace836 --- /dev/null +++ b/phpunit/experimental/media/class-gutenberg-rest-attachments-controller-test.php @@ -0,0 +1,314 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public function set_up() { + parent::set_up(); + + $this->remove_added_uploads(); + } + + public function tear_down() { + $this->remove_added_uploads(); + + parent::tear_down(); + } + + /** + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/media', $routes ); + $this->assertCount( 2, $routes['/wp/v2/media'] ); + $this->assertArrayHasKey( '/wp/v2/media/(?P[\d]+)', $routes ); + $this->assertCount( 3, $routes['/wp/v2/media/(?P[\d]+)'] ); + $this->assertArrayHasKey( '/wp/v2/media/(?P[\d]+)/sideload', $routes ); + $this->assertCount( 1, $routes['/wp/v2/media/(?P[\d]+)/sideload'] ); + } + + public function test_get_items() { + $this->markTestSkipped( 'No need to implement' ); + } + + public function test_get_item() { + $this->markTestSkipped( 'No need to implement' ); + } + + public function test_update_item() { + $this->markTestSkipped( 'No need to implement' ); + } + + public function test_delete_item() { + $this->markTestSkipped( 'No need to implement' ); + } + + public function test_get_item_schema() { + $this->markTestSkipped( 'No need to implement' ); + } + + public function test_context_param() { + $this->markTestSkipped( 'No need to implement' ); + } + + /** + * Verifies that skipping sub-size generation works. + * + * @covers ::create_item + * @covers ::create_item_permissions_check + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_param( 'title', 'My title is very cool' ); + $request->set_param( 'caption', 'This is a better caption.' ); + $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); + $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' ); + $request->set_param( 'generate_sub_sizes', false ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( 'image', $data['media_type'] ); + $this->assertArrayHasKey( 'missing_image_sizes', $data ); + $this->assertNotEmpty( $data['missing_image_sizes'] ); + } + + /** + * Verifies that skipping sub-size generation works. + * + * @covers ::create_item + * @covers ::create_item_permissions_check + */ + public function test_create_item_insert_additional_metadata() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_param( 'title', 'My title is very cool' ); + $request->set_param( 'caption', 'This is a better caption.' ); + $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); + $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' ); + $request->set_param( 'generate_sub_sizes', false ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + + remove_filter( 'wp_generate_attachment_metadata', '__return_empty_array', 1 ); + + $this->assertSame( 201, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertArrayHasKey( 'media_details', $data ); + $this->assertArrayHasKey( 'image_meta', $data['media_details'] ); + } + + public function test_prepare_item() { + $this->markTestSkipped( 'No need to implement' ); + } + + /** + * @covers ::prepare_item_for_response + */ + public function test_prepare_item_lists_missing_image_sizes_for_pdfs() { + wp_set_current_user( self::$admin_id ); + + $attachment_id = self::factory()->attachment->create_object( + DIR_TESTDATA . '/images/test-alpha.pdf', + 0, + array( + 'post_mime_type' => 'application/pdf', + 'post_excerpt' => 'A sample caption', + ) + ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/media/%d', $attachment_id ) ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'missing_image_sizes', $data ); + $this->assertNotEmpty( $data['missing_image_sizes'] ); + $this->assertArrayHasKey( 'filename', $data ); + $this->assertArrayHasKey( 'filesize', $data ); + } + + /** + * @covers ::sideload_item + * @covers ::sideload_item_permissions_check + */ + public function test_sideload_item() { + wp_set_current_user( self::$admin_id ); + + $attachment_id = self::factory()->attachment->create_object( + DIR_TESTDATA . '/images/canola.jpg', + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + wp_update_attachment_metadata( + $attachment_id, + wp_generate_attachment_metadata( $attachment_id, DIR_TESTDATA . '/images/canola.jpg' ) + ); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/$attachment_id/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-777x777.jpg' ); + $request->set_param( 'image_size', 'medium' ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'image', $data['media_type'] ); + $this->assertArrayHasKey( 'missing_image_sizes', $data ); + $this->assertEmpty( $data['missing_image_sizes'] ); + $this->assertArrayHasKey( 'media_details', $data ); + $this->assertArrayHasKey( 'sizes', $data['media_details'] ); + $this->assertArrayHasKey( 'medium', $data['media_details']['sizes'] ); + $this->assertArrayHasKey( 'file', $data['media_details']['sizes']['medium'] ); + $this->assertSame( 'canola-777x777.jpg', $data['media_details']['sizes']['medium']['file'] ); + } + + /** + * @covers ::sideload_item + * @covers ::sideload_item_permissions_check + */ + public function test_sideload_item_year_month_based_folders() { + if ( version_compare( get_bloginfo( 'version' ), '6.6-beta1', '<' ) ) { + $this->markTestSkipped( 'This test requires WordPress 6.6+' ); + } + + update_option( 'uploads_use_yearmonth_folders', 1 ); + + wp_set_current_user( self::$admin_id ); + + $published_post = self::factory()->post->create( + array( + 'post_status' => 'publish', + 'post_date' => '2017-02-14 00:00:00', + 'post_date_gmt' => '2017-02-14 00:00:00', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-year-month.jpg' ); + $request->set_param( 'post', $published_post ); + $request->set_param( 'generate_sub_sizes', false ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $attachment_id = $data['id']; + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/$attachment_id/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-year-month-777x777.jpg' ); + $request->set_param( 'image_size', 'medium' ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + update_option( 'uploads_use_yearmonth_folders', 0 ); + + $this->assertSame( 200, $response->get_status() ); + + $attachment = get_post( $data['id'] ); + + $this->assertSame( $attachment->post_parent, $data['post'] ); + $this->assertSame( $attachment->post_parent, $published_post ); + $this->assertSame( wp_get_attachment_url( $attachment->ID ), $data['source_url'] ); + $this->assertStringContainsString( '2017/02', $data['source_url'] ); + } + + /** + * @covers ::sideload_item + * @covers ::sideload_item_permissions_check + */ + public function test_sideload_item_year_month_based_folders_page_post_type() { + if ( version_compare( get_bloginfo( 'version' ), '6.6-beta1', '<' ) ) { + $this->markTestSkipped( 'This test requires WordPress 6.6+' ); + } + + update_option( 'uploads_use_yearmonth_folders', 1 ); + + wp_set_current_user( self::$admin_id ); + + $published_post = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_date' => '2017-02-14 00:00:00', + 'post_date_gmt' => '2017-02-14 00:00:00', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-year-month-page.jpg' ); + $request->set_param( 'post', $published_post ); + $request->set_param( 'generate_sub_sizes', false ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $attachment_id = $data['id']; + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/$attachment_id/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola-year-month-page-777x777.jpg' ); + $request->set_param( 'image_size', 'medium' ); + + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + update_option( 'uploads_use_yearmonth_folders', 0 ); + + $time = current_time( 'mysql' ); + $y = substr( $time, 0, 4 ); + $m = substr( $time, 5, 2 ); + $subdir = "/$y/$m"; + + $this->assertSame( 200, $response->get_status() ); + + $attachment = get_post( $data['id'] ); + + $this->assertSame( $attachment->post_parent, $data['post'] ); + $this->assertSame( $attachment->post_parent, $published_post ); + $this->assertSame( wp_get_attachment_url( $attachment->ID ), $data['source_url'] ); + $this->assertStringNotContainsString( '2017/02', $data['source_url'] ); + $this->assertStringContainsString( $subdir, $data['source_url'] ); + } +} diff --git a/phpunit/experimental/media/media-processing-test.php b/phpunit/experimental/media/media-processing-test.php new file mode 100644 index 00000000000000..2717d2582879ff --- /dev/null +++ b/phpunit/experimental/media/media-processing-test.php @@ -0,0 +1,197 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public function set_up() { + parent::set_up(); + + self::$image_file = get_temp_dir() . 'canola.jpg'; + if ( ! file_exists( self::$image_file ) ) { + copy( DIR_TESTDATA . '/images/canola.jpg', self::$image_file ); + } + } + + public function tear_down() { + $this->remove_added_uploads(); + + parent::tear_down(); + } + + /** + * @covers gutenberg_get_all_image_sizes + */ + public function test_get_all_image_sizes() { + $sizes = gutenberg_get_all_image_sizes(); + $this->assertNotEmpty( $sizes ); + foreach ( $sizes as $size ) { + $this->assertIsInt( $size['width'] ); + $this->assertIsInt( $size['height'] ); + $this->assertIsString( $size['name'] ); + } + } + + /** + * @covers gutenberg_filter_attachment_post_type_args + */ + public function test_filter_attachment_post_type_args() { + $post_type_object = get_post_type_object( 'attachment' ); + $this->assertInstanceOf( Gutenberg_REST_Attachments_Controller::class, $post_type_object->get_rest_controller() ); + + $this->assertSame( + array( 'rest_controller_class' => Gutenberg_REST_Attachments_Controller::class ), + gutenberg_filter_attachment_post_type_args( array(), 'attachment' ) + ); + $this->assertSame( + array(), + gutenberg_filter_attachment_post_type_args( array(), 'post' ) + ); + } + + /** + * @covers ::gutenberg_rest_get_attachment_filesize + */ + public function test_rest_get_attachment_filesize() { + $attachment_id = self::factory()->attachment->create_object( + self::$image_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $this->assertSame( wp_filesize( self::$image_file ), gutenberg_rest_get_attachment_filesize( array( 'id' => $attachment_id ) ) ); + } + + /** + * @covers ::gutenberg_rest_get_attachment_filename + */ + public function test_rest_get_attachment_filename() { + $attachment_id = self::factory()->attachment->create_object( + self::$image_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $this->assertSame( 'canola.jpg', gutenberg_rest_get_attachment_filename( array( 'id' => $attachment_id ) ) ); + } + + /** + * @covers ::gutenberg_media_processing_filter_rest_index + */ + public function test_get_rest_index_should_return_additional_settings() { + $server = new WP_REST_Server(); + + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + + $this->assertArrayNotHasKey( 'image_size_threshold', $data ); + $this->assertArrayNotHasKey( 'image_output_formats', $data ); + $this->assertArrayNotHasKey( 'jpeg_interlaced', $data ); + $this->assertArrayNotHasKey( 'png_interlaced', $data ); + $this->assertArrayNotHasKey( 'gif_interlaced', $data ); + $this->assertArrayNotHasKey( 'image_sizes', $data ); + } + + /** + * @covers ::gutenberg_media_processing_filter_rest_index + */ + public function test_get_rest_index_should_return_additional_settings_can_upload_files() { + wp_set_current_user( self::$admin_id ); + + $server = new WP_REST_Server(); + + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + + $this->assertArrayHasKey( 'image_size_threshold', $data ); + $this->assertArrayHasKey( 'image_output_formats', $data ); + $this->assertArrayHasKey( 'jpeg_interlaced', $data ); + $this->assertArrayHasKey( 'png_interlaced', $data ); + $this->assertArrayHasKey( 'gif_interlaced', $data ); + $this->assertArrayHasKey( 'image_sizes', $data ); + } + + /** + * @covers ::gutenberg_add_crossorigin_attributes + */ + public function test_add_crossorigin_attributes() { + $html = << + + + + + + + + + + + +HTML; + + $expected = << + + + + + + + + + + + +HTML; + + $actual = gutenberg_add_crossorigin_attributes( $html ); + + $this->assertSame( $expected, $actual ); + } + + /** + * @covers ::gutenberg_override_media_templates + */ + public function test_gutenberg_override_media_templates(): void { + if ( ! function_exists( '\wp_print_media_templates' ) ) { + require_once ABSPATH . WPINC . '/media-template.php'; + } + + gutenberg_override_media_templates(); + + ob_start(); + do_action( 'admin_footer' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound + $output = ob_get_clean(); + + $this->assertStringContainsString( '