diff --git a/.eslintrc.js b/.eslintrc.js index a9ce5392e2f2e..4bb1966dde375 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -104,7 +104,7 @@ module.exports = { { name: '@emotion/css', message: - 'Please use `@emotion/react` and `@emotion/styled` in order to maintain iframe support', + 'Please use `@emotion/react` and `@emotion/styled` in order to maintain iframe support. As a replacement for the `cx` function, please use the `useCx` hook defined in `@wordpress/components` instead.', }, ], }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 934b853ba6c61..b22eec7c45d40 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -40,9 +40,11 @@ # Widgets /packages/edit-widgets @draganescu @talldan @noisysocks @tellthemachines @adamziel @kevin940726 +/packages/customize-widgets @noisysocks +/packages/widgets @noisysocks # Navigation -/packages/edit-navigation @draganescu @talldan @noisysocks @tellthemachines @adamziel @kevin940726 @getdave +/packages/edit-navigation @draganescu @talldan @tellthemachines @adamziel @kevin940726 @getdave # Full Site Editing /packages/edit-site @@ -125,6 +127,7 @@ /lib/theme.json @timothybjabocs @spacedmonkey @nosolosw /lib/class-wp-theme-json-gutenberg.php @timothybjabocs @spacedmonkey @nosolosw /lib/class-wp-theme-json-resolver-gutenberg.php @timothybjabocs @spacedmonkey @nosolosw +/lib/full-site-editing @janw-me /phpunit/class-wp-theme-json-test.php @nosolosw # Web App diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml index a104c70d1072f..50944218a52b8 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -1,6 +1,5 @@ name: Bug report description: Report a bug with the WordPress block editor or Gutenberg plugin -title: '' body: - type: markdown attributes: @@ -49,10 +48,25 @@ body: validations: required: false - - type: checkboxes + - type: dropdown + id: existing attributes: - label: Pre-checks - description: Please check if the bug has already been reported by searching https://github.com/WordPress/gutenberg/issues and make sure the bug is not related to another plugin. + label: Please confirm that you have searched existing issues in the repo. + description: You can do this by searching https://github.com/WordPress/gutenberg/issues and making sure the bug is not related to another plugin. + multiple: true options: - - label: I have searched the existing issues. - - label: I have tested with all plugins deactivated except Gutenberg. + - 'Yes' + - 'No' + validations: + required: true + + - type: dropdown + id: plugins + attributes: + label: Please confirm that you have tested with all plugins deactivated except Gutenberg. + multiple: true + options: + - 'Yes' + - 'No' + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/Bug_report_mobile.md b/.github/ISSUE_TEMPLATE/Bug_report_mobile.md index 06b6fad1bb6bc..8347a6649ea8d 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report_mobile.md +++ b/.github/ISSUE_TEMPLATE/Bug_report_mobile.md @@ -44,7 +44,7 @@ the bug. --> ## WordPress information -- WordPress version: <!-- e.g. "5.6.0". Find this in Tools → Site Health → Info → WordPress --> +- WordPress version: <!-- e.g. "5.8.0". Find this in Tools → Site Health → Info → WordPress --> - Gutenberg version: <!-- e.g. "9.4.0" or "Not installed" --> - Are all plugins except Gutenberg deactivated? <!-- "Yes" or "No" --> - Are you using a default theme (e.g. Twenty Twenty-One)? <!-- "Yes" or "No" --> diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 18eda2ad1ef5a..8bb83dfd7b6ec 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -15,9 +15,7 @@ concurrency: jobs: test: runs-on: macos-latest - # The false value below disables the test while we investigate a - # foundational error causing failures - if: ${{ false && (github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request') }} + if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: matrix: native-test-name: [gutenberg-editor-initial-html] @@ -41,9 +39,10 @@ jobs: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} - - uses: reactivecircus/android-emulator-runner@d2799957d660add41c61a5103e2fbb9e2889eb73 # v2.15.0 + - uses: reactivecircus/android-emulator-runner@5de26e4bd23bf523e8a4b7f077df8bfb8e52b50e # v2.19.1 with: api-level: 28 + emulator-build: 7425822 # https://git.io/JE3jX profile: pixel_xl script: npm run native test:e2e:android:local ${{ matrix.native-test-name }} diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index d7561fe3b296f..59cfed85c1b8f 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -79,8 +79,14 @@ jobs: - name: Commit the Changelog update run: | git add changelog.txt - git commit -m "Update Changelog for ${TAG#v}" - git push --set-upstream origin "${{ matrix.branch }}" + # Remove files that are not meant to be commited + # ie. release_notes.txt created on the previous step. + git clean -fd + # Only attempt to commit changelog if it has been modified. + if ! git diff-index --quiet HEAD --; then + git commit -m "Update Changelog for ${TAG#v}" + git push --set-upstream origin "${{ matrix.branch }}" + fi - name: Upload Changelog artifact uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 diff --git a/.vscode/launch-example.json b/.vscode/launch-example.json index 3e333f450a001..67b7a8859eb20 100644 --- a/.vscode/launch-example.json +++ b/.vscode/launch-example.json @@ -9,6 +9,23 @@ "pathMappings": { "/var/www/html/wp-content/plugins/gutenberg": "${workspaceRoot}/" } + }, + { + "type": "node", + "request": "launch", + "name": "Debug current e2e test", + "program": "${workspaceRoot}/node_modules/@wordpress/scripts/bin/wp-scripts.js", + "args": [ + "test-e2e", + "--config=${workspaceRoot}/packages/e2e-tests/jest.config.js", + "--verbose=true", + "--runInBand", + "--watch", + "${file}" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "trace": "all" } ] -} \ No newline at end of file +} diff --git a/bin/packages/build.js b/bin/packages/build.js index eff2fcd732020..250492506ae7d 100755 --- a/bin/packages/build.js +++ b/bin/packages/build.js @@ -263,7 +263,8 @@ stream console.error( error ); } - if ( ended && ++complete === files.length ) { + ++complete; + if ( ended && complete === files.length ) { workerFarm.end( worker ); } } ) diff --git a/bin/plugin/commands/changelog.js b/bin/plugin/commands/changelog.js index 73d8afdae4edf..dca2ce62406e7 100644 --- a/bin/plugin/commands/changelog.js +++ b/bin/plugin/commands/changelog.js @@ -780,7 +780,9 @@ async function createChangelog( settings ) { try { changelog = await getChangelog( settings ); } catch ( error ) { - changelog = formats.error( error.stack ); + if ( error instanceof Error ) { + changelog = formats.error( error.stack ); + } } log( changelog ); diff --git a/changelog.txt b/changelog.txt index a25a0c2a499fd..ffc9af781783f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,442 @@ == Changelog == += 11.5.0-rc.1 = + +### Features + +- Add Dark Mode-specific help section images. ([34361](https://github.com/WordPress/gutenberg/pull/34361)) + +#### Block Library +- Group block: Add a row variation. ([34535](https://github.com/WordPress/gutenberg/pull/34535)) + +#### Design Tools +- Block Support: Add gap block support feature. ([33991](https://github.com/WordPress/gutenberg/pull/33991)) + + +### Enhancements + +- Add isRawAttribute to entity configuration. ([34388](https://github.com/WordPress/gutenberg/pull/34388)) +- Consolidate the PATHS_WITH_MERGE constant to one instance. ([34407](https://github.com/WordPress/gutenberg/pull/34407)) +- Fix title missing in bug report form. ([34504](https://github.com/WordPress/gutenberg/pull/34504)) +- General Interface: Make permalinks documentation URL translatable. ([34282](https://github.com/WordPress/gutenberg/pull/34282)) +- Update bug form to use drop downs. ([34458](https://github.com/WordPress/gutenberg/pull/34458)) + +#### Block Library +- Update Site Logo block description to be concise. ([34471](https://github.com/WordPress/gutenberg/pull/34471)) +- Update the Table block description to be concise. ([34475](https://github.com/WordPress/gutenberg/pull/34475)) +- Use blockGap between Columns blocks. ([34456](https://github.com/WordPress/gutenberg/pull/34456)) +- Video Block: Use existing video poster image on insert. ([34415](https://github.com/WordPress/gutenberg/pull/34415)) +- [Query Pagination Next/Previous]: Add an arrow attribute and sync next/previous block's arrow. ([33656](https://github.com/WordPress/gutenberg/pull/33656)) + +#### Design Tools +- Add wide alignment control only if theme provides `layout.wideSize`. ([34586](https://github.com/WordPress/gutenberg/pull/34586)) +- Gap block support: Force gap change to cause the block to re-render (fix Safari issue). ([34567](https://github.com/WordPress/gutenberg/pull/34567)) +- Post Author Block: Add duotone suport. ([34408](https://github.com/WordPress/gutenberg/pull/34408)) +- ToolsPanel: Change icon from horizontal to vertical ellipsis. ([34369](https://github.com/WordPress/gutenberg/pull/34369)) + +#### Navigation Screen +- Add undo redo buttons in navigation editor. ([34533](https://github.com/WordPress/gutenberg/pull/34533)) +- Disable "block-nav-menus" feature for the purposes of removing the "experimental" status on the Navigation Editor. ([34444](https://github.com/WordPress/gutenberg/pull/34444)) +- Preload menu REST API requests on new navigation editor. ([34364](https://github.com/WordPress/gutenberg/pull/34364)) +- Update navigation editor placeholder. ([34568](https://github.com/WordPress/gutenberg/pull/34568)) + +#### Widgets Editor +- Add 'Widget Group' block to widgets screens. ([34484](https://github.com/WordPress/gutenberg/pull/34484)) +- Legacy widget rendering endpoint. ([34230](https://github.com/WordPress/gutenberg/pull/34230)) + +#### Global Styles +- Allow disabling `text` and `background` color via `theme.json`. ([34420](https://github.com/WordPress/gutenberg/pull/34420)) +- Make global styles available to all themes. ([34334](https://github.com/WordPress/gutenberg/pull/34334)) + +#### Block Editor +- Media Placeholder: Change media URL input type to allow a local URL path. ([29138](https://github.com/WordPress/gutenberg/pull/29138)) + +#### Block Variations +- Remove horizontal and vertical navigation block variations from inserter. ([34614](https://github.com/WordPress/gutenberg/pull/34614)) + +#### Post Editor +- Try: Title block gap. ([34570](https://github.com/WordPress/gutenberg/pull/34570)) + +#### Themes +- Add default editor styles applied to themes without theme.json and without editor styles. ([34439](https://github.com/WordPress/gutenberg/pull/34439)) + +#### Components +- MenuItem: Add right padding for unchecked radio and checkbox items. ([34406](https://github.com/WordPress/gutenberg/pull/34406)) + +#### Full Site Editing +- Limit FSE admin notices to the Themes screen. ([34353](https://github.com/WordPress/gutenberg/pull/34353)) + +#### List View +- Block Navigation List: Do not show appender and avoid closing the modal on block select. ([34337](https://github.com/WordPress/gutenberg/pull/34337)) + +#### CSS & Styling +- Block Styles: Fix long strings of text without spaces overflow the block. ([34222](https://github.com/WordPress/gutenberg/pull/34222)) + +#### Testing +- Debug e2e-tests in vscode. ([29788](https://github.com/WordPress/gutenberg/pull/29788)) + + +### New APIs + +#### Design Tools +- Allow themes with theme.json to opt-out of block gap styles. ([34491](https://github.com/WordPress/gutenberg/pull/34491)) + + +### Bug Fixes + +- Block Toolbar & Popover component - Prevent sticky position from causing permanently obscured areas of the selected block. ([33981](https://github.com/WordPress/gutenberg/pull/33981)) +- Core Data: Adds 'include' to the query key. ([34583](https://github.com/WordPress/gutenberg/pull/34583)) +- ESLint: Add useSelect to direct function calls list. ([34301](https://github.com/WordPress/gutenberg/pull/34301)) +- Fix menu item padding regression. ([34435](https://github.com/WordPress/gutenberg/pull/34435)) +- Fix text-menu min widths. ([34532](https://github.com/WordPress/gutenberg/pull/34532)) +- Scripts: Only use svgr/webpack in js files. ([34394](https://github.com/WordPress/gutenberg/pull/34394)) +- Use resolveSelect instead of select in saveEntityRecord. ([34584](https://github.com/WordPress/gutenberg/pull/34584)) + +#### Block Library +- Fix Column bottom sheet Android close button. ([34332](https://github.com/WordPress/gutenberg/pull/34332)) +- Fix Page List styles inside responsive Navigation. ([34517](https://github.com/WordPress/gutenberg/pull/34517)) +- Fix navigation block classname issues. ([34344](https://github.com/WordPress/gutenberg/pull/34344)) +- Fix responsive menu height regression. ([34488](https://github.com/WordPress/gutenberg/pull/34488)) +- Fix submenu layout in navigation page list. ([34342](https://github.com/WordPress/gutenberg/pull/34342)) +- Fix undo/redo 'trap' in navigation link block. ([34565](https://github.com/WordPress/gutenberg/pull/34565)) +- Fix various React warnings in development log. ([34428](https://github.com/WordPress/gutenberg/pull/34428)) +- Gallery block: Fix bug with stalled upload when image size too large. ([34371](https://github.com/WordPress/gutenberg/pull/34371)) +- Gallery block: Fix media placeholder height in site editor. ([34629](https://github.com/WordPress/gutenberg/pull/34629)) +- Gallery block: Fix problem with overflowing captions on new gallery block format. ([34402](https://github.com/WordPress/gutenberg/pull/34402)) +- Site title: Allow empty title in edit mode. ([34274](https://github.com/WordPress/gutenberg/pull/34274)) +- Try: Fix so submenus only take up space when visible. ([34382](https://github.com/WordPress/gutenberg/pull/34382)) +- Video Block: Fix TypeError when removing poster. ([34411](https://github.com/WordPress/gutenberg/pull/34411)) + +#### Components +- Align labels on focal point picker position controls above the inputs. ([34209](https://github.com/WordPress/gutenberg/pull/34209)) +- Check if in browser env before calling `CSS.supports`. ([34572](https://github.com/WordPress/gutenberg/pull/34572)) +- CustomSelectControl: Add describedBy fallback. ([34385](https://github.com/WordPress/gutenberg/pull/34385)) +- DateTime Component: Fix sizing of help info. ([34370](https://github.com/WordPress/gutenberg/pull/34370)) +- Fix `ToggleGroupControlBackdrop` not updating size when `isAdaptiveWidth` prop changes. ([34595](https://github.com/WordPress/gutenberg/pull/34595)) +- Fix selected value computation in `CustomSelectControl` when no initial `value` is set. ([34490](https://github.com/WordPress/gutenberg/pull/34490)) +- Fix subheadings from wrapping. ([34319](https://github.com/WordPress/gutenberg/pull/34319)) + +#### Design Tools +- Border Controls: Display color indicator and check selected color. ([34467](https://github.com/WordPress/gutenberg/pull/34467)) +- Border Support: Fix check for displaying border support panel. ([34516](https://github.com/WordPress/gutenberg/pull/34516)) +- Letter Spacing: Group letter spacing correctly under typography supports. ([34515](https://github.com/WordPress/gutenberg/pull/34515)) + +#### Widgets Editor +- Fix Block Settings sidebar unexpectedly collapsing. ([34543](https://github.com/WordPress/gutenberg/pull/34543)) +- Legacy widget's preview functionality is broken when the page is moved. ([34384](https://github.com/WordPress/gutenberg/pull/34384)) + +#### Global Styles +- Fix block-level global styles color panels. ([34293](https://github.com/WordPress/gutenberg/pull/34293)) +- Font Appearance Control: Fix error in global styles for Site Title in TT1-Blocks. ([34520](https://github.com/WordPress/gutenberg/pull/34520)) + +#### Testing +- Jest Preset: Restore the default setting for the `verbose` option. ([34327](https://github.com/WordPress/gutenberg/pull/34327)) +- Make Test_Widget compatible with WP_Widget. ([34355](https://github.com/WordPress/gutenberg/pull/34355)) + +#### Meta Boxes +- Change default value of enableCustomFields to undefined. ([33931](https://github.com/WordPress/gutenberg/pull/33931)) +- Fix metabox reordering. ([30617](https://github.com/WordPress/gutenberg/pull/30617)) + +#### Block API +- Blocks: Register block when invalid value provided for the icon. ([34350](https://github.com/WordPress/gutenberg/pull/34350)) + +#### Accessibility +- Fix button block focus trap after a URL has been added. ([34314](https://github.com/WordPress/gutenberg/pull/34314)) + +#### REST API +- Default batch processor: Respect the batch endpoint's maxItems. ([34280](https://github.com/WordPress/gutenberg/pull/34280)) + +#### Navigation Screen +- Decode entities in the menu names. ([34263](https://github.com/WordPress/gutenberg/pull/34263)) + +#### Rich Text +- [Block Editor]: Fix caret position on block merging. ([34169](https://github.com/WordPress/gutenberg/pull/34169)) + +#### Block Editor +- Keep id on paste if internal link points to it. ([31107](https://github.com/WordPress/gutenberg/pull/31107)) + +#### Build Tooling +- Fix build hang on Windows 10. ([23589](https://github.com/WordPress/gutenberg/pull/23589)) + + +### Performance + +- Improve the getBlock and getBlocks performance. ([34241](https://github.com/WordPress/gutenberg/pull/34241)) +- Remove duplicated `useValidAlignment` hook. ([34593](https://github.com/WordPress/gutenberg/pull/34593)) +- core-data: Move locks state from store to local variable. ([34374](https://github.com/WordPress/gutenberg/pull/34374)) + +#### Global Styles +- Remove colors classes from the packages that are already provided by global styles. ([34510](https://github.com/WordPress/gutenberg/pull/34510)) + + +### Experiments + +#### Block Library +- Allow Site Title and Logo inside Navigation block. ([33316](https://github.com/WordPress/gutenberg/pull/33316)) +- [Social Links]: Use the new `flex` layout. ([34493](https://github.com/WordPress/gutenberg/pull/34493)) + +#### Global Styles +- Add unit tests for edit site editor utils. ([34401](https://github.com/WordPress/gutenberg/pull/34401)) + + +### Documentation + +- Correct typo in Blocks Documentation. ([34396](https://github.com/WordPress/gutenberg/pull/34396)) +- Eslint: Add no-unsafe-wp-apis to rules list in the documentation. ([34416](https://github.com/WordPress/gutenberg/pull/34416)) +- RNMobile: Fix links, images, and formatting in documentation. ([34300](https://github.com/WordPress/gutenberg/pull/34300)) +- Replace withSelect references with useSelect. ([34549](https://github.com/WordPress/gutenberg/pull/34549)) +- Update DuotonePicker documentation for accuracy. ([34494](https://github.com/WordPress/gutenberg/pull/34494)) +- Updated Template section copy. ([34383](https://github.com/WordPress/gutenberg/pull/34383)) +- [Docs]: Update block variations documentation about `block` scope. ([34455](https://github.com/WordPress/gutenberg/pull/34455)) +- [Prettier] Updated README.md file with the correct syntax. ([34600](https://github.com/WordPress/gutenberg/pull/34600)) + +#### Components +- Fix/update documentation alignment matrix control. ([34624](https://github.com/WordPress/gutenberg/pull/34624)) + + +### Code Quality + +- Add getFilename method to the URL package. ([34313](https://github.com/WordPress/gutenberg/pull/34313)) +- Block Editor: Ensure that `blockType` is defined when accessing `apiVersion`. ([34346](https://github.com/WordPress/gutenberg/pull/34346)) +- Block Editor: Migrate `lightBlockWrapper` support to `apiVersion` for blocks. ([34459](https://github.com/WordPress/gutenberg/pull/34459)) +- Code cleanup to the getBlock refactoring. ([34326](https://github.com/WordPress/gutenberg/pull/34326)) +- Fix linting error in trunk. ([34464](https://github.com/WordPress/gutenberg/pull/34464)) +- Fix linting errors. ([34596](https://github.com/WordPress/gutenberg/pull/34596)) +- Linting: Remove global event listener warning. ([34528](https://github.com/WordPress/gutenberg/pull/34528)) +- Migrate canUser resolver to thunks. ([34580](https://github.com/WordPress/gutenberg/pull/34580)) +- Migrate entities.js to thunks. ([34582](https://github.com/WordPress/gutenberg/pull/34582)) +- Migrate getAutosaves resolver to thunks. ([34581](https://github.com/WordPress/gutenberg/pull/34581)) +- Migrate getEntityRecord resolver to thunks. ([34576](https://github.com/WordPress/gutenberg/pull/34576)) +- Migrate getEntityRecords resolver to thunks. ([34578](https://github.com/WordPress/gutenberg/pull/34578)) +- Migrate resolvers to thunks: GetAuthors,_getCurrentUser,__getCurrentTheme,__getThemeSupports. ([34579](https://github.com/WordPress/gutenberg/pull/34579)) +- Refactor deleteEntityRecord to use thunks instead of generators. ([34386](https://github.com/WordPress/gutenberg/pull/34386)) +- Refactor editEntityRecord, undo, and redo to be thunks instead of generators. ([34387](https://github.com/WordPress/gutenberg/pull/34387)) +- Refactor saveEntityRecord from redux-rungen to async thunks. ([33201](https://github.com/WordPress/gutenberg/pull/33201)) +- Remove confusing punctuation. ([34322](https://github.com/WordPress/gutenberg/pull/34322)) +- Remove extraction of raw values in saveEntityRecords. ([34502](https://github.com/WordPress/gutenberg/pull/34502)) +- Rich text: Replace global event handlers with local ones. ([34492](https://github.com/WordPress/gutenberg/pull/34492)) +- core-data: Remove the PROCESS_PENDING_LOCK_REQUESTS action. ([34343](https://github.com/WordPress/gutenberg/pull/34343)) +- i18n: Add context to 'none' strings for better translations. ([34341](https://github.com/WordPress/gutenberg/pull/34341)) +- useDropZone: Ensure drag event targets HTMLElement. ([34272](https://github.com/WordPress/gutenberg/pull/34272)) + +#### Block Library +- Button block: Replace global shortcut event handlers with local ones. ([34498](https://github.com/WordPress/gutenberg/pull/34498)) +- Gallery Block : Remove IE specific CSS hacks. ([34372](https://github.com/WordPress/gutenberg/pull/34372)) +- Gallery block: Add docblock comments to the new gallery hooks. ([34562](https://github.com/WordPress/gutenberg/pull/34562)) +- Navigation link block: Replace global shortcut event handlers with local ones. ([34500](https://github.com/WordPress/gutenberg/pull/34500)) +- Refactor navigation block to use generic classnames. ([34171](https://github.com/WordPress/gutenberg/pull/34171)) +- Remove redundant css selector. ([34277](https://github.com/WordPress/gutenberg/pull/34277)) + +#### Components +- CustomGradientBar: Replace global shortcut event handlers with local ones. ([34505](https://github.com/WordPress/gutenberg/pull/34505)) +- Guide: Replace global shortcut event handlers with local ones. ([34503](https://github.com/WordPress/gutenberg/pull/34503)) +- Navigate regions: Use React events for shortcuts (portal bubbles & contextual). ([33633](https://github.com/WordPress/gutenberg/pull/33633)) +- Rename `PolymorphicComponent*` types to `WordPressComponent*`. ([34330](https://github.com/WordPress/gutenberg/pull/34330)) +- Simplify Modal with hooks. ([34412](https://github.com/WordPress/gutenberg/pull/34412)) +- Try: Simplify & polish heading levels. ([34378](https://github.com/WordPress/gutenberg/pull/34378)) + +#### Post Editor +- Editor package: Replace hardcoded store key. ([34296](https://github.com/WordPress/gutenberg/pull/34296)) +- Fix gray W menu color. ([34318](https://github.com/WordPress/gutenberg/pull/34318)) + +#### Block Editor +- Fix Animated warning log. ([34197](https://github.com/WordPress/gutenberg/pull/34197)) +- Rich text (core): OnFocus method can be replaced with HTMLElement.focus. ([32054](https://github.com/WordPress/gutenberg/pull/32054)) + +#### CSS & Styling +- Navigation: Use gap instead of margin. ([32367](https://github.com/WordPress/gutenberg/pull/32367)) + + +### Tools + +- Added janw-me to the Codeowners for the PHP FSE folder. ([32990](https://github.com/WordPress/gutenberg/pull/32990)) + +#### Build Tooling +- ESLint: Update error message for `@emotion/css` with info about the `useCx` hook. ([34418](https://github.com/WordPress/gutenberg/pull/34418)) +- More work on the stability of the performance metrics. ([34229](https://github.com/WordPress/gutenberg/pull/34229)) + + +### Various + +- Upgrade gradle to 7.1.1 agp to 4.2.2. ([34048](https://github.com/WordPress/gutenberg/pull/34048)) + +#### Components +- Combobox Component: Only force expanded state if the input has focus. ([34090](https://github.com/WordPress/gutenberg/pull/34090)) +- Try: Vertical heading levels menu. ([32926](https://github.com/WordPress/gutenberg/pull/32926)) +- [ToggleGroupControl]: Update stories to use knobs. ([34497](https://github.com/WordPress/gutenberg/pull/34497)) + +#### Plugin +- Update the minimum supported WordPress version to 5.7. ([34536](https://github.com/WordPress/gutenberg/pull/34536)) + +#### Accessibility +- Accessibility improvement for font weight screen reader description. ([34312](https://github.com/WordPress/gutenberg/pull/34312)) + +#### Widgets Editor +- Prevent focus trap in Legacy Widget block’s preview iframe. ([33614](https://github.com/WordPress/gutenberg/pull/33614)) + +#### Post Editor +- Expose ThemeSupportCheck component. ([20506](https://github.com/WordPress/gutenberg/pull/20506)) + + + + + += 11.4.1 = + +### Bug Fixes + +- Post title: Remove class from old div to fix alignment. ([34489](https://github.com/WordPress/gutenberg/pull/34489)) + + += 11.4.0 = + +### Enhancements +- Accessibility + - Cover Block: Allow alt text in Cover blocks. ([33226](https://github.com/WordPress/gutenberg/pull/33226)) + - Add `aria-describedby` to custom select control component to describe currently-selected font size. ([33941](https://github.com/WordPress/gutenberg/pull/33941)) +- Block Editor + - Block Lists: improve iframe block, pattern and template previews. ([28165](https://github.com/WordPress/gutenberg/pull/28165)) +- Block Library + - Query Loop: update Post Template sub-block icon. ([34204](https://github.com/WordPress/gutenberg/pull/34204)) + - Convert Gallery block to use Image blocks. ([25940](https://github.com/WordPress/gutenberg/pull/25940)) + - Post Featured Image: add duotone block supports. ([34113](https://github.com/WordPress/gutenberg/pull/34113)) + - Post Featured Image: add contextual help text to the `scale`property. ([34158](https://github.com/WordPress/gutenberg/pull/34158)) + - File block: update transform from image to use image filename if caption is empty. ([34256](https://github.com/WordPress/gutenberg/pull/34256)) + - Post date Block: add font weight support to the block. ([34070](https://github.com/WordPress/gutenberg/pull/34070)) + - Post terms: add font weight support to the block. ([34142](https://github.com/WordPress/gutenberg/pull/34142)) + - Site Tagline: add font weight support. ([33983](https://github.com/WordPress/gutenberg/pull/33983)) + - Button: update spacing support to use axial padding. ([33859](https://github.com/WordPress/gutenberg/pull/33859)) +- Components + - Add deprecated props adapter for ColorPicker. ([34014](https://github.com/WordPress/gutenberg/pull/34014)) + - Wrap SegmentedControl in a BaseControl with an added `help` property. ([34017](https://github.com/WordPress/gutenberg/pull/34017)) + - Combobox: update the current selection if the list of suggestions is filtered. ([33928](https://github.com/WordPress/gutenberg/pull/33928)) + - Post Title: use rich text hook and updating tag to `h1` ([31569](https://github.com/WordPress/gutenberg/pull/31569)) +- Design Tools + - Add layout default value support for blocks. ([34194](https://github.com/WordPress/gutenberg/pull/34194)) + - Dimensions Panel: add padding tool as default for blocks where this is a common setting. ([34026](https://github.com/WordPress/gutenberg/pull/34026)) +- Navigation Screen + - Update navigation screen topbar. ([34166](https://github.com/WordPress/gutenberg/pull/34166)) +- Packages + - Updates the "settings" icon, which toggles the display of additional controls in an interface. ([34165](https://github.com/WordPress/gutenberg/pull/34165)) +- Post Editor + - Migrate post editor feature preferences to the interface package. ([34154](https://github.com/WordPress/gutenberg/pull/34154)) +- Widgets Editor + - Migrate customize widgets feature preferences to interface package. ([34135](https://github.com/WordPress/gutenberg/pull/34135)) + - Refactor editor 'feature' preferences to interface package. ([33774](https://github.com/WordPress/gutenberg/pull/33774)) + +### Bug Fixes +- Block API + - Spacing/Dimensions Supports: separate spacing from dimensions for compatibility purposes. ([34059](https://github.com/WordPress/gutenberg/pull/34059)) +- Block Editor + - Font-size adjustment for tablet and mobile device previews. ([33342](https://github.com/WordPress/gutenberg/pull/33342)) + - Fix single block selection by holding `shift` key. ([34137](https://github.com/WordPress/gutenberg/pull/34137)) + - Fix unwanted additional spaces added around pasted text on Windows. ([33607](https://github.com/WordPress/gutenberg/pull/33607)) + - Inserter: prevent non-deterministic order of inserter items. ([34078](https://github.com/WordPress/gutenberg/pull/34078)) + - Try: Fix multiselect toolbar indent and reformat `BlockContextualToolbar()`. ([34038](https://github.com/WordPress/gutenberg/pull/34038)) ([34173](https://github.com/WordPress/gutenberg/pull/34173)) +- Block Library + - Latest Comments: use site locale in the editor. ([33944](https://github.com/WordPress/gutenberg/pull/33944)) + - Navigation: fix vertical layout on the frontend. ([34226](https://github.com/WordPress/gutenberg/pull/34226)) + - Navigation: add z-index value to responsive menu overlay. ([34228](https://github.com/WordPress/gutenberg/pull/34228)) + - Navigation: enable flex on container to fix space between. ([34258](https://github.com/WordPress/gutenberg/pull/34258)) + - Navigation: fix submenu icon positioning. ([34168](https://github.com/WordPress/gutenberg/pull/34168)) + - Navigation block: add missing `</ul>` closing tag. ([34077](https://github.com/WordPress/gutenberg/pull/34077)) + - Post Excerpt: remove interactive formatting. ([34083](https://github.com/WordPress/gutenberg/pull/34083)) + - RichText: fix `space` key for button and summary elements. ([30244](https://github.com/WordPress/gutenberg/pull/30244)) + - Search Block: add space between generated border class names. ([34025](https://github.com/WordPress/gutenberg/pull/34025)) +- Build Tooling + - Webpack: Fix watch on `.json` and `.php` files. ([34024](https://github.com/WordPress/gutenberg/pull/34024)) + - Pin TypeScript dependency to a specific version to avoid pulling in breaking changes. ([34422](https://github.com/WordPress/gutenberg/pull/34422)) +- Components + - Fix RTL on `Flex` component. ([33729](https://github.com/WordPress/gutenberg/pull/33729)) + - NavigationSidebar: fix template content for content-navigation-item preview. ([34203](https://github.com/WordPress/gutenberg/pull/34203)) + - Remove deprecated import style for storybook/addon-docs. ([34095](https://github.com/WordPress/gutenberg/pull/34095)) + - ToolsPanel: add tools panel item deregistration. ([34085](https://github.com/WordPress/gutenberg/pull/34085)) + - Post Title: remove wrapper div and fix border style. ([34167](https://github.com/WordPress/gutenberg/pull/34167)) +- Core Data + - `GetEntityRecords` returns items even if some included IDs don't exist. ([34034](https://github.com/WordPress/gutenberg/pull/34034)) +- Design Tools + - Allow zero values for `theme.json` styles. ([34251](https://github.com/WordPress/gutenberg/pull/34251)) +- Global Styles + - Site editor: fix for how CSS custom properties are generated. ([33932](https://github.com/WordPress/gutenberg/pull/33932)) +- Packages + - Rich Text: add check to `toTree()` in replacements before accessing its type. ([34020](https://github.com/WordPress/gutenberg/pull/34020)) +- Post Editor + - Fix selector params in `isPluginItemPinned()` selector. ([34155](https://github.com/WordPress/gutenberg/pull/34155)) + + +### Performance +- Data Layer + - Data: Add a batch function to the data module to batch actions. ([34046](https://github.com/WordPress/gutenberg/pull/34046)) + +### Experiments +- Block API + - Block Editor: absorb parent block toolbar controls. ([33955](https://github.com/WordPress/gutenberg/pull/33955)) + - Block Editor: use groups for InspectorControls. ([34069](https://github.com/WordPress/gutenberg/pull/34069)) +- Block Library + - Add generic classnames to children of Navigation. ([33918](https://github.com/WordPress/gutenberg/pull/33918)) +- Global Styles + - Add slashes back to the Theme JSON. ([33919](https://github.com/WordPress/gutenberg/pull/33919)) + - Add block spacing gap configuration to `theme.json` and add support for this CSS variable to the "flow/default" layout. ([33812](https://github.com/WordPress/gutenberg/pull/33812)) + + +### Documentation +- Handbook + - Alphabetize glossary entries. ([34058](https://github.com/WordPress/gutenberg/pull/34058)) + - Correct minor typos in `wp-plugin.md` ([34185](https://github.com/WordPress/gutenberg/pull/34185)) + - Remove extraneous params from `block_type_metadata` hook. ([34151](https://github.com/WordPress/gutenberg/pull/34151)) + - Update incorrect settings examples in "Global Settings & Styles". ([34084](https://github.com/WordPress/gutenberg/pull/34084)) + - Use `block.json` to add attributes in create block tutorial. ([33978](https://github.com/WordPress/gutenberg/pull/33978)) + - Fix typo in block gap documentation in `theme-json.md`. ([34231](https://github.com/WordPress/gutenberg/pull/34231)) + - Fix broken mobile testing documentation link in `testing-overview.md`. ([34187](https://github.com/WordPress/gutenberg/pull/34187)) + - Fix typo in `legacy-widget-block.md`. ([34103](https://github.com/WordPress/gutenberg/pull/34103)) + - Update spelling and `fontSize` examples in `create-block-theme.md`. ([34152](https://github.com/WordPress/gutenberg/pull/34152)) +- Library + - Bump mobile version in experiments page for Gallery. ([34220](https://github.com/WordPress/gutenberg/pull/34220)) +- Packages + - Add documentation for mobile components directory. ([33872](https://github.com/WordPress/gutenberg/pull/33872)) + +### Code Quality +- Block Editor + - Render head and body with single portal for block previews. ([34208](https://github.com/WordPress/gutenberg/pull/34208)) + - BlockList: refactor element context for style/svg appending. ([34183](https://github.com/WordPress/gutenberg/pull/34183)) + - BlockList: use InnerBlocks internally. ([29895](https://github.com/WordPress/gutenberg/pull/29895)) +- Components + - Unit Control: add unit tests for `getValidParsedUnit` utility method. ([34029](https://github.com/WordPress/gutenberg/pull/34029)) + - Rename `SegmentedControl` to `ToggleGroupControl`. ([34111](https://github.com/WordPress/gutenberg/pull/34111)) + - Dropdown Menu: remove min-width from the dropdown component and add whitespace rule to avoid wrapping ([33995](https://github.com/WordPress/gutenberg/pull/33995)) +- Core Data + - Allow passing store definitions to controls. ([34170](https://github.com/WordPress/gutenberg/pull/34170)) +- Site Editor + - Remove extra DOM element used for template part overlay. ([34012](https://github.com/WordPress/gutenberg/pull/34012)) + +### Tools +- Build Tooling + - Automated Changelog: force group all documentation tasks under `Documentation`. ([34042](https://github.com/WordPress/gutenberg/pull/34042)) + - Automated Changelog: rename "Editor" grouping to "Post Editor" to avoid ambiguity with other editors. ([34093](https://github.com/WordPress/gutenberg/pull/34093)) + - Automated Changelog: sort feature groups by issue name. ([34071](https://github.com/WordPress/gutenberg/pull/34071)) + - Automated Changelog: use nested headings for feature groups instead of indenting lists. ([34040](https://github.com/WordPress/gutenberg/pull/34040)) + - Automated Changelog: remove `Uncategorized` header in output and place items at top. ([34037](https://github.com/WordPress/gutenberg/pull/34037)) + - Add Typescript extensions to watched files. ([34094](https://github.com/WordPress/gutenberg/pull/34094)) + - Remove obsolete step that pushes tags in NPM publishing flow. ([34114](https://github.com/WordPress/gutenberg/pull/34114)) + - Release workflow: only commit modified changelogs. ([34211](https://github.com/WordPress/gutenberg/pull/34211)) +- ESLint + - Eslint plugin: use `@typescript-eslint/no-duplicate-imports` in TS projects. ([34055](https://github.com/WordPress/gutenberg/pull/34055)) +- GitHub Contributor Templates + - Issue Forms: simplify the bug report form template. ([34007](https://github.com/WordPress/gutenberg/pull/34007)) +- Logs + - Hide deprecation logs under a console group. ([34163](https://github.com/WordPress/gutenberg/pull/34163)) +- Testing + - Emulate reduced-motion in end-to-end tests. ([34132](https://github.com/WordPress/gutenberg/pull/34132)) + - Re-enable Android end-to-end tests. ([34243](https://github.com/WordPress/gutenberg/pull/34243)) + - Remove extra props from Cover deprecations. ([34066](https://github.com/WordPress/gutenberg/pull/34066)) + - Remove the `ENVIRONMENT_DIRECTORY` env variable that was added to the performance jobs. ([34086](https://github.com/WordPress/gutenberg/pull/34086)) + - Add snapshot test for changelog formatting. ([34049](https://github.com/WordPress/gutenberg/pull/34049)) + - Experiment with using REST API in end-to-end tests to build up states. ([33414](https://github.com/WordPress/gutenberg/pull/33414)) + + + + + = 11.3.0 = ### Enhancements diff --git a/docs/contributors/code/getting-started-native-mobile.md b/docs/contributors/code/getting-started-native-mobile.md index a3dbeedcd98a3..dbdeba751aa50 100644 --- a/docs/contributors/code/getting-started-native-mobile.md +++ b/docs/contributors/code/getting-started-native-mobile.md @@ -9,15 +9,15 @@ For a developer experience closer to the one the project maintainers current hav - git - [nvm](https://github.com/creationix/nvm) - Node.js and npm (use nvm to install them) -- [AndroidStudio](https://developer.android.com/studio/) to be able to compile the Android version of the app +- [Android Studio](https://developer.android.com/studio/) to be able to compile the Android version of the app - [Xcode](https://developer.apple.com/xcode/) to be able to compile the iOS app -- CocoaPods(`sudo gem install cocoapods`) needed to fetch React and third-party dependencies. +- CocoaPods (`sudo gem install cocoapods`) needed to fetch React and third-party dependencies. Note that the OS platform used by the maintainers is macOS but the tools and setup should be usable in other platforms too. ## Clone the project -``` +```sh git clone https://github.com/WordPress/gutenberg.git ``` @@ -25,14 +25,14 @@ git clone https://github.com/WordPress/gutenberg.git Note that the commands described here should be run in the top-level directory of the cloned project. Before running the demo app, you need to download and install the project dependencies. This is done via the following command: -``` +```sh nvm install npm ci ``` ## Run -``` +```sh npm run native start:reset ``` @@ -40,7 +40,7 @@ Runs the packager (Metro) in development mode. The packager stays running to ser With the packager running, open another terminal window and use the following command to compile and run the Android app: -``` +```sh npm run native android ``` @@ -48,7 +48,7 @@ The app should now open in a connected device or a running emulator and fetch th To compile and run the iOS variant of the app using the _default_ simulator device, use: -``` +```sh npm run native ios ``` @@ -58,13 +58,13 @@ which will attempt to open your app in the iOS Simulator if you're on a Mac and To compile and run the app using a different device simulator, use the following, noting the double sets of `--` to pass the simulator option down to the `react-native` CLI. -``` +```sh npm run native ios -- -- --simulator="DEVICE_NAME" ``` For example, if you'd like to run in an iPhone Xs Max, try: -``` +```sh npm run native ios -- -- --simulator="iPhone Xs Max" ``` @@ -86,7 +86,7 @@ One of the extensions we are using is the [React Native Tools](https://marketpla Use the following command to run the test suite: -``` +```sh npm run native test ``` @@ -94,7 +94,7 @@ It will run the [jest](https://github.com/facebook/jest) test runner on your tes To run the tests with debugger support, start it with the following CLI command: -``` +```sh npm run native test:debug ``` @@ -114,15 +114,21 @@ This repository uses Appium to run UI tests. The tests live in `__device-tests__ Then, to run the UI tests on iOS: -`npm run native test:e2e:ios:local` +```sh +npm run native test:e2e:ios:local +``` and for Android: -`npm run native test:e2e:android:local` +```sh +npm run native test:e2e:android:local +``` To run a single test instead of the entire suite, use `npm run native device-tests:local`. Here's an example that runs only `gutenberg-editor-gallery.test.js`: -`npm run native test:e2e:android:local gutenberg-editor-gallery.test.js` +```sh +npm run native test:e2e:android:local gutenberg-editor-gallery.test.js +``` Note: You might experience problems that seem to be related to the tests starting the Appium server, e.g. errors that say `Connection Refused`, `Connection Reset` or `The requested environment is not available`. For now, you can manually start the Appium server via [appium desktop](https://github.com/appium/appium-desktop) or the CLI, then change the port number in the tests while (optionally) commenting out related code in the `beforeAll` and `afterAll` block. diff --git a/docs/contributors/code/native-mobile-integration-test-guide.md b/docs/contributors/code/native-mobile-integration-test-guide.md index 46bcd271094b2..14cb479dae194 100644 --- a/docs/contributors/code/native-mobile-integration-test-guide.md +++ b/docs/contributors/code/native-mobile-integration-test-guide.md @@ -27,7 +27,7 @@ This part usually is covered by using the Jest callbacks `beforeAll` and `before Here is an example of a common pattern if we expect all core blocks to be available: -``` +```js beforeAll( () => { // Register all core blocks registerCoreBlocks(); @@ -42,7 +42,7 @@ Before introducing the testing logic, we have to render the components that we w Here is an example of rendering the Cover block (extracted from [this code](https://github.com/WordPress/gutenberg/blob/86cd187873984f80ddeeec3e82454b486dd1860f/packages/block-library/src/cover/test/edit.native.js#L82-L91)): -``` +```js // This import points to the index file of the block import { metadata, settings, name } from '../index'; @@ -83,7 +83,7 @@ const { getByText, findByText } = render( Here is an example of rendering the Buttons block (extracted from [this code](https://github.com/WordPress/gutenberg/blob/9201906891a68ca305daf7f8b6cd006e2b26291e/packages/block-library/src/buttons/test/edit.native.js#L32-L39)): -``` +```js const initialHtml = `<!-- wp:buttons --> <div class="wp-block-buttons"><!-- wp:button {"style":{"border":{"radius":"5px"}}} --> <div class="wp-block-button"><a class="wp-block-button__link" style="border-radius:5px" >Hello</a></div> @@ -106,15 +106,15 @@ When querying we should follow this priority order: Here are some examples: -``` +```js const mediaLibraryButton = getByText( 'WordPress Media Library' ); ``` -``` +```js const missingBlock = getByA11yLabel( /Unsupported Block\. Row 1/ ); ``` -``` +```js const radiusSlider = getByTestId( 'Slider Border Radius' ); ``` @@ -126,19 +126,19 @@ After rendering the components or firing an event, side effects might happen due Here are some examples: -``` +```js const mediaLibraryButton = await waitFor( () => getByText( 'WordPress Media Library' ) ); ``` -``` +```js const missingBlock = await waitFor( () => getByA11yLabel( /Unsupported Block\. Row 1/ ) ); ``` -``` +```js const radiusSlider = await waitFor( () => getByTestId( 'Slider Border Radius' ) ); @@ -152,13 +152,11 @@ NOTE: The `react-native-testing-library` package provides the `query*` and `find It’s also possible to query elements contained in other elements via the `within` function, here is an example: -``` +```js const missingBlock = await waitFor( () => getByA11yLabel( /Unsupported Block\. Row 1/ ) ); -const translatedTableTitle = within( missingBlock ).getByText( - 'Tabla' -); +const translatedTableTitle = within( missingBlock ).getByText( 'Tabla' ); ``` ## Fire events @@ -169,7 +167,7 @@ Here is an example of a press event: **Press event:** -``` +```js fireEvent.press( settingsButton ); ``` @@ -177,7 +175,7 @@ We can also trigger any type of event, including custom events, in the following **Custom event – onValueChange:** -``` +```js fireEvent( heightSlider, 'valueChange', '50' ); ``` @@ -187,18 +185,16 @@ After querying elements and firing events, we have to verify that the logic work Here is an example: -``` -const translatedTableTitle = within( missingBlock ).getByText( - 'Tabla' -); +```js +const translatedTableTitle = within( missingBlock ).getByText( 'Tabla' ); expect( translatedTableTitle ).toBeDefined(); ``` Additionally when rendering the entire editor, we can also verify if the HTML output is what we expect: -``` +```js expect( getEditorHtml() ).toBe( -'<!-- wp:spacer {"height":50} -->\n<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>\n<!-- /wp:spacer -->' + '<!-- wp:spacer {"height":50} -->\n<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>\n<!-- /wp:spacer -->' ); ``` @@ -206,7 +202,7 @@ expect( getEditorHtml() ).toBe( And finally, we have to clean up any potential modifications we’ve made that could affect the following tests. Here is an example of a typical cleanup after registering blocks that implies unregistering all blocks: -``` +```js afterAll( () => { // Clean up registered blocks getBlockTypes().forEach( ( block ) => { @@ -221,9 +217,9 @@ afterAll( () => { A common way to query a block is by its accessibility label, here is an example: -``` +```js const spacerBlock = await waitFor( () => -getByA11yLabel( /Spacer Block\. Row 1/ ) + getByA11yLabel( /Spacer Block\. Row 1/ ) ); ``` @@ -233,7 +229,7 @@ For further information about the accessibility label of a block, you can check Here is an example of how to insert a Paragraph block: -``` +```js // Open the inserter menu fireEvent.press( await waitFor( () => getByA11yLabel( 'Add block' ) ) ); @@ -255,12 +251,10 @@ fireEvent.press( await waitFor( () => getByText( `Paragraph` ) ) ); The block settings can be accessed by tapping the "Open Settings" button after selecting the block, here is an example: -``` +```js fireEvent.press( block ); -const settingsButton = await waitFor( () => - getByA11yLabel( 'Open Settings' ) -); +const settingsButton = await waitFor( () => getByA11yLabel( 'Open Settings' ) ); fireEvent.press( settingsButton ); ``` @@ -268,10 +262,10 @@ fireEvent.press( settingsButton ); When using the scoped component approach, we need first to render the `SlotFillProvider` and the `BottomSheetSettings` (note that we’re passing the `isVisible` prop to force the bottom sheet to be displayed) along with the block: -``` +```js <SlotFillProvider> -<BlockEdit isSelected name={ name } clientId={ 0 } { ...props } /> -<BottomSheetSettings isVisible /> + <BlockEdit isSelected name={ name } clientId={ 0 } { ...props } /> + <BottomSheetSettings isVisible /> </SlotFillProvider> ``` @@ -285,7 +279,7 @@ The `FlatList` component renders its items depending on the scroll position, the Here is an example of the FlatList used for rendering the block list in the inserter menu: -``` +```js const blockList = getByTestId( 'InserterUI-Blocks' ); // onScroll event used to force the FlatList to render all items fireEvent.scroll( blockList, { @@ -301,7 +295,7 @@ fireEvent.scroll( blockList, { Sliders found in bottom sheets should be queried using their `testID`: -``` +```js const radiusSlider = await waitFor( () => getByTestId( 'Slider Border Radius' ) ); @@ -314,7 +308,7 @@ Note that a slider’s `testID` is "Slider " + label. So for a slider with a lab One caveat when adding blocks is that if they contain inner blocks, these inner blocks are not rendered. The following example shows how we can make a Buttons block render its inner Button blocks (assumes we’ve already obtained a reference to the Buttons block as `buttonsBlock`): -``` +```js const innerBlockListWrapper = await waitFor( () => within( buttonsBlock ).getByTestId( 'block-list-wrapper' ) ); @@ -338,7 +332,7 @@ fireEvent.press( buttonInnerBlock ); If you have trouble locating an element’s identifier, you may wish to use Xcode’s Accessibility Inspector. Most identifiers are cross-platform, so even though the tests are run on Android by default, the Accessibility Inspector can be used to find the right identifier. -<img src="../../assets/xcode-accessibility-inspector-screenshot.png" alt="Screenshot of the Xcode Accessibility Inspector app. The screenshot shows how to choose the correct target in the device dropdown, enable target mode, and locate accessibility labels after tapping on screen elements"/> +<img src="https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/xcode-accessibility-inspector-screenshot.png" alt="Screenshot of the Xcode Accessibility Inspector app. The screenshot shows how to choose the correct target in the device dropdown, enable target mode, and locate accessibility labels after tapping on screen elements"/> ## Common pitfalls and caveats @@ -362,7 +356,7 @@ By default, all tests run in Jest use the Android platform, so in case we need t In case we only need to test logic controlled by the Platform object, we can mock the module with the following code (in this case it’s changing the platform to iOS): -``` +```js jest.mock( 'Platform', () => { const Platform = jest.requireActual( 'Platform' ); Platform.OS = 'ios'; diff --git a/docs/contributors/code/native-mobile.md b/docs/contributors/code/native-mobile.md index d216720eda4c3..2db44df33fef3 100644 --- a/docs/contributors/code/native-mobile.md +++ b/docs/contributors/code/native-mobile.md @@ -21,7 +21,7 @@ Also, the mobile client is packaged and released via the [official WordPress app If you encounter a failed Android/iOS test on your pull request, we recommend the following steps: 1. Re-running the failed GitHub Action job ([guide for how to re-run](https://docs.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#viewing-your-workflow-history)) - This should fix failed tests the majority of the time. Cases where you need to re-run tests for a pass should go down in the near future as flakiness in tests is actively being worked on. See the following GitHub issue for updated info on known failures: https://github.com/WordPress/gutenberg/issues/23949 -2. You can check if the test is failing locally by following the steps to run the E2E test on your machine from the [mobile getting started guide](/docs/contributors/code/getting-started-with-code-contribution-native-mobile.md#ui-tests), with even more relevant info in the [relevant directory README.md](https://github.com/WordPress/gutenberg/tree/HEAD/packages/react-native-editor/__device-tests__#running-the-tests-locally) +2. You can check if the test is failing locally by following the steps to run the E2E test on your machine from the [mobile getting started guide](/docs/contributors/code/getting-started-native-mobile.md#ui-tests), with even more relevant info in the [relevant directory README.md](https://github.com/WordPress/gutenberg/tree/HEAD/packages/react-native-editor/__device-tests__#running-the-tests-locally) 3. In addition to reading the logs from the E2E test, you can download a video recording from the Artifacts section of the GitHub job that may have additional useful information. 4. Check if any changes in your PR would require corresponding changes to `.native.js` versions of files. 5. Lastly, if you're stuck on a failing mobile test, feel free to reach out to contributors on Slack in the #mobile or #core-editor chats in the WordPress Core Slack, [free to join](https://make.wordpress.org/chat/). diff --git a/docs/contributors/code/testing-overview.md b/docs/contributors/code/testing-overview.md index b4b9e06270181..5f288d618233f 100644 --- a/docs/contributors/code/testing-overview.md +++ b/docs/contributors/code/testing-overview.md @@ -21,7 +21,7 @@ When writing tests consider the following: Tests for JavaScript use [Jest](https://jestjs.io/) as the test runner and its API for [globals](https://jestjs.io/docs/en/api.html) (`describe`, `test`, `beforeEach` and so on) [assertions](https://jestjs.io/docs/en/expect.html), [mocks](https://jestjs.io/docs/en/mock-functions.html), [spies](https://jestjs.io/docs/en/jest-object.html#jestspyonobject-methodname) and [mock functions](https://jestjs.io/docs/en/mock-function-api.html). If needed, you can also use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) for React component testing. -It should be noted that in the past, React components were unit tested with [Enzyme](https://github.com/airbnb/enzyme). However, for new tests, it is preferred to use React Testing Library (RTL) and over time old tests should be refactored to use RTL too (typically when working on code that touches an old test). +_It should be noted that in the past, React components were unit tested with [Enzyme](https://github.com/airbnb/enzyme). However, React Testing Library (RTL) should be used for new tests instead, and over time old tests should be refactored to use RTL too (typically when working on code that touches an old test)._ Assuming you've followed the [instructions](/docs/contributors/code/getting-started-with-code-contribution.md) to install Node and project dependencies, tests can be run from the command-line with NPM: @@ -273,23 +273,22 @@ You should never create or modify a snapshot directly, they are generated and up Snapshot are mostly targeted at component testing. They make us conscious of changes to a component's structure which makes them _ideal_ for refactoring. If a snapshot is kept up to date over the course of a series of commits, the snapshot diffs record the evolution of a component's structure. Pretty cool 😎 -```js -import { shallow } from 'enzyme'; +```jsx +import { render, screen } from '@testing-library/react'; import SolarSystem from 'solar-system'; -import { Mars } from 'planets'; describe( 'SolarSystem', () => { test( 'should render', () => { - const wrapper = shallow( <SolarSystem /> ); + const { container } = render( <SolarSystem /> ); - expect( wrapper ).toMatchSnapshot(); + expect( container.firstChild ).toMatchSnapshot(); } ); test( 'should contain mars if planets is true', () => { - const wrapper = shallow( <SolarSystem planets /> ); + const { container } = render( <SolarSystem planets /> ); - expect( wrapper ).toMatchSnapshot(); - expect( wrapper.find( Mars ) ).toHaveLength( 1 ); + expect( container.firstChild ).toMatchSnapshot(); + expect( screen.getByText( /mars/i ) ).toBeInTheDocument(); } ); } ); ``` @@ -332,23 +331,41 @@ If you're starting a refactor, snapshots are quite nice, you can add them as the Snapshots themselves don't express anything about what we expect. Snapshots are best used in conjunction with other tests that describe our expectations, like in the example above: -```js +```jsx test( 'should contain mars if planets is true', () => { - const wrapper = shallow( <SolarSystem planets /> ); + const { container } = render( <SolarSystem planets /> ); // Snapshot will catch unintended changes - expect( wrapper ).toMatchSnapshot(); + expect( container.firstChild ).toMatchSnapshot(); // This is what we actually expect to find in our test - expect( wrapper.find( Mars ) ).toHaveLength( 1 ); + expect( screen.getByText( /mars/i ) ).toBeInTheDocument(); } ); ``` -[`shallow`](http://airbnb.io/enzyme/docs/api/shallow.html) rendering is your friend: +Another good technique is to use the `toMatchDiffSnapshot` function (provided by the [`snapshot-diff` package](https://github.com/jest-community/snapshot-diff)), which allows to snapshot only the difference between two different states of the DOM. This approach is useful to test the effects of a prop change on the resulting DOM while generating a much smaller snapshot, like in this example: -> Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren't indirectly asserting on behavior of child components. +```jsx +test( 'should render a darker background when isShady is true', () => { + const { container } = render( <CardBody>Body</CardBody> ); + const { container: containerShady } = render( + <CardBody isShady>Body</CardBody> + ); + expect( container ).toMatchDiffSnapshot( containerShady ); +} ); +``` -It's tempting to snapshot deep renders, but that makes for huge snapshots. Additionally, deep renders no longer test a single component, but an entire tree. With `shallow`, we snapshot just the components that are directly rendered by the component we want to test. +Similarly, the `toMatchStyleDiffSnapshot` function allows to snapshot only the difference between the _styles_ associated to two different states of a component, like in this example: + +```jsx +test( 'should render margin', () => { + const { container: spacer } = render( <Spacer /> ); + const { container: spacerWithMargin } = render( <Spacer margin={ 5 } /> ); + expect( spacerWithMargin.firstChild ).toMatchStyleDiffSnapshot( + spacer.firstChild + ); +} ); +``` #### Troubleshooting @@ -380,11 +397,11 @@ To locally run the tests in debug mode, follow these steps: ### Native mobile end-to-end tests -Contributors to Gutenberg will note that PRs include continuous integration E2E tests running the native mobile E2E tests on Android and iOS. For troubleshooting failed tests, check our guide on [native mobile tests in continious integration](docs/contributors/native-mobile.md#native-mobile-e2e-tests-in-continuous-integration). More information on running these tests locally can be found in the [relevant directory README.md](https://github.com/WordPress/gutenberg/tree/HEAD/packages/react-native-editor/__device-tests__). +Contributors to Gutenberg will note that PRs include continuous integration E2E tests running the native mobile E2E tests on Android and iOS. For troubleshooting failed tests, check our guide on [native mobile tests in continuous integration](/docs/contributors/code/native-mobile.md#native-mobile-e2e-tests-in-continuous-integration). More information on running these tests locally can be found in [here](/packages/react-native-editor/__device-tests__/README.md). ### Native mobile integration tests -There is an ongoing effort to add integration tests to the native mobile project using the [`react-native-testing-library`](https://testing-library.com/docs/react-native-testing-library/intro/) library. A guide to writing integration tests can be found [here](native-mobile-integration-test-guide.md). +There is an ongoing effort to add integration tests to the native mobile project using the [`react-native-testing-library`](https://testing-library.com/docs/react-native-testing-library/intro/) library. A guide to writing integration tests can be found [here](/docs/contributors/code/native-mobile-integration-test-guide.md). ## End-to-end Testing diff --git a/docs/getting-started/tutorials/create-block/wp-plugin.md b/docs/getting-started/tutorials/create-block/wp-plugin.md index b3d4a4a636e8c..ff1d1db7f455c 100644 --- a/docs/getting-started/tutorials/create-block/wp-plugin.md +++ b/docs/getting-started/tutorials/create-block/wp-plugin.md @@ -59,7 +59,7 @@ This will start your local WordPress site and use the current directory as your ### Confirm Plugin Installed -The generated plugin should now be listed on the Plugins admin page in your WordPress install. Switch WorPress to the plugins page and activate. +The generated plugin should now be listed on the Plugins admin page in your WordPress install. Switch WordPress to the plugins page and activate. For more on creating a WordPress plugin see [Plugin Basics](https://developer.wordpress.org/plugins/plugin-basics/), and [Plugin Header requirements](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) for explanation and additional fields you can include in your plugin header. @@ -67,7 +67,7 @@ For more on creating a WordPress plugin see [Plugin Basics](https://developer.wo The `package.json` file defines the JavaScript properties for your project. This is a standard file used by NPM for defining properties and scripts it can run, the file and process is not specific to WordPress. -A `package.json` file was created with the create script, this defines the dependecies and scripts needed. You can install dependencies. The only initial dependency is the `@wordpress/scripts` package that bundles the tools and configurations needed to build blocks. +A `package.json` file was created with the create script, this defines the dependencies and scripts needed. You can install dependencies. The only initial dependency is the `@wordpress/scripts` package that bundles the tools and configurations needed to build blocks. In `package.json`, there is a `scripts` property that defines what command to run when using `npm run (cmd)`. In our generated `package.json` file, the two main scripts point to the commands in the `wp-scripts` package: diff --git a/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md b/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md index bf86d303fc643..770fa53ec7c15 100644 --- a/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md +++ b/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md @@ -22,7 +22,7 @@ The following code example shows how to create a dynamic block that shows only t ```jsx import { registerBlockType } from '@wordpress/blocks'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { useBlockProps } from '@wordpress/block-editor'; registerBlockType( 'gutenberg-examples/example-dynamic', { @@ -31,12 +31,11 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { icon: 'megaphone', category: 'widgets', - edit: withSelect( ( select ) => { - return { - posts: select( 'core' ).getEntityRecords( 'postType', 'post' ), - }; - } )( ( { posts } ) => { + edit: () => { const blockProps = useBlockProps(); + const posts = useSelect( ( select ) => { + return select( 'core' ).getEntityRecords( 'postType', 'post' ); + }, [] ); return ( <div { ...blockProps }> @@ -49,7 +48,7 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { ) } </div> ); - } ), + }, } ); ``` @@ -59,7 +58,7 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { ( function ( blocks, element, data, blockEditor ) { var el = element.createElement, registerBlockType = blocks.registerBlockType, - withSelect = data.withSelect, + useSelect = data.useSelect, useBlockProps = blockEditor.useBlockProps; registerBlockType( 'gutenberg-examples/example-dynamic', { @@ -67,24 +66,23 @@ registerBlockType( 'gutenberg-examples/example-dynamic', { title: 'Example: last post', icon: 'megaphone', category: 'widgets', - edit: withSelect( function ( select ) { - return { - posts: select( 'core' ).getEntityRecords( 'postType', 'post' ), - }; - } )( function ( props ) { - var blockProps = useBlockProps(); + edit: function () { var content; - if ( ! props.posts ) { + var blockProps = useBlockProps(); + var posts = useSelect( function ( select ) { + return select( 'core' ).getEntityRecords( 'postType', 'post' ); + }, [] ); + if ( ! posts ) { content = 'Loading...'; - } else if ( props.posts.length === 0 ) { + } else if ( posts.length === 0 ) { content = 'No posts'; } else { - var post = props.posts[ 0 ]; + var post = posts[ 0 ]; content = el( 'a', { href: post.link }, post.title.rendered ); } return el( 'div', blockProps, content ); - } ), + }, } ); } )( window.wp.blocks, diff --git a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md index 3c6e8cb2418aa..c7e2805767170 100644 --- a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md +++ b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md @@ -137,7 +137,7 @@ const MY_TEMPLATE = [ {% end %} -Use the `templateLock` property to lock down the template. Using `all` locks the template complete, no changes can be made. Using `insert` prevents additional blocks to be inserted, but existing blocks can be reordered. See [templateLock documentation](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md#templatelock) for additional information. +Use the `templateLock` property to lock down the template. Using `all` locks the template completely so no changes can be made. Using `insert` prevents additional blocks from being inserted, but existing blocks can be reordered. See [templateLock documentation](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md#templatelock) for additional information. ### Post Template diff --git a/docs/how-to-guides/format-api/2-toolbar-button.md b/docs/how-to-guides/format-api/2-toolbar-button.md index ddc10df8c9877..5cbfabbdec05e 100644 --- a/docs/how-to-guides/format-api/2-toolbar-button.md +++ b/docs/how-to-guides/format-api/2-toolbar-button.md @@ -74,31 +74,26 @@ The following sample code renders the previously shown button only on Paragraph ```js ( function ( wp ) { - var withSelect = wp.data.withSelect; - var ifCondition = wp.compose.ifCondition; - var compose = wp.compose.compose; - var MyCustomButton = function ( props ) { - return wp.element.createElement( wp.editor.RichTextToolbarButton, { + var el = wp.element.createElement; + var useSelect = wp.data.useSelect; + + function ConditionalButton( props ) { + var selectedBlock = useSelect( function ( select ) { + return select( 'core/block-editor' ).getSelectedBlock(); + }, [] ); + + if ( selectedBlock && selectedBlock.name !== 'core/paragraph' ) { + return null; + } + + return el( wp.blockEditor.RichTextToolbarButton, { icon: 'editor-code', title: 'Sample output', onClick: function () { - console.log( 'toggle format' ); + console.log( 'toggle format!' ); }, } ); }; - var ConditionalButton = compose( - withSelect( function ( select ) { - return { - selectedBlock: select( 'core/editor' ).getSelectedBlock(), - }; - } ), - ifCondition( function ( props ) { - return ( - props.selectedBlock && - props.selectedBlock.name === 'core/paragraph' - ); - } ) - )( MyCustomButton ); wp.richText.registerFormatType( 'my-custom-format/sample-output', { title: 'Sample output', @@ -112,12 +107,19 @@ The following sample code renders the previously shown button only on Paragraph {% ESNext %} ```js -import { compose, ifCondition } from '@wordpress/compose'; import { registerFormatType } from '@wordpress/rich-text'; import { RichTextToolbarButton } from '@wordpress/block-editor'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; + +function ConditionalButton( props ) { + const selectedBlock = useSelect( ( select ) => { + return select( 'core/block-editor' ).getSelectedBlock(); + }, [] ); + + if ( selectedBlock && selectedBlock.name !== 'core/paragraph' ) { + return null; + } -const MyCustomButton = ( props ) => { return ( <RichTextToolbarButton icon="editor-code" @@ -127,20 +129,7 @@ const MyCustomButton = ( props ) => { } } /> ); -}; - -const ConditionalButton = compose( - withSelect( function ( select ) { - return { - selectedBlock: select( 'core/editor' ).getSelectedBlock(), - }; - } ), - ifCondition( function ( props ) { - return ( - props.selectedBlock && props.selectedBlock.name === 'core/paragraph' - ); - } ) -)( MyCustomButton ); +} registerFormatType( 'my-custom-format/sample-output', { title: 'Sample output', @@ -152,6 +141,6 @@ registerFormatType( 'my-custom-format/sample-output', { {% end %} -Don't forget adding `wp-compose` and `wp-data` to the dependencies array in the PHP script. +Don't forget adding `wp-data` to the dependencies array in the PHP script. More advanced conditions can be used, e.g., only render the button depending on specific attributes of the block. diff --git a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-0.md b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-0.md index 750a12236d660..3a6d87c7a50b8 100644 --- a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-0.md +++ b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-0.md @@ -9,4 +9,3 @@ In the next sections, you're going to create a custom sidebar for a plugin that 3. [Register a new meta field](/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-3-register-meta.md) 4. [Initialize the input control with the meta field value](/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-4-initialize-input.md) 5. [Update the meta field value when input's content changes](/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md) -6. [Finishing touches](/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md) diff --git a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-4-initialize-input.md b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-4-initialize-input.md index 106bd31925e44..1df2b7bfb3675 100644 --- a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-4-initialize-input.md +++ b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-4-initialize-input.md @@ -41,17 +41,9 @@ Now that the field is available in the editor store, it can be surfaced to the U Now you can focus solely on the `MetaBlockField` component. The goal is to initialize it with the value of `sidebar_plugin_meta_block_field`, but also to keep it updated when that value changes. -WordPress has [some utilities to work with data](/packages/data/README.md) from the stores. The first you're going to use is [withSelect](/packages/data/README.md#withselect-mapselecttoprops-function-function), whose signature is: +WordPress has [some utilities to work with data](/packages/data/README.md) from the stores. The first you're going to use is [useSelect](/packages/data/README.md#useselect). -```js -withSelect()(); -// a function that takes `select` as input -// and returns an object containing data -// a function that takes the previous data as input -// and returns a component -``` - -`withSelect` is used to pass data to other components, and update them when the original data changes. Let's update the code to use it: +The `useSelect` is used to fetch data for the current component and update it when the original data changes. Let's update the code to use it: ```js ( function ( wp ) { @@ -59,30 +51,22 @@ withSelect()(); var PluginSidebar = wp.editPost.PluginSidebar; var el = wp.element.createElement; var Text = wp.components.TextControl; - var withSelect = wp.data.withSelect; - - var mapSelectToProps = function ( select ) { - return { - metaFieldValue: select( 'core/editor' ).getEditedPostAttribute( - 'meta' - )[ 'sidebar_plugin_meta_block_field' ], - }; - }; + var useSelect = wp.data.useSelect; + + var MetaBlockField = function () { + var metaFieldValue = useSelect( function ( select ) { + return select( 'core/editor' ).getEditedPostAttribute( 'meta' )[ 'sidebar_plugin_meta_block_field' ]; + }, [] ); - var MetaBlockField = function ( props ) { return el( Text, { label: 'Meta Block Field', - value: props.metaFieldValue, + value: metaFieldValue, onChange: function ( content ) { console.log( 'content has changed to ', content ); }, } ); }; - var MetaBlockFieldWithData = withSelect( mapSelectToProps )( - MetaBlockField - ); - registerPlugin( 'my-plugin-sidebar', { render: function () { return el( @@ -95,7 +79,7 @@ withSelect()(); el( 'div', { className: 'plugin-sidebar-content' }, - el( MetaBlockFieldWithData ) + el( MetaBlockField ) ) ); }, @@ -103,12 +87,11 @@ withSelect()(); } )( window.wp ); ``` -Copy this code to the JavaScript file. Note that it now uses the `wp.data.withSelect` utility to be found in the `@wordpress/data` package. Go ahead and add `wp-data` as a dependency in the PHP script. +Copy this code to the JavaScript file. Note that it now uses the `wp.data.useSelect` utility to be found in the `@wordpress/data` package. Go ahead and add `wp-data` as a dependency in the PHP script. This is how the code changes from the previous section: -- The `MetaBlockField` function has now a `props` argument as input. It contains the data object returned by the `mapSelectToProps` function, which it uses to initialize its value property. -- The component rendered within the `div` element was also updated, the plugin now uses `MetaBlockFieldWithData`. This will be updated every time the original data changes. +- The `MetaBlockField` component will be updated every time the original data changes. - [getEditedPostAttribute](/docs/reference-guides/data/data-core-editor.md#geteditedpostattribute) is used to retrieve data instead of [getCurrentPost](/docs/reference-guides/data/data-core-editor.md#getcurrentpost) because it returns the most recent values of the post, including user editions that haven't been yet saved. Update the code and open the sidebar. The input's content is no longer `Initial value` but a void string. Users can't type values yet, but let's check that the component is updated if the value in the store changes. Open the browser's console, execute diff --git a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md index 4fadd285f6942..9a5a8c8be78df 100644 --- a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md +++ b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md @@ -1,8 +1,8 @@ # Update the Meta Field When the Input's Content Changes -The last step in the journey is to update the meta field when the input content changes. To do that, you'll use another utility from the `@wordpress/data` package, [withDispatch](/packages/data/README.md#withdispatch-mapdispatchtoprops-function-function). +The last step in the journey is to update the meta field when the input content changes. To do that, you'll use another utility from the `@wordpress/data` package, [useDispatch](/packages/data/README.md#usedispatch). These utilities are also known as _hooks_. -`withDispatch` works similarly to `withSelect`. It takes two functions, the first returns an object with data, and the second takes that data object as input and returns a new UI component. Let's see how to use it: +The `useDispatch` takes store name as its only argument and returns methods that you can use to update the store. ```js ( function ( wp ) { @@ -10,44 +10,27 @@ The last step in the journey is to update the meta field when the input content var PluginSidebar = wp.editPost.PluginSidebar; var el = wp.element.createElement; var Text = wp.components.TextControl; - var withSelect = wp.data.withSelect; - var withDispatch = wp.data.withDispatch; - - var mapSelectToProps = function ( select ) { - return { - metaFieldValue: select( 'core/editor' ).getEditedPostAttribute( - 'meta' - )[ 'sidebar_plugin_meta_block_field' ], - }; - }; - - var mapDispatchToProps = function ( dispatch ) { - return { - setMetaFieldValue: function ( value ) { - dispatch( 'core/editor' ).editPost( { - meta: { sidebar_plugin_meta_block_field: value }, - } ); - }, - }; - }; + var useSelect = wp.data.useSelect; + var useDispatch = wp.data.useDispatch; var MetaBlockField = function ( props ) { + var metaFieldValue = useSelect( function ( select ) { + return select( 'core/editor' ).getEditedPostAttribute( 'meta' )[ 'sidebar_plugin_meta_block_field' ]; + }, [] ); + + var editPost = useDispatch( 'core/editor' ).editPost; + return el( Text, { label: 'Meta Block Field', - value: props.metaFieldValue, + value: metaFieldValue, onChange: function ( content ) { - props.setMetaFieldValue( content ); + editPost( { + meta: { sidebar_plugin_meta_block_field: content }, + } ); }, } ); }; - var MetaBlockFieldWithData = withSelect( mapSelectToProps )( - MetaBlockField - ); - var MetaBlockFieldWithDataAndActions = withDispatch( mapDispatchToProps )( - MetaBlockFieldWithData - ); - registerPlugin( 'my-plugin-sidebar', { render: function () { return el( @@ -60,7 +43,7 @@ The last step in the journey is to update the meta field when the input content el( 'div', { className: 'plugin-sidebar-content' }, - el( MetaBlockFieldWithDataAndActions ) + el( MetaBlockField ) ) ); }, @@ -70,9 +53,8 @@ The last step in the journey is to update the meta field when the input content Here's how it changed from the previous section: -- Added a new `mapDispatchToProps` function that will be passed to `withDispatch`. It takes `dispatch` as input and returns an object containing functions to update the internal data structures of the editor. These functions are also known as _actions_. -- By calling `setMetaFieldValue` every time the user types something within the input control, we're effectively updating the editor store on each key stroke. -- The `props` argument to the `MetaBlockField` component contains now the data passed by `mapSelectToProps` and the actions passed by `mapDispatchToProps`. +- The component now use `editPost` function returned by `useDispatch` hook. These functions are also known as _actions_. +- By calling `editPost` every time the user types something within the input control, we're effectively updating the editor store on each key stroke. Copy this new code to the JavaScript file, load the sidebar and see how the input value gets updated as you type. You may want to check that the internal data structures are updated as well. Type something in the input control, and execute the following instruction in your browser's console: diff --git a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md b/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md deleted file mode 100644 index cd2d2f2d45c01..0000000000000 --- a/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md +++ /dev/null @@ -1,199 +0,0 @@ -# Finishing Touches - -Your JavaScript code now works as expected, here are a few ways to simplify and make it easier to change in the future. - -The first step is to convert the functions `mapSelectToProps` and `mapDispatchToProps` to anonymous functions that get passed directly to `withSelect` and `withData`, respectively: - -```js -( function ( wp ) { - var registerPlugin = wp.plugins.registerPlugin; - var PluginSidebar = wp.editPost.PluginSidebar; - var el = wp.element.createElement; - var Text = wp.components.TextControl; - var withSelect = wp.data.withSelect; - var withDispatch = wp.data.withDispatch; - - var MetaBlockField = function ( props ) { - return el( Text, { - label: 'Meta Block Field', - value: props.metaFieldValue, - onChange: function ( content ) { - props.setMetaFieldValue( content ); - }, - } ); - }; - - var MetaBlockFieldWithData = withSelect( function ( select ) { - return { - metaFieldValue: select( 'core/editor' ).getEditedPostAttribute( - 'meta' - )[ 'sidebar_plugin_meta_block_field' ], - }; - } )( MetaBlockField ); - - var MetaBlockFieldWithDataAndActions = withDispatch( function ( dispatch ) { - return { - setMetaFieldValue: function ( value ) { - dispatch( 'core/editor' ).editPost( { - meta: { sidebar_plugin_meta_block_field: value }, - } ); - }, - }; - } )( MetaBlockFieldWithData ); - - registerPlugin( 'my-plugin-sidebar', { - render: function () { - return el( - PluginSidebar, - { - name: 'my-plugin-sidebar', - icon: 'admin-post', - title: 'My plugin sidebar', - }, - el( - 'div', - { className: 'plugin-sidebar-content' }, - el( MetaBlockFieldWithDataAndActions ) - ) - ); - }, - } ); -} )( window.wp ); -``` - -Next, merge `MetaBlockField`, `MetaBlockFieldWithData`, and `MetaBlockFieldWithDataAndActions` into one function called `MetaBlockField` that gets passed to the `div` element. The `@wordpress/compose` package offers an utility to concatenate functions called `compose`. Don't forget adding `wp-compose` to the dependencies array in the PHP script. - -```js -( function ( wp ) { - var registerPlugin = wp.plugins.registerPlugin; - var PluginSidebar = wp.editPost.PluginSidebar; - var el = wp.element.createElement; - var Text = wp.components.TextControl; - var withSelect = wp.data.withSelect; - var withDispatch = wp.data.withDispatch; - var compose = wp.compose.compose; - - var MetaBlockField = compose( - withDispatch( function ( dispatch ) { - return { - setMetaFieldValue: function ( value ) { - dispatch( 'core/editor' ).editPost( { - meta: { sidebar_plugin_meta_block_field: value }, - } ); - }, - }; - } ), - withSelect( function ( select ) { - return { - metaFieldValue: select( 'core/editor' ).getEditedPostAttribute( - 'meta' - )[ 'sidebar_plugin_meta_block_field' ], - }; - } ) - )( function ( props ) { - return el( Text, { - label: 'Meta Block Field', - value: props.metaFieldValue, - onChange: function ( content ) { - props.setMetaFieldValue( content ); - }, - } ); - } ); - - registerPlugin( 'my-plugin-sidebar', { - render: function () { - return el( - PluginSidebar, - { - name: 'my-plugin-sidebar', - icon: 'admin-post', - title: 'My plugin sidebar', - }, - el( - 'div', - { className: 'plugin-sidebar-content' }, - el( MetaBlockField ) - ) - ); - }, - } ); -} )( window.wp ); -``` - -Finally, extract the meta field name (`sidebar_plugin_meta_block_field`) from the `withSelect` and `withDispatch` functions to a single place, so it's easier to change in the future. You can leverage the fact that `withSelect` and `withDispatch` first functions can take the props of the UI component they wrap as a second argument. For example: - -```js -// ... -var MetaBlockFieldWithData = withSelect( function ( select, props ) { - console.log( props.metaFieldName ); -} )( MetaBlockField ); - -// ... -el( MetaBlockFieldWithData, { - metaFieldName: 'sidebar_plugin_meta_block_field', -} ); -// ... -``` - -Notice how the `metaFieldName` can be accessed within `withSelect`. Let's change the code to take advantage of that: - -```js -( function ( wp ) { - var registerPlugin = wp.plugins.registerPlugin; - var PluginSidebar = wp.editPost.PluginSidebar; - var el = wp.element.createElement; - var Text = wp.components.TextControl; - var withSelect = wp.data.withSelect; - var withDispatch = wp.data.withDispatch; - var compose = wp.compose.compose; - - var MetaBlockField = compose( - withDispatch( function ( dispatch, props ) { - return { - setMetaFieldValue: function ( value ) { - dispatch( 'core/editor' ).editPost( { - meta: { [ props.fieldName ]: value }, - } ); - }, - }; - } ), - withSelect( function ( select, props ) { - return { - metaFieldValue: select( 'core/editor' ).getEditedPostAttribute( - 'meta' - )[ props.fieldName ], - }; - } ) - )( function ( props ) { - return el( Text, { - label: 'Meta Block Field', - value: props.metaFieldValue, - onChange: function ( content ) { - props.setMetaFieldValue( content ); - }, - } ); - } ); - - registerPlugin( 'my-plugin-sidebar', { - render: function () { - return el( - PluginSidebar, - { - name: 'my-plugin-sidebar', - icon: 'admin-post', - title: 'My plugin sidebar', - }, - el( - 'div', - { className: 'plugin-sidebar-content' }, - el( MetaBlockField, { - fieldName: 'sidebar_plugin_meta_block_field', - } ) - ) - ); - }, - } ); -} )( window.wp ); -``` - -That's it. You have now a compact version of the original code. Go ahead and add more functionality to your plugin, review other tutorials, or create your next gig! diff --git a/docs/how-to-guides/themes/create-block-theme.md b/docs/how-to-guides/themes/create-block-theme.md index 72fea1cb8ec97..45df340c1e19d 100644 --- a/docs/how-to-guides/themes/create-block-theme.md +++ b/docs/how-to-guides/themes/create-block-theme.md @@ -628,7 +628,7 @@ To add custom font sizes, create a new section called `typography` under `settin `fontSizes` is the equivalent of `add_theme_support( 'editor-font-sizes' )`. ```json -"typograhy": { +"typography": { "fontSizes": [ ] } @@ -641,7 +641,7 @@ The keys used by `fontSizes` are: - `name` The visible name in the editor. ```json -"typograhy": { +"typography": { "fontSizes": [ { "slug": "normal", diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index 460a43ba9161e..6335a614f9b3c 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -222,13 +222,15 @@ The settings section has the following structure: "customWidth": false }, "color": { + "background": true, "custom": true, "customDuotone": true, "customGradient": true, "duotone": [], "gradients": [], "link": false, - "palette": [] + "palette": [], + "text": true }, "custom": {}, "layout": { @@ -615,7 +617,6 @@ Each block declares which style properties it exposes via the [block supports me "text": "value" }, "spacing": { - "blockGap": "value", "margin": { "top": "value", "right": "value", diff --git a/docs/how-to-guides/widgets/legacy-widget-block.md b/docs/how-to-guides/widgets/legacy-widget-block.md index 7e629d482688c..e349829acb25c 100644 --- a/docs/how-to-guides/widgets/legacy-widget-block.md +++ b/docs/how-to-guides/widgets/legacy-widget-block.md @@ -33,7 +33,7 @@ Note that all of the widget's event handlers are added in the `widget-added` cal The Legacy Widget block will display a preview of the widget when the Legacy Widget block is not selected. -A "No preview available." message is automatically shown by the Legacy Widget block when the widget's `widget()` function does not render anytihng or only renders empty HTML elements. +A "No preview available." message is automatically shown by the Legacy Widget block when the widget's `widget()` function does not render anything or only renders empty HTML elements. Widgets may take advantage of this by returning early from `widget()` when a preview should not be displayed. diff --git a/docs/manifest.json b/docs/manifest.json index e424b1381a3c6..a07a780786517 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -239,12 +239,6 @@ "markdown_source": "../docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md", "parent": "plugin-sidebar-0" }, - { - "title": "Finishing Touches", - "slug": "plugin-sidebar-6-finishing-touches", - "markdown_source": "../docs/how-to-guides/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md", - "parent": "plugin-sidebar-0" - }, { "title": "Blocks", "slug": "block-tutorial", @@ -2021,6 +2015,12 @@ "markdown_source": "../docs/contributors/code/native-mobile.md", "parent": "code" }, + { + "title": "React Native Integration Test Guide", + "slug": "native-mobile-integration-test-guide", + "markdown_source": "../docs/contributors/code/native-mobile-integration-test-guide.md", + "parent": "code" + }, { "title": "Getting Started for the React Native based Mobile Gutenberg", "slug": "getting-started-native-mobile", diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md index 1078608345e52..d1de0e98fa12a 100644 --- a/docs/reference-guides/block-api/block-transforms.md +++ b/docs/reference-guides/block-api/block-transforms.md @@ -42,7 +42,7 @@ A transformation of type `block` is an object that takes the following parameter - **type** _(string)_: the value `block`. - **blocks** _(array)_: a list of known block types. It also accepts the wildcard value (`"*"`), meaning that the transform is available to _all_ block types (eg: all blocks can transform into `core/group`). - **transform** _(function)_: a callback that receives the attributes and inner blocks of the block being processed. It should return a block object or an array of block objects. -- **isMatch** _(function, optional)_: a callback that receives the block attributes and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user. +- **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user. - **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If true, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. False by default. - **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set. diff --git a/docs/reference-guides/block-api/block-variations.md b/docs/reference-guides/block-api/block-variations.md index 2c9e6b3bc6520..4aa2122dcfd76 100644 --- a/docs/reference-guides/block-api/block-variations.md +++ b/docs/reference-guides/block-api/block-variations.md @@ -45,7 +45,7 @@ An object describing a variation defined for the block type can contain the foll - `example` (optional, type `Object`) – Example provides structured data for the block preview. You can set to `undefined` to disable the preview shown for the block type. - `scope` (optional, type `WPBlockVariationScope[]`) - the list of scopes where the variation is applicable. When not provided, it defaults to `block` and `inserter`. Available options: - `inserter` - Block Variation is shown on the inserter. - - `block` - Used by blocks to filter specific block variations. Mostly used in Placeholder patterns like `Columns` block. + - `block` - Used by blocks to filter specific block variations. `Columns` and `Query Loop` blocks have such variations and are passed to the [experimental BlockVariationPicker](/packages/block-editor/src/components/block-variation-picker/README.md) component, which is handling the displaying of variations and the ability to select one from them. - `transform` - Block Variation will be shown in the component for Block Variations transformations. - `keywords` (optional, type `string[]`) - An array of terms (which can be translated) that help users discover the variation while searching. - `isActive` (optional, type `Function|string[]`) - This can be a function or an array of block attributes. Function that accepts a block's attributes and the variation's attributes and determines if a variation is active. This function doesn't try to find a match dynamically based on all block's attributes, as in many cases some attributes are irrelevant. An example would be for `embed` block where we only care about `providerNameSlug` attribute's value. We can also use a `string[]` to tell which attributes should be compared as a shorthand. Each attributes will be matched and the variation will be active if all of them are matching. diff --git a/docs/reference-guides/data/data-core-block-editor.md b/docs/reference-guides/data/data-core-block-editor.md index 3cc75b5048168..9f07302f001a8 100644 --- a/docs/reference-guides/data/data-core-block-editor.md +++ b/docs/reference-guides/data/data-core-block-editor.md @@ -281,12 +281,6 @@ Returns all block objects for the current post being edited as an array in the order they appear in the post. Note that this will exclude child blocks of nested inner block controllers. -Note: It's important to memoize this selector to avoid return a new instance -on each call. We use the block cache state for each top-level block of the -given clientID. This way, the selector only refreshes on changes to blocks -associated with the given entity, and does not refresh when changes are made -to blocks which are part of different inner block controllers. - _Parameters_ - _state_ `Object`: Editor state. @@ -1221,6 +1215,8 @@ _Parameters_ ### receiveBlocks +> **Deprecated** + Returns an action object used in signalling that blocks have been received. Unlike resetBlocks, these should be appended to the existing known set, not replacing. diff --git a/docs/reference-guides/data/data-core-edit-post.md b/docs/reference-guides/data/data-core-edit-post.md index 2348d6544020d..72a76d56fad15 100644 --- a/docs/reference-guides/data/data-core-edit-post.md +++ b/docs/reference-guides/data/data-core-edit-post.md @@ -6,6 +6,18 @@ Namespace: `core/edit-post`. <!-- START TOKEN(Autogenerated selectors|../../../packages/edit-post/src/store/selectors.js) --> +### areMetaBoxesInitialized + +Returns true if meta boxes are initialized. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether meta boxes are initialized. + ### getActiveGeneralSidebarName Returns the current active general sidebar name, or null if there is no @@ -351,6 +363,10 @@ _Returns_ - `Object`: Action object. +### initializeMetaBoxes + +Initializes WordPress `postboxes` script and the logic for saving meta boxes. + ### metaBoxUpdatesFailure Returns an action object used to signal a failed meta box update. @@ -502,16 +518,12 @@ _Returns_ ### toggleFeature -Returns an action object used to toggle a feature flag. +Triggers an action used to toggle a feature flag. _Parameters_ - _feature_ `string`: Feature name. -_Returns_ - -- `Object`: Action object. - ### togglePinnedPluginItem Triggers an action object used to toggle a plugin name flag. diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index 7ad08fa7f9ff2..16f4d6305fecf 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -535,7 +535,7 @@ _Parameters_ - _recordId_ `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 control descriptor. +- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. ### editEntityRecord @@ -680,6 +680,10 @@ _Returns_ Action triggered to redo the last undoed edit to an entity record, if any. +_Returns_ + +- `undefined`: + ### saveEditedEntityRecord Action triggered to save an entity record's edits. @@ -702,11 +706,15 @@ _Parameters_ - _record_ `Object`: Record to be saved. - _options_ `Object`: Saving options. - _options.isAutosave_ `[boolean]`: Whether this is an autosave. -- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a control descriptor. +- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. ### undo Action triggered to undo the last edit to an entity record, if any. +_Returns_ + +- `undefined`: + <!-- END TOKEN(Autogenerated actions|../../../packages/core-data/src/actions.js) --> diff --git a/docs/toc.json b/docs/toc.json index c8ce6140ef94f..875e514901517 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -98,9 +98,6 @@ }, { "docs/how-to-guides/sidebar-tutorial/plugin-sidebar-5-update-meta.md": [] - }, - { - "docs/how-to-guides/sidebar-tutorial/plugin-sidebar-6-finishing-touches.md": [] } ] }, @@ -178,11 +175,13 @@ }, { "docs/how-to-guides/accessibility.md": [] }, { "docs/how-to-guides/internationalization.md": [] }, - { "docs/how-to-guides/widgets/README.md": [ - { "docs/how-to-guides/widgets/overview.md": [] }, - { "docs/how-to-guides/widgets/opting-out.md": [] }, - { "docs/how-to-guides/widgets/legacy-widget-block.md": [] } - ] } + { + "docs/how-to-guides/widgets/README.md": [ + { "docs/how-to-guides/widgets/overview.md": [] }, + { "docs/how-to-guides/widgets/opting-out.md": [] }, + { "docs/how-to-guides/widgets/legacy-widget-block.md": [] } + ] + } ] }, { @@ -318,6 +317,9 @@ { "docs/contributors/code/managing-packages.md": [] }, { "docs/contributors/code/release.md": [] }, { "docs/contributors/code/native-mobile.md": [] }, + { + "docs/contributors/code/native-mobile-integration-test-guide.md": [] + }, { "docs/contributors/code/getting-started-native-mobile.md": [] } diff --git a/gutenberg.php b/gutenberg.php index 63d4d1c4eb095..5e8552586f685 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,9 +3,9 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Requires at least: 5.6 + * Requires at least: 5.7 * Requires PHP: 5.6 - * Version: 11.3.0 + * Version: 11.5.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * @@ -26,7 +26,7 @@ function gutenberg_wordpress_version_notice() { echo '<div class="error"><p>'; /* translators: %s: Minimum required version */ - printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.6' ); + printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.7' ); echo '</p></div>'; deactivate_plugins( array( 'gutenberg/gutenberg.php' ) ); @@ -64,7 +64,7 @@ function gutenberg_pre_init() { // Compare against major release versions (X.Y) rather than minor (X.Y.Z) // unless a minor release is the actual minimum requirement. WordPress reports // X.Y for its major releases. - if ( version_compare( $version, '5.6', '<' ) ) { + if ( version_compare( $version, '5.7', '<' ) ) { add_action( 'admin_notices', 'gutenberg_wordpress_version_notice' ); return; } diff --git a/lib/block-supports/align.php b/lib/block-supports/align.php deleted file mode 100644 index a33a7700a5d0b..0000000000000 --- a/lib/block-supports/align.php +++ /dev/null @@ -1,59 +0,0 @@ -<?php -/** - * Align block support flag. - * - * @package gutenberg - */ - -/** - * Registers the align block attribute for block types that support it. - * - * @param WP_Block_Type $block_type Block Type. - */ -function gutenberg_register_alignment_support( $block_type ) { - $has_align_support = gutenberg_block_has_support( $block_type, array( 'align' ), false ); - if ( $has_align_support ) { - if ( ! $block_type->attributes ) { - $block_type->attributes = array(); - } - - if ( ! array_key_exists( 'align', $block_type->attributes ) ) { - $block_type->attributes['align'] = array( - 'type' => 'string', - 'enum' => array( 'left', 'center', 'right', 'wide', 'full', '' ), - ); - } - } -} - -/** - * Add CSS classes for block alignment to the incoming attributes array. - * This will be applied to the block markup in the front-end. - * - * @param WP_Block_Type $block_type Block Type. - * @param array $block_attributes Block attributes. - * - * @return array Block alignment CSS classes and inline styles. - */ -function gutenberg_apply_alignment_support( $block_type, $block_attributes ) { - $attributes = array(); - $has_align_support = gutenberg_block_has_support( $block_type, array( 'align' ), false ); - if ( $has_align_support ) { - $has_block_alignment = array_key_exists( 'align', $block_attributes ); - - if ( $has_block_alignment ) { - $attributes['class'] = sprintf( 'align%s', $block_attributes['align'] ); - } - } - - return $attributes; -} - -// Register the block support. -WP_Block_Supports::get_instance()->register( - 'align', - array( - 'register_attribute' => 'gutenberg_register_alignment_support', - 'apply' => 'gutenberg_apply_alignment_support', - ) -); diff --git a/lib/block-supports/custom-classname.php b/lib/block-supports/custom-classname.php deleted file mode 100644 index 612eaf30bd50c..0000000000000 --- a/lib/block-supports/custom-classname.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php -/** - * Custom classname block support flag. - * - * @package gutenberg - */ - -/** - * Registers the custom classname block attribute for block types that support it. - * - * @param WP_Block_Type $block_type Block Type. - */ -function gutenberg_register_custom_classname_support( $block_type ) { - $has_custom_classname_support = gutenberg_block_has_support( $block_type, array( 'customClassName' ), true ); - - if ( $has_custom_classname_support ) { - if ( ! $block_type->attributes ) { - $block_type->attributes = array(); - } - - if ( ! array_key_exists( 'className', $block_type->attributes ) ) { - $block_type->attributes['className'] = array( - 'type' => 'string', - ); - } - } -} - -/** - * Add the custom classnames to the output. - * - * @param WP_Block_Type $block_type Block Type. - * @param array $block_attributes Block attributes. - * - * @return array Block CSS classes and inline styles. - */ -function gutenberg_apply_custom_classname_support( $block_type, $block_attributes ) { - $has_custom_classname_support = gutenberg_block_has_support( $block_type, array( 'customClassName' ), true ); - $attributes = array(); - if ( $has_custom_classname_support ) { - $has_custom_classnames = array_key_exists( 'className', $block_attributes ); - - if ( $has_custom_classnames ) { - $attributes['class'] = $block_attributes['className']; - } - } - - return $attributes; -} - -// Register the block support. -WP_Block_Supports::get_instance()->register( - 'custom-classname', - array( - 'register_attribute' => 'gutenberg_register_custom_classname_support', - 'apply' => 'gutenberg_apply_custom_classname_support', - ) -); diff --git a/lib/block-supports/generated-classname.php b/lib/block-supports/generated-classname.php deleted file mode 100644 index 8be75f6431fb2..0000000000000 --- a/lib/block-supports/generated-classname.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php -/** - * Generated classname block support flag. - * - * @package gutenberg - */ - -/** - * Get the generated classname from a given block name. - * - * @param string $block_name Block Name. - * @return string Generated classname. - */ -function gutenberg_get_block_default_classname( $block_name ) { - // Generated HTML classes for blocks follow the `wp-block-{name}` nomenclature. - // Blocks provided by WordPress drop the prefixes 'core/' or 'core-' (historically used in 'core-embed/'). - $classname = 'wp-block-' . preg_replace( - '/^core-/', - '', - str_replace( '/', '-', $block_name ) - ); - - /** - * Filters the default block className for server rendered blocks. - * - * @param string $class_name The current applied classname. - * @param string $block_name The block name. - */ - $classname = apply_filters( 'block_default_classname', $classname, $block_name ); - - return $classname; -} - -/** - * Add the generated classnames to the output. - * - * @param WP_Block_Type $block_type Block Type. - * - * @return array Block CSS classes and inline styles. - */ -function gutenberg_apply_generated_classname_support( $block_type ) { - $attributes = array(); - $has_generated_classname_support = gutenberg_block_has_support( $block_type, array( 'className' ), true ); - if ( $has_generated_classname_support ) { - $block_classname = gutenberg_get_block_default_classname( $block_type->name ); - - if ( $block_classname ) { - $attributes['class'] = $block_classname; - } - } - - return $attributes; -} - -// Register the block support. -WP_Block_Supports::get_instance()->register( - 'generated-classname', - array( - 'apply' => 'gutenberg_apply_generated_classname_support', - ) -); diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index 56d893ae7d3df..7b43e37a3cc8b 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -28,12 +28,13 @@ function gutenberg_register_layout_support( $block_type ) { /** * Generates the CSS corresponding to the provided layout. * - * @param string $selector CSS selector. - * @param array $layout Layout object. + * @param string $selector CSS selector. + * @param array $layout Layout object. The one that is passed has already checked the existance of default block layout. + * @param boolean $has_block_gap_support Whether the theme has support for the block gap. * * @return string CSS style. */ -function gutenberg_get_layout_style( $selector, $layout ) { +function gutenberg_get_layout_style( $selector, $layout, $has_block_gap_support = false ) { $layout_type = isset( $layout['type'] ) ? $layout['type'] : 'default'; $style = ''; @@ -63,13 +64,34 @@ function gutenberg_get_layout_style( $selector, $layout ) { $style .= "$selector .alignleft { float: left; margin-right: 2em; }"; $style .= "$selector .alignright { float: right; margin-left: 2em; }"; - $style .= "$selector > * + * { margin-top: var( --wp--style--block-gap ); margin-bottom: 0; }"; + if ( $has_block_gap_support ) { + $style .= "$selector > * + * { margin-top: var( --wp--style--block-gap ); margin-bottom: 0; }"; + } } elseif ( 'flex' === $layout_type ) { + $justify_content_options = array( + 'left' => 'flex-start', + 'right' => 'flex-end', + 'center' => 'center', + 'space-between' => 'space-between', + ); + $style = "$selector {"; $style .= 'display: flex;'; - $style .= 'gap: var( --wp--style--block-gap, 0.5em );'; + if ( $has_block_gap_support ) { + $style .= 'gap: var( --wp--style--block-gap, 0.5em );'; + } else { + $style .= 'gap: 0.5em;'; + } $style .= 'flex-wrap: wrap;'; $style .= 'align-items: center;'; + /** + * Add this style only if is not empty for backwards compatibility, + * since we intend to convert blocks that had flex layout implemented + * by custom css. + */ + if ( ! empty( $layout['justifyContent'] ) && array_key_exists( $layout['justifyContent'], $justify_content_options ) ) { + $style .= "justify-content: {$justify_content_options[ $layout['justifyContent'] ]};"; + } $style .= '}'; $style .= "$selector > * { margin: 0; }"; @@ -93,11 +115,13 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { return $block_content; } - $default_block_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() ); - $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout; + $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( array(), 'theme' ); + $theme_settings = $tree->get_settings(); + $default_layout = _wp_array_get( $theme_settings, array( 'layout' ) ); + $has_block_gap_support = isset( $theme_settings['spacing']['blockGap'] ) ? null !== $theme_settings['spacing']['blockGap'] : false; + $default_block_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout', 'default' ), array() ); + $used_layout = isset( $block['attrs']['layout'] ) ? $block['attrs']['layout'] : $default_block_layout; if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] ) { - $tree = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( array(), 'theme' ); - $default_layout = _wp_array_get( $tree->get_settings(), array( 'layout' ) ); if ( ! $default_layout ) { return $block_content; } @@ -105,7 +129,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { } $id = uniqid(); - $style = gutenberg_get_layout_style( ".wp-container-$id", $used_layout ); + $style = gutenberg_get_layout_style( ".wp-container-$id", $used_layout, $has_block_gap_support ); // This assumes the hook only applies to blocks with a single wrapper. // I think this is a reasonable limitation for that particular hook. $content = preg_replace( @@ -151,11 +175,11 @@ function () use ( $style ) { */ function gutenberg_restore_group_inner_container( $block_content, $block ) { $group_with_inner_container_regex = '/(^\s*<div\b[^>]*wp-block-group(\s|")[^>]*>)(\s*<div\b[^>]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/'; - if ( 'core/group' !== $block['blockName'] || WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() || - 1 === preg_match( $group_with_inner_container_regex, $block_content ) + 1 === preg_match( $group_with_inner_container_regex, $block_content ) || + ( isset( $block['attrs']['layout']['type'] ) && 'default' !== $block['attrs']['layout']['type'] ) ) { return $block_content; } @@ -171,7 +195,8 @@ function( $matches ) { return $updated_content; } -// This can be removed when plugin support requires WordPress 5.8.0+. -if ( ! function_exists( 'wp_restore_group_inner_container' ) ) { - add_filter( 'render_block', 'gutenberg_restore_group_inner_container', 10, 2 ); +if ( function_exists( 'wp_restore_group_inner_container' ) ) { + remove_filter( 'render_block', 'wp_restore_group_inner_container', 10, 2 ); } +add_filter( 'render_block', 'gutenberg_restore_group_inner_container', 10, 2 ); + diff --git a/lib/block-supports/spacing.php b/lib/block-supports/spacing.php index 78f5b59f90fb0..ca7b77f43864b 100644 --- a/lib/block-supports/spacing.php +++ b/lib/block-supports/spacing.php @@ -90,6 +90,57 @@ function gutenberg_skip_spacing_serialization( $block_type ) { $spacing_support['__experimentalSkipSerialization']; } + +/** + * Renders the spacing gap support to the block wrapper, to ensure + * that the CSS variable is rendered in all environments. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_spacing_gap_support( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $has_gap_support = gutenberg_block_has_support( $block_type, array( 'spacing', 'blockGap' ), false ); + if ( ! $has_gap_support || ! isset( $block['attrs']['style']['spacing']['blockGap'] ) ) { + return $block_content; + } + + $gap_value = $block['attrs']['style']['spacing']['blockGap']; + + // Skip if gap value contains unsupported characters. + // Regex for CSS value borrowed from `safecss_filter_attr`, and used here + // because we only want to match against the value, not the CSS attribute. + if ( preg_match( '%[\\\(&=}]|/\*%', $gap_value ) ) { + return $block_content; + } + + $style = sprintf( + '--wp--style--block-gap: %s', + esc_attr( $gap_value ) + ); + + // Attempt to update an existing style attribute on the wrapper element. + $injected_style = preg_replace( + '/^([^>.]+?)(' . preg_quote( 'style="', '/' ) . ')(?=.+?>)/', + '$1$2' . $style . '; ', + $block_content, + 1 + ); + + // If there is no existing style attribute, add one to the wrapper element. + if ( $injected_style === $block_content ) { + $injected_style = preg_replace( + '/<([a-zA-Z0-9]+)([ >])/', + '<$1 style="' . $style . '"$2', + $block_content, + 1 + ); + }; + + return $injected_style; +} + // Register the block support. WP_Block_Supports::get_instance()->register( 'spacing', @@ -98,3 +149,5 @@ function gutenberg_skip_spacing_serialization( $block_type ) { 'apply' => 'gutenberg_apply_spacing_support', ) ); + +add_filter( 'render_block', 'gutenberg_render_spacing_gap_support', 10, 2 ); diff --git a/lib/blocks.php b/lib/blocks.php index cc3f4c8662254..77dcaae989b55 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -106,9 +106,11 @@ function gutenberg_reregister_core_block_types() { __DIR__ . '/../build/widgets/blocks/' => array( 'block_folders' => array( 'legacy-widget', + 'widget-group', ), 'block_names' => array( 'legacy-widget.php' => 'core/legacy-widget', + 'widget-group.php' => 'core/widget-group', ), ), ); diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 1b96923323b46..29cca882ffa13 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -85,6 +85,7 @@ class WP_Theme_JSON_Gutenberg { 'customWidth' => null, ), 'color' => array( + 'background' => null, 'custom' => null, 'customDuotone' => null, 'customGradient' => null, @@ -92,6 +93,7 @@ class WP_Theme_JSON_Gutenberg { 'gradients' => null, 'link' => null, 'palette' => null, + 'text' => null, ), 'custom' => null, 'layout' => array( @@ -99,9 +101,11 @@ class WP_Theme_JSON_Gutenberg { 'wideSize' => null, ), 'spacing' => array( + 'blockGap' => null, 'customMargin' => null, 'customPadding' => null, 'units' => null, + 'blockGap' => null, ), 'typography' => array( 'customFontSize' => null, @@ -548,8 +552,9 @@ private static function compute_style_properties( $styles ) { foreach ( self::PROPERTIES_METADATA as $css_property => $value_path ) { $value = self::get_property_value( $styles, $value_path ); - // Skip if empty or value represents array of longhand values. - if ( empty( $value ) || is_array( $value ) ) { + // Skip if empty and not "0" or value represents array of longhand values. + $has_missing_value = empty( $value ) && ! is_numeric( $value ); + if ( $has_missing_value || is_array( $value ) ) { continue; } @@ -798,8 +803,36 @@ private function get_css_variables( $nodes ) { * style-property-one: value; * } * - * Additionally, it'll also create new rulesets - * as classes for each preset value such as: + * @param array $style_nodes Nodes with styles. + * + * @return string The new stylesheet. + */ + private function get_block_classes( $style_nodes ) { + $block_rules = ''; + + foreach ( $style_nodes as $metadata ) { + if ( null === $metadata['selector'] ) { + continue; + } + + $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); + $selector = $metadata['selector']; + $declarations = self::compute_style_properties( $node ); + $block_rules .= self::to_ruleset( $selector, $declarations ); + + if ( self::ROOT_BLOCK_SELECTOR === $selector ) { + $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; + if ( $has_block_gap_support ) { + $block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); margin-bottom: 0; }'; + } + } + } + + return $block_rules; + } + + /** + * Creates new rulesets as classes for each preset value such as: * * .has-value-color { * color: value; @@ -820,30 +853,14 @@ private function get_css_variables( $nodes ) { * p.has-value-gradient-background { * background: value; * } - * - * @param array $style_nodes Nodes with styles. + * @param array $setting_nodes Nodes with settings. * * @return string The new stylesheet. */ - private function get_block_styles( $style_nodes, $setting_nodes ) { - $block_rules = ''; - foreach ( $style_nodes as $metadata ) { - if ( null === $metadata['selector'] ) { - continue; - } - - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $selector = $metadata['selector']; - $declarations = self::compute_style_properties( $node ); - $block_rules .= self::to_ruleset( $selector, $declarations ); - - if ( self::ROOT_BLOCK_SELECTOR === $selector ) { - $block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); margin-bottom: 0; }'; - } - } - + private function get_preset_classes( $setting_nodes ) { $preset_rules = ''; + foreach ( $setting_nodes as $metadata ) { if ( null === $metadata['selector'] ) { continue; @@ -854,7 +871,7 @@ private function get_block_styles( $style_nodes, $setting_nodes ) { $preset_rules .= self::compute_preset_classes( $node, $selector ); } - return $block_rules . $preset_rules; + return $preset_rules; } /** @@ -1052,7 +1069,11 @@ private static function get_setting_nodes( $theme_json, $selectors = array() ) { * Returns the stylesheet that results of processing * the theme.json structure this object represents. * - * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'. + * @param string $type Type of stylesheet. It accepts: + * 'all': css variables, block classes, preset classes. The default. + * 'block_styles': only block & preset classes. + * 'css_variables': only css variables. + * 'presets': only css variables and preset classes. * @return string Stylesheet. */ public function get_stylesheet( $type = 'all' ) { @@ -1062,11 +1083,13 @@ public function get_stylesheet( $type = 'all' ) { switch ( $type ) { case 'block_styles': - return $this->get_block_styles( $style_nodes, $setting_nodes ); + return $this->get_block_classes( $style_nodes ) . $this->get_preset_classes( $setting_nodes ); case 'css_variables': return $this->get_css_variables( $setting_nodes ); + case 'presets': + return $this->get_css_variables( $setting_nodes ) . $this->get_preset_classes( $setting_nodes ); default: - return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes ); + return $this->get_css_variables( $setting_nodes ) . $this->get_block_classes( $style_nodes ) . $this->get_preset_classes( $setting_nodes ); } } diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index b96c43f8ddf4f..a208cbd726d88 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -407,10 +407,17 @@ public static function get_user_data() { * @return WP_Theme_JSON_Gutenberg */ public static function get_merged_data( $settings = array(), $origin = 'user' ) { - $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( $settings ); - $result = new WP_Theme_JSON_Gutenberg(); $result->merge( self::get_core_data() ); + + if ( + ! get_theme_support( 'experimental-link-color' ) && // link color support needs the presets CSS variables regardless of the presence of theme.json file. + ! WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() + ) { + return $result; + } + + $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( $settings ); $result->merge( self::get_theme_data( $theme_support_data ) ); if ( 'user' === $origin ) { diff --git a/lib/client-assets.php b/lib/client-assets.php index 603576ed45f5f..2f8d3ed48fa81 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -96,13 +96,6 @@ function gutenberg_override_script( $scripts, $handle, $src, $deps = array(), $v $scripts->set_translations( $handle, 'default' ); } - // Remove this check once the minimum supported WordPress version is at least 5.7. - if ( 'wp-i18n' === $handle ) { - $ltr = 'rtl' === _x( 'ltr', 'text direction', 'default' ) ? 'rtl' : 'ltr'; - $output = sprintf( "wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ '%s' ] }, 'default' );", $ltr ); - $scripts->add_inline_script( 'wp-i18n', $output, 'after' ); - } - /* * Wp-editor module is exposed as window.wp.editor. * Problem: there is quite some code expecting window.wp.oldEditor object available under window.wp.editor. @@ -223,18 +216,6 @@ function gutenberg_register_vendor_scripts( $scripts ) { 'https://unpkg.com/react-dom@17.0.1/umd/react-dom' . $react_suffix . '.js', array( 'react' ) ); - - /* - * This script registration and the corresponding function should be removed - * removed once the plugin is updated to support WordPress 5.7.0 and newer. - */ - gutenberg_register_vendor_script( - $scripts, - 'object-fit-polyfill', - 'https://unpkg.com/objectFitPolyfill@2.3.5/dist/objectFitPolyfill.min.js', - array(), - '2.3.5' - ); } add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts' ); @@ -797,31 +778,3 @@ function gutenberg_extend_block_editor_styles_html() { add_action( 'admin_footer-post.php', 'gutenberg_extend_block_editor_styles_html' ); add_action( 'admin_footer-post-new.php', 'gutenberg_extend_block_editor_styles_html' ); add_action( 'admin_footer-widgets.php', 'gutenberg_extend_block_editor_styles_html' ); - -/** - * Adds a polyfill for object-fit in environments which do not support it. - * - * The script registration occurs in `gutenberg_register_vendor_scripts`, which - * should be removed in coordination with this function. - * - * Remove this when the minimum supported version is WordPress 5.7 - * - * @see gutenberg_register_vendor_scripts - * @see https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit - * - * @since 9.1.0 - * - * @param WP_Scripts $scripts WP_Scripts object. - */ -function gutenberg_add_object_fit_polyfill( $scripts ) { - did_action( 'init' ) && $scripts->add_inline_script( - 'wp-polyfill', - wp_get_script_polyfill( - $scripts, - array( - '"objectFit" in document.documentElement.style' => 'object-fit-polyfill', - ) - ) - ); -} -add_action( 'wp_default_scripts', 'gutenberg_add_object_fit_polyfill', 20 ); diff --git a/lib/compat/wordpress-5.8.1/index.php b/lib/compat/wordpress-5.8.1/index.php new file mode 100644 index 0000000000000..d825f2d93c916 --- /dev/null +++ b/lib/compat/wordpress-5.8.1/index.php @@ -0,0 +1,9 @@ +<?php +/** + * Temporary compatibility shims for features present in Gutenberg. + * + * @package gutenberg + */ + +// Load the polyfills. +require_once __DIR__ . '/widget-render-api-endpoint/index.php'; diff --git a/lib/compat/wordpress-5.8.1/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php b/lib/compat/wordpress-5.8.1/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php new file mode 100644 index 0000000000000..6c8f2ee2a51e3 --- /dev/null +++ b/lib/compat/wordpress-5.8.1/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php @@ -0,0 +1,121 @@ +<?php +/** + * REST API: GB_REST_Widget_Render_Endpoint_Polyfill class + * + * @package gutenberg + */ + +/** + * Polyfill API class to render widgets via the REST API. + * + * @see \WP_REST_Controller + */ +class GB_REST_Widget_Render_Endpoint_Polyfill extends \WP_REST_Widget_Types_Controller { + + /** + * Registers the widget type routes. + * + * @see register_rest_route() + */ + public function register_routes() { + $route = '/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/render'; + + // Don't override if already registered. + $registered_routes = rest_get_server()->get_routes( 'wp/v2' ); + if ( array_key_exists( $route, $registered_routes ) ) { + return; + } + + register_rest_route( + $this->namespace, + $route, + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'callback' => array( $this, 'render' ), + 'args' => array( + 'id' => array( + 'description' => __( 'The widget type id.', 'default' ), + 'type' => 'string', + 'required' => true, + ), + 'instance' => array( + 'description' => __( 'Current instance settings of the widget.', 'default' ), + 'type' => 'object', + ), + ), + ), + ) + ); + } + + /** + * Renders a single Legacy Widget and wraps it in a JSON-encodable array. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return array An array with rendered Legacy Widget HTML. + */ + public function render( $request ) { + return array( + 'preview' => $this->render_legacy_widget_preview_iframe( + $request['id'], + isset( $request['instance'] ) ? $request['instance'] : null + ), + ); + } + + /** + * Renders a page containing a preview of the requested Legacy Widget block. + * + * @param string $id_base The id base of the requested widget. + * @param array $instance The widget instance attributes. + * + * @return string Rendered Legacy Widget block preview. + */ + private function render_legacy_widget_preview_iframe( $id_base, $instance ) { + if ( ! defined( 'IFRAME_REQUEST' ) ) { + define( 'IFRAME_REQUEST', true ); + } + + ob_start(); + ?> + <!doctype html> + <html <?php language_attributes(); ?>> + <head> + <meta charset="<?php bloginfo( 'charset' ); ?>" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="profile" href="https://gmpg.org/xfn/11" /> + <?php wp_head(); ?> + <style> + /* Reset theme styles */ + html, body, #page, #content { + padding: 0 !important; + margin: 0 !important; + } + </style> + </head> + <body <?php body_class(); ?>> + <div id="page" class="site"> + <div id="content" class="site-content"> + <?php + $registry = WP_Block_Type_Registry::get_instance(); + $block = $registry->get_registered( 'core/legacy-widget' ); + echo $block->render( + array( + 'idBase' => $id_base, + 'instance' => $instance, + ) + ); + ?> + </div><!-- #content --> + </div><!-- #page --> + <?php wp_footer(); ?> + </body> + </html> + <?php + return ob_get_clean(); + } + +} diff --git a/lib/compat/wordpress-5.8.1/widget-render-api-endpoint/index.php b/lib/compat/wordpress-5.8.1/widget-render-api-endpoint/index.php new file mode 100644 index 0000000000000..32e6106966a38 --- /dev/null +++ b/lib/compat/wordpress-5.8.1/widget-render-api-endpoint/index.php @@ -0,0 +1,22 @@ +<?php +/** + * Shims the /wp/v2/widget-types/<id>/render endpoint in WP versions where it's missing + * + * @package gutenberg + */ + +// Load the polyfill class. +require_once __DIR__ . '/class-gb-rest-widget-render-endpoint-polyfill.php'; + +/** + * Registers routes from the GB_REST_Widget_Render_Endpoint_Polyfill class. + */ +function setup_widget_render_api_endpoint_polyfill() { + $polyfill = new GB_REST_Widget_Render_Endpoint_Polyfill(); + $polyfill->register_routes(); +} + +// Priority should be larger than 99 which is the one used for registering the core routes. +add_action( 'rest_api_init', 'setup_widget_render_api_endpoint_polyfill', 100 ); + + diff --git a/lib/compat/wordpress-5.8/index.php b/lib/compat/wordpress-5.8/index.php index 29a97fb20de81..35b0f11eeae1f 100644 --- a/lib/compat/wordpress-5.8/index.php +++ b/lib/compat/wordpress-5.8/index.php @@ -164,3 +164,39 @@ function gutenberg_register_legacy_query_loop_block() { add_action( 'init', 'gutenberg_register_legacy_query_loop_block' ); } + +if ( ! function_exists( 'get_query_pagination_arrow' ) ) { + /** + * Helper function that returns the proper pagination arrow html for + * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based + * on the provided `paginationArrow` from `QueryPagination` context. + * + * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. + * + * @param WP_Block $block Block instance. + * @param boolean $is_next Flag for hanlding `next/previous` blocks. + * + * @return string|null Returns the constructed WP_Query arguments. + */ + function get_query_pagination_arrow( $block, $is_next ) { + $arrow_map = array( + 'none' => '', + 'arrow' => array( + 'next' => '→', + 'previous' => '←', + ), + 'chevron' => array( + 'next' => '»', + 'previous' => '«', + ), + ); + if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { + $pagination_type = $is_next ? 'next' : 'previous'; + $arrow_attribute = $block->context['paginationArrow']; + $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; + $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; + return "<span class='$arrow_classes'>$arrow</span>"; + } + return null; + } +} diff --git a/lib/compat/wordpress-5.9/default-editor-styles.php b/lib/compat/wordpress-5.9/default-editor-styles.php new file mode 100644 index 0000000000000..9ad83c1b19f8e --- /dev/null +++ b/lib/compat/wordpress-5.9/default-editor-styles.php @@ -0,0 +1,38 @@ +<?php +/** + * Loads the default editor styles. + * + * @package gutenberg + */ + +/** + * Load the default editor styles. + * These styles are used if the "no theme styles" options is triggered + * or on themes without their own editor styles. + * + * @param array $settings Default editor settings. + * + * @return array Filtered editor settings. + */ +function gutenberg_extend_block_editor_settings_with_default_editor_styles( $settings ) { + $default_editor_styles_file = gutenberg_dir_path() . 'build/block-editor/default-editor-styles.css'; + $settings['defaultEditorStyles'] = array( + array( + 'css' => file_get_contents( $default_editor_styles_file ), + ), + ); + + // Remove the default font addition from Core Code. + $styles_without_core_styles = array(); + if ( isset( $settings['styles'] ) ) { + foreach ( $settings['styles'] as $style ) { + if ( 'core' !== $style['__unstableType'] ) { + $styles_without_core_styles[] = $style; + } + } + } + $settings['styles'] = $styles_without_core_styles; + + return $settings; +} +add_filter( 'block_editor_settings_all', 'gutenberg_extend_block_editor_settings_with_default_editor_styles' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 224f058edaddf..ef9f17dea1057 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -58,7 +58,7 @@ function gutenberg_initialize_experiments_settings() { 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test a new gallery block that uses nested image blocks (Warning: The new gallery is not compatible with WordPress mobile apps prior to version 18.1. If you use the mobile app, please update to the latest version to avoid content loss.)', 'gutenberg' ), + 'label' => __( 'Test a new gallery block that uses nested image blocks (Warning: The new gallery is not compatible with WordPress mobile apps prior to version 18.2. If you use the mobile app, please update to the latest version to avoid content loss.)', 'gutenberg' ), 'id' => 'gutenberg-gallery-refactor', ) ); diff --git a/lib/full-site-editing/full-site-editing.php b/lib/full-site-editing/full-site-editing.php index 0d00847b6b7d2..d540b840a2dab 100644 --- a/lib/full-site-editing/full-site-editing.php +++ b/lib/full-site-editing/full-site-editing.php @@ -27,7 +27,7 @@ function gutenberg_supports_block_templates() { * Show a notice when a Full Site Editing theme is used. */ function gutenberg_full_site_editing_notice() { - if ( ! gutenberg_is_fse_theme() ) { + if ( ! gutenberg_is_fse_theme() || 'themes' !== get_current_screen()->base ) { return; } ?> diff --git a/lib/global-styles.php b/lib/global-styles.php index a749a66c40cf0..59e392830517b 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -10,7 +10,7 @@ * the corresponding stylesheet. * * @param WP_Theme_JSON_Gutenberg $tree Input tree. - * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', and 'css_variables'. + * @param string $type Type of stylesheet we want accepts 'all', 'block_styles', 'css_variables', and 'presets'. * * @return string Stylesheet. */ @@ -48,16 +48,14 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree, $type = 'al * and enqueues the resulting stylesheet. */ function gutenberg_experimental_global_styles_enqueue_assets() { - if ( - ! get_theme_support( 'experimental-link-color' ) && // link color support needs the presets CSS variables regardless of the presence of theme.json file. - ! WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { - return; - } - $settings = gutenberg_get_default_block_editor_settings(); $all = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( $settings ); - $stylesheet = gutenberg_experimental_global_styles_get_stylesheet( $all ); + $type = 'all'; + if ( ! WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { + $type = 'presets'; + } + $stylesheet = gutenberg_experimental_global_styles_get_stylesheet( $all, $type ); if ( empty( $stylesheet ) ) { return; } @@ -141,15 +139,17 @@ function_exists( 'gutenberg_is_edit_site_page' ) && } // Reset existing global styles. - foreach ( $settings['styles'] as $key => $style ) { - if ( isset( $style['__unstableType'] ) && 'globalStyles' === $style['__unstableType'] ) { - unset( $settings['styles'][ $key ] ); + $styles_without_existing_global_styles = array(); + foreach ( $settings['styles'] as $style ) { + if ( ! isset( $style['__unstableType'] ) || 'globalStyles' !== $style['__unstableType'] ) { + $styles_without_existing_global_styles[] = $style; } } // Add the new ones. - $settings['styles'][] = $css_variables; - $settings['styles'][] = $block_styles; + $styles_without_existing_global_styles[] = $css_variables; + $styles_without_existing_global_styles[] = $block_styles; + $settings['styles'] = $styles_without_existing_global_styles; } // Copied from get_block_editor_settings() at wordpress-develop/block-editor.php. diff --git a/lib/load.php b/lib/load.php index 43456b27da16b..bf9d85e40e5e6 100644 --- a/lib/load.php +++ b/lib/load.php @@ -82,6 +82,8 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat.php'; require __DIR__ . '/compat/wordpress-5.8/index.php'; +require __DIR__ . '/compat/wordpress-5.8.1/index.php'; +require __DIR__ . '/compat/wordpress-5.9/default-editor-styles.php'; require __DIR__ . '/utils.php'; require __DIR__ . '/editor-settings.php'; @@ -120,12 +122,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/global-styles.php'; require __DIR__ . '/pwa.php'; -require __DIR__ . '/block-supports/generated-classname.php'; require __DIR__ . '/block-supports/elements.php'; require __DIR__ . '/block-supports/colors.php'; -require __DIR__ . '/block-supports/align.php'; require __DIR__ . '/block-supports/typography.php'; -require __DIR__ . '/block-supports/custom-classname.php'; require __DIR__ . '/block-supports/border.php'; require __DIR__ . '/block-supports/layout.php'; require __DIR__ . '/block-supports/spacing.php'; diff --git a/lib/navigation-page.php b/lib/navigation-page.php index a751ae32d4c60..568fca8cb3bbf 100644 --- a/lib/navigation-page.php +++ b/lib/navigation-page.php @@ -32,10 +32,19 @@ function gutenberg_navigation_init( $hook ) { return; } + $preload_paths = array( + '/__experimental/menu-locations', + array( '/wp/v2/pages', 'OPTIONS' ), + array( '/wp/v2/posts', 'OPTIONS' ), + ); + $settings = array_merge( gutenberg_get_default_block_editor_settings(), array( - 'blockNavMenus' => get_theme_support( 'block-nav-menus' ), + 'blockNavMenus' => false, + // We should uncomment the line below when the block-nav-menus feature becomes stable. + // @see https://github.com/WordPress/gutenberg/issues/34265. + /*'blockNavMenus' => get_theme_support( 'block-nav-menus' ),*/ ) ); $settings = gutenberg_experimental_global_styles_settings( $settings ); @@ -46,6 +55,7 @@ function gutenberg_navigation_init( $hook ) { array( 'initializer_name' => 'initialize', 'editor_settings' => $settings, + 'preload_paths' => $preload_paths, ) ); diff --git a/lib/navigation.php b/lib/navigation.php index 7bfe3416647c6..0a4ddab251efa 100644 --- a/lib/navigation.php +++ b/lib/navigation.php @@ -152,7 +152,10 @@ function gutenberg_output_block_nav_menu_item( $item_output, $item, $depth, $arg * @return array Updated menu items, sorted by each menu item's menu order. */ function gutenberg_remove_block_nav_menu_items( $menu_items ) { - if ( current_theme_supports( 'block-nav-menus' ) ) { + // We should uncomment the line below when the block-nav-menus feature becomes stable. + // @see https://github.com/WordPress/gutenberg/issues/34265. + /*if ( current_theme_supports( 'block-nav-menus' ) ) {*/ + if ( false ) { return $menu_items; } @@ -246,7 +249,10 @@ function gutenberg_convert_menu_items_to_blocks( * @return string|null Nav menu output to short-circuit with. */ function gutenberg_output_block_nav_menu( $output, $args ) { - if ( ! current_theme_supports( 'block-nav-menus' ) ) { + // We should uncomment the line below when the block-nav-menus feature becomes stable. + // @see https://github.com/WordPress/gutenberg/issues/34265. + /*if ( ! current_theme_supports( 'block-nav-menus' ) ) {*/ + if ( true ) { return null; } diff --git a/lib/theme.json b/lib/theme.json index 6b52969009bee..f32734b6efe0f 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -2,6 +2,7 @@ "version": 1, "settings": { "color": { + "background": true, "palette": [ { "name": "Black", @@ -171,7 +172,8 @@ "custom": true, "customDuotone": true, "customGradient": true, - "link": false + "link": false, + "text": true }, "typography": { "dropCap": true, @@ -211,6 +213,7 @@ ] }, "spacing": { + "blockGap": null, "customMargin": false, "customPadding": false, "units": [ "px", "em", "rem", "vh", "vw", "%" ] diff --git a/package-lock.json b/package-lock.json index ac5226529cc7b..e9597dac37377 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "11.3.0", + "version": "11.5.0-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2286,16 +2286,22 @@ "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" }, "@es-joy/jsdoccomment": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.4.4.tgz", - "integrity": "sha512-ua4qDt9dQb4qt5OI38eCZcQZYE5Bq3P0GzgvDARdT8Lt0mAUpxKTPy8JGGqEvF77tG1irKDZ3WreeezEa3P43w==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.10.8.tgz", + "integrity": "sha512-3P1JiGL4xaR9PoTKUHa2N/LKwa2/eUdRqGwijMWWgBqbFEqJUVpmaOi2TcjcemrsRMgFLBzQCK4ToPhrSVDiFQ==", "dev": true, "requires": { - "comment-parser": "^1.1.5", + "comment-parser": "1.2.4", "esquery": "^1.4.0", - "jsdoctypeparser": "^9.0.0" + "jsdoc-type-pratt-parser": "1.1.1" }, "dependencies": { + "comment-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz", + "integrity": "sha512-pm0b+qv+CkWNriSTMsfnjChF9kH0kxz55y44Wo5le9qLxMj5xDQAaEd9ZN1ovSuk9CsrncWaFwgpOMg7ClJwkw==", + "dev": true + }, "esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", @@ -2819,53 +2825,6 @@ } } }, - "@hapi/address": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.0.0.tgz", - "integrity": "sha512-mV6T0IYqb0xL1UALPFplXYQmR0twnXG0M6jUswpquqT2sD12BOiCiLy3EvMp/Fy7s3DZElC4/aPjEjo2jeZpvw==", - "dev": true - }, - "@hapi/hoek": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-6.2.4.tgz", - "integrity": "sha512-HOJ20Kc93DkDVvjwHyHawPwPkX44sIrbXazAUDiUXaY2R9JwQGo2PhFfnQtdrsIe4igjG2fPgMra7NYw7qhy0A==", - "dev": true - }, - "@hapi/joi": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.0.tgz", - "integrity": "sha512-n6kaRQO8S+kepUTbXL9O/UOL788Odqs38/VOfoCrATDtTvyfiO3fgjlSRaNkHabpTLgM7qru9ifqXlXbXk8SeQ==", - "dev": true, - "requires": { - "@hapi/address": "2.x.x", - "@hapi/hoek": "6.x.x", - "@hapi/marker": "1.x.x", - "@hapi/topo": "3.x.x" - } - }, - "@hapi/marker": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@hapi/marker/-/marker-1.0.0.tgz", - "integrity": "sha512-JOfdekTXnJexfE8PyhZFyHvHjt81rBFSAbTIRAhF2vv/2Y1JzoKsGqxH/GpZJoF7aEfYok8JVcAHmSz1gkBieA==", - "dev": true - }, - "@hapi/topo": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.2.tgz", - "integrity": "sha512-r+aumOqJ5QbD6aLPJWqVjMAPsx5pZKz+F5yPqXZ/WWG9JTtHbQqlzrJoknJ0iJxLj9vlXtmpSdjlkszseeG8OA==", - "dev": true, - "requires": { - "@hapi/hoek": "8.x.x" - }, - "dependencies": { - "@hapi/hoek": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.0.2.tgz", - "integrity": "sha512-O6o6mrV4P65vVccxymuruucb+GhP2zl9NLCG8OdoFRS8BEGw3vwpPp20wpAtpbQQxz1CEUtmxJGgWhjq1XA3qw==", - "dev": true - } - } - }, "@istanbuljs/load-nyc-config": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", @@ -7824,12 +7783,12 @@ } }, "@react-native-community/masked-view": { - "version": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#d849a1c7ed318196394aa20c3ae329431435e01f", - "from": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#v0.1.11-wp" + "version": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#ed1c3812e4982075279a3dca952afaa243ea1b4c", + "from": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#v0.1.11-wp-1" }, "@react-native-community/slider": { - "version": "git+https://github.com/wordpress-mobile/react-native-slider.git#af01fce403b2c559c704fc87654f363ed2aea9fd", - "from": "git+https://github.com/wordpress-mobile/react-native-slider.git#v3.0.2-wp" + "version": "git+https://github.com/wordpress-mobile/react-native-slider.git#159fe48cb616cfbe684e06c2fbea5561f5c9cbfd", + "from": "git+https://github.com/wordpress-mobile/react-native-slider.git#v3.0.2-wp-1" }, "@react-native/assets": { "version": "1.0.0", @@ -16403,16 +16362,13 @@ } }, "@types/classnames": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz", - "integrity": "sha512-1UzDldn9GfYYEsWWnn/P4wkTlkZDH7lDb0wBMGbtIQc9zXEQq7FlKBdZUn6OBqD8sKZZ2RQO2mAjGpXiDGoRmQ==", - "dev": true - }, - "@types/clipboard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.1.tgz", - "integrity": "sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA==", - "dev": true + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==", + "dev": true, + "requires": { + "classnames": "*" + } }, "@types/color-convert": { "version": "2.0.0", @@ -16430,9 +16386,9 @@ "dev": true }, "@types/eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-hqzmggoxkOubpgTdcOltkfc5N8IftRJqU70d1jbOISjjZVPvjcr+CLi2CI70hx1SUIRkLgpglTy9w28nGe2Hsw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.28.0.tgz", + "integrity": "sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==", "dev": true, "requires": { "@types/estree": "*", @@ -16450,9 +16406,9 @@ } }, "@types/estree": { - "version": "0.0.44", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.44.tgz", - "integrity": "sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g==", + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", "dev": true }, "@types/events": { @@ -16487,9 +16443,9 @@ } }, "@types/hammerjs": { - "version": "2.0.39", - "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.39.tgz", - "integrity": "sha512-lYR2Y/tV2ujpk/WyUc7S0VLI0a9hrtVIN9EwnrNo5oSEJI2cK2/XrgwOQmXLL3eTulOESvh9qP6si9+DWM9cOA==" + "version": "2.0.40", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.40.tgz", + "integrity": "sha512-VbjwR1fhsn2h2KXAY4oy1fm7dCxaKy0D+deTb8Ilc3Eo3rc5+5eA4rfYmZaHgNJKxVyI0f6WIXzO2zLkVmQPHA==" }, "@types/hast": { "version": "2.3.1", @@ -16501,9 +16457,9 @@ } }, "@types/highlight-words-core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/highlight-words-core/-/highlight-words-core-1.2.0.tgz", - "integrity": "sha512-yy+e7t3P5ABzT/Bl0Wy0hxworXGKKSJVQljaUQxco9ddXY5OZVbRm+yyzZAPBhP4C7KwfkZRRhNOCYkLbloFYw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/highlight-words-core/-/highlight-words-core-1.2.1.tgz", + "integrity": "sha512-9VZUA5omXBfn+hDxFjUDu1FOJTBM3LmvqfDey+Z6Aa8B8/JmF5SMj6FBrjfgJ/Q3YXOZd3qyTDfJyMZSs/wCUA==", "dev": true }, "@types/html-minifier-terser": { @@ -16575,9 +16531,9 @@ } }, "@types/lodash": { - "version": "4.14.149", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", - "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==" + "version": "4.14.172", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz", + "integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==" }, "@types/markdown-to-jsx": { "version": "6.11.3", @@ -16667,9 +16623,9 @@ "dev": true }, "@types/npm-package-arg": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.0.tgz", - "integrity": "sha512-vbt5fb0y1svMhu++1lwtKmZL76d0uPChFlw7kEzyUmTwfmpHRcFb8i0R8ElT69q/L+QLgK2hgECivIAvaEDwag==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-452/1Kp9IdM/oR10AyqAgZOxUt7eLbm+EMJ194L6oarMYdZNiFIFAOJ7IIr0OrZXTySgfHjJezh2oiyk2kc3ag==", "dev": true }, "@types/npmlog": { @@ -16696,9 +16652,9 @@ "dev": true }, "@types/prettier": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.0.tgz", - "integrity": "sha512-gDE8JJEygpay7IjA/u3JiIURvwZW08f0cZSZLAzFoX/ZmeqvS0Sqv+97aKuHpNsalAMMhwPe+iAS6fQbfmbt7A==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.3.2.tgz", + "integrity": "sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog==", "dev": true }, "@types/pretty-hrtime": { @@ -16719,9 +16675,9 @@ "dev": true }, "@types/qs": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz", - "integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==", + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "dev": true }, "@types/reach__router": { @@ -16767,9 +16723,9 @@ } }, "@types/requestidlecallback": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@types/requestidlecallback/-/requestidlecallback-0.3.1.tgz", - "integrity": "sha512-BnnRkgWYijCIndUn+LgoqKHX/hNpJC5G03B9y7mZya/C2gUQTSn75fEj3ZP1/Rl2E6EYeXh2/7/8UNEZ4X7HuQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@types/requestidlecallback/-/requestidlecallback-0.3.4.tgz", + "integrity": "sha512-aTSyiZuRemRLTQkJPb25L7A4/eR2Teo5l4yJ1V6P3+MFxEZckTDkNKNtr/V1zEOMzS6H8DgxF22U6jPAPrzQvw==", "dev": true }, "@types/responselike": { @@ -16782,13 +16738,10 @@ } }, "@types/semver": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.2.0.tgz", - "integrity": "sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ==", - "dev": true, - "requires": { - "@types/node": "*" - } + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==", + "dev": true }, "@types/source-list-map": { "version": "0.1.2", @@ -16818,9 +16771,9 @@ } }, "@types/tinycolor2": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.2.tgz", - "integrity": "sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.3.tgz", + "integrity": "sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ==", "dev": true }, "@types/uglify-js": { @@ -16847,9 +16800,9 @@ "dev": true }, "@types/uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.1.tgz", + "integrity": "sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==", "dev": true }, "@types/vfile": { @@ -18112,7 +18065,6 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", - "@wordpress/icons": "file:packages/icons", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/shortcode": "file:packages/shortcode", "hpq": "^1.3.0", @@ -18183,7 +18135,7 @@ "version": "file:packages/compose", "requires": { "@babel/runtime": "^7.13.10", - "@types/lodash": "4.14.149", + "@types/lodash": "^4.14.172", "@types/mousetrap": "^1.6.8", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", @@ -18191,7 +18143,7 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/priority-queue": "file:packages/priority-queue", - "clipboard": "^2.0.1", + "clipboard": "^2.0.8", "lodash": "^4.17.21", "mousetrap": "^1.6.5", "react-resize-aware": "^3.1.0", @@ -18205,7 +18157,6 @@ "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/blocks": "file:packages/blocks", "@wordpress/data": "file:packages/data", - "@wordpress/data-controls": "file:packages/data-controls", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/element": "file:packages/element", "@wordpress/html-entities": "file:packages/html-entities", @@ -18424,6 +18375,7 @@ "@wordpress/icons": "file:packages/icons", "@wordpress/interface": "file:packages/interface", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", + "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/url": "file:packages/url", @@ -18627,7 +18579,7 @@ "eslint-config-prettier": "^7.1.0", "eslint-plugin-import": "^2.23.4", "eslint-plugin-jest": "^24.1.3", - "eslint-plugin-jsdoc": "^34.1.0", + "eslint-plugin-jsdoc": "^36.0.8", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.22.0", @@ -18907,8 +18859,8 @@ "requires": { "@babel/runtime": "^7.13.10", "@react-native-community/blur": "3.6.0", - "@react-native-community/masked-view": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#v0.1.11-wp", - "@react-native-community/slider": "git+https://github.com/wordpress-mobile/react-native-slider.git#v3.0.2-wp", + "@react-native-community/masked-view": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#v0.1.11-wp-1", + "@react-native-community/slider": "git+https://github.com/wordpress-mobile/react-native-slider.git#v3.0.2-wp-1", "@react-navigation/core": "5.12.0", "@react-navigation/native": "5.7.0", "@react-navigation/routers": "5.4.9", @@ -18931,23 +18883,23 @@ "jsdom-jscore-rn": "git+https://github.com/iamcco/jsdom-jscore-rn.git#a562f3d57c27c13e5bfc8cf82d496e69a3ba2800", "node-fetch": "^2.6.0", "react-native": "0.64.0", - "react-native-gesture-handler": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#1.10.1-wp", - "react-native-get-random-values": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#v1.4.0-wp", + "react-native-gesture-handler": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#1.10.1-wp-3", + "react-native-get-random-values": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#v1.4.0-wp-1", "react-native-hr": "git+https://github.com/Riglerr/react-native-hr.git#2d01a5cf77212d100e8b99e0310cce5234f977b3", - "react-native-hsv-color-picker": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker.git#v1.0.1-wp", + "react-native-hsv-color-picker": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker.git#v1.0.1-wp-1", "react-native-keyboard-aware-scroll-view": "git+https://github.com/wordpress-mobile/react-native-keyboard-aware-scroll-view.git#v0.8.8-wp", - "react-native-linear-gradient": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp", + "react-native-linear-gradient": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp-1", "react-native-modal": "^11.10.0", - "react-native-prompt-android": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#v1.0.0-wp", - "react-native-reanimated": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1.9.0-wp", + "react-native-prompt-android": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#v1.0.0-wp-1", + "react-native-reanimated": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1.9.0-wp-1", "react-native-safe-area": "^0.5.0", - "react-native-safe-area-context": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#v3.2.0-wp", + "react-native-safe-area-context": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#v3.2.0-wp-1", "react-native-sass-transformer": "^1.1.1", - "react-native-screens": "git+https://github.com/wordpress-mobile/react-native-screens.git#2.9.0-wp", - "react-native-svg": "git+https://github.com/wordpress-mobile/react-native-svg.git#v9.13.7-wp", + "react-native-screens": "git+https://github.com/wordpress-mobile/react-native-screens.git#2.9.0-wp-1", + "react-native-svg": "git+https://github.com/wordpress-mobile/react-native-svg.git#v9.13.7-wp-1", "react-native-url-polyfill": "^1.1.2", - "react-native-video": "git+https://github.com/wordpress-mobile/react-native-video.git#5.0.2-wp", - "react-native-webview": "git+https://github.com/wordpress-mobile/react-native-webview.git#v11.6.5-wp" + "react-native-video": "git+https://github.com/wordpress-mobile/react-native-video.git#5.0.2-wp-1", + "react-native-webview": "git+https://github.com/wordpress-mobile/react-native-webview.git#v11.6.5-wp-1" } }, "@wordpress/readable-js-assets-webpack-plugin": { @@ -19029,7 +18981,7 @@ "filenamify": "^4.2.0", "jest": "^26.6.3", "jest-circus": "^26.6.3", - "jest-dev-server": "^4.4.0", + "jest-dev-server": "^5.0.3", "jest-environment-node": "^26.6.2", "markdownlint": "^0.23.1", "markdownlint-cli": "^0.27.1", @@ -28151,6 +28103,15 @@ "integrity": "sha512-5Kgy8Cz6LPC9DJcNb3yjAXTu3XihQgEdnIg50c//zOC/MyLP0Clg+Y8Sh9ZjjnvBrDZU4DgXS9C3T9r4/scGZQ==", "dev": true }, + "axios": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.3.tgz", + "integrity": "sha512-JtoZ3Ndke/+Iwt5n+BgSli/3idTvpt5OjKyoCmz4LX5+lPiY5l7C1colYezhlxThjNa/NhngCUWZSZFypIFuaA==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -30501,9 +30462,9 @@ "dev": true }, "clipboard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.1.tgz", - "integrity": "sha512-7yhQBmtN+uYZmfRjjVjKa0dZdWuabzpSKGtyQZN+9C8xlC788SSJjOHWh7tzurfwTqTD5UDYAhIv5fRJg3sHjQ==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz", + "integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==", "requires": { "good-listener": "^1.2.2", "select": "^1.1.2", @@ -30663,12 +30624,27 @@ } }, "color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", - "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } } }, "color-convert": { @@ -30685,9 +30661,9 @@ "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" }, "color-string": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", - "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz", + "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -32279,7 +32255,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, "requires": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -32288,8 +32263,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -34705,26 +34679,32 @@ } }, "eslint-plugin-jsdoc": { - "version": "34.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-34.1.0.tgz", - "integrity": "sha512-7uk6vD92LCGBLwl7imvf7YzZrMbLmHZVSULBJClZpYTNdTpPXOtuPNKDi8nLcXYtZf3UopNs5qR7coapBSaUtw==", + "version": "36.0.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-36.0.8.tgz", + "integrity": "sha512-brNjHvRuBy5CaV01mSp6WljrO/T8fHNj0DXG38odOGDnhI7HdcbLKX7DpSvg2Rfcifwh8GlnNFzx13sI05t3bg==", "dev": true, "requires": { - "@es-joy/jsdoccomment": "^0.4.4", - "comment-parser": "1.1.5", - "debug": "^4.3.1", + "@es-joy/jsdoccomment": "0.10.8", + "comment-parser": "1.2.4", + "debug": "^4.3.2", "esquery": "^1.4.0", - "jsdoctypeparser": "^9.0.0", + "jsdoc-type-pratt-parser": "^1.1.1", "lodash": "^4.17.21", - "regextras": "^0.7.1", + "regextras": "^0.8.0", "semver": "^7.3.5", "spdx-expression-parse": "^3.0.1" }, "dependencies": { + "comment-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz", + "integrity": "sha512-pm0b+qv+CkWNriSTMsfnjChF9kH0kxz55y44Wo5le9qLxMj5xDQAaEd9ZN1ovSuk9CsrncWaFwgpOMg7ClJwkw==", + "dev": true + }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { "ms": "2.1.2" @@ -34745,12 +34725,6 @@ "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -36555,41 +36529,36 @@ } }, "find-process": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.3.tgz", - "integrity": "sha512-+IA+AUsQCf3uucawyTwMWcY+2M3FXq3BRvw3S+j5Jvydjk31f/+NPWpYZOJs+JUs2GvxH4Yfr6Wham0ZtRLlPA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.4.tgz", + "integrity": "sha512-rRSuT1LE4b+BFK588D2V8/VG9liW0Ark1XJgroxZXI0LtwmQJOb490DvDYvbm+Hek9ETFzTutGfJ90gumITPhQ==", "dev": true, "requires": { - "chalk": "^2.0.1", - "commander": "^2.11.0", - "debug": "^2.6.8" + "chalk": "^4.0.0", + "commander": "^5.1.0", + "debug": "^4.1.1" }, "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } + }, + "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 } } }, @@ -36661,6 +36630,12 @@ "readable-stream": "^2.0.4" } }, + "follow-redirects": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", + "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -40844,69 +40819,36 @@ } }, "jest-dev-server": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-4.4.0.tgz", - "integrity": "sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-5.0.3.tgz", + "integrity": "sha512-aJR3a5KdY18Lsz+VbREKwx2HM3iukiui+J9rlv9o6iYTwZCSsJazSTStcD9K1q0AIF3oA+FqLOKDyo/sc7+fJw==", "dev": true, "requires": { - "chalk": "^3.0.0", + "chalk": "^4.1.1", "cwd": "^0.10.0", - "find-process": "^1.4.3", - "prompts": "^2.3.0", - "spawnd": "^4.4.0", + "find-process": "^1.4.4", + "prompts": "^2.4.1", + "spawnd": "^5.0.0", "tree-kill": "^1.2.2", - "wait-on": "^3.3.0" + "wait-on": "^5.3.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "prompts": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", + "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", "dev": true, "requires": { - "color-name": "~1.1.4" + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -44268,10 +44210,10 @@ } } }, - "jsdoctypeparser": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz", - "integrity": "sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==", + "jsdoc-type-pratt-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-1.1.1.tgz", + "integrity": "sha512-uelRmpghNwPBuZScwgBG/OzodaFk5RbO5xaivBdsAY70icWfShwZ7PCMO0x1zSkOa8T1FzHThmrdoyg/0AwV5g==", "dev": true }, "jsdom": { @@ -46284,8 +46226,7 @@ "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, "mdurl": { "version": "1.0.1", @@ -47939,9 +47880,9 @@ "optional": true }, "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==" }, "nanomatch": { "version": "1.2.13", @@ -53389,8 +53330,8 @@ } }, "react-native-gesture-handler": { - "version": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#5282a8f3b5f4ef8439c9c60f90c99629658c4cc3", - "from": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#1.10.1-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#f1ae186d300b8de79b03c143001552165bf04756", + "from": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#1.10.1-wp-3", "requires": { "@egjs/hammerjs": "^2.0.17", "fbjs": "^3.0.0", @@ -53400,8 +53341,8 @@ } }, "react-native-get-random-values": { - "version": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#91140c28d87d6fa4f7303296ff2b6e214a078eed", - "from": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#v1.4.0-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#fe4994554df3a16fb9401e28242e6c81a7726cc8", + "from": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#v1.4.0-wp-1", "requires": { "fast-base64-decode": "^1.0.0" } @@ -53411,10 +53352,10 @@ "from": "git+https://github.com/Riglerr/react-native-hr.git#2d01a5cf77212d100e8b99e0310cce5234f977b3" }, "react-native-hsv-color-picker": { - "version": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker.git#0b0b717c7f1c4f453922be8c574b0cf638b8f104", - "from": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker.git#v1.0.1-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker.git#c13b48ec55b049e4c59a8629521097c6938f7992", + "from": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker.git#v1.0.1-wp-1", "requires": { - "react-native-linear-gradient": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp", + "react-native-linear-gradient": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp-1", "tinycolor2": "^1.4.1" } }, @@ -53432,8 +53373,8 @@ } }, "react-native-linear-gradient": { - "version": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#875ec02beab4d1e97e5f4cca57ea3e436f4e8a1f", - "from": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp" + "version": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#7526050edcd00d0ed84e8bd58986b8108e3b3962", + "from": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp-1" }, "react-native-modal": { "version": "11.10.0", @@ -53445,12 +53386,12 @@ } }, "react-native-prompt-android": { - "version": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#ea43e81db49aed3c284cd1decdbe2ac2fdabd58c", - "from": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#v1.0.0-wp" + "version": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#09b2fb1abde2cf2ae3c16b83852689ec2c86e76f", + "from": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#v1.0.0-wp-1" }, "react-native-reanimated": { - "version": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1e482d83d83d5694dffa81f333ef570607d7b303", - "from": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1.9.0-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#4f4aa06ad3088215e6bd681a5060ee5ca8ea7bf0", + "from": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1.9.0-wp-1", "requires": { "fbjs": "^1.0.0" }, @@ -53486,8 +53427,8 @@ } }, "react-native-safe-area-context": { - "version": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#efdae6b42ecff783f9b30cdd794f87e335307aac", - "from": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#v3.2.0-wp" + "version": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#12c8baa168a60bc6c19924f6665416cda6293351", + "from": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#v3.2.0-wp-1" }, "react-native-sass-transformer": { "version": "1.4.0", @@ -53507,12 +53448,12 @@ } }, "react-native-screens": { - "version": "git+https://github.com/wordpress-mobile/react-native-screens.git#218b47b8ce22ddcdb17f3278cbc2955a00e22a78", - "from": "git+https://github.com/wordpress-mobile/react-native-screens.git#2.9.0-wp" + "version": "git+https://github.com/wordpress-mobile/react-native-screens.git#ff79e1dfb50d0a8de5fa2d5ce0322e7bb2b8948d", + "from": "git+https://github.com/wordpress-mobile/react-native-screens.git#2.9.0-wp-1" }, "react-native-svg": { - "version": "git+https://github.com/wordpress-mobile/react-native-svg.git#44af900ff8cd37a2c781029feca0cb18a22dddac", - "from": "git+https://github.com/wordpress-mobile/react-native-svg.git#v9.13.7-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-svg.git#49e0d3b47158e188093f6bf9f76970e455988c2d", + "from": "git+https://github.com/wordpress-mobile/react-native-svg.git#v9.13.7-wp-1", "requires": { "css-select": "^2.0.2", "css-tree": "^1.0.0-alpha.37" @@ -53529,15 +53470,6 @@ "nth-check": "^1.0.2" } }, - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, "css-what": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", @@ -53552,11 +53484,6 @@ "domelementtype": "1" } }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -53564,11 +53491,6 @@ "requires": { "boolbase": "~1.0.0" } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -53593,15 +53515,15 @@ } }, "react-native-video": { - "version": "git+https://github.com/wordpress-mobile/react-native-video.git#c12d84e7972f5f763c336237b2003f0e6717051c", - "from": "git+https://github.com/wordpress-mobile/react-native-video.git#5.0.2-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-video.git#8a286f65cdb85a496793f725e510bd0e084b46b5", + "from": "git+https://github.com/wordpress-mobile/react-native-video.git#5.0.2-wp-1", "requires": { "prop-types": "^15.5.10" } }, "react-native-webview": { - "version": "git+https://github.com/wordpress-mobile/react-native-webview.git#ef42e662f4642cb1c236fb0e24f6608e519cdad1", - "from": "git+https://github.com/wordpress-mobile/react-native-webview.git#v11.6.5-wp", + "version": "git+https://github.com/wordpress-mobile/react-native-webview.git#ba81efba75f88c436b24432acdca012a21c136a5", + "from": "git+https://github.com/wordpress-mobile/react-native-webview.git#v11.6.5-wp-1", "requires": { "escape-string-regexp": "2.0.0", "invariant": "2.2.4" @@ -54316,9 +54238,9 @@ } }, "regextras": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", - "integrity": "sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", "dev": true }, "regjsgen": { @@ -56449,17 +56371,23 @@ "dev": true }, "spawnd": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-4.4.0.tgz", - "integrity": "sha512-jLPOfB6QOEgMOQY15Z6+lwZEhH3F5ncXxIaZ7WHPIapwNNLyjrs61okj3VJ3K6tmP5TZ6cO0VAu9rEY4MD4YQg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-5.0.0.tgz", + "integrity": "sha512-28+AJr82moMVWolQvlAIv3JcYDkjkFTEmfDc503wxrF5l2rQ3dFz6DpbXp3kD4zmgGGldfM4xM4v1sFj/ZaIOA==", "dev": true, "requires": { "exit": "^0.1.2", - "signal-exit": "^3.0.2", + "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", - "wait-port": "^0.2.7" + "wait-port": "^0.2.9" }, "dependencies": { + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -59110,9 +59038,9 @@ "dev": true }, "tiny-emitter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz", - "integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tiny-lr": { "version": "1.1.1", @@ -59462,9 +59390,9 @@ } }, "typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", - "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.2.tgz", + "integrity": "sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==", "dev": true }, "ua-parser-js": { @@ -60227,36 +60155,39 @@ } }, "wait-on": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-3.3.0.tgz", - "integrity": "sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-5.3.0.tgz", + "integrity": "sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg==", "dev": true, "requires": { - "@hapi/joi": "^15.0.3", - "core-js": "^2.6.5", - "minimist": "^1.2.0", - "request": "^2.88.0", - "rx": "^4.1.0" + "axios": "^0.21.1", + "joi": "^17.3.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^6.6.3" }, "dependencies": { - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", - "dev": true + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } }, - "rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true } } }, "wait-port": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.7.tgz", - "integrity": "sha512-pJ6cSBIa0w1sDg4y/wXN4bmvhM9OneOvwdFHo647L2NShBi/oXG4lRaLic5cO1HaYGbUhEvratPfl/WMlIC+tg==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.9.tgz", + "integrity": "sha512-hQ/cVKsNqGZ/UbZB/oakOGFqic00YAMM5/PEj3Bt4vKarv2jWIWzDbqlwT94qMs/exAQAsvMOq99sZblV92zxQ==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -60282,12 +60213,12 @@ "dev": true }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { diff --git a/package.json b/package.json index 6d3ff4a2f2977..d17df424be1b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "11.3.0", + "version": "11.5.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -106,20 +106,19 @@ "@testing-library/jest-dom": "5.11.9", "@testing-library/react": "11.2.2", "@testing-library/react-native": "7.1.0", - "@types/classnames": "2.2.10", - "@types/clipboard": "2.0.1", - "@types/eslint": "6.8.0", - "@types/estree": "0.0.44", - "@types/highlight-words-core": "1.2.0", - "@types/lodash": "4.14.149", - "@types/npm-package-arg": "6.1.0", - "@types/prettier": "1.19.0", - "@types/qs": "6.9.1", - "@types/requestidlecallback": "0.3.1", - "@types/semver": "7.2.0", + "@types/classnames": "2.3.1", + "@types/eslint": "7.28.0", + "@types/estree": "0.0.50", + "@types/highlight-words-core": "1.2.1", + "@types/lodash": "4.14.172", + "@types/npm-package-arg": "6.1.1", + "@types/prettier": "2.3.2", + "@types/qs": "6.9.7", + "@types/requestidlecallback": "0.3.4", + "@types/semver": "7.3.8", "@types/sprintf-js": "1.1.2", - "@types/tinycolor2": "1.4.2", - "@types/uuid": "8.3.0", + "@types/tinycolor2": "1.4.3", + "@types/uuid": "8.3.1", "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/babel-plugin-makepot": "file:packages/babel-plugin-makepot", "@wordpress/babel-preset-default": "file:packages/babel-preset-default", @@ -209,7 +208,7 @@ "sprintf-js": "1.1.1", "style-loader": "3.2.1", "terser-webpack-plugin": "5.1.4", - "typescript": "4.1.3", + "typescript": "4.4.2", "uglify-js": "3.13.7", "uuid": "8.3.0", "wd": "1.12.1", diff --git a/packages/admin-manifest/src/index.js b/packages/admin-manifest/src/index.js index 38fa248c6d9f1..a19032ac4565c 100644 --- a/packages/admin-manifest/src/index.js +++ b/packages/admin-manifest/src/index.js @@ -101,7 +101,6 @@ function getAdminBarColors() { }; } -// eslint-disable-next-line @wordpress/no-global-event-listener window.addEventListener( 'load', () => { if ( ! ( 'serviceWorker' in window.navigator ) ) { return; diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 95244ac008b6a..b53046ace2124 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.2.1", + "version": "2.2.2", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index 7faa28d3063d5..cdb47947993e6 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Change + +- Remove the background-colors, foreground-colors, and gradient-colors mixins. + ## 2.0.0 (2020-07-07) ### Breaking Changes diff --git a/packages/base-styles/_colors.native.scss b/packages/base-styles/_colors.native.scss index a34e5012eecf9..ba76d005b622a 100644 --- a/packages/base-styles/_colors.native.scss +++ b/packages/base-styles/_colors.native.scss @@ -53,6 +53,7 @@ $gray-text-min: darken($gray, 18%); //#537994 $gray-lighten-10: lighten($gray, 10%); // #a8bece $gray-lighten-20: lighten($gray, 20%); // #c8d7e1 $gray-lighten-30: lighten($gray, 30%); // #e9eff3 +$gray-darken-10: darken($gray, 10%); $gray-darken-20: darken($gray, 20%); // #4f748e $gray-darken-30: darken($gray, 30%); // #3d596d @@ -102,6 +103,8 @@ $app-background-dark-alt: $background-dark-elevated; $modal-background: $white; $modal-background-dark: $background-dark-elevated; +$sub-heading: $gray-text-min; +$sub-heading-dark: $white; /** * Deprecated colors. * Please avoid using these. diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index fed727e4e4a6e..f3e24b1f99d62 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -479,186 +479,48 @@ } } -@mixin background-colors() { - .has-pale-pink-background-color { - background-color: #f78da7; - } - - .has-vivid-red-background-color { - background-color: #cf2e2e; - } - - .has-luminous-vivid-orange-background-color { - background-color: #ff6900; - } - - .has-luminous-vivid-amber-background-color { - background-color: #fcb900; - } - - .has-light-green-cyan-background-color { - background-color: #7bdcb5; - } - - .has-vivid-green-cyan-background-color { - background-color: #00d084; - } - - .has-pale-cyan-blue-background-color { - background-color: #8ed1fc; - } - - .has-vivid-cyan-blue-background-color { - background-color: #0693e3; - } - - .has-vivid-purple-background-color { - background-color: #9b51e0; - } - - .has-white-background-color { - background-color: #fff; - } - - // Deprecated from UI, kept for back-compat. +// Deprecated from UI, kept for back-compat. +@mixin background-colors-deprecated() { .has-very-light-gray-background-color { background-color: #eee; } - .has-cyan-bluish-gray-background-color { - background-color: #abb8c3; - } - - // Deprecated from UI, kept for back-compat. .has-very-dark-gray-background-color { background-color: #313131; } - - .has-black-background-color { - background-color: #000; - } } -@mixin foreground-colors() { - .has-pale-pink-color { - color: #f78da7; - } - - .has-vivid-red-color { - color: #cf2e2e; - } - - .has-luminous-vivid-orange-color { - color: #ff6900; - } - - .has-luminous-vivid-amber-color { - color: #fcb900; - } - - .has-light-green-cyan-color { - color: #7bdcb5; - } - - .has-vivid-green-cyan-color { - color: #00d084; - } - - .has-pale-cyan-blue-color { - color: #8ed1fc; - } - - .has-vivid-cyan-blue-color { - color: #0693e3; - } - - .has-vivid-purple-color { - color: #9b51e0; - } - - .has-white-color { - color: #fff; - } - - // Deprecated from UI, kept for back-compat. +// Deprecated from UI, kept for back-compat. +@mixin foreground-colors-deprecated() { .has-very-light-gray-color { color: #eee; } - .has-cyan-bluish-gray-color { - color: #abb8c3; - } - - // Deprecated from UI, kept for back-compat. .has-very-dark-gray-color { color: #313131; } - - .has-black-color { - color: #000; - } } -@mixin gradient-colors() { - // Our classes uses the same values we set for gradient value attributes, and we can not use spacing because of WP multi site kses rule. +// Deprecated from UI, kept for back-compat. +@mixin gradient-colors-deprecated() { + /* + * Our classes uses the same values we set for gradient value attributes, + * and we can not use spacing because of WP multi site kses rule. + */ /* stylelint-disable function-comma-space-after */ - .has-vivid-cyan-blue-to-vivid-purple-gradient-background { - background: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%); - } - .has-vivid-green-cyan-to-vivid-cyan-blue-gradient-background { background: linear-gradient(135deg,rgba(0,208,132,1) 0%,rgba(6,147,227,1) 100%); } - .has-light-green-cyan-to-vivid-green-cyan-gradient-background { - background: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%); - } - - .has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background { - background: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%); - } - - .has-luminous-vivid-orange-to-vivid-red-gradient-background { - background: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%); - } - - .has-very-light-gray-to-cyan-bluish-gray-gradient-background { - background: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%); - } - - .has-cool-to-warm-spectrum-gradient-background { - background: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%); - } - - .has-blush-light-purple-gradient-background { - background: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%); - } - - .has-blush-bordeaux-gradient-background { - background: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%); - } - .has-purple-crush-gradient-background { background: linear-gradient(135deg,rgb(52,226,228) 0%,rgb(71,33,251) 50%,rgb(171,29,254) 100%); } - .has-luminous-dusk-gradient-background { - background: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%); - } - .has-hazy-dawn-gradient-background { background: linear-gradient(135deg,rgb(250,172,168) 0%,rgb(218,208,236) 100%); } - .has-pale-ocean-gradient-background { - background: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%); - } - - .has-electric-grass-gradient-background { - background: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%); - } - .has-subdued-olive-gradient-background { background: linear-gradient(135deg,rgb(250,250,225) 0%,rgb(103,166,113) 100%); } diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index de9ec9d6917e5..5b7ddc1a0f0e1 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -43,8 +43,8 @@ $z-layers: ( ".wp-block-template-part__placeholder-preview-filter-input": 1, // Navigation menu dropdown. - ".has-child .wp-block-navigation-link__container": 28, - ".has-child:hover .wp-block-navigation-link__container": 29, + ".has-child .wp-block-navigation__submenu-container": 28, + ".has-child:hover .wp-block-navigation__submenu-container": 29, // Active pill button ".components-button {:focus or .is-primary}": 1, diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index f33aa6099bfa9..ce59cc46bb061 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "3.0.0", + "version": "3.0.1", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 03ef4ee43b038..b742a81e3dc8a 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -438,9 +438,7 @@ Undocumented declaration. ### InspectorAdvancedControls -_Related_ - -- <https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-advanced-controls/README.md> +Undocumented declaration. ### InspectorControls diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 0550430cde029..63a1cd006a459 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "7.0.0", + "version": "7.0.1", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/src/components/block-controls/fill.js b/packages/block-editor/src/components/block-controls/fill.js index 2392c3d07ee3a..10b9472830686 100644 --- a/packages/block-editor/src/components/block-controls/fill.js +++ b/packages/block-editor/src/components/block-controls/fill.js @@ -15,18 +15,18 @@ import { /** * Internal dependencies */ -import useDisplayBlockControls from '../use-display-block-controls'; -import groups from './groups'; +import useBlockControlsFill from './hook'; export default function BlockControlsFill( { group = 'default', controls, children, + __experimentalExposeToChildren = false, } ) { - if ( ! useDisplayBlockControls() ) { + const Fill = useBlockControlsFill( group, __experimentalExposeToChildren ); + if ( ! Fill ) { return null; } - const Fill = groups[ group ].Fill; return ( <StyleProvider document={ document }> diff --git a/packages/block-editor/src/components/block-controls/groups.js b/packages/block-editor/src/components/block-controls/groups.js index 42a94a4ab7a81..9b9dfec8d8d45 100644 --- a/packages/block-editor/src/components/block-controls/groups.js +++ b/packages/block-editor/src/components/block-controls/groups.js @@ -7,12 +7,14 @@ const BlockControlsDefault = createSlotFill( 'BlockControls' ); const BlockControlsBlock = createSlotFill( 'BlockControlsBlock' ); const BlockControlsInline = createSlotFill( 'BlockFormatControls' ); const BlockControlsOther = createSlotFill( 'BlockControlsOther' ); +const BlockControlsParent = createSlotFill( 'BlockControlsParent' ); const groups = { default: BlockControlsDefault, block: BlockControlsBlock, inline: BlockControlsInline, other: BlockControlsOther, + parent: BlockControlsParent, }; export default groups; diff --git a/packages/block-editor/src/components/block-controls/hook.js b/packages/block-editor/src/components/block-controls/hook.js new file mode 100644 index 0000000000000..d907a9aad5c36 --- /dev/null +++ b/packages/block-editor/src/components/block-controls/hook.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { store as blocksStore } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import groups from './groups'; +import { store as blockEditorStore } from '../../store'; +import { useBlockEditContext } from '../block-edit/context'; +import useDisplayBlockControls from '../use-display-block-controls'; + +export default function useBlockControlsFill( group, exposeToChildren ) { + const isDisplayed = useDisplayBlockControls(); + const { clientId } = useBlockEditContext(); + const isParentDisplayed = useSelect( + ( select ) => { + const { getBlockName, hasSelectedInnerBlock } = select( + blockEditorStore + ); + const { hasBlockSupport } = select( blocksStore ); + return ( + exposeToChildren && + hasBlockSupport( + getBlockName( clientId ), + '__experimentalExposeControlsToChildren', + false + ) && + hasSelectedInnerBlock( clientId ) + ); + }, + [ exposeToChildren, clientId ] + ); + + if ( isDisplayed ) { + return groups[ group ]?.Fill; + } + if ( isParentDisplayed ) { + return groups.parent.Fill; + } + return null; +} diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 0746699eb145d..94b1adb474109 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -51,10 +51,7 @@ export const Edit = ( props ) => { // them preferentially as the render value for the block. const Component = blockType.edit || blockType.save; - if ( - blockType.apiVersion > 1 || - hasBlockSupport( blockType, 'lightBlockWrapper', false ) - ) { + if ( blockType.apiVersion > 1 ) { return <Component { ...props } context={ context } />; } diff --git a/packages/block-editor/src/components/block-icon/index.native.js b/packages/block-editor/src/components/block-icon/index.native.js index 3e1bca650976a..4926949f88d56 100644 --- a/packages/block-editor/src/components/block-icon/index.native.js +++ b/packages/block-editor/src/components/block-icon/index.native.js @@ -17,6 +17,8 @@ import styles from './style.scss'; export function BlockIcon( { icon, + fill, + size, showColors = false, getStylesFromColorScheme, } ) { @@ -29,10 +31,13 @@ export function BlockIcon( { const renderedIcon = ( <Icon icon={ icon && icon.src ? icon.src : icon } - { ...getStylesFromColorScheme( - styles.iconPlaceholder, - styles.iconPlaceholderDark - ) } + { ...( fill && { fill } ) } + { ...( size && { size } ) } + { ...( ! fill && + getStylesFromColorScheme( + styles.iconPlaceholder, + styles.iconPlaceholderDark + ) ) } /> ); const style = showColors diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index 2a8f7ffeb40db..0ef6a790d1688 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -19,8 +19,10 @@ import { useSelect } from '@wordpress/data'; */ import SkipToSelectedBlock from '../skip-to-selected-block'; import BlockCard from '../block-card'; -import InspectorControls from '../inspector-controls'; -import InspectorAdvancedControls from '../inspector-advanced-controls'; +import { + default as InspectorControls, + InspectorAdvancedControls, +} from '../inspector-controls'; import BlockStyles from '../block-styles'; import MultiSelectionInspector from '../multi-selection-inspector'; import DefaultStylePicker from '../default-style-picker'; @@ -128,18 +130,15 @@ const BlockInspectorSingleBlock = ( { ) } <InspectorControls.Slot bubblesVirtually={ bubblesVirtually } /> <div> - <AdvancedControls - slotName={ InspectorAdvancedControls.slotName } - bubblesVirtually={ bubblesVirtually } - /> + <AdvancedControls bubblesVirtually={ bubblesVirtually } /> </div> <SkipToSelectedBlock key="back" /> </div> ); }; -const AdvancedControls = ( { slotName, bubblesVirtually } ) => { - const slot = useSlot( slotName ); +const AdvancedControls = ( { bubblesVirtually } ) => { + const slot = useSlot( InspectorAdvancedControls.slotName ); const hasFills = Boolean( slot.fills && slot.fills.length ); if ( ! hasFills ) { @@ -152,7 +151,8 @@ const AdvancedControls = ( { slotName, bubblesVirtually } ) => { title={ __( 'Advanced' ) } initialOpen={ false } > - <InspectorAdvancedControls.Slot + <InspectorControls.Slot + __experimentalGroup="advanced" bubblesVirtually={ bubblesVirtually } /> </PanelBody> diff --git a/packages/block-editor/src/components/block-list/block-list-item.native.js b/packages/block-editor/src/components/block-list/block-list-item.native.js index fae95956fa735..857f0ed66ff51 100644 --- a/packages/block-editor/src/components/block-list/block-list-item.native.js +++ b/packages/block-editor/src/components/block-list/block-list-item.native.js @@ -199,7 +199,7 @@ export default compose( [ isBlockInsertionPointVisible, getSettings, getBlockParents, - __unstableGetBlockWithoutInnerBlocks, + getBlock, } = select( blockEditorStore ); const blockClientIds = getBlockOrder( rootClientId ); @@ -225,14 +225,11 @@ export default compose( [ const isReadOnly = getSettings().readOnly; - const block = __unstableGetBlockWithoutInnerBlocks( clientId ); - const { attributes, name } = block || {}; + const { attributes, name } = getBlock( clientId ) || {}; const { align } = attributes || {}; const parents = getBlockParents( clientId, true ); const hasParents = !! parents.length; - const parentBlock = hasParents - ? __unstableGetBlockWithoutInnerBlocks( parents[ 0 ] ) - : {}; + const parentBlock = hasParents ? getBlock( parents[ 0 ] ) : {}; const { align: parentBlockAlignment } = parentBlock?.attributes || {}; const { name: parentBlockName } = parentBlock || {}; diff --git a/packages/block-editor/src/components/block-list/block-selection-button.native.js b/packages/block-editor/src/components/block-list/block-selection-button.native.js index 97a790482c765..ef066caf3d3f3 100644 --- a/packages/block-editor/src/components/block-list/block-selection-button.native.js +++ b/packages/block-editor/src/components/block-list/block-selection-button.native.js @@ -5,6 +5,7 @@ import { Icon } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { getBlockType } from '@wordpress/blocks'; +import { BlockIcon } from '@wordpress/block-editor'; /** * External dependencies @@ -58,11 +59,13 @@ const BlockSelectionButton = ( { /> </View>, ] } - <Icon - size={ 24 } - icon={ blockInformation?.icon?.src } - fill={ styles.icon.color } - /> + { blockInformation?.icon && ( + <BlockIcon + size={ 24 } + icon={ blockInformation.icon } + fill={ styles.icon.color } + /> + ) } <Text maxFontSizeMultiplier={ 1.25 } ellipsizeMode="tail" diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 8b8ccc82dd0da..960e3a6117297 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -17,7 +17,6 @@ import { getBlockType, getSaveContent, isUnmodifiedDefaultBlock, - hasBlockSupport, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { withDispatch, withSelect, useDispatch } from '@wordpress/data'; @@ -110,12 +109,9 @@ function BlockListBlock( { ); const blockType = getBlockType( name ); - const lightBlockWrapper = - blockType.apiVersion > 1 || - hasBlockSupport( blockType, 'lightBlockWrapper', false ); // Determine whether the block has props to apply to the wrapper. - if ( blockType.getEditWrapperProps ) { + if ( blockType?.getEditWrapperProps ) { wrapperProps = mergeWrapperProps( wrapperProps, blockType.getEditWrapperProps( attributes ) @@ -159,7 +155,7 @@ function BlockListBlock( { </Block> </> ); - } else if ( lightBlockWrapper ) { + } else if ( blockType?.apiVersion > 1 ) { block = blockEdit; } else { block = <Block { ...wrapperProps }>{ blockEdit }</Block>; diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index 3f700ec22a292..699145b6ffc52 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -51,6 +51,7 @@ function BlockForType( { baseGlobalStyles, } ) { const defaultColors = useSetting( 'color.palette' ) || emptyArray; + const fontSizes = useSetting( 'typography.fontSizes' ) || emptyArray; const globalStyle = useGlobalStyles(); const mergedStyle = useMemo( () => { return getMergedGlobalStyles( @@ -59,7 +60,8 @@ function BlockForType( { wrapperProps.style, attributes, defaultColors, - name + name, + fontSizes ); }, [ defaultColors, @@ -305,7 +307,7 @@ export default compose( [ getBlockIndex, getSettings, isBlockSelected, - __unstableGetBlockWithoutInnerBlocks, + getBlock, getSelectedBlockClientId, getLowestCommonAncestorWithSelectedBlock, getBlockParents, @@ -315,7 +317,7 @@ export default compose( [ const order = getBlockIndex( clientId, rootClientId ); const isSelected = isBlockSelected( clientId ); const isInnerBlockSelected = hasSelectedInnerBlock( clientId ); - const block = __unstableGetBlockWithoutInnerBlocks( clientId ); + const block = getBlock( clientId ); const { name, attributes, isValid } = block || {}; const blockType = getBlockType( name || 'core/missing' ); diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 4ce520ed81875..697a9ffc19daa 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -262,8 +262,6 @@ export class BlockList extends Component { { flex: isRootList ? 1 : 0 }, ! isRootList && styles.overflowVisible, ] } - horizontal={ horizontal } - numColumns={ 1 } extraData={ this.getExtraData() } scrollEnabled={ isRootList } contentContainerStyle={ [ diff --git a/packages/block-editor/src/components/block-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss index fe63d30b4d0b2..6ff09277e9ea5 100644 --- a/packages/block-editor/src/components/block-list/style.native.scss +++ b/packages/block-editor/src/components/block-list/style.native.scss @@ -6,7 +6,6 @@ .horizontalContentContainer { flex-direction: row; - flex-wrap: wrap; justify-content: flex-start; align-items: stretch; overflow: visible; diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index ff6c46c7948f4..4801e52709c96 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -11,7 +11,6 @@ import { __, sprintf } from '@wordpress/i18n'; import { __unstableGetBlockProps as getBlockProps, getBlockType, - hasBlockSupport, } from '@wordpress/blocks'; import { useMergeRefs } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; @@ -68,11 +67,11 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { index, mode, name, + blockApiVersion, blockTitle, isPartOfSelection, adjustScrolling, enableAnimation, - lightBlockWrapper, } = useSelect( ( select ) => { const { @@ -99,6 +98,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { index: getBlockIndex( clientId, rootClientId ), mode: getBlockMode( clientId ), name: blockName, + blockApiVersion: blockType?.apiVersion || 1, blockTitle: blockType?.title, isPartOfSelection: isSelected || isPartOfMultiSelection, adjustScrolling: @@ -106,9 +106,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { enableAnimation: ! isTyping() && getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, - lightBlockWrapper: - blockType?.apiVersion > 1 || - hasBlockSupport( blockType, 'lightBlockWrapper', false ), }; }, [ clientId ] @@ -139,7 +136,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { const blockEditContext = useBlockEditContext(); // Ensures it warns only inside the `edit` implementation for the block. - if ( ! lightBlockWrapper && clientId === blockEditContext.clientId ) { + if ( blockApiVersion < 2 && clientId === blockEditContext.clientId ) { warning( `Block type "${ name }" must support API version 2 or higher to work correctly with "useBlockProps" method.` ); diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js index 205d5de9c5594..2bc250be56cb1 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { hasBlockSupport, getBlockType } from '@wordpress/blocks'; +import { getBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -32,9 +32,7 @@ export function useBlockCustomClassName( clientId ) { } const blockType = getBlockType( getBlockName( clientId ) ); - const hasLightBlockWrapper = - blockType.apiVersion > 1 || - hasBlockSupport( blockType, 'lightBlockWrapper', false ); + const hasLightBlockWrapper = blockType?.apiVersion > 1; if ( ! hasLightBlockWrapper ) { return; diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js index fa84fd8d0be1d..7877ceb96490c 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js @@ -2,11 +2,7 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { - hasBlockSupport, - getBlockType, - getBlockDefaultClassName, -} from '@wordpress/blocks'; +import { getBlockType, getBlockDefaultClassName } from '@wordpress/blocks'; /** * Internal dependencies @@ -26,9 +22,7 @@ export function useBlockDefaultClassName( clientId ) { ( select ) => { const name = select( blockEditorStore ).getBlockName( clientId ); const blockType = getBlockType( name ); - const hasLightBlockWrapper = - blockType?.apiVersion > 1 || - hasBlockSupport( blockType, 'lightBlockWrapper', false ); + const hasLightBlockWrapper = blockType?.apiVersion > 1; if ( ! hasLightBlockWrapper ) { return; diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js index 89030f65439c0..c0072357257d8 100644 --- a/packages/block-editor/src/components/block-preview/auto.js +++ b/packages/block-editor/src/components/block-preview/auto.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { Disabled } from '@wordpress/components'; -import { useResizeObserver, pure } from '@wordpress/compose'; +import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; /** @@ -46,7 +46,7 @@ function AutoBlockPreview( { viewportWidth, __experimentalPadding } ) { > <Iframe head={ <EditorStyles styles={ styles } /> } - contentRef={ ( bodyElement ) => { + contentRef={ useRefEffect( ( bodyElement ) => { const { ownerDocument: { documentElement }, } = bodyElement; @@ -54,7 +54,7 @@ function AutoBlockPreview( { viewportWidth, __experimentalPadding } ) { documentElement.style.width = '100%'; bodyElement.style.padding = __experimentalPadding + 'px'; - } } + }, [] ) } aria-hidden tabIndex={ -1 } style={ { diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 599c936eeeb73..def5b425760d1 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -123,6 +123,10 @@ export default function BlockToolbar( { hideDragHandle } ) { </div> { shouldShowVisualToolbar && ( <> + <BlockControls.Slot + group="parent" + className="block-editor-block-toolbar__slot" + /> <BlockControls.Slot group="block" className="block-editor-block-toolbar__slot" diff --git a/packages/block-editor/src/components/block-tools/block-popover.js b/packages/block-editor/src/components/block-tools/block-popover.js index 8371cfff9bbba..3797f68edadff 100644 --- a/packages/block-editor/src/components/block-tools/block-popover.js +++ b/packages/block-editor/src/components/block-tools/block-popover.js @@ -213,6 +213,9 @@ function BlockPopover( { // Observe movement for block animations (especially horizontal). __unstableObserveElement={ node } shouldAnchorIncludePadding + // Used to safeguard sticky position behavior against cases where it would permanently + // obscure specific sections of a block. + __unstableEditorCanvasWrapper={ __unstableContentRef?.current } > { ( shouldShowContextualToolbar || isToolbarForced ) && ( <div @@ -281,7 +284,7 @@ function wrapperSelector( select ) { getSelectedBlockClientId, getFirstMultiSelectedBlockClientId, getBlockRootClientId, - __unstableGetBlockWithoutInnerBlocks, + getBlock, getBlockParents, __experimentalGetBlockListSettingsForBlocks, } = select( blockEditorStore ); @@ -293,8 +296,7 @@ function wrapperSelector( select ) { return; } - const { name, attributes = {}, isValid } = - __unstableGetBlockWithoutInnerBlocks( clientId ) || {}; + const { name, attributes = {}, isValid } = getBlock( clientId ) || {}; const blockParentsClientIds = getBlockParents( clientId ); // Get Block List Settings for all ancestors of the current Block clientId diff --git a/packages/block-editor/src/components/block-tools/block-selection-button.js b/packages/block-editor/src/components/block-tools/block-selection-button.js index 12b4bb2d0407a..8eb9a5b695068 100644 --- a/packages/block-editor/src/components/block-tools/block-selection-button.js +++ b/packages/block-editor/src/components/block-tools/block-selection-button.js @@ -54,15 +54,13 @@ function BlockSelectionButton( { clientId, rootClientId, blockElement } ) { const selected = useSelect( ( select ) => { const { - __unstableGetBlockWithoutInnerBlocks, + getBlock, getBlockIndex, hasBlockMovingClientId, getBlockListSettings, } = select( blockEditorStore ); const index = getBlockIndex( clientId, rootClientId ); - const { name, attributes } = __unstableGetBlockWithoutInnerBlocks( - clientId - ); + const { name, attributes } = getBlock( clientId ); const blockMovingMode = hasBlockMovingClientId(); return { index, diff --git a/packages/block-editor/src/components/block-types-list/index.native.js b/packages/block-editor/src/components/block-types-list/index.native.js index 485cde15fbf1f..81795d37e8778 100644 --- a/packages/block-editor/src/components/block-types-list/index.native.js +++ b/packages/block-editor/src/components/block-types-list/index.native.js @@ -22,7 +22,13 @@ import styles from './style.scss'; const MIN_COL_NUM = 3; -export default function BlockTypesList( { name, items, onSelect, listProps } ) { +export default function BlockTypesList( { + name, + items, + onSelect, + listProps, + initialNumToRender = 3, +} ) { const [ numberOfColumns, setNumberOfColumns ] = useState( MIN_COL_NUM ); const [ itemWidth, setItemWidth ] = useState(); const [ maxWidth, setMaxWidth ] = useState(); @@ -81,7 +87,7 @@ export default function BlockTypesList( { name, items, onSelect, listProps } ) { keyboardShouldPersistTaps="always" numColumns={ numberOfColumns } data={ items } - initialNumToRender={ 3 } + initialNumToRender={ initialNumToRender } ItemSeparatorComponent={ () => ( <TouchableWithoutFeedback accessible={ false }> <View diff --git a/packages/block-editor/src/components/block-variation-picker/README.md b/packages/block-editor/src/components/block-variation-picker/README.md index 117e79a6ee648..1de44a9bcd406 100644 --- a/packages/block-editor/src/components/block-variation-picker/README.md +++ b/packages/block-editor/src/components/block-variation-picker/README.md @@ -1,8 +1,12 @@ # Block Variation Picker -The `BlockVariationPicker` component allows to display for certain types of blocks their different variations, and to choose one of them. +<div class="callout callout-alert"> +This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +</div> -This component is currently used by the "Columns" block to display and choose the number and structure of columns. It is also used by the "Post Hierarchical Terms Block" block. +The `BlockVariationPicker` component allows certain types of blocks to display their different variations, and to choose one of them. Variations provided are usually filtered by their inclusion of the `block` value in their `scope` attribute. + +This component is currently used by "Columns" and "Query Loop" blocks. ![Columns block variations](https://make.wordpress.org/core/files/2020/09/colums-block-variations.png) @@ -18,33 +22,62 @@ This component is currently used by the "Columns" block to display and choose th Renders the variations of a block. ```jsx -import { BlockVariationPicker } from '@wordpress/block-editor'; - -const MyBlockVariationPicker = () => ( - <BlockVariationPicker variations={ variations } /> -); +import { useSelect } from '@wordpress/data'; +import { + __experimentalBlockVariationPicker as BlockVariationPicker, + store as blockEditorStore, +} from '@wordpress/block-editor'; + +const MyBlockVariationPicker = ( { blockName } ) => { + const variations = useSelect( + ( select ) => { + const { getBlockVariations } = select( blocksStore ); + return getBlockVariations( blockName, 'block' ); + }, + [ blockName ] + ); + return <BlockVariationPicker variations={ variations } />; +}; ``` ### Props -#### label - -The label of each variation of the block. +#### `label` - Type: `String` +- Required: No +- Default: `Choose variation` -#### instructions +The label of each variation of the block. -The instructions to choose a block variation. +#### `instructions` - Type: `String` +- Required: No +- Default: `Select a variation to start with.` + +The instructions to choose a block variation. -#### variations +#### `variations` -- Type: `Array` +- Type: `Array<WPBlockVariation>` The different variations of the block. +#### `onSelect` + +- Type: `Function` + +Callback called when a block variation is selected. It recieves the selected variation as a parameter. + +#### `icon` + +- Type: `Icon component` +- Required: No +- Default: `layout` + +Icon to be displayed at the top of the component before the `label`. + ## Related components Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [BlockEditorProvider](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/block-variation-picker/style.native.scss b/packages/block-editor/src/components/block-variation-picker/style.native.scss index cfee074a24a98..20b1297a593b1 100644 --- a/packages/block-editor/src/components/block-variation-picker/style.native.scss +++ b/packages/block-editor/src/components/block-variation-picker/style.native.scss @@ -17,6 +17,8 @@ .cancelButton { color: $blue-wordpress; font-size: 16px; + padding-left: $grid-unit-20; + padding-right: $grid-unit-20; } .cancelButtonDark { @@ -25,4 +27,6 @@ .closeIcon { color: $gray; + margin-left: $grid-unit-20; + padding: $grid-unit-20; } diff --git a/packages/block-editor/src/components/editor-styles/index.js b/packages/block-editor/src/components/editor-styles/index.js index 4b0286d8eba71..c2c5f77ff600b 100644 --- a/packages/block-editor/src/components/editor-styles/index.js +++ b/packages/block-editor/src/components/editor-styles/index.js @@ -68,6 +68,7 @@ export default function EditorStyles( { styles } ) { () => transformStyles( styles, EDITOR_STYLES_SELECTOR ), [ styles ] ); + return ( <> { /* Use an empty style element to have a document reference, diff --git a/packages/block-editor/src/components/font-appearance-control/index.js b/packages/block-editor/src/components/font-appearance-control/index.js index 224f9361d32f2..711ded6b70835 100644 --- a/packages/block-editor/src/components/font-appearance-control/index.js +++ b/packages/block-editor/src/components/font-appearance-control/index.js @@ -163,12 +163,42 @@ export default function FontAppearanceControl( props ) { return __( 'Appearance' ); }; + // Adjusts screen reader description based on styles or weights. + const getDescribedBy = () => { + if ( ! currentSelection ) { + return __( 'No selected font appearance' ); + } + + if ( ! hasFontStyles ) { + return sprintf( + // translators: %s: Currently selected font weight. + __( 'Currently selected font weight: %s' ), + currentSelection.name + ); + } + + if ( ! hasFontWeights ) { + return sprintf( + // translators: %s: Currently selected font style. + __( 'Currently selected font style: %s' ), + currentSelection.name + ); + } + + return sprintf( + // translators: %s: Currently selected font appearance. + __( 'Currently selected font appearance: %s' ), + currentSelection.name + ); + }; + return ( <fieldset className="components-font-appearance-control"> { hasStylesOrWeights && ( <CustomSelectControl className="components-font-appearance-control__select" label={ getLabel() } + describedBy={ getDescribedBy() } options={ selectOptions } value={ currentSelection } onChange={ ( { selectedItem } ) => diff --git a/packages/block-editor/src/components/font-sizes/index.native.js b/packages/block-editor/src/components/font-sizes/index.native.js index 2ebb67cf494ba..55ef51a02aa4d 100644 --- a/packages/block-editor/src/components/font-sizes/index.native.js +++ b/packages/block-editor/src/components/font-sizes/index.native.js @@ -1 +1,7 @@ -export { getFontSize, getFontSizeClass } from './utils'; +export { + getFontSize, + getFontSizeClass, + getFontSizeObjectByValue, +} from './utils'; +export { default as FontSizePicker } from './font-size-picker'; +export { default as withFontSizes } from './with-font-sizes'; diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 12c9812a21840..e5dbb6907094b 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -1,17 +1,21 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ import { useState, createPortal, - useCallback, forwardRef, useEffect, useMemo, useReducer, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { useMergeRefs } from '@wordpress/compose'; +import { useMergeRefs, useRefEffect } from '@wordpress/compose'; import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; /** @@ -137,32 +141,6 @@ function bubbleEvents( doc ) { } } -/** - * Sets the document direction. - * - * Sets the `editor-styles-wrapper` class name on the body. - * - * Copies the `admin-color-*` class name to the body so that the admin color - * scheme applies to components in the iframe. - * - * @param {Document} doc Document to add class name to. - */ -function setBodyClassName( doc ) { - doc.dir = document.dir; - doc.body.className = BODY_CLASS_NAME; - - for ( const name of document.body.classList ) { - if ( name.startsWith( 'admin-color-' ) ) { - doc.body.classList.add( name ); - } else if ( name === 'wp-embed-responsive' ) { - // Ideally ALL classes that are added through get_body_class should - // be added in the editor too, which we'll somehow have to get from - // the server in the future (which will run the PHP filters). - doc.body.classList.add( 'wp-embed-responsive' ); - } - } -} - function useParsedAssets( html ) { return useMemo( () => { const doc = document.implementation.createHTMLDocument( '' ); @@ -171,9 +149,9 @@ function useParsedAssets( html ) { }, [ html ] ); } -async function loadScript( doc, { id, src } ) { +async function loadScript( head, { id, src } ) { return new Promise( ( resolve, reject ) => { - const script = doc.createElement( 'script' ); + const script = head.ownerDocument.createElement( 'script' ); script.id = id; if ( src ) { script.src = src; @@ -182,57 +160,45 @@ async function loadScript( doc, { id, src } ) { } else { resolve(); } - doc.head.appendChild( script ); + head.appendChild( script ); } ); } function Iframe( { contentRef, children, head, tabIndex = 0, ...props }, ref ) { const [ , forceRender ] = useReducer( () => ( {} ) ); const [ iframeDocument, setIframeDocument ] = useState(); + const [ bodyClasses, setBodyClasses ] = useState( [] ); const styles = useParsedAssets( window.__editorAssets.styles ); const scripts = useParsedAssets( window.__editorAssets.scripts ); const clearerRef = useBlockSelectionClearer(); const [ before, writingFlowRef, after ] = useWritingFlow(); - const setRef = useCallback( ( node ) => { - if ( ! node ) { - return; - } - + const setRef = useRefEffect( ( node ) => { function setDocumentIfReady() { - const { contentDocument } = node; - const { readyState, body, documentElement } = contentDocument; + const { contentDocument, ownerDocument } = node; + const { readyState, documentElement } = contentDocument; if ( readyState !== 'interactive' && readyState !== 'complete' ) { return false; } - if ( typeof contentRef === 'function' ) { - contentRef( body ); - } else if ( contentRef ) { - contentRef.current = body; - } - - setBodyClassName( contentDocument ); bubbleEvents( contentDocument ); - setBodyClassName( contentDocument ); setIframeDocument( contentDocument ); clearerRef( documentElement ); - clearerRef( body ); - writingFlowRef( body ); - - scripts - .reduce( - ( promise, script ) => - promise.then( () => - loadScript( contentDocument, script ) - ), - Promise.resolve() + + // Ideally ALL classes that are added through get_body_class should + // be added in the editor too, which we'll somehow have to get from + // the server in the future (which will run the PHP filters). + setBodyClasses( + Array.from( ownerDocument.body.classList ).filter( + ( name ) => + name.startsWith( 'admin-color-' ) || + name === 'wp-embed-responsive' ) - .finally( () => { - // When script are loaded, re-render blocks to allow them - // to initialise. - forceRender(); - } ); + ); + + contentDocument.dir = ownerDocument.dir; + documentElement.removeChild( contentDocument.head ); + documentElement.removeChild( contentDocument.body ); return true; } @@ -246,6 +212,20 @@ function Iframe( { contentRef, children, head, tabIndex = 0, ...props }, ref ) { setDocumentIfReady(); } ); }, [] ); + const headRef = useRefEffect( ( element ) => { + scripts + .reduce( + ( promise, script ) => + promise.then( () => loadScript( element, script ) ), + Promise.resolve() + ) + .finally( () => { + // When script are loaded, re-render blocks to allow them + // to initialise. + forceRender(); + } ); + }, [] ); + const bodyRef = useMergeRefs( [ contentRef, clearerRef, writingFlowRef ] ); useEffect( () => { if ( iframeDocument ) { @@ -288,12 +268,22 @@ function Iframe( { contentRef, children, head, tabIndex = 0, ...props }, ref ) { > { iframeDocument && createPortal( - <StyleProvider document={ iframeDocument }> - { children } - </StyleProvider>, - iframeDocument.body + <> + <head ref={ headRef }>{ head }</head> + <body + ref={ bodyRef } + className={ classnames( + BODY_CLASS_NAME, + ...bodyClasses + ) } + > + <StyleProvider document={ iframeDocument }> + { children } + </StyleProvider> + </body> + </>, + iframeDocument.documentElement ) } - { iframeDocument && createPortal( head, iframeDocument.head ) } </iframe> { tabIndex >= 0 && after } </> diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 8cf025bdcc10a..99c1042b99173 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -52,8 +52,10 @@ export { default as InnerBlocks, useInnerBlocksProps as __experimentalUseInnerBlocksProps, } from './inner-blocks'; -export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; -export { default as InspectorControls } from './inspector-controls'; +export { + default as InspectorControls, + InspectorAdvancedControls, +} from './inspector-controls'; export { JustifyToolbar, JustifyContentControl, diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 89fa80d8b44c8..995042b7d65cd 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -22,8 +22,10 @@ export { default as InnerBlocks, useInnerBlocksProps as __experimentalUseInnerBlocksProps, } from './inner-blocks'; -export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; -export { default as InspectorControls } from './inspector-controls'; +export { + default as InspectorControls, + InspectorAdvancedControls, +} from './inspector-controls'; export { JustifyToolbar, JustifyContentControl, diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 2f8f0cefede31..dcf07e7391fc8 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -9,7 +9,11 @@ import classnames from 'classnames'; import { useViewportMatch, useMergeRefs } from '@wordpress/compose'; import { forwardRef } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { getBlockType, withBlockContentContext } from '@wordpress/blocks'; +import { + getBlockType, + store as blocksStore, + withBlockContentContext, +} from '@wordpress/blocks'; /** * Internal dependencies @@ -137,10 +141,10 @@ const ForwardedInnerBlocks = forwardRef( ( props, ref ) => { export function useInnerBlocksProps( props = {}, options = {} ) { const { clientId } = useBlockEditContext(); const isSmallScreen = useViewportMatch( 'medium', '<' ); - const hasOverlay = useSelect( + const { __experimentalCaptureToolbars, hasOverlay } = useSelect( ( select ) => { if ( ! clientId ) { - return; + return {}; } const { @@ -149,13 +153,22 @@ export function useInnerBlocksProps( props = {}, options = {} ) { hasSelectedInnerBlock, isNavigationMode, } = select( blockEditorStore ); + const blockName = getBlockName( clientId ); const enableClickThrough = isNavigationMode() || isSmallScreen; - return ( - getBlockName( clientId ) !== 'core/template' && - ! isBlockSelected( clientId ) && - ! hasSelectedInnerBlock( clientId, true ) && - enableClickThrough - ); + return { + __experimentalCaptureToolbars: select( + blocksStore + ).hasBlockSupport( + blockName, + '__experimentalExposeControlsToChildren', + false + ), + hasOverlay: + blockName !== 'core/template' && + ! isBlockSelected( clientId ) && + ! hasSelectedInnerBlock( clientId, true ) && + enableClickThrough, + }; }, [ clientId, isSmallScreen ] ); @@ -167,11 +180,14 @@ export function useInnerBlocksProps( props = {}, options = {} ) { } ), ] ); + const innerBlocksProps = { + __experimentalCaptureToolbars, + ...options, + }; const InnerBlocks = - options.value && options.onChange + innerBlocksProps.value && innerBlocksProps.onChange ? ControlledInnerBlocks : UncontrolledInnerBlocks; - return { ...props, ref, @@ -183,7 +199,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { } ), children: clientId ? ( - <InnerBlocks { ...options } clientId={ clientId } /> + <InnerBlocks { ...innerBlocksProps } clientId={ clientId } /> ) : ( <BlockListItems { ...options } /> ), diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 198bb761329e3..8b5f2b1919705 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -38,9 +38,11 @@ function InserterMenu( { const [ filterValue, setFilterValue ] = useState( '' ); const [ showTabs, setShowTabs ] = useState( true ); // eslint-disable-next-line no-undef - const [ showSearchForm, setShowSearchForm ] = useState( __DEV__ ); + const [ showSearchForm, setShowSearchForm ] = useState( true ); const [ tabIndex, setTabIndex ] = useState( 0 ); + const isIOS = Platform.OS === 'ios'; + const { showInsertionPoint, hideInsertionPoint, @@ -199,18 +201,24 @@ function InserterMenu( { </> } hasNavigation - setMinHeightToMaxHeight={ showSearchForm } - contentStyle={ styles.list } + setMinHeightToMaxHeight={ true } + contentStyle={ styles[ 'inserter-menu__list' ] } + isFullScreen={ ! isIOS && showSearchForm } + allowDragIndicator={ true } > <BottomSheetConsumer> { ( { listProps } ) => ( - <TouchableHighlight accessible={ false }> + <TouchableHighlight + accessible={ false } + style={ styles[ 'inserter-menu__list-wrapper' ] } + > { ! showTabs || filterValue ? ( <InserterSearchResults rootClientId={ rootClientId } filterValue={ filterValue } onSelect={ onSelectItem } listProps={ listProps } + isFullScreen={ ! isIOS && showSearchForm } /> ) : ( <InserterTabs diff --git a/packages/block-editor/src/components/inserter/search-results.native.js b/packages/block-editor/src/components/inserter/search-results.native.js index 81ebf0b4d2995..a48109e5c12fe 100644 --- a/packages/block-editor/src/components/inserter/search-results.native.js +++ b/packages/block-editor/src/components/inserter/search-results.native.js @@ -12,18 +12,34 @@ import InserterNoResults from './no-results'; import { store as blockEditorStore } from '../../store'; import useBlockTypeImpressions from './hooks/use-block-type-impressions'; +const NON_BLOCK_CATEGORIES = [ 'reusable' ]; +const ALLOWED_EMBED_VARIATIONS = [ 'core/embed' ]; + function InserterSearchResults( { filterValue, onSelect, listProps, rootClientId, + isFullScreen, } ) { const { blockTypes } = useSelect( ( select ) => { const allItems = select( blockEditorStore ).getInserterItems( rootClientId ); - const filteredItems = searchItems( allItems, filterValue ); + + const blockItems = allItems.filter( + ( { id, category } ) => + ! NON_BLOCK_CATEGORIES.includes( category ) && + // We don't want to show all possible embed variations + // as different blocks in the inserter. We'll only show a + // few popular ones. + ( category !== 'embed' || + ( category === 'embed' && + ALLOWED_EMBED_VARIATIONS.includes( id ) ) ) + ); + + const filteredItems = searchItems( blockItems, filterValue ); return { blockTypes: filteredItems }; }, @@ -46,6 +62,7 @@ function InserterSearchResults( { return ( <BlockTypesList name="Blocks" + initialNumToRender={ isFullScreen ? 10 : 3 } { ...{ items, onSelect: handleSelect, listProps } } /> ); diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss index 8fcb9c1cd0452..105af1f72b7d9 100644 --- a/packages/block-editor/src/components/inserter/style.native.scss +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -8,7 +8,11 @@ color: $blue-30; } -.list { +.inserter-menu__list-wrapper { + flex: 1; +} + +.inserter-menu__list { padding-bottom: 20; padding-top: 8; } @@ -51,13 +55,12 @@ .inserter-tabs__wrapper { overflow: hidden; + flex: 1; } .inserter-tabs__container { height: 100%; width: 100%; -} - -.inserter-tabs__item { - position: absolute; + flex: 1; + flex-direction: row; } diff --git a/packages/block-editor/src/components/inserter/tabs.native.js b/packages/block-editor/src/components/inserter/tabs.native.js index 06ab3bc859036..73396ec63a6e9 100644 --- a/packages/block-editor/src/components/inserter/tabs.native.js +++ b/packages/block-editor/src/components/inserter/tabs.native.js @@ -100,13 +100,7 @@ function InserterTabs( { > <Animated.View style={ containerStyle }> { tabs.map( ( { component: TabComponent }, index ) => ( - <View - key={ `tab-${ index }` } - style={ [ - styles[ 'inserter-tabs__item' ], - { left: index * wrapperWidth }, - ] } - > + <View key={ `tab-${ index }` }> <TabComponent rootClientId={ rootClientId } onSelect={ onSelect } diff --git a/packages/block-editor/src/components/inspector-advanced-controls/README.md b/packages/block-editor/src/components/inspector-advanced-controls/README.md deleted file mode 100644 index b14b0cb6a73e2..0000000000000 --- a/packages/block-editor/src/components/inspector-advanced-controls/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# InspectorAdvancedControls - -<img src="https://user-images.githubusercontent.com/150562/94028603-df90bf00-fdb3-11ea-9e6f-eb15c5631d85.png" width="280" alt="inspector-advanced-controls"> - -Inspector Advanced Controls appear under the _Advanced_ panel of a block's [InspectorControls](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md) -- that is, they appear as a specific set of controls within a block's settings panels. As the name suggests, `InspectorAdvancedControls` is meant for controls that most users aren't meant to interact with most of the time, such as adding an HTML anchor or custom CSS classes to a block. - -## Usage - -{% codetabs %} -{% ESNext %} - -```js -const { - TextControl, -} = wp.components; -const { - InspectorControls, - InspectorAdvancedControls, -} = wp.editor; - -function MyBlockEdit( { attributes, setAttributes } ) { - return ( - <> - <div> - { /* Block markup goes here */ } - </div - <InspectorControls> - { /* Regular control goes here */ - </InspectorControls> - <InspectorAdvancedControls> - <TextControl - label="HTML anchor" - value={ attributes.anchor } - onChange={ ( nextValue ) => { - setAttributes( { - anchor: nextValue, - } ); - } } - /> - </InspectorAdvancedControls> - </> - ); -} -``` - -{% ES5 %} - -```js -var el = wp.element.createElement, - Fragment = wp.element.Fragment, - InspectorControls = wp.editor.InspectorControls, - InspectorAdvancedControlsControls = wp.editor.InspectorAdvancedControls, - TextControl = wp.components.TextControl, - -function MyBlockEdit( props ) { - return el( Fragment, null, - el( 'div', null, /* Block markup goes here */ null ), - el( InspectorControls, null, /* Regular control goes here */ null ), - el( InspectorAdvancedControls, null, - el( TextControl, { - label: 'HTML anchor', - value: props.attributes.anchor, - onChange: function( nextValue ) { - props.setAttributes( { anchor: nextValue } ); - } - } ) - ) - ); -} -``` - -{% end %} diff --git a/packages/block-editor/src/components/inspector-advanced-controls/index.js b/packages/block-editor/src/components/inspector-advanced-controls/index.js deleted file mode 100644 index 4ed9b6341142d..0000000000000 --- a/packages/block-editor/src/components/inspector-advanced-controls/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createSlotFill, - __experimentalStyleProvider as StyleProvider, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { useBlockEditContext } from '../block-edit/context'; - -const name = 'InspectorAdvancedControls'; -const { Fill, Slot } = createSlotFill( name ); - -function InspectorAdvancedControls( { children } ) { - const { isSelected } = useBlockEditContext(); - return isSelected ? ( - <StyleProvider document={ document }> - <Fill>{ children }</Fill> - </StyleProvider> - ) : null; -} - -InspectorAdvancedControls.slotName = name; -InspectorAdvancedControls.Slot = Slot; - -/** - * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-advanced-controls/README.md - */ -export default InspectorAdvancedControls; diff --git a/packages/block-editor/src/components/inspector-controls/README.md b/packages/block-editor/src/components/inspector-controls/README.md index 8310ae3a8db7e..2799431917b75 100644 --- a/packages/block-editor/src/components/inspector-controls/README.md +++ b/packages/block-editor/src/components/inspector-controls/README.md @@ -6,201 +6,6 @@ Inspector Controls appear in the post settings sidebar when a block is being edi ## Usage -{% codetabs %} -{% ES5 %} - -```js -var el = wp.element.createElement, - Fragment = wp.element.Fragment, - registerBlockType = wp.blocks.registerBlockType, - RichText = wp.editor.RichText, - InspectorControls = wp.blockEditor.InspectorControls, - useBlockProps = wp.blockEditor.useBlockProps, - CheckboxControl = wp.components.CheckboxControl, - RadioControl = wp.components.RadioControl, - TextControl = wp.components.TextControl, - ToggleControl = wp.components.ToggleControl, - SelectControl = wp.components.SelectControl, - PanelBody = wp.components.PanelBody; - -registerBlockType( 'my-plugin/inspector-controls-example', { - apiVersion: 2, - - title: 'Inspector controls example', - - icon: 'universal-access-alt', - - category: 'design', - - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - checkboxField: { - type: 'boolean', - default: true, - }, - radioField: { - type: 'string', - default: 'yes', - }, - textField: { - type: 'string', - }, - toggleField: { - type: 'boolean', - }, - selectField: { - type: 'string', - }, - }, - - edit: function ( props ) { - var blockProps = useBlockProps(); - - var content = props.attributes.content, - checkboxField = props.attributes.checkboxField, - radioField = props.attributes.radioField, - textField = props.attributes.textField, - toggleField = props.attributes.toggleField, - selectField = props.attributes.selectField; - - function onChangeContent( newContent ) { - props.setAttributes( { content: newContent } ); - } - - function onChangeCheckboxField( newValue ) { - props.setAttributes( { checkboxField: newValue } ); - } - - function onChangeRadioField( newValue ) { - props.setAttributes( { radioField: newValue } ); - } - - function onChangeTextField( newValue ) { - props.setAttributes( { textField: newValue } ); - } - - function onChangeToggleField( newValue ) { - props.setAttributes( { toggleField: newValue } ); - } - - function onChangeSelectField( newValue ) { - props.setAttributes( { selectField: newValue } ); - } - - return el( - Fragment, - null, - el( - InspectorControls, - null, - el( - PanelBody, - { - title: 'Settings', - }, - el( CheckboxControl, { - heading: 'Checkbox Field', - label: 'Tick Me', - help: 'Additional help text', - checked: checkboxField, - onChange: onChangeCheckboxField, - } ), - el( RadioControl, { - label: 'Radio Field', - selected: radioField, - options: [ - { - label: 'Yes', - value: 'yes', - }, - { - label: 'No', - value: 'no', - }, - ], - onChange: onChangeRadioField, - } ), - el( TextControl, { - label: 'Text Field', - help: 'Additional help text', - value: textField, - onChange: onChangeTextField, - } ), - el( ToggleControl, { - label: 'Toggle Field', - checked: toggleField, - onChange: onChangeToggleField, - } ), - el( SelectControl, { - label: 'Select Field', - value: selectField, - options: [ - { - value: 'a', - label: 'Option A', - }, - { - value: 'b', - label: 'Option B', - }, - { - value: 'c', - label: 'Option C', - }, - ], - onChange: onChangeSelectField, - } ) - ) - ), - el( - RichText, - Object.assing( blockProps, { - key: 'editable', - tagName: 'p', - onChange: onChangeContent, - value: content, - } ) - ) - ); - }, - - save: function ( props ) { - var blockProps = useBlockProps.save(); - var content = props.attributes.content, - checkboxField = props.attributes.checkboxField, - radioField = props.attributes.radioField, - textField = props.attributes.textField, - toggleField = props.attributes.toggleField, - selectField = props.attributes.selectField; - - return el( - 'div', - blockProps, - el( RichText.Content, { - value: content, - tagName: 'p', - } ), - el( 'h2', null, 'Inspector Control Fields' ), - el( - 'ul', - null, - el( 'li', null, 'Checkbox Field: ', checkboxField ), - el( 'li', null, 'Radio Field: ', radioField ), - el( 'li', null, 'Text Field: ', textField ), - el( 'li', null, 'Toggle Field: ', toggleField ), - el( 'li', null, 'Select Field: ', selectField ) - ) - ); - }, -} ); -``` - -{% ESNext %} - ```js import { registerBlockType } from '@wordpress/blocks'; import { @@ -209,7 +14,7 @@ import { TextControl, ToggleControl, SelectControl, - PanelBody + PanelBody, } from '@wordpress/components'; import { RichText, @@ -289,7 +94,7 @@ registerBlockType( 'my-plugin/inspector-controls-example', { return ( <> <InspectorControls> - <PanelBody title={__('Settings')}> + <PanelBody title={ __( 'Settings' ) }> <CheckboxControl heading="Checkbox Field" label="Tick Me" @@ -374,4 +179,44 @@ registerBlockType( 'my-plugin/inspector-controls-example', { } ); ``` -{% end %} +## InspectorAdvancedControls + +<img src="https://user-images.githubusercontent.com/150562/94028603-df90bf00-fdb3-11ea-9e6f-eb15c5631d85.png" width="280" alt="inspector-advanced-controls"> + +Inspector Advanced Controls appear under the _Advanced_ panel of a block's [InspectorControls](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md) -- that is, they appear as a specific set of controls within a block's settings panels. As the name suggests, `InspectorAdvancedControls` is meant for controls that most users aren't meant to interact with most of the time, such as adding an HTML anchor or custom CSS classes to a block. + +### Usage + +```js +import { + TextControl, +} from '@wordpress/components'; +import { + InspectorControls, + InspectorAdvancedControls, +} from '@wordpress/block-editor'; + +function MyBlockEdit( { attributes, setAttributes } ) { + return ( + <> + <div> + { /* Block markup goes here */ } + </div + <InspectorControls> + { /* Regular control goes here */ + </InspectorControls> + <InspectorAdvancedControls> + <TextControl + label="HTML anchor" + value={ attributes.anchor } + onChange={ ( nextValue ) => { + setAttributes( { + anchor: nextValue, + } ); + } } + /> + </InspectorAdvancedControls> + </> + ); +} +``` diff --git a/packages/block-editor/src/components/inspector-controls/fill.js b/packages/block-editor/src/components/inspector-controls/fill.js new file mode 100644 index 0000000000000..bb1c8fd7accdf --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls/fill.js @@ -0,0 +1,32 @@ +/** + * WordPress dependencies + */ +import { __experimentalStyleProvider as StyleProvider } from '@wordpress/components'; +import warning from '@wordpress/warning'; + +/** + * Internal dependencies + */ +import useDisplayBlockControls from '../use-display-block-controls'; +import groups from './groups'; + +export default function InspectorControlsFill( { + __experimentalGroup: group = 'default', + children, +} ) { + const isDisplayed = useDisplayBlockControls(); + const Fill = groups[ group ]?.Fill; + if ( ! Fill ) { + warning( `Unknown InspectorControl group "${ group }" provided.` ); + return null; + } + if ( ! isDisplayed ) { + return null; + } + + return ( + <StyleProvider document={ document }> + <Fill>{ children }</Fill> + </StyleProvider> + ); +} diff --git a/packages/block-editor/src/components/inspector-controls/index.native.js b/packages/block-editor/src/components/inspector-controls/fill.native.js similarity index 57% rename from packages/block-editor/src/components/inspector-controls/index.native.js rename to packages/block-editor/src/components/inspector-controls/fill.native.js index ed131ddb4ce6a..01ac32b256500 100644 --- a/packages/block-editor/src/components/inspector-controls/index.native.js +++ b/packages/block-editor/src/components/inspector-controls/fill.native.js @@ -7,18 +7,27 @@ import { View } from 'react-native'; * WordPress dependencies */ import { Children } from '@wordpress/element'; -import { createSlotFill, BottomSheetConsumer } from '@wordpress/components'; +import { BottomSheetConsumer } from '@wordpress/components'; +import warning from '@wordpress/warning'; /** * Internal dependencies */ +import groups from './groups'; import { useBlockEditContext } from '../block-edit/context'; import { BlockSettingsButton } from '../block-settings'; -const { Fill, Slot } = createSlotFill( 'InspectorControls' ); - -const FillWithSettingsButton = ( { children, ...props } ) => { +export default function InspectorControlsFill( { + children, + __experimentalGroup: group = 'default', + ...props +} ) { const { isSelected } = useBlockEditContext(); + const Fill = groups[ group ]?.Fill; + if ( ! Fill ) { + warning( `Unknown InspectorControl group "${ group }" provided.` ); + return null; + } if ( ! isSelected ) { return null; } @@ -35,13 +44,4 @@ const FillWithSettingsButton = ( { children, ...props } ) => { { Children.count( children ) > 0 && <BlockSettingsButton /> } </> ); -}; - -const InspectorControls = FillWithSettingsButton; - -InspectorControls.Slot = Slot; - -/** - * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md - */ -export default InspectorControls; +} diff --git a/packages/block-editor/src/components/inspector-controls/groups.js b/packages/block-editor/src/components/inspector-controls/groups.js new file mode 100644 index 0000000000000..a989132afd4c7 --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls/groups.js @@ -0,0 +1,14 @@ +/** + * WordPress dependencies + */ +import { createSlotFill } from '@wordpress/components'; + +const InspectorControlsDefault = createSlotFill( 'InspectorControls' ); +const InspectorControlsAdvanced = createSlotFill( 'InspectorAdvancedControls' ); + +const groups = { + default: InspectorControlsDefault, + advanced: InspectorControlsAdvanced, +}; + +export default groups; diff --git a/packages/block-editor/src/components/inspector-controls/index.js b/packages/block-editor/src/components/inspector-controls/index.js index 9a6cb7bab61a7..4cb36cdc84399 100644 --- a/packages/block-editor/src/components/inspector-controls/index.js +++ b/packages/block-editor/src/components/inspector-controls/index.js @@ -1,27 +1,25 @@ -/** - * WordPress dependencies - */ -import { - __experimentalStyleProvider as StyleProvider, - createSlotFill, -} from '@wordpress/components'; - /** * Internal dependencies */ -import useDisplayBlockControls from '../use-display-block-controls'; +import InspectorControlsFill from './fill'; +import InspectorControlsSlot from './slot'; -const { Fill, Slot } = createSlotFill( 'InspectorControls' ); +const InspectorControls = InspectorControlsFill; -function InspectorControls( { children } ) { - return useDisplayBlockControls() ? ( - <StyleProvider document={ document }> - <Fill>{ children }</Fill> - </StyleProvider> - ) : null; -} +InspectorControls.Slot = InspectorControlsSlot; -InspectorControls.Slot = Slot; +// This is just here for backward compatibility. +export const InspectorAdvancedControls = ( props ) => { + return ( + <InspectorControlsFill { ...props } __experimentalGroup="advanced" /> + ); +}; +InspectorAdvancedControls.Slot = ( props ) => { + return ( + <InspectorControlsSlot { ...props } __experimentalGroup="advanced" /> + ); +}; +InspectorAdvancedControls.slotName = 'InspectorAdvancedControls'; /** * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md diff --git a/packages/block-editor/src/components/inspector-controls/slot.js b/packages/block-editor/src/components/inspector-controls/slot.js new file mode 100644 index 0000000000000..f17714cfe41b6 --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls/slot.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { __experimentalUseSlot as useSlot } from '@wordpress/components'; +import warning from '@wordpress/warning'; + +/** + * Internal dependencies + */ +import groups from './groups'; + +export default function InspectorControlsSlot( { + __experimentalGroup: group = 'default', + bubblesVirtually = true, + ...props +} ) { + const Slot = groups[ group ]?.Slot; + const slot = useSlot( Slot?.__unstableName ); + if ( ! Slot || ! slot ) { + warning( `Unknown InspectorControl group "${ group }" provided.` ); + return null; + } + + const hasFills = Boolean( slot.fills && slot.fills.length ); + if ( ! hasFills ) { + return null; + } + + return <Slot { ...props } bubblesVirtually={ bubblesVirtually } />; +} diff --git a/packages/block-editor/src/components/inspector-controls/slot.native.js b/packages/block-editor/src/components/inspector-controls/slot.native.js new file mode 100644 index 0000000000000..adf4da06965e4 --- /dev/null +++ b/packages/block-editor/src/components/inspector-controls/slot.native.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import warning from '@wordpress/warning'; + +/** + * Internal dependencies + */ +import groups from './groups'; + +export default function InspectorControlsSlot( { + __experimentalGroup: group = 'default', + ...props +} ) { + const Slot = groups[ group ]?.Slot; + if ( ! Slot ) { + warning( `Unknown InspectorControl group "${ group }" provided.` ); + return null; + } + + return <Slot { ...props } />; +} diff --git a/packages/block-editor/src/components/line-height-control/index.native.js b/packages/block-editor/src/components/line-height-control/index.native.js new file mode 100644 index 0000000000000..95645805592cf --- /dev/null +++ b/packages/block-editor/src/components/line-height-control/index.native.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { UnitControl } from '@wordpress/components'; +/** + * Internal dependencies + */ +import { BASE_DEFAULT_VALUE, STEP, isLineHeightDefined } from './utils'; + +export default function LineHeightControl( { value: lineHeight, onChange } ) { + const isDefined = isLineHeightDefined( lineHeight ); + const value = isDefined ? lineHeight : BASE_DEFAULT_VALUE; + return ( + <UnitControl + label={ __( 'Line Height' ) } + min={ 0 } + max={ 5 } + step={ STEP } + value={ value } + onChange={ onChange } + units={ false } + /> + ); +} diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 1fa3f75b64ced..82909cd957368 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -11,6 +11,7 @@ import { useMemo, useRef, useReducer, + forwardRef, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -48,15 +49,19 @@ const expanded = ( state, action ) => { * @param {boolean} props.showOnlyCurrentHierarchy Flag to limit the list to the current hierarchy of blocks. * @param {boolean} props.__experimentalFeatures Flag to enable experimental features. * @param {boolean} props.__experimentalPersistentListViewFeatures Flag to enable features for the Persistent List View experiment. + * @param {Object} ref Forwarded ref */ -export default function ListView( { - blocks, - showOnlyCurrentHierarchy, - onSelect = noop, - __experimentalFeatures, - __experimentalPersistentListViewFeatures, - ...props -} ) { +function ListView( + { + blocks, + showOnlyCurrentHierarchy, + onSelect = noop, + __experimentalFeatures, + __experimentalPersistentListViewFeatures, + ...props + }, + ref +) { const { clientIdsTree, selectedClientIds } = useListViewClientIds( blocks, showOnlyCurrentHierarchy, @@ -74,7 +79,7 @@ export default function ListView( { const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone(); const elementRef = useRef(); - const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef ] ); + const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] ); const isMounted = useRef( false ); useEffect( () => { @@ -144,3 +149,4 @@ export default function ListView( { </> ); } +export default forwardRef( ListView ); diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index f00eae7650e48..1fb11cd0aa089 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -36,7 +36,7 @@ const InsertFromURLPopover = ( { src, onChange, onSubmit, onClose } ) => ( > <input className="block-editor-media-placeholder__url-input-field" - type="url" + type="text" aria-label={ __( 'URL' ) } placeholder={ __( 'Paste or type URL' ) } onChange={ onChange } diff --git a/packages/block-editor/src/components/media-placeholder/style.scss b/packages/block-editor/src/components/media-placeholder/style.scss index 88fa0f90deddc..e8eaa4bf43d94 100644 --- a/packages/block-editor/src/components/media-placeholder/style.scss +++ b/packages/block-editor/src/components/media-placeholder/style.scss @@ -11,6 +11,8 @@ // Selector requires a lot of specificity to override base styles. input[type="url"].block-editor-media-placeholder__url-input-field { width: 100%; + min-width: 200px; + @include break-small() { width: 300px; } diff --git a/packages/block-editor/src/components/media-replace-flow/style.scss b/packages/block-editor/src/components/media-replace-flow/style.scss index 110194f490b46..f954777954829 100644 --- a/packages/block-editor/src/components/media-replace-flow/style.scss +++ b/packages/block-editor/src/components/media-replace-flow/style.scss @@ -5,22 +5,16 @@ display: none; } -// Forcing some space above the list of options in -// the dropdown to visually balance them. -.block-editor-media-replace-flow__options .components-popover__content > div { - padding-top: $grid-unit-20; -} - .block-editor-media-replace-flow__indicator { margin-left: 4px; } .block-editor-media-flow__url-input { border-top: $border-width solid $gray-900; - margin-top: $grid-unit-15; - margin-right: -$grid-unit-15; - margin-left: -$grid-unit-15; - padding: $grid-unit-15 $grid-unit-30 0; + margin-top: $grid-unit-10; + margin-right: -$grid-unit-10; + margin-left: -$grid-unit-10; + padding: $grid-unit-20; .block-editor-media-replace-flow__image-url-label { display: block; diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 807a901485015..7ea9832740e4e 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -7,7 +7,13 @@ import { omit } from 'lodash'; /** * WordPress dependencies */ -import { RawHTML, useRef, useCallback, forwardRef } from '@wordpress/element'; +import { + RawHTML, + useRef, + useCallback, + forwardRef, + createContext, +} from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { children as childrenSource } from '@wordpress/blocks'; import { useInstanceId, useMergeRefs } from '@wordpress/compose'; @@ -36,9 +42,14 @@ import { useInputRules } from './use-input-rules'; import { useEnter } from './use-enter'; import { useFormatTypes } from './use-format-types'; import { useRemoveBrowserShortcuts } from './use-remove-browser-shortcuts'; +import { useShortcuts } from './use-shortcuts'; +import { useInputEvents } from './use-input-events'; import FormatEdit from './format-edit'; import { getMultilineTag, getAllowedFormats } from './utils'; +export const keyboardShortcutContext = createContext(); +export const inputEventContext = createContext(); + /** * Removes props used for the native version of RichText so that they are not * passed to the DOM element and log warnings. @@ -213,7 +224,7 @@ function RichTextWrapper( ); } - const { value, onChange, onFocus, ref: richTextRef } = useRichText( { + const { value, onChange, ref: richTextRef } = useRichText( { value: adjustedValue, onChange( html, { __unstableFormats, __unstableText } ) { adjustedOnChange( html ); @@ -244,6 +255,9 @@ function RichTextWrapper( useCaretInFormat( { value } ); useMarkPersistent( { html: adjustedValue, value } ); + const keyboardShortcuts = useRef( new Set() ); + const inputEvents = useRef( new Set() ); + function onKeyDown( event ) { const { keyCode } = event; @@ -283,20 +297,26 @@ function RichTextWrapper( } } + function onFocus() { + anchorRef.current.focus(); + } + const TagName = tagName; const content = ( <> - { isSelected && - children && - children( { value, onChange, onFocus } ) } { isSelected && ( - <FormatEdit - value={ value } - onChange={ onChange } - onFocus={ onFocus } - formatTypes={ formatTypes } - forwardedRef={ anchorRef } - /> + <keyboardShortcutContext.Provider value={ keyboardShortcuts }> + <inputEventContext.Provider value={ inputEvents }> + { children && children( { value, onChange, onFocus } ) } + <FormatEdit + value={ value } + onChange={ onChange } + onFocus={ onFocus } + formatTypes={ formatTypes } + forwardedRef={ anchorRef } + /> + </inputEventContext.Provider> + </keyboardShortcutContext.Provider> ) } { isSelected && hasFormats && ( <FormatToolbarContainer @@ -323,6 +343,8 @@ function RichTextWrapper( onReplace, } ), useRemoveBrowserShortcuts(), + useShortcuts( keyboardShortcuts ), + useInputEvents( inputEvents ), useUndoAutomaticChange(), usePasteHandler( { isSelected, diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 5c9af75b2796f..85c1e64785300 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -125,7 +125,7 @@ function RichTextWrapper( getSelectionEnd, getSettings, didAutomaticChange, - __unstableGetBlockWithoutInnerBlocks, + getBlock, isMultiSelecting, hasMultiSelection, } = select( blockEditorStore ); @@ -149,8 +149,7 @@ function RichTextWrapper( // If the block of this RichText is unmodified then it's a candidate for replacing when adding a new block. // In order to fix https://github.com/wordpress-mobile/gutenberg-mobile/issues/1126, let's blur on unmount in that case. // This apparently assumes functionality the BlockHlder actually - const block = - clientId && __unstableGetBlockWithoutInnerBlocks( clientId ); + const block = clientId && getBlock( clientId ); const shouldBlurOnUnmount = block && isSelected && isUnmodifiedDefaultBlock( block ); extraProps = { diff --git a/packages/block-editor/src/components/rich-text/input-event.js b/packages/block-editor/src/components/rich-text/input-event.js index 016e370095681..ab79a9a51dd98 100644 --- a/packages/block-editor/src/components/rich-text/input-event.js +++ b/packages/block-editor/src/components/rich-text/input-event.js @@ -1,30 +1,31 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; +import { useEffect, useContext, useRef } from '@wordpress/element'; -export class __unstableRichTextInputEvent extends Component { - constructor() { - super( ...arguments ); +/** + * Internal dependencies + */ +import { inputEventContext } from './'; - this.onInput = this.onInput.bind( this ); - } +export function __unstableRichTextInputEvent( { inputType, onInput } ) { + const callbacks = useContext( inputEventContext ); + const onInputRef = useRef(); + onInputRef.current = onInput; - onInput( event ) { - if ( event.inputType === this.props.inputType ) { - this.props.onInput(); + useEffect( () => { + function callback( event ) { + if ( event.inputType === inputType ) { + onInputRef.current(); + event.preventDefault(); + } } - } - - componentDidMount() { - document.addEventListener( 'input', this.onInput, true ); - } - componentWillUnmount() { - document.removeEventListener( 'input', this.onInput, true ); - } + callbacks.current.add( callback ); + return () => { + callbacks.current.delete( callback ); + }; + }, [ inputType ] ); - render() { - return null; - } + return null; } diff --git a/packages/block-editor/src/components/rich-text/shortcut.js b/packages/block-editor/src/components/rich-text/shortcut.js index 4f93c6c99fad1..05aa0bcb61ddc 100644 --- a/packages/block-editor/src/components/rich-text/shortcut.js +++ b/packages/block-editor/src/components/rich-text/shortcut.js @@ -1,17 +1,32 @@ /** * WordPress dependencies */ -import { useKeyboardShortcut } from '@wordpress/compose'; -import { rawShortcut } from '@wordpress/keycodes'; +import { isKeyboardEvent } from '@wordpress/keycodes'; +import { useEffect, useContext, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { keyboardShortcutContext } from './'; export function RichTextShortcut( { character, type, onUse } ) { - const callback = () => { - onUse(); - return false; - }; - useKeyboardShortcut( rawShortcut[ type ]( character ), callback, { - bindGlobal: true, - } ); + const keyboardShortcuts = useContext( keyboardShortcutContext ); + const onUseRef = useRef(); + onUseRef.current = onUse; + + useEffect( () => { + function callback( event ) { + if ( isKeyboardEvent[ type ]( event, character ) ) { + onUseRef.current(); + event.preventDefault(); + } + } + + keyboardShortcuts.current.add( callback ); + return () => { + keyboardShortcuts.current.delete( callback ); + }; + }, [ character, type ] ); return null; } diff --git a/packages/block-editor/src/components/rich-text/use-input-events.js b/packages/block-editor/src/components/rich-text/use-input-events.js new file mode 100644 index 0000000000000..4305e126fda7e --- /dev/null +++ b/packages/block-editor/src/components/rich-text/use-input-events.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; + +export function useInputEvents( inputEvents ) { + return useRefEffect( ( element ) => { + function onInput( event ) { + for ( const keyboardShortcut of inputEvents.current ) { + keyboardShortcut( event ); + } + } + + element.addEventListener( 'input', onInput ); + return () => { + element.removeEventListener( 'input', onInput ); + }; + }, [] ); +} diff --git a/packages/block-editor/src/components/rich-text/use-shortcuts.js b/packages/block-editor/src/components/rich-text/use-shortcuts.js new file mode 100644 index 0000000000000..61b2e3f6ce31b --- /dev/null +++ b/packages/block-editor/src/components/rich-text/use-shortcuts.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; + +export function useShortcuts( keyboardShortcuts ) { + return useRefEffect( ( element ) => { + function onKeyDown( event ) { + for ( const keyboardShortcut of keyboardShortcuts.current ) { + keyboardShortcut( event ); + } + } + + element.addEventListener( 'keydown', onKeyDown ); + return () => { + element.removeEventListener( 'keydown', onKeyDown ); + }; + }, [] ); +} diff --git a/packages/block-editor/src/components/tool-selector/style.scss b/packages/block-editor/src/components/tool-selector/style.scss index ad605ef037fe7..03774fe0f6b9d 100644 --- a/packages/block-editor/src/components/tool-selector/style.scss +++ b/packages/block-editor/src/components/tool-selector/style.scss @@ -1,10 +1,10 @@ .block-editor-tool-selector__help { margin-top: $grid-unit-10; - margin-left: -$grid-unit-15; - margin-right: -$grid-unit-15; - margin-bottom: -$grid-unit-15; - padding: $grid-unit-15 ($grid-unit-15 + $grid-unit-10); - border-top: 1px solid $gray-300; + margin-left: -$grid-unit-10; + margin-right: -$grid-unit-10; + margin-bottom: -$grid-unit-10; + padding: $grid-unit-20; + border-top: $border-width solid $gray-300; color: $gray-700; min-width: 280px; } diff --git a/packages/block-editor/src/components/use-display-block-controls/index.js b/packages/block-editor/src/components/use-display-block-controls/index.js index a3f4e7c4362f6..605556f295b96 100644 --- a/packages/block-editor/src/components/use-display-block-controls/index.js +++ b/packages/block-editor/src/components/use-display-block-controls/index.js @@ -11,11 +11,10 @@ import { store as blockEditorStore } from '../../store'; export default function useDisplayBlockControls() { const { isSelected, clientId, name } = useBlockEditContext(); - const isFirstAndSameTypeMultiSelected = useSelect( + return useSelect( ( select ) => { - // Don't bother checking, see OR statement below. if ( isSelected ) { - return; + return true; } const { @@ -24,16 +23,14 @@ export default function useDisplayBlockControls() { getMultiSelectedBlockClientIds, } = select( blockEditorStore ); - if ( ! isFirstMultiSelectedBlock( clientId ) ) { - return false; + if ( isFirstMultiSelectedBlock( clientId ) ) { + return getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === name + ); } - return getMultiSelectedBlockClientIds().every( - ( id ) => getBlockName( id ) === name - ); + return false; }, [ clientId, isSelected, name ] ); - - return isSelected || isFirstAndSameTypeMultiSelected; } diff --git a/packages/block-editor/src/components/use-setting/index.js b/packages/block-editor/src/components/use-setting/index.js index f82b7e38f0d2e..74f03a3545cdd 100644 --- a/packages/block-editor/src/components/use-setting/index.js +++ b/packages/block-editor/src/components/use-setting/index.js @@ -7,6 +7,7 @@ import { get } from 'lodash'; * WordPress dependencies */ import { useSelect } from '@wordpress/data'; +import { __EXPERIMENTAL_PATHS_WITH_MERGE as PATHS_WITH_MERGE } from '@wordpress/blocks'; /** * Internal dependencies @@ -49,13 +50,6 @@ const deprecatedFlags = { 'spacing.customPadding': ( settings ) => settings.enableCustomSpacing, }; -const PATHS_WITH_MERGE = { - 'color.gradients': true, - 'color.palette': true, - 'typography.fontFamilies': true, - 'typography.fontSizes': true, -}; - /** * Hook that retrieves the editor setting. * It works with nested objects using by finding the value at path. diff --git a/packages/block-editor/src/components/writing-flow/use-tab-nav.js b/packages/block-editor/src/components/writing-flow/use-tab-nav.js index 96a6d522b4629..3b3b3cf607d45 100644 --- a/packages/block-editor/src/components/writing-flow/use-tab-nav.js +++ b/packages/block-editor/src/components/writing-flow/use-tab-nav.js @@ -179,17 +179,13 @@ export default function useTabNav() { } } - node.ownerDocument.defaultView.addEventListener( - 'keydown', - preventScrollOnTab - ); + const { ownerDocument } = node; + const { defaultView } = ownerDocument; + defaultView.addEventListener( 'keydown', preventScrollOnTab ); node.addEventListener( 'keydown', onKeyDown ); node.addEventListener( 'focusout', onFocusOut ); return () => { - node.ownerDocument.defaultView.removeEventListener( - 'keydown', - preventScrollOnTab - ); + defaultView.removeEventListener( 'keydown', preventScrollOnTab ); node.removeEventListener( 'keydown', onKeyDown ); node.removeEventListener( 'focusout', onFocusOut ); }; diff --git a/packages/block-editor/src/default-editor-styles.scss b/packages/block-editor/src/default-editor-styles.scss new file mode 100644 index 0000000000000..e63291a30d7d1 --- /dev/null +++ b/packages/block-editor/src/default-editor-styles.scss @@ -0,0 +1,24 @@ +/** + * Default editor styles. + * + * These styles are shown if a theme does not register its own editor style, + * a theme.json file, or has toggled off "Use theme styles" in preferences. + */ + +body { + font-family: $default-font; + font-size: 18px; + line-height: 1.5; + --wp--style--block-gap: 2em; +} + +p { + line-height: 1.8; +} + +.editor-post-title__block { + margin-top: 2em; + margin-bottom: 1em; + font-size: 2.5em; + font-weight: 800; +} diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 7cfd5f8b13fde..c72b1aa29f796 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -125,9 +125,6 @@ export const withToolbarControls = createHigherOrderComponent( getBlockSupport( blockName, 'align' ), hasBlockSupport( blockName, 'alignWide', true ) ); - const validAlignments = useAvailableAlignments( - blockAllowedAlignments - ); const updateAlignment = ( nextAlign ) => { if ( ! nextAlign ) { @@ -140,18 +137,20 @@ export const withToolbarControls = createHigherOrderComponent( props.setAttributes( { align: nextAlign } ); }; - return [ - validAlignments.length > 0 && props.isSelected && ( - <BlockControls key="align-controls" group="block"> - <BlockAlignmentControl - value={ props.attributes.align } - onChange={ updateAlignment } - controls={ validAlignments } - /> - </BlockControls> - ), - <BlockEdit key="edit" { ...props } />, - ]; + return ( + <> + { blockAllowedAlignments.length > 0 && ( + <BlockControls group="block" __experimentalExposeToChildren> + <BlockAlignmentControl + value={ props.attributes.align } + onChange={ updateAlignment } + controls={ blockAllowedAlignments } + /> + </BlockControls> + ) } + <BlockEdit { ...props } /> + </> + ); }, 'withToolbarControls' ); diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 3e1e2b813072e..07024b78c51f6 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -16,7 +16,7 @@ import { Platform } from '@wordpress/element'; /** * Internal dependencies */ -import { InspectorControls, InspectorAdvancedControls } from '../components'; +import { InspectorControls } from '../components'; /** * Regular expression matching invalid anchor characters for replacement. @@ -107,9 +107,9 @@ export const withInspectorControl = createHigherOrderComponent( <> <BlockEdit { ...props } /> { isWeb && ( - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> { textControl } - </InspectorAdvancedControls> + </InspectorControls> ) } { /* * We plan to remove scoping anchors to 'core/heading' to support diff --git a/packages/block-editor/src/hooks/border-color.js b/packages/block-editor/src/hooks/border-color.js index 036ff345a8ccd..9060f21236d69 100644 --- a/packages/block-editor/src/hooks/border-color.js +++ b/packages/block-editor/src/hooks/border-color.js @@ -9,6 +9,7 @@ import classnames from 'classnames'; import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; import { createHigherOrderComponent } from '@wordpress/compose'; +import { useState } from '@wordpress/element'; /** * Internal dependencies @@ -48,8 +49,18 @@ export function BorderColorEdit( props ) { const colors = useSetting( 'color.palette' ) || EMPTY_ARRAY; const disableCustomColors = ! useSetting( 'color.custom' ); const disableCustomGradients = ! useSetting( 'color.customGradient' ); + const [ colorValue, setColorValue ] = useState( + () => + getColorObjectByAttributeValues( + colors, + borderColor, + style?.border?.color + )?.color + ); const onChangeColor = ( value ) => { + setColorValue( value ); + const colorObject = getColorObjectByColorValue( colors, value ); const newStyle = { ...style, @@ -71,7 +82,7 @@ export function BorderColorEdit( props ) { return ( <ColorGradientControl label={ __( 'Color' ) } - value={ borderColor || style?.border?.color } + colorValue={ colorValue } colors={ colors } gradients={ undefined } disableCustomColors={ disableCustomColors } diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index d89e91dae87dd..fa8fb9e0cbdd2 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -222,6 +222,8 @@ export function ColorEdit( props ) { const areCustomSolidsEnabled = useSetting( 'color.custom' ); const areCustomGradientsEnabled = useSetting( 'color.customGradient' ); const isLinkEnabled = useSetting( 'color.link' ); + const isTextEnabled = useSetting( 'color.text' ); + const isBackgroundEnabled = useSetting( 'color.background' ); // Shouldn't be needed but right now the ColorGradientsPanel // can trigger both onChangeColor and onChangeBackground @@ -242,9 +244,11 @@ export function ColorEdit( props ) { ( solids.length > 0 || areCustomSolidsEnabled ); const hasTextColor = hasTextColorSupport( blockName ) && + isTextEnabled && ( solids.length > 0 || areCustomSolidsEnabled ); const hasBackgroundColor = hasBackgroundColorSupport( blockName ) && + isBackgroundEnabled && ( solids.length > 0 || areCustomSolidsEnabled ); const hasGradientColor = hasGradientSupport( blockName ) && diff --git a/packages/block-editor/src/hooks/compat.js b/packages/block-editor/src/hooks/compat.js new file mode 100644 index 0000000000000..515d26dae5164 --- /dev/null +++ b/packages/block-editor/src/hooks/compat.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { hasBlockSupport } from '@wordpress/blocks'; +import { addFilter } from '@wordpress/hooks'; + +function migrateLightBlockWrapper( settings ) { + const { apiVersion = 1 } = settings; + if ( + apiVersion < 2 && + hasBlockSupport( settings, 'lightBlockWrapper', false ) + ) { + settings.apiVersion = 2; + } + + return settings; +} + +addFilter( + 'blocks.registerBlockType', + 'core/compat/migrateLightBlockWrapper', + migrateLightBlockWrapper +); diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index ae5b623887cca..0ed09f8975c5c 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -15,7 +15,7 @@ import { createHigherOrderComponent } from '@wordpress/compose'; /** * Internal dependencies */ -import { InspectorAdvancedControls } from '../components'; +import { InspectorControls } from '../components'; /** * Filters registered block settings, extending attributes with anchor using ID @@ -59,7 +59,7 @@ export const withInspectorControl = createHigherOrderComponent( return ( <> <BlockEdit { ...props } /> - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> <TextControl autoComplete="off" label={ __( 'Additional CSS class(es)' ) } @@ -76,7 +76,7 @@ export const withInspectorControl = createHigherOrderComponent( 'Separate multiple classes with spaces.' ) } /> - </InspectorAdvancedControls> + </InspectorControls> </> ); } diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 697955670b497..d85ce67104b39 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -13,6 +13,13 @@ import { getBlockSupport } from '@wordpress/blocks'; * Internal dependencies */ import InspectorControls from '../components/inspector-controls'; +import { + GapEdit, + hasGapSupport, + hasGapValue, + resetGap, + useIsGapDisabled, +} from './gap'; import { MarginEdit, hasMarginSupport, @@ -41,6 +48,7 @@ export const AXIAL_SIDES = [ 'vertical', 'horizontal' ]; * @return {WPElement} Inspector controls for spacing support features. */ export function DimensionsPanel( props ) { + const isGapDisabled = useIsGapDisabled( props ); const isPaddingDisabled = useIsPaddingDisabled( props ); const isMarginDisabled = useIsMarginDisabled( props ); const isDisabled = useIsDimensionsDisabled( props ); @@ -64,6 +72,7 @@ export function DimensionsPanel( props ) { ...style, spacing: { ...style?.spacing, + blockGap: undefined, margin: undefined, padding: undefined, }, @@ -98,6 +107,17 @@ export function DimensionsPanel( props ) { <MarginEdit { ...props } /> </ToolsPanelItem> ) } + { ! isGapDisabled && ( + <ToolsPanelItem + className="single-column" + hasValue={ () => hasGapValue( props ) } + label={ __( 'Block gap' ) } + onDeselect={ () => resetGap( props ) } + isShownByDefault={ defaultSpacingControls?.blockGap } + > + <GapEdit { ...props } /> + </ToolsPanelItem> + ) } </ToolsPanel> </InspectorControls> ); @@ -115,7 +135,11 @@ export function hasDimensionsSupport( blockName ) { return false; } - return hasPaddingSupport( blockName ) || hasMarginSupport( blockName ); + return ( + hasGapSupport( blockName ) || + hasPaddingSupport( blockName ) || + hasMarginSupport( blockName ) + ); } /** @@ -126,10 +150,11 @@ export function hasDimensionsSupport( blockName ) { * @return {boolean} If spacing support is completely disabled. */ const useIsDimensionsDisabled = ( props = {} ) => { + const gapDisabled = useIsGapDisabled( props ); const paddingDisabled = useIsPaddingDisabled( props ); const marginDisabled = useIsMarginDisabled( props ); - return paddingDisabled && marginDisabled; + return gapDisabled && paddingDisabled && marginDisabled; }; /** diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index bf62161e1d182..0bde4cdbe2c60 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -140,7 +140,7 @@ function DuotonePanel( { attributes, setAttributes } ) { } return ( - <BlockControls group="block"> + <BlockControls group="block" __experimentalExposeToChildren> <DuotoneControl duotonePalette={ duotonePalette } colorPalette={ colorPalette } diff --git a/packages/block-editor/src/hooks/gap.js b/packages/block-editor/src/hooks/gap.js new file mode 100644 index 0000000000000..d29a63774fbb6 --- /dev/null +++ b/packages/block-editor/src/hooks/gap.js @@ -0,0 +1,145 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Platform } from '@wordpress/element'; +import { getBlockSupport } from '@wordpress/blocks'; +import { + __experimentalUseCustomUnits as useCustomUnits, + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { __unstableUseBlockRef as useBlockRef } from '../components/block-list/use-block-props/use-block-refs'; +import useSetting from '../components/use-setting'; +import { SPACING_SUPPORT_KEY } from './dimensions'; +import { cleanEmptyObject } from './utils'; + +/** + * Determines if there is gap support. + * + * @param {string|Object} blockType Block name or Block Type object. + * @return {boolean} Whether there is support. + */ +export function hasGapSupport( blockType ) { + const support = getBlockSupport( blockType, SPACING_SUPPORT_KEY ); + return !! ( true === support || support?.blockGap ); +} + +/** + * Checks if there is a current value in the gap block support attributes. + * + * @param {Object} props Block props. + * @return {boolean} Whether or not the block has a gap value set. + */ +export function hasGapValue( props ) { + return props.attributes.style?.spacing?.blockGap !== undefined; +} + +/** + * Resets the gap block support attribute. This can be used when disabling + * the gap support controls for a block via a progressive discovery panel. + * + * @param {Object} props Block props. + * @param {Object} props.attributes Block's attributes. + * @param {Object} props.setAttributes Function to set block's attributes. + */ +export function resetGap( { attributes = {}, setAttributes } ) { + const { style } = attributes; + + setAttributes( { + style: { + ...style, + spacing: { + ...style?.spacing, + blockGap: undefined, + }, + }, + } ); +} + +/** + * Custom hook that checks if gap settings have been disabled. + * + * @param {string} name The name of the block. + * @return {boolean} Whether the gap setting is disabled. + */ +export function useIsGapDisabled( { name: blockName } = {} ) { + const isDisabled = ! useSetting( 'spacing.blockGap' ); + return ! hasGapSupport( blockName ) || isDisabled; +} + +/** + * Inspector control panel containing the gap related configuration + * + * @param {Object} props + * + * @return {WPElement} Gap edit element. + */ +export function GapEdit( props ) { + const { + clientId, + attributes: { style }, + setAttributes, + } = props; + + const units = useCustomUnits( { + availableUnits: useSetting( 'spacing.units' ) || [ + '%', + 'px', + 'em', + 'rem', + 'vw', + ], + } ); + + const ref = useBlockRef( clientId ); + + if ( useIsGapDisabled( props ) ) { + return null; + } + + const onChange = ( next ) => { + const newStyle = { + ...style, + spacing: { + ...style?.spacing, + blockGap: next, + }, + }; + + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + + // In Safari, changing the `gap` CSS value on its own will not trigger the layout + // to be recalculated / re-rendered. To force the updated gap to re-render, here + // we replace the block's node with itself. + const isSafari = + window?.navigator.userAgent && + window.navigator.userAgent.includes( 'Safari' ) && + ! window.navigator.userAgent.includes( 'Chrome ' ) && + ! window.navigator.userAgent.includes( 'Chromium ' ); + + if ( ref.current && isSafari ) { + ref.current.parentNode?.replaceChild( ref.current, ref.current ); + } + }; + + return Platform.select( { + web: ( + <> + <UnitControl + label={ __( 'Block gap' ) } + min={ 0 } + onChange={ onChange } + units={ units } + value={ style?.spacing?.blockGap } + /> + </> + ), + native: null, + } ); +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index e8a976277f970..7572b4792812f 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import './compat'; import './align'; import './anchor'; import './custom-class-name'; diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index b0aac50354da6..82312c905ce55 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import './compat'; import './align'; import './anchor'; import './custom-class-name'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 796de8c479a2f..a1f17c525c0c6 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -40,23 +40,32 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { return getSettings().supportsLayout; }, [] ); - if ( ! themeSupportsLayout ) { - return null; - } - + const layoutBlockSupport = getBlockSupport( + blockName, + layoutBlockSupportKey, + {} + ); const { - allowSwitching: canBlockSwitchLayout, + allowSwitching, allowEditing = true, allowInheriting = true, default: defaultBlockLayout, - } = getBlockSupport( blockName, layoutBlockSupportKey ) || {}; + } = layoutBlockSupport; if ( ! allowEditing ) { return null; } - const usedLayout = layout ? layout : defaultBlockLayout || {}; + const usedLayout = layout || defaultBlockLayout || {}; const { inherit = false, type = 'default' } = usedLayout; + /** + * `themeSupportsLayout` is only relevant to the `default/flow` + * layout and it should not be taken into account when other + * `layout` types are used. + */ + if ( type === 'default' && ! themeSupportsLayout ) { + return null; + } const layoutType = getLayoutType( type ); const onChangeType = ( newType ) => @@ -65,33 +74,45 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { setAttributes( { layout: newLayout } ); return ( - <InspectorControls> - <PanelBody title={ __( 'Layout' ) }> - { allowInheriting && !! defaultThemeLayout && ( - <ToggleControl - label={ __( 'Inherit default layout' ) } - checked={ !! inherit } - onChange={ () => - setAttributes( { layout: { inherit: ! inherit } } ) - } - /> - ) } - - { ! inherit && canBlockSwitchLayout && ( - <LayoutTypeSwitcher - type={ type } - onChange={ onChangeType } - /> - ) } - - { ! inherit && layoutType && ( - <layoutType.edit - layout={ usedLayout } - onChange={ onChangeLayout } - /> - ) } - </PanelBody> - </InspectorControls> + <> + <InspectorControls> + <PanelBody title={ __( 'Layout' ) }> + { allowInheriting && !! defaultThemeLayout && ( + <ToggleControl + label={ __( 'Inherit default layout' ) } + checked={ !! inherit } + onChange={ () => + setAttributes( { + layout: { inherit: ! inherit }, + } ) + } + /> + ) } + + { ! inherit && allowSwitching && ( + <LayoutTypeSwitcher + type={ type } + onChange={ onChangeType } + /> + ) } + + { ! inherit && layoutType && ( + <layoutType.inspectorControls + layout={ usedLayout } + onChange={ onChangeLayout } + layoutBlockSupport={ layoutBlockSupport } + /> + ) } + </PanelBody> + </InspectorControls> + { ! inherit && layoutType && ( + <layoutType.toolBarControls + layout={ usedLayout } + onChange={ onChangeLayout } + layoutBlockSupport={ layoutBlockSupport } + /> + ) } + </> ); } diff --git a/packages/block-editor/src/hooks/letter-spacing.js b/packages/block-editor/src/hooks/letter-spacing.js index 6de6193fe4d81..25369ae9daaa8 100644 --- a/packages/block-editor/src/hooks/letter-spacing.js +++ b/packages/block-editor/src/hooks/letter-spacing.js @@ -14,7 +14,8 @@ import { cleanEmptyObject } from './utils'; * Key within block settings' supports array indicating support for letter-spacing * e.g. settings found in `block.json`. */ -export const LETTER_SPACING_SUPPORT_KEY = '__experimentalLetterSpacing'; +export const LETTER_SPACING_SUPPORT_KEY = + 'typography.__experimentalLetterSpacing'; /** * Inspector control panel containing the letter-spacing options. diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 87bf4685d50d4..9bc7c0e5997c8 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -144,7 +144,14 @@ function addAttribute( settings ) { return settings; } -const skipSerializationPaths = { +/** + * A dictionary of paths to flag skipping block support serialization as the key, + * with values providing the style paths to be omitted from serialization. + * + * @constant + * @type {Record<string, string[]>} + */ +const skipSerializationPathsEdit = { [ `${ BORDER_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ 'border' ], [ `${ COLOR_SUPPORT_KEY }.__experimentalSkipSerialization` ]: [ COLOR_SUPPORT_KEY, @@ -157,23 +164,46 @@ const skipSerializationPaths = { ], }; +/** + * A dictionary of paths to flag skipping block support serialization as the key, + * with values providing the style paths to be omitted from serialization. + * + * Extends the Edit skip paths to enable skipping additional paths in just + * the Save component. This allows a block support to be serialized within the + * editor, while using an alternate approach, such as server-side rendering, when + * the support is saved. + * + * @constant + * @type {Record<string, string[]>} + */ +const skipSerializationPathsSave = { + ...skipSerializationPathsEdit, + [ `${ SPACING_SUPPORT_KEY }` ]: [ 'spacing.blockGap' ], +}; + /** * Override props assigned to save component to inject the CSS variables definition. * - * @param {Object} props Additional props applied to save element. - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. + * @param {Object} props Additional props applied to save element. + * @param {Object} blockType Block type. + * @param {Object} attributes Block attributes. + * @param {?Record<string, string[]>} skipPaths An object of keys and paths to skip serialization. * * @return {Object} Filtered props applied to save element. */ -export function addSaveProps( props, blockType, attributes ) { +export function addSaveProps( + props, + blockType, + attributes, + skipPaths = skipSerializationPathsSave +) { if ( ! hasStyleSupport( blockType ) ) { return props; } let { style } = attributes; - forEach( skipSerializationPaths, ( path, indicator ) => { + forEach( skipPaths, ( path, indicator ) => { if ( getBlockSupport( blockType, indicator ) ) { style = omit( style, path ); } @@ -207,7 +237,12 @@ export function addEditProps( settings ) { props = existingGetEditWrapperProps( attributes ); } - return addSaveProps( props, settings, attributes ); + return addSaveProps( + props, + settings, + attributes, + skipSerializationPathsEdit + ); }; return settings; diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index 706d9a93ed0ba..e8c3264eeba6b 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -24,11 +24,13 @@ describe( 'getInlineStyles', () => { color: '#21759b', }, spacing: { + blockGap: '1em', padding: { top: '10px' }, margin: { bottom: '15px' }, }, } ) ).toEqual( { + '--wp--style--block-gap': '1em', backgroundColor: 'black', borderColor: '#21759b', borderRadius: '10px', @@ -96,11 +98,13 @@ describe( 'getInlineStyles', () => { expect( getInlineStyles( { spacing: { + blockGap: '1em', margin: '10px', padding: '20px', }, } ) ).toEqual( { + '--wp--style--block-gap': '1em', margin: '10px', padding: '20px', } ); diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 1b961990acaaa..5bd009bb78dc3 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -6,7 +6,6 @@ import { hasBlockSupport } from '@wordpress/blocks'; * External dependencies */ import { PanelBody } from '@wordpress/components'; -import { Platform } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -83,11 +82,8 @@ export function TypographyPanel( props ) { } const hasTypographySupport = ( blockName ) => { - return ( - Platform.OS === 'web' && - TYPOGRAPHY_SUPPORT_KEYS.some( ( key ) => - hasBlockSupport( blockName, key ) - ) + return TYPOGRAPHY_SUPPORT_KEYS.some( ( key ) => + hasBlockSupport( blockName, key ) ); }; diff --git a/packages/block-editor/src/hooks/typography.native.js b/packages/block-editor/src/hooks/typography.native.js new file mode 100644 index 0000000000000..396d48439f3a3 --- /dev/null +++ b/packages/block-editor/src/hooks/typography.native.js @@ -0,0 +1,64 @@ +/** + * WordPress dependencies + */ +import { hasBlockSupport } from '@wordpress/blocks'; +/** + * External dependencies + */ +import { PanelBody } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import InspectorControls from '../components/inspector-controls'; + +import { + LINE_HEIGHT_SUPPORT_KEY, + LineHeightEdit, + useIsLineHeightDisabled, +} from './line-height'; +import { + FONT_SIZE_SUPPORT_KEY, + FontSizeEdit, + useIsFontSizeDisabled, +} from './font-size'; + +export const TYPOGRAPHY_SUPPORT_KEY = 'typography'; +export const TYPOGRAPHY_SUPPORT_KEYS = [ + LINE_HEIGHT_SUPPORT_KEY, + FONT_SIZE_SUPPORT_KEY, +]; + +export function TypographyPanel( props ) { + const isDisabled = useIsTypographyDisabled( props ); + const isSupported = hasTypographySupport( props.name ); + + // only enable TypographyPanel for development + // eslint-disable-next-line no-undef + if ( isDisabled || ! isSupported || ! __DEV__ ) return null; + + return ( + <InspectorControls> + <PanelBody title={ __( 'Typography' ) }> + <FontSizeEdit { ...props } /> + <LineHeightEdit { ...props } /> + </PanelBody> + </InspectorControls> + ); +} + +const hasTypographySupport = ( blockName ) => { + return TYPOGRAPHY_SUPPORT_KEYS.some( ( key ) => + hasBlockSupport( blockName, key ) + ); +}; + +function useIsTypographyDisabled( props = {} ) { + const configs = [ + useIsFontSizeDisabled( props ), + useIsLineHeightDisabled( props ), + ]; + + return configs.filter( Boolean ).length === configs.length; +} diff --git a/packages/block-editor/src/layouts/flex.js b/packages/block-editor/src/layouts/flex.js index d130468b93cee..2c1b9f69be4a5 100644 --- a/packages/block-editor/src/layouts/flex.js +++ b/packages/block-editor/src/layouts/flex.js @@ -1,30 +1,76 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; /** * Internal dependencies */ import { appendSelectors } from './utils'; +import useSetting from '../components/use-setting'; +import { BlockControls, JustifyContentControl } from '../components'; + +const justifyContentMap = { + left: 'flex-start', + right: 'flex-end', + center: 'center', + 'space-between': 'space-between', +}; export default { name: 'flex', - label: __( 'Flex' ), - - edit() { - return null; + inspectorControls: function FlexLayoutInspectorControls( { + layout = {}, + onChange, + } ) { + return ( + <FlexLayoutJustifyContentControl + layout={ layout } + onChange={ onChange } + /> + ); }, - - save: function FlexLayoutStyle( { selector } ) { + toolBarControls: function FlexLayoutToolbarControls( { + layout = {}, + onChange, + layoutBlockSupport, + } ) { + if ( layoutBlockSupport?.allowSwitching ) { + return null; + } + return ( + <BlockControls group="block" __experimentalExposeToChildren> + <FlexLayoutJustifyContentControl + layout={ layout } + onChange={ onChange } + isToolbar + /> + </BlockControls> + ); + }, + save: function FlexLayoutStyle( { selector, layout } ) { + const blockGapSupport = useSetting( 'spacing.blockGap' ); + const hasBlockGapStylesSupport = blockGapSupport !== null; + const justifyContent = + justifyContentMap[ layout.justifyContent ] || 'flex-start'; return ( <style>{ ` ${ appendSelectors( selector ) } { display: flex; - gap: var( --wp--style--block-gap, 0.5em ); + gap: ${ + hasBlockGapStylesSupport + ? 'var( --wp--style--block-gap, 0.5em )' + : '0.5em' + }; flex-wrap: wrap; align-items: center; + flex-direction: row; + justify-content: ${ justifyContent }; } ${ appendSelectors( selector, '> *' ) } { @@ -33,12 +79,71 @@ export default { ` }</style> ); }, - getOrientation() { return 'horizontal'; }, - getAlignments() { return []; }, }; + +function FlexLayoutJustifyContentControl( { + layout, + onChange, + isToolbar = false, +} ) { + const { justifyContent = 'left' } = layout; + if ( isToolbar ) { + return ( + <JustifyContentControl + allowedControls={ [ + 'left', + 'center', + 'right', + 'space-between', + ] } + value={ justifyContent } + onChange={ ( value ) => { + onChange( { + ...layout, + justifyContent: value, + } ); + } } + popoverProps={ { + position: 'bottom right', + isAlternate: true, + } } + /> + ); + } + return ( + <ToggleGroupControl + label={ __( 'Justify content' ) } + value={ justifyContent } + onChange={ ( value ) => { + onChange( { + ...layout, + justifyContent: value, + } ); + } } + isBlock + > + <ToggleGroupControlOption + value="left" + label={ _x( 'Left', 'Justify content option' ) } + /> + <ToggleGroupControlOption + value="center" + label={ _x( 'Center', 'Justify content option' ) } + /> + <ToggleGroupControlOption + value="right" + label={ _x( 'Right', 'Justify content option' ) } + /> + <ToggleGroupControlOption + value="space-between" + label={ _x( 'Space between', 'Justify content option' ) } + /> + </ToggleGroupControl> + ); +} diff --git a/packages/block-editor/src/layouts/flow.js b/packages/block-editor/src/layouts/flow.js index 2a57f34822d16..796dad1d78a18 100644 --- a/packages/block-editor/src/layouts/flow.js +++ b/packages/block-editor/src/layouts/flow.js @@ -17,10 +17,11 @@ import { appendSelectors } from './utils'; export default { name: 'default', - label: __( 'Flow' ), - - edit: function LayoutDefaultEdit( { layout, onChange } ) { + inspectorControls: function DefaultLayoutInspectorControls( { + layout, + onChange, + } ) { const { wideSize, contentSize } = layout; const units = useCustomUnits( { availableUnits: useSetting( 'spacing.units' ) || [ @@ -101,9 +102,13 @@ export default { </> ); }, - + toolBarControls: function DefaultLayoutToolbarControls() { + return null; + }, save: function DefaultLayoutStyle( { selector, layout = {} } ) { const { contentSize, wideSize } = layout; + const blockGapSupport = useSetting( 'spacing.blockGap' ); + const hasBlockGapStylesSupport = blockGapSupport !== null; let style = !! contentSize || !! wideSize @@ -113,11 +118,11 @@ export default { margin-left: auto !important; margin-right: auto !important; } - + ${ appendSelectors( selector, '> [data-align="wide"]' ) } { max-width: ${ wideSize ?? contentSize }; } - + ${ appendSelectors( selector, '> [data-align="full"]' ) } { max-width: none; } @@ -129,32 +134,43 @@ export default { float: left; margin-right: 2em; } - + ${ appendSelectors( selector, '> [data-align="right"]' ) } { float: right; margin-left: 2em; } - ${ appendSelectors( selector, '> * + *' ) } { - margin-top: var( --wp--style--block-gap ); - margin-bottom: 0; - } `; + if ( hasBlockGapStylesSupport ) { + style += ` + ${ appendSelectors( selector, '> * + *' ) } { + margin-top: var( --wp--style--block-gap ); + margin-bottom: 0; + } + `; + } + return <style>{ style }</style>; }, - getOrientation() { return 'vertical'; }, - getAlignments( layout ) { if ( layout.alignments !== undefined ) { return layout.alignments; } - return layout.contentSize || layout.wideSize - ? [ 'wide', 'full', 'left', 'center', 'right' ] - : [ 'left', 'center', 'right' ]; + const alignments = [ 'left', 'center', 'right' ]; + + if ( layout.contentSize ) { + alignments.unshift( 'full' ); + } + + if ( layout.wideSize ) { + alignments.unshift( 'wide' ); + } + + return alignments; }, }; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 23ef155e31aff..0e6a3f5097411 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -123,6 +123,7 @@ export function* validateBlocksToTemplate( blocks ) { * text value. See `wp.richText.create`. */ +/* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that selection state should be * reset to the specified selection. @@ -138,6 +139,7 @@ export function resetSelection( selectionEnd, initialPosition ) { + /* eslint-enable jsdoc/valid-types */ return { type: 'RESET_SELECTION', selectionStart, @@ -151,11 +153,18 @@ export function resetSelection( * Unlike resetBlocks, these should be appended to the existing known set, not * replacing. * + * @deprecated + * * @param {Object[]} blocks Array of block objects. * * @return {Object} Action object. */ export function receiveBlocks( blocks ) { + deprecated( 'wp.data.dispatch( "core/block-editor" ).receiveBlocks', { + since: '5.9', + alternative: 'resetBlocks or insertBlocks', + } ); + return { type: 'RECEIVE_BLOCKS', blocks, @@ -202,6 +211,7 @@ export function updateBlock( clientId, updates ) { }; } +/* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that the block with the * specified client ID has been selected, optionally accepting a position @@ -215,6 +225,7 @@ export function updateBlock( clientId, updates ) { * @return {Object} Action object. */ export function selectBlock( clientId, initialPosition = 0 ) { + /* eslint-enable jsdoc/valid-types */ return { type: 'SELECT_BLOCK', initialPosition, @@ -382,6 +393,7 @@ function getBlocksWithDefaultStylesApplied( blocks, blockEditorSettings ) { } ); } +/* eslint-disable jsdoc/valid-types */ /** * Returns an action object signalling that a blocks should be replaced with * one or more replacement blocks. @@ -401,6 +413,7 @@ export function* replaceBlocks( initialPosition = 0, meta ) { + /* eslint-enable jsdoc/valid-types */ clientIds = castArray( clientIds ); blocks = getBlocksWithDefaultStylesApplied( castArray( blocks ), @@ -587,6 +600,7 @@ export function insertBlock( ); } +/* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that an array of blocks should * be inserted, optionally at a specific index respective a root block list. @@ -607,6 +621,7 @@ export function* insertBlocks( initialPosition = 0, meta ) { + /* eslint-enable jsdoc/valid-types */ if ( isObject( initialPosition ) ) { meta = initialPosition; initialPosition = 0; @@ -878,7 +893,8 @@ export function* mergeBlocks( firstBlockClientId, secondBlockClientId ) { }, }, ...blocksWithTheSameType.slice( 1 ), - ] + ], + 0 // If we don't pass the `indexToSelect` it will default to the last block. ); } @@ -946,6 +962,7 @@ export function removeBlock( clientId, selectPrevious ) { return removeBlocks( [ clientId ], selectPrevious ); } +/* eslint-disable jsdoc/valid-types */ /** * Returns an action object used in signalling that the inner blocks with the * specified client ID should be replaced. @@ -962,6 +979,7 @@ export function replaceInnerBlocks( updateSelection = false, initialPosition = 0 ) { + /* eslint-enable jsdoc/valid-types */ return { type: 'REPLACE_INNER_BLOCKS', rootClientId, diff --git a/packages/block-editor/src/store/defaults.native.js b/packages/block-editor/src/store/defaults.native.js index e660d82178405..8e58d6bde851c 100644 --- a/packages/block-editor/src/store/defaults.native.js +++ b/packages/block-editor/src/store/defaults.native.js @@ -13,6 +13,12 @@ const SETTINGS_DEFAULTS = { __unstableGalleryWithImageBlocks: __DEV__, alignWide: true, supportsLayout: false, + __experimentalFeatures: { + color: { + text: true, + background: true, + }, + }, }; export { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS }; diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index db5477661d50c..4fd2e38efa0d4 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -13,7 +13,6 @@ import { isEqual, isEmpty, identity, - difference, omitBy, pickBy, } from 'lodash'; @@ -215,150 +214,211 @@ export function isUpdatingSameBlockAttribute( action, lastAction ) { ); } -/** - * Utility returning an object with an empty object value for each key. - * - * @param {Array} objectKeys Keys to fill. - * @return {Object} Object filled with empty object as values for each clientId. - */ -const fillKeysWithEmptyObject = ( objectKeys ) => { - return objectKeys.reduce( ( result, key ) => { - result[ key ] = {}; - return result; - }, {} ); -}; +function buildBlockTree( state, blocks ) { + const result = {}; + const stack = [ ...blocks ]; + const flattenedBlocks = [ ...blocks ]; + while ( stack.length ) { + const block = stack.shift(); + stack.push( ...block.innerBlocks ); + flattenedBlocks.push( ...block.innerBlocks ); + } + // Create objects before mutating them, that way it's always defined. + for ( const block of flattenedBlocks ) { + result[ block.clientId ] = {}; + } + for ( const block of flattenedBlocks ) { + result[ block.clientId ] = Object.assign( result[ block.clientId ], { + ...state.byClientId[ block.clientId ], + attributes: state.attributes[ block.clientId ], + innerBlocks: block.innerBlocks.map( + ( subBlock ) => result[ subBlock.clientId ] + ), + } ); + } + + return result; +} + +function updateParentInnerBlocksInTree( state, tree, updatedClientIds ) { + const clientIds = new Set( [] ); + const controlledParents = new Set(); + for ( const clientId of updatedClientIds ) { + let current = clientId; + do { + if ( state.controlledInnerBlocks[ current ] ) { + controlledParents.add( current ); + // If we reach a controlled parent, break out of the loop. + break; + } else { + clientIds.add( current ); + } + // Should stop on controlled blocks. + current = state.parents[ current ]; + } while ( current !== undefined ); + } + + // To make sure the order of assignments doesn't matter, + // we first create empty objects and mutates the inner blocks later. + for ( const clientId of clientIds ) { + tree[ clientId ] = { + ...tree[ clientId ], + }; + } + for ( const clientId of clientIds ) { + tree[ clientId ].innerBlocks = ( state.order[ clientId ] || [] ).map( + ( subClientId ) => tree[ subClientId ] + ); + } + // Controlled parent blocks, need a dedicated key for their inner blocks + // to be used when doing getBlocks( controlledBlockClientId ). + for ( const clientId of controlledParents ) { + tree[ 'controlled||' + clientId ] = { + innerBlocks: ( state.order[ clientId ] || [] ).map( + ( subClientId ) => tree[ subClientId ] + ), + }; + } + + return tree; +} /** - * Higher-order reducer intended to compute a cache key for each block in the post. - * A new instance of the cache key (empty object) is created each time the block object - * needs to be refreshed (for any change in the block or its children). + * Higher-order reducer intended to compute full block objects key for each block in the post. + * This is a denormalization to optimize the performance of the getBlock selectors and avoid + * recomputing the block objects and avoid heavy memoization. * * @param {Function} reducer Original reducer function. * * @return {Function} Enhanced reducer function. */ -const withBlockCache = ( reducer ) => ( state = {}, action ) => { +const withBlockTree = ( reducer ) => ( state = {}, action ) => { const newState = reducer( state, action ); if ( newState === state ) { return state; } - newState.cache = state.cache ? state.cache : {}; - - /** - * For each clientId provided, traverses up parents, adding the provided clientIds - * and each parent's clientId to the returned array. - * - * When calling this function consider that it uses the old state, so any state - * modifications made by the `reducer` will not be present. - * - * @param {Array} clientIds an Array of block clientIds. - * - * @return {Array} The provided clientIds and all of their parent clientIds. - */ - const getBlocksWithParentsClientIds = ( clientIds ) => { - return clientIds.reduce( ( result, clientId ) => { - let current = clientId; - do { - result.push( current ); - current = state.parents[ current ]; - } while ( current && ! state.controlledInnerBlocks[ current ] ); - return result; - }, [] ); - }; + newState.tree = state.tree ? state.tree : {}; switch ( action.type ) { - case 'RESET_BLOCKS': - newState.cache = mapValues( - flattenBlocks( action.blocks ), - () => ( {} ) - ); - break; case 'RECEIVE_BLOCKS': case 'INSERT_BLOCKS': { - const updatedBlockUids = keys( flattenBlocks( action.blocks ) ); - if ( - action.rootClientId && - ! state.controlledInnerBlocks[ action.rootClientId ] - ) { - updatedBlockUids.push( action.rootClientId ); - } - newState.cache = { - ...newState.cache, - ...fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( updatedBlockUids ) - ), - }; + const subTree = buildBlockTree( newState, action.blocks ); + newState.tree = updateParentInnerBlocksInTree( + newState, + { + ...newState.tree, + ...subTree, + }, + action.rootClientId ? [ action.rootClientId ] : [ '' ] + ); break; } case 'UPDATE_BLOCK': - newState.cache = { - ...newState.cache, - ...fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( [ action.clientId ] ) - ), - }; + newState.tree = updateParentInnerBlocksInTree( + newState, + { + ...newState.tree, + [ action.clientId ]: { + ...newState.byClientId[ action.clientId ], + attributes: newState.attributes[ action.clientId ], + }, + }, + [ action.clientId ] + ); break; - case 'UPDATE_BLOCK_ATTRIBUTES': - newState.cache = { - ...newState.cache, - ...fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( action.clientIds ) - ), - }; + case 'UPDATE_BLOCK_ATTRIBUTES': { + const newSubTree = action.clientIds.reduce( + ( result, clientId ) => { + result[ clientId ] = { + ...newState.tree[ clientId ], + attributes: newState.attributes[ clientId ], + }; + return result; + }, + {} + ); + newState.tree = updateParentInnerBlocksInTree( + newState, + { + ...newState.tree, + ...newSubTree, + }, + action.clientIds + ); break; - case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': - const parentClientIds = fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( action.replacedClientIds ) + } + case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': { + const subTree = buildBlockTree( newState, action.blocks ); + newState.tree = updateParentInnerBlocksInTree( + newState, + { + ...omit( + newState.tree, + action.replacedClientIds.concat( + action.replacedClientIds.map( + ( clientId ) => 'controlled||' + clientId + ) + ) + ), + ...subTree, + }, + action.blocks.map( ( b ) => b.clientId ) ); - - newState.cache = { - ...omit( newState.cache, action.replacedClientIds ), - ...omit( parentClientIds, action.replacedClientIds ), - ...fillKeysWithEmptyObject( - keys( flattenBlocks( action.blocks ) ) - ), - }; break; + } case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': - newState.cache = { - ...omit( newState.cache, action.removedClientIds ), - ...fillKeysWithEmptyObject( - difference( - getBlocksWithParentsClientIds( action.clientIds ), - action.clientIds + const parentsOfRemovedBlocks = []; + for ( const clientId of action.clientIds ) { + if ( + state.parents[ clientId ] !== undefined && + ( state.parents[ clientId ] === '' || + newState.byClientId[ state.parents[ clientId ] ] ) + ) { + parentsOfRemovedBlocks.push( state.parents[ clientId ] ); + } + } + newState.tree = updateParentInnerBlocksInTree( + newState, + omit( + newState.tree, + action.removedClientIds.concat( + action.removedClientIds.map( + ( clientId ) => 'controlled||' + clientId + ) ) ), - }; + parentsOfRemovedBlocks + ); break; case 'MOVE_BLOCKS_TO_POSITION': { - const updatedBlockUids = [ ...action.clientIds ]; + const updatedBlockUids = []; if ( action.fromRootClientId ) { updatedBlockUids.push( action.fromRootClientId ); } if ( action.toRootClientId ) { updatedBlockUids.push( action.toRootClientId ); } - newState.cache = { - ...newState.cache, - ...fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( updatedBlockUids ) - ), - }; + if ( ! action.fromRootClientId || ! action.fromRootClientId ) { + updatedBlockUids.push( '' ); + } + newState.tree = updateParentInnerBlocksInTree( + newState, + newState.tree, + updatedBlockUids + ); break; } case 'MOVE_BLOCKS_UP': case 'MOVE_BLOCKS_DOWN': { - const updatedBlockUids = []; - if ( action.rootClientId ) { - updatedBlockUids.push( action.rootClientId ); - } - newState.cache = { - ...newState.cache, - ...fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( updatedBlockUids ) - ), - }; + const updatedBlockUids = [ + action.rootClientId ? action.rootClientId : '', + ]; + newState.tree = updateParentInnerBlocksInTree( + newState, + newState.tree, + updatedBlockUids + ); break; } case 'SAVE_REUSABLE_BLOCK_SUCCESS': { @@ -371,12 +431,21 @@ const withBlockCache = ( reducer ) => ( state = {}, action ) => { } ) ); - newState.cache = { - ...newState.cache, - ...fillKeysWithEmptyObject( - getBlocksWithParentsClientIds( updatedBlockUids ) - ), - }; + newState.tree = updateParentInnerBlocksInTree( + newState, + { + ...newState.tree, + ...updatedBlockUids.reduce( ( result, clientId ) => { + result[ clientId ] = { + ...newState.byClientId[ clientId ], + attributes: newState.attributes[ clientId ], + innerBlocks: newState.tree[ clientId ].innerBlocks, + }; + return result; + }, {} ), + }, + updatedBlockUids + ); } } @@ -531,21 +600,21 @@ const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { * @return {Function} Enhanced reducer function. */ const withBlockReset = ( reducer ) => ( state, action ) => { - if ( state && action.type === 'RESET_BLOCKS' ) { + if ( action.type === 'RESET_BLOCKS' ) { /** * A list of client IDs associated with the top level entity (like a * post or template). It excludes the client IDs of blocks associated * with other entities, like inner block controllers or reusable blocks. */ const visibleClientIds = getNestedBlockClientIds( - state.order, + state?.order ?? {}, '', - state.controlledInnerBlocks + state?.controlledInnerBlocks ?? {} ); // pickBy returns only the truthy values from controlledInnerBlocks const controlledInnerBlocks = Object.keys( - pickBy( state.controlledInnerBlocks ) + pickBy( state?.controlledInnerBlocks ?? {} ) ); /** @@ -569,35 +638,43 @@ const withBlockReset = ( reducer ) => ( state, action ) => { * new value was used, template parts would disappear from the editor * whenever you try to undo a change in the top level entity. */ - return { + const newState = { ...state, byClientId: { - ...omit( state.byClientId, visibleClientIds ), + ...omit( state?.byClientId, visibleClientIds ), ...getFlattenedBlocksWithoutAttributes( action.blocks ), }, attributes: { - ...omit( state.attributes, visibleClientIds ), + ...omit( state?.attributes, visibleClientIds ), ...getFlattenedBlockAttributes( action.blocks ), }, order: { - ...omit( state.order, visibleClientIds ), + ...omit( state?.order, visibleClientIds ), ...omit( mapBlockOrder( action.blocks ), controlledInnerBlocks ), }, parents: { - ...omit( state.parents, visibleClientIds ), + ...omit( state?.parents, visibleClientIds ), ...mapBlockParents( action.blocks ), }, - cache: { - ...omit( state.cache, visibleClientIds ), - ...omit( - mapValues( flattenBlocks( action.blocks ), () => ( {} ) ), - controlledInnerBlocks + controlledInnerBlocks: state?.controlledInnerBlocks || {}, + }; + + const subTree = buildBlockTree( newState, action.blocks ); + newState.tree = { + ...omit( state?.tree, visibleClientIds ), + ...subTree, + // Root + '': { + innerBlocks: action.blocks.map( + ( subBlock ) => subTree[ subBlock.clientId ] ), }, }; + + return newState; } return reducer( state, action ); @@ -727,7 +804,7 @@ const withSaveReusableBlock = ( reducer ) => ( state, action ) => { export const blocks = flow( combineReducers, withSaveReusableBlock, // needs to be before withBlockCache - withBlockCache, // needs to be before withInnerBlocksRemoveCascade + withBlockTree, // needs to be before withInnerBlocksRemoveCascade withInnerBlocksRemoveCascade, withReplaceInnerBlocks, // needs to be after withInnerBlocksRemoveCascade withBlockReset, @@ -736,9 +813,6 @@ export const blocks = flow( )( { byClientId( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': - return getFlattenedBlocksWithoutAttributes( action.blocks ); - case 'RECEIVE_BLOCKS': case 'INSERT_BLOCKS': return { @@ -785,9 +859,6 @@ export const blocks = flow( attributes( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': - return getFlattenedBlockAttributes( action.blocks ); - case 'RECEIVE_BLOCKS': case 'INSERT_BLOCKS': return { @@ -873,15 +944,14 @@ export const blocks = flow( order( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': - return mapBlockOrder( action.blocks ); - - case 'RECEIVE_BLOCKS': + case 'RECEIVE_BLOCKS': { + const blockOrder = mapBlockOrder( action.blocks ); return { ...state, - ...omit( mapBlockOrder( action.blocks ), '' ), + ...omit( blockOrder, '' ), + '': ( state?.[ '' ] || [] ).concat( blockOrder ), }; - + } case 'INSERT_BLOCKS': { const { rootClientId = '' } = action; const subState = state[ rootClientId ] || []; @@ -1049,9 +1119,6 @@ export const blocks = flow( // an optimization for the selectors which derive the ancestry of a block. parents( state = {}, action ) { switch ( action.type ) { - case 'RESET_BLOCKS': - return mapBlockParents( action.blocks ); - case 'RECEIVE_BLOCKS': return { ...state, @@ -1214,9 +1281,9 @@ function selectionHelper( state = {}, action ) { return state; } - const indexToSelect = - action.indexToSelect || action.blocks.length - 1; - const blockToSelect = action.blocks[ indexToSelect ]; + const blockToSelect = + action.blocks[ action.indexToSelect ] || + action.blocks[ action.blocks.length - 1 ]; if ( ! blockToSelect ) { return {}; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 61501dae210e8..6a51a62209d98 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -132,30 +132,14 @@ export function getBlockAttributes( state, clientId ) { * * @return {Object} Parsed block object. */ -export const getBlock = createSelector( - ( state, clientId ) => { - const block = state.blocks.byClientId[ clientId ]; - if ( ! block ) { - return null; - } +export function getBlock( state, clientId ) { + const block = state.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } - return { - ...block, - attributes: getBlockAttributes( state, clientId ), - innerBlocks: areInnerBlocksControlled( state, clientId ) - ? EMPTY_ARRAY - : getBlocks( state, clientId ), - }; - }, - ( state, clientId ) => [ - // Normally, we'd have both `getBlockAttributes` dependencies and - // `getBlocks` (children) dependencies here but for performance reasons - // we use a denormalized cache key computed in the reducer that takes both - // the attributes and inner blocks into account. The value of the cache key - // is being changed whenever one of these dependencies is out of date. - state.blocks.cache[ clientId ], - ] -); + return state.blocks.tree[ clientId ]; +} export const __unstableGetBlockWithoutInnerBlocks = createSelector( ( state, clientId ) => { @@ -180,81 +164,18 @@ export const __unstableGetBlockWithoutInnerBlocks = createSelector( * the order they appear in the post. Note that this will exclude child blocks * of nested inner block controllers. * - * Note: It's important to memoize this selector to avoid return a new instance - * on each call. We use the block cache state for each top-level block of the - * given clientID. This way, the selector only refreshes on changes to blocks - * associated with the given entity, and does not refresh when changes are made - * to blocks which are part of different inner block controllers. - * * @param {Object} state Editor state. * @param {?string} rootClientId Optional root client ID of block list. * * @return {Object[]} Post blocks. */ -export const getBlocks = createSelector( - ( state, rootClientId ) => { - return map( getBlockOrder( state, rootClientId ), ( clientId ) => - getBlock( state, clientId ) - ); - }, - ( state, rootClientId ) => - map( - state.blocks.order[ rootClientId || '' ], - ( id ) => state.blocks.cache[ id ] - ) -); - -/** - * Similar to getBlock, except it will include the entire nested block tree as - * inner blocks. The normal getBlock selector will exclude sections of the block - * tree which belong to different entities. - * - * @param {Object} state Editor state. - * @param {string} clientId Client ID of the block to get. - * - * @return {Object} The block with all - */ -export const __unstableGetBlockWithBlockTree = createSelector( - ( state, clientId ) => { - const block = state.blocks.byClientId[ clientId ]; - if ( ! block ) { - return null; - } - - return { - ...block, - attributes: getBlockAttributes( state, clientId ), - innerBlocks: __unstableGetBlockTree( state, clientId ), - }; - }, - ( state ) => [ - state.blocks.byClientId, - state.blocks.order, - state.blocks.attributes, - ] -); - -/** - * Similar to getBlocks, except this selector returns the entire block tree - * represented in the block-editor store from the given root regardless of any - * inner block controllers. - * - * @param {Object} state Editor state. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {Object[]} Post blocks. - */ -export const __unstableGetBlockTree = createSelector( - ( state, rootClientId = '' ) => - map( getBlockOrder( state, rootClientId ), ( clientId ) => - __unstableGetBlockWithBlockTree( state, clientId ) - ), - ( state ) => [ - state.blocks.byClientId, - state.blocks.order, - state.blocks.attributes, - ] -); +export function getBlocks( state, rootClientId ) { + const treeKey = + ! rootClientId || ! areInnerBlocksControlled( state, rootClientId ) + ? rootClientId || '' + : 'controlled||' + rootClientId; + return state.blocks.tree[ treeKey ]?.innerBlocks || EMPTY_ARRAY; +} /** * Returns a stripped down block object containing only its client ID, @@ -369,11 +290,11 @@ export const getBlocksByClientId = createSelector( map( castArray( clientIds ), ( clientId ) => getBlock( state, clientId ) ), - ( state ) => [ - state.blocks.byClientId, - state.blocks.order, - state.blocks.attributes, - ] + ( state, clientIds ) => + map( + castArray( clientIds ), + ( clientId ) => state.blocks.tree[ clientId ] + ) ); /** @@ -715,6 +636,7 @@ export function getNextBlockClientId( state, startClientId ) { return getAdjacentBlockClientId( state, startClientId, 1 ); } +/* eslint-disable jsdoc/valid-types */ /** * Returns the initial caret position for the selected block. * This position is to used to position the caret properly when the selected block changes. @@ -725,6 +647,7 @@ export function getNextBlockClientId( state, startClientId ) { * @return {0|-1|null} Initial position. */ export function getSelectedBlocksInitialCaretPosition( state ) { + /* eslint-enable jsdoc/valid-types */ return state.initialPosition; } diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index b4ad1c12ec198..e3c7cdbc05bf2 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { values, noop } from 'lodash'; +import { values, noop, omit } from 'lodash'; import deepFreeze from 'deep-freeze'; /** @@ -236,7 +236,8 @@ describe( 'state', () => { chicken: '', 'chicken-child': 'chicken', }, - cache: { + tree: { + '': {}, chicken: {}, 'chicken-child': {}, }, @@ -258,7 +259,7 @@ describe( 'state', () => { const state = blocks( existingState, action ); - expect( state ).toEqual( { + expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { @@ -289,14 +290,10 @@ describe( 'state', () => { [ newChildBlockId ]: 'chicken', chicken: '', }, - cache: { - chicken: {}, - [ newChildBlockId ]: {}, - }, controlledInnerBlocks: {}, } ); - expect( state.cache.chicken ).not.toBe( - existingState.cache.chicken + expect( state.tree.chicken ).not.toBe( + existingState.tree.chicken ); } ); @@ -319,7 +316,10 @@ describe( 'state', () => { parents: { chicken: '', }, - cache: { + tree: { + '': { + innerBlocks: [], + }, chicken: {}, }, controlledInnerBlocks: {}, @@ -340,7 +340,7 @@ describe( 'state', () => { const state = blocks( existingState, action ); - expect( state ).toEqual( { + expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { @@ -371,15 +371,27 @@ describe( 'state', () => { [ newChildBlockId ]: 'chicken', chicken: '', }, - cache: { - chicken: {}, - [ newChildBlockId ]: {}, - }, controlledInnerBlocks: {}, } ); - expect( state.cache.chicken ).not.toBe( - existingState.cache.chicken + expect( state.tree.chicken ).not.toBe( + existingState.tree.chicken + ); + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.chicken ); + expect( state.tree.chicken.innerBlocks[ 0 ] ).toBe( + state.tree[ newChildBlockId ] + ); + expect( state.tree[ newChildBlockId ] ).toEqual( { + clientId: newChildBlockId, + innerBlocks: [], + isValid: true, + name: 'core/test-child-block', + attributes: { + attr: false, + attr2: 'perfect', + }, + } ); } ); it( 'can replace multiple child blocks', () => { @@ -421,11 +433,7 @@ describe( 'state', () => { 'chicken-child': 'chicken', 'chicken-child-2': 'chicken', }, - cache: { - chicken: {}, - 'chicken-child': {}, - 'chicken-child-2': {}, - }, + tree: {}, controlledInnerBlocks: {}, } ); @@ -455,7 +463,7 @@ describe( 'state', () => { const state = blocks( existingState, action ); - expect( state ).toEqual( { + expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { @@ -511,14 +519,31 @@ describe( 'state', () => { [ newChildBlockId2 ]: 'chicken', [ newChildBlockId3 ]: 'chicken', }, - cache: { - chicken: {}, - [ newChildBlockId1 ]: {}, - [ newChildBlockId2 ]: {}, - [ newChildBlockId3 ]: {}, - }, controlledInnerBlocks: {}, } ); + + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.chicken + ); + expect( state.tree.chicken.innerBlocks[ 0 ] ).toBe( + state.tree[ newChildBlockId1 ] + ); + expect( state.tree.chicken.innerBlocks[ 1 ] ).toBe( + state.tree[ newChildBlockId2 ] + ); + expect( state.tree.chicken.innerBlocks[ 2 ] ).toBe( + state.tree[ newChildBlockId3 ] + ); + expect( state.tree[ newChildBlockId1 ] ).toEqual( { + innerBlocks: [], + clientId: newChildBlockId1, + name: 'core/test-child-block', + isValid: true, + attributes: { + attr: false, + attr2: 'perfect', + }, + } ); } ); it( 'can replace a child block that has other children', () => { @@ -556,10 +581,8 @@ describe( 'state', () => { 'chicken-child': 'chicken', 'chicken-grand-child': 'chicken-child', }, - cache: { + tree: { chicken: {}, - 'chicken-child': {}, - 'chicken-grand-child': {}, }, controlledInnerBlocks: {}, } ); @@ -576,7 +599,7 @@ describe( 'state', () => { const state = blocks( existingState, action ); - expect( state ).toEqual( { + expect( omit( state, [ 'tree' ] ) ).toEqual( { isPersistentChange: true, isIgnoredChange: false, byClientId: { @@ -604,16 +627,12 @@ describe( 'state', () => { chicken: '', [ newChildBlockId ]: 'chicken', }, - cache: { - chicken: {}, - [ newChildBlockId ]: {}, - }, controlledInnerBlocks: {}, } ); - // the cache key of the parent should be updated - expect( existingState.cache.chicken ).not.toBe( - state.cache.chicken + // the block object of the parent should be updated + expect( state.tree.chicken ).not.toBe( + existingState.tree.chicken ); } ); } ); @@ -628,7 +647,7 @@ describe( 'state', () => { parents: {}, isPersistentChange: true, isIgnoredChange: false, - cache: {}, + tree: {}, controlledInnerBlocks: {}, } ); } ); @@ -648,9 +667,13 @@ describe( 'state', () => { '': [ 'bananas' ], bananas: [], } ); - expect( state.cache ).toEqual( { - bananas: {}, + expect( state.tree.bananas ).toEqual( { + clientId: 'bananas', + innerBlocks: [], } ); + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.bananas + ); } ); } ); @@ -674,10 +697,6 @@ describe( 'state', () => { apples: [], bananas: [ 'apples' ], } ); - expect( state.cache ).toEqual( { - bananas: {}, - apples: {}, - } ); } ); it( 'should insert block', () => { @@ -710,12 +729,17 @@ describe( 'state', () => { chicken: [], ribs: [], } ); - expect( state.cache ).toEqual( { - chicken: {}, - ribs: {}, + + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.chicken + ); + expect( state.tree[ '' ].innerBlocks[ 1 ] ).toBe( state.tree.ribs ); + expect( state.tree.chicken ).toEqual( { + clientId: 'chicken', + name: 'core/test-block', + attributes: {}, + innerBlocks: [], } ); - // The cache key is the same because the block has not been modified. - expect( original.cache.chicken ).toBe( state.cache.chicken ); } ); it( 'should replace the block', () => { @@ -754,10 +778,16 @@ describe( 'state', () => { expect( state.parents ).toEqual( { wings: '', } ); - expect( state.cache ).toEqual( { - wings: {}, + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.wings + ); + expect( state.tree.wings ).toEqual( { + clientId: 'wings', + name: 'core/freeform', + innerBlocks: [], } ); } ); + it( 'should replace the block and remove references to its inner blocks', () => { const original = blocks( undefined, { type: 'RESET_BLOCKS', @@ -797,8 +827,13 @@ describe( 'state', () => { expect( state.parents ).toEqual( { wings: '', } ); - expect( state.cache ).toEqual( { - wings: {}, + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.wings + ); + expect( state.tree.wings ).toEqual( { + clientId: 'wings', + name: 'core/freeform', + innerBlocks: [], } ); } ); @@ -813,22 +848,12 @@ describe( 'state', () => { blocks: [ wrapperBlock ], } ); - const originalWrapperBlockCacheKey = - original.cache[ wrapperBlock.clientId ]; - const state = blocks( original, { type: 'REPLACE_BLOCKS', clientIds: [ nestedBlock.clientId ], blocks: [ replacementBlock ], } ); - const newWrapperBlockCacheKey = - state.cache[ wrapperBlock.clientId ]; - - expect( newWrapperBlockCacheKey ).not.toBe( - originalWrapperBlockCacheKey - ); - expect( state.order ).toEqual( { '': [ wrapperBlock.clientId ], [ wrapperBlock.clientId ]: [ replacementBlock.clientId ], @@ -840,9 +865,15 @@ describe( 'state', () => { [ replacementBlock.clientId ]: wrapperBlock.clientId, } ); - expect( state.cache ).toEqual( { - [ wrapperBlock.clientId ]: {}, - [ replacementBlock.clientId ]: {}, + expect( state.tree[ wrapperBlock.clientId ].innerBlocks[ 0 ] ).toBe( + state.tree[ replacementBlock.clientId ] + ); + expect( state.tree[ replacementBlock.clientId ] ).toEqual( { + clientId: replacementBlock.clientId, + name: 'core/test-block', + innerBlocks: [], + attributes: {}, + isValid: true, } ); } ); @@ -884,11 +915,8 @@ describe( 'state', () => { '': [ 'chicken' ], chicken: [], } ); - expect( replacedState.cache ).toEqual( { - chicken: {}, - } ); - expect( originalState.cache.chicken ).not.toBe( - replacedState.cache.chicken + expect( originalState.tree.chicken ).not.toBe( + replacedState.tree.chicken ); const nestedBlock = { @@ -963,11 +991,18 @@ describe( 'state', () => { expect( state.attributes.chicken ).toEqual( { content: 'ribs', } ); - - expect( state.cache ).toEqual( { - chicken: {}, + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.chicken + ); + expect( state.tree.chicken ).toEqual( { + clientId: 'chicken', + name: 'core/test-block', + innerBlocks: [], + attributes: { + content: 'ribs', + }, + isValid: true, } ); - expect( state.cache.chicken ).not.toBe( original.cache.chicken ); } ); it( 'should update the reusable block reference if the temporary id is swapped', () => { @@ -1001,10 +1036,19 @@ describe( 'state', () => { expect( state.attributes.chicken ).toEqual( { ref: 3, } ); - expect( state.cache ).toEqual( { - chicken: {}, + + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( + state.tree.chicken + ); + expect( state.tree.chicken ).toEqual( { + clientId: 'chicken', + name: 'core/block', + isValid: false, + innerBlocks: [], + attributes: { + ref: 3, + }, } ); - expect( state.cache.chicken ).not.toBe( original.cache.chicken ); } ); it( 'should move the block up', () => { @@ -1031,8 +1075,11 @@ describe( 'state', () => { } ); expect( state.order[ '' ] ).toEqual( [ 'ribs', 'chicken' ] ); - expect( state.cache.ribs ).toBe( original.cache.ribs ); - expect( state.cache.chicken ).toBe( original.cache.chicken ); + expect( state.tree[ '' ].innerBlocks[ 0 ] ).toBe( state.tree.ribs ); + expect( state.tree[ '' ].innerBlocks[ 1 ] ).toBe( + state.tree.chicken + ); + expect( state.tree.chicken ).toBe( original.tree.chicken ); } ); it( 'should move the nested block up', () => { @@ -1061,14 +1108,15 @@ describe( 'state', () => { [ movedBlock.clientId ]: [], [ siblingBlock.clientId ]: [], } ); - expect( state.cache[ wrapperBlock.clientId ] ).not.toBe( - original.cache[ wrapperBlock.clientId ] + + expect( state.tree[ wrapperBlock.clientId ].innerBlocks[ 0 ] ).toBe( + state.tree[ movedBlock.clientId ] ); - expect( state.cache[ movedBlock.clientId ] ).toBe( - original.cache[ movedBlock.clientId ] + expect( state.tree[ wrapperBlock.clientId ].innerBlocks[ 1 ] ).toBe( + state.tree[ siblingBlock.clientId ] ); - expect( state.cache[ siblingBlock.clientId ] ).toBe( - original.cache[ siblingBlock.clientId ] + expect( state.tree[ movedBlock.clientId ] ).toBe( + original.tree[ movedBlock.clientId ] ); } ); @@ -1351,9 +1399,7 @@ describe( 'state', () => { expect( state.attributes ).toEqual( { ribs: {}, } ); - expect( state.cache ).toEqual( { - ribs: {}, - } ); + expect( state.tree[ '' ].innerBlocks ).toHaveLength( 1 ); } ); it( 'should remove multiple blocks', () => { @@ -2351,6 +2397,30 @@ describe( 'state', () => { expect( state.selectionEnd ).toEqual( expected.selectionEnd ); } ); + it( 'should replace the selected block when is explicitly passed (`indexToSelect`)', () => { + const original = deepFreeze( { + selectionStart: { clientId: 'chicken' }, + selectionEnd: { clientId: 'chicken' }, + } ); + const action = { + type: 'REPLACE_BLOCKS', + clientIds: [ 'chicken' ], + blocks: [ + { clientId: 'rigas' }, + { clientId: 'chicken' }, + { clientId: 'wings' }, + ], + indexToSelect: 0, + }; + const state = selection( original, action ); + expect( state ).toEqual( + expect.objectContaining( { + selectionStart: { clientId: 'rigas' }, + selectionEnd: { clientId: 'rigas' }, + } ) + ); + } ); + it( 'should reset if replacing with empty set', () => { const original = deepFreeze( { selectionStart: { clientId: 'chicken' }, diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 1d96c678b2b48..e6f514f8faab9 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -228,19 +228,19 @@ describe( 'selectors', () => { parents: { 123: '', }, - cache: { - 123: {}, + tree: { + 123: { + clientId: 123, + name: 'core/paragraph', + attributes: {}, + innerBlocks: [], + }, }, controlledInnerBlocks: {}, }, }; - expect( getBlock( state, 123 ) ).toEqual( { - clientId: 123, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [], - } ); + expect( getBlock( state, 123 ) ).toBe( state.blocks.tree[ 123 ] ); } ); it( 'should return null if the block is not present in state', () => { @@ -250,55 +250,19 @@ describe( 'selectors', () => { attributes: {}, order: {}, parents: {}, - cache: {}, - controlledInnerBlocks: {}, - }, - }; - - expect( getBlock( state, 123 ) ).toBe( null ); - } ); - - it( 'should include inner blocks', () => { - const state = { - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/paragraph' }, - 456: { clientId: 456, name: 'core/paragraph' }, - }, - attributes: { - 123: {}, - 456: {}, - }, - order: { - '': [ 123 ], - 123: [ 456 ], - 456: [], - }, - parents: { - 123: '', - 456: 123, - }, - cache: { - 123: {}, - 456: {}, + tree: { + 123: { + clientId: 123, + name: 'core/paragraph', + attributes: {}, + innerBlocks: [], + }, }, controlledInnerBlocks: {}, }, }; - expect( getBlock( state, 123 ) ).toEqual( { - clientId: 123, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [ - { - clientId: 456, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [], - }, - ], - } ); + expect( getBlock( state, 123 ) ).toBe( null ); } ); } ); @@ -321,7 +285,23 @@ describe( 'selectors', () => { 123: '', 23: '', }, - cache: { + tree: { + '': { + innerBlocks: [ + { + clientId: 123, + name: 'core/paragraph', + attributes: {}, + innerBlocks: [], + }, + { + clientId: 23, + name: 'core/heading', + attributes: {}, + innerBlocks: [], + }, + ], + }, 123: {}, 23: {}, }, @@ -329,92 +309,9 @@ describe( 'selectors', () => { }, }; - expect( getBlocks( state ) ).toEqual( [ - { - clientId: 123, - name: 'core/paragraph', - attributes: {}, - innerBlocks: [], - }, - { - clientId: 23, - name: 'core/heading', - attributes: {}, - innerBlocks: [], - }, - ] ); - } ); - it( 'only returns a new value if the cache key of a direct child changes', () => { - const cacheRef = {}; - const state = { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - }, - attributes: { - 23: {}, - }, - order: { - '': [ 23 ], - }, - parents: { - 23: '', - }, - cache: { - 23: cacheRef, - }, - controlledInnerBlocks: {}, - }, - }; - const oldBlocks = getBlocks( state ); - - const newStateSameCache = { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - }, - attributes: { - 23: {}, - }, - order: { - '': [ 23 ], - }, - parents: { - 23: '', - }, - cache: { - 23: cacheRef, - }, - controlledInnerBlocks: {}, - }, - }; - // Makes sure blocks are referentially equal if the cache key stays the same. - const newBlocks = getBlocks( newStateSameCache ); - expect( oldBlocks ).toBe( newBlocks ); - - const newStateNewCache = { - blocks: { - byClientId: { - 23: { clientId: 23, name: 'core/heading' }, - }, - attributes: { - 23: {}, - }, - order: { - '': [ 23 ], - }, - parents: { - 23: '', - }, - cache: { - 23: {}, - }, - controlledInnerBlocks: {}, - }, - }; - // Blocks are referentially different if the cache key changes. - const newBlocksNewCache = getBlocks( newStateNewCache ); - expect( oldBlocks ).not.toBe( newBlocksNewCache ); + expect( getBlocks( state ) ).toBe( + state.blocks.tree[ '' ].innerBlocks + ); } ); } ); @@ -963,6 +860,14 @@ describe( 'selectors', () => { 23: '', 123: '', }, + tree: { + 23: { + clientId: 23, + name: 'core/heading', + attributes: {}, + innerBlocks: [], + }, + }, }, selection: { selectionStart: {}, @@ -993,6 +898,14 @@ describe( 'selectors', () => { 123: '', 23: '', }, + tree: { + 23: { + clientId: 23, + name: 'core/heading', + attributes: {}, + innerBlocks: [], + }, + }, }, selection: { selectionStart: { clientId: 23 }, @@ -1023,8 +936,13 @@ describe( 'selectors', () => { 123: '', 23: '', }, - cache: { - 23: {}, + tree: { + 23: { + clientId: 23, + name: 'core/heading', + attributes: {}, + innerBlocks: [], + }, }, controlledInnerBlocks: {}, }, @@ -1034,12 +952,9 @@ describe( 'selectors', () => { }, }; - expect( getSelectedBlock( state ) ).toEqual( { - clientId: 23, - name: 'core/heading', - attributes: {}, - innerBlocks: [], - } ); + expect( getSelectedBlock( state ) ).toEqual( + getBlock( state, 23 ) + ); } ); } ); @@ -2560,7 +2475,11 @@ describe( 'selectors', () => { attributes: {}, order: {}, parents: {}, - cache: {}, + tree: { + '': { + innerBlocks: [], + }, + }, }, settings: { __experimentalReusableBlocks: [ @@ -2636,9 +2555,19 @@ describe( 'selectors', () => { block3: '', block4: '', }, - cache: { - block3: {}, - block4: {}, + tree: { + block3: { + clientId: 'block3', + name: 'core/test-block-a', + attributes: {}, + innerBlocks: [], + }, + block4: { + clientId: 'block4', + name: 'core/test-block-a', + attributes: {}, + innerBlocks: [], + }, }, controlledInnerBlocks: {}, }, @@ -2726,8 +2655,13 @@ describe( 'selectors', () => { order: { '': [ 'block1' ], }, - cache: { - block1: {}, + tree: { + block1: { + clientId: 'block1', + name: 'core/test-block-b', + attributes: {}, + innerBlocks: [], + }, }, controlledInnerBlocks: {}, }, @@ -2929,8 +2863,13 @@ describe( 'selectors', () => { order: { '': [ 'block1' ], }, - cache: { - block1: {}, + tree: { + block1: { + clientId: 'block1', + name: 'core/with-tranforms-c', + attributes: {}, + innerBlocks: [], + }, }, controlledInnerBlocks: {}, }, diff --git a/packages/block-editor/src/utils/parse-css-unit-to-px.js b/packages/block-editor/src/utils/parse-css-unit-to-px.js new file mode 100644 index 0000000000000..c8451fcc3e1d6 --- /dev/null +++ b/packages/block-editor/src/utils/parse-css-unit-to-px.js @@ -0,0 +1,230 @@ +/** + * Converts string to object { value, unit }. + * + * @param {string} cssUnit + * @return {Object} parsedUnit + */ +function parseUnit( cssUnit ) { + const match = cssUnit + ?.trim() + .match( + /^(0?[-.]?\d+)(r?e[m|x]|v[h|w|min|max]+|p[x|t|c]|[c|m]m|%|in|ch|Q|lh)$/ + ); + if ( ! isNaN( cssUnit ) && ! isNaN( parseFloat( cssUnit ) ) ) { + return { value: parseFloat( cssUnit ), unit: 'px' }; + } + return match + ? { value: parseFloat( match[ 1 ] ) || match[ 1 ], unit: match[ 2 ] } + : { value: cssUnit, unit: undefined }; +} +/** + * Evaluate a math expression. + * + * @param {string} expression + * @return {number} evaluated expression. + */ +function calculate( expression ) { + return Function( `'use strict'; return (${ expression })` )(); +} + +/** + * Calculates the css function value for the supported css functions such as max, min, clamp and calc. + * + * @param {string} functionUnitValue string should be in a particular format (for example min(12px,12px) ) no nested loops. + * @param {Object} options + * @return {string} unit containing the unit in PX. + */ +function getFunctionUnitValue( functionUnitValue, options ) { + const functionUnit = functionUnitValue.split( /[(),]/g ).filter( Boolean ); + + const units = functionUnit + .slice( 1 ) + .map( ( unit ) => parseUnit( getPxFromCssUnit( unit, options ) ).value ) + .filter( Boolean ); + + switch ( functionUnit[ 0 ] ) { + case 'min': + return Math.min( ...units ) + 'px'; + case 'max': + return Math.max( ...units ) + 'px'; + case 'clamp': + if ( units.length !== 3 ) { + return null; + } + if ( units[ 1 ] < units[ 0 ] ) { + return units[ 0 ] + 'px'; + } + if ( units[ 1 ] > units[ 2 ] ) { + return units[ 2 ] + 'px'; + } + return units[ 1 ] + 'px'; + case 'calc': + return units[ 0 ] + 'px'; + } +} + +/** + * Take a css function such as min, max, calc, clamp and returns parsedUnit + * + * How this works for the nested function is that it first replaces the inner function call. + * Then it tackles the outer onces. + * So for example: min( max(25px, 35px), 40px ) + * in the first pass we would replace max(25px, 35px) with 35px. + * then we would try to evaluate min( 35px, 40px ) + * and then finally return 35px. + * + * @param {string} cssUnit + * @return {Object} parsedUnit object. + */ +function parseUnitFunction( cssUnit ) { + while ( true ) { + const currentCssUnit = cssUnit; + const regExp = /(max|min|calc|clamp)\(([^()]*)\)/g; + const matches = regExp.exec( cssUnit ) || []; + if ( matches[ 0 ] ) { + const functionUnitValue = getFunctionUnitValue( matches[ 0 ] ); + cssUnit = cssUnit.replace( matches[ 0 ], functionUnitValue ); + } + + // if the unit hasn't been modified or we have a single value break free. + if ( cssUnit === currentCssUnit || parseFloat( cssUnit ) ) { + break; + } + } + + return parseUnit( cssUnit ); +} +/** + * Return true if we think this is a math expression. + * + * @param {string} cssUnit the cssUnit value being evaluted. + * @return {boolean} Whether the cssUnit is a math expression. + */ +function isMathExpression( cssUnit ) { + for ( let i = 0; i < cssUnit.length; i++ ) { + if ( [ '+', '-', '/', '*' ].includes( cssUnit[ i ] ) ) { + return true; + } + } + return false; +} +/** + * Evaluates the math expression and return a px value. + * + * @param {string} cssUnit the cssUnit value being evaluted. + * @return {string} return a converfted value to px. + */ +function evalMathExpression( cssUnit ) { + let errorFound = false; + // Convert every part of the expression to px values. + const cssUnitsBits = cssUnit.split( /[+-/*/]/g ).filter( Boolean ); + for ( const unit of cssUnitsBits ) { + // Standardize the unit to px and extract the value. + const parsedUnit = parseUnit( getPxFromCssUnit( unit ) ); + if ( ! parseFloat( parsedUnit.value ) ) { + errorFound = true; + // end early since we are dealing with a null value. + break; + } + cssUnit = cssUnit.replace( unit, parsedUnit.value ); + } + + return errorFound ? null : calculate( cssUnit ).toFixed( 0 ) + 'px'; +} +/** + * Convert a parsedUnit object to px value. + * + * @param {Object} parsedUnit + * @param {Object} options + * @return {string} or {null} returns the converted with in a px value format. + */ +function convertParsedUnitToPx( parsedUnit, options ) { + const PIXELS_PER_INCH = 96; + const ONE_PERCENT = 0.01; + + const defaultProperties = { + fontSize: 16, + lineHeight: 16, + width: 375, + height: 812, + type: 'font', + }; + + const setOptions = Object.assign( {}, defaultProperties, options ); + + const relativeUnits = { + em: setOptions.fontSize, + rem: setOptions.fontSize, + vh: setOptions.height * ONE_PERCENT, + vw: setOptions.width * ONE_PERCENT, + vmin: + ( setOptions.width < setOptions.height + ? setOptions.width + : setOptions.height ) * ONE_PERCENT, + vmax: + ( setOptions.width > setOptions.height + ? setOptions.width + : setOptions.height ) * ONE_PERCENT, + '%': + ( setOptions.type === 'font' + ? setOptions.fontSize + : setOptions.width ) * ONE_PERCENT, + ch: 8, // The advance measure (width) of the glyph "0" of the element's font. Approximate + ex: 7.15625, // x-height of the element's font. Approximate + lh: setOptions.lineHeight, + }; + + const absoluteUnits = { + in: PIXELS_PER_INCH, + cm: PIXELS_PER_INCH / 2.54, + mm: PIXELS_PER_INCH / 25.4, + pt: PIXELS_PER_INCH / 72, + pc: PIXELS_PER_INCH / 6, + px: 1, + Q: PIXELS_PER_INCH / 2.54 / 40, + }; + + if ( relativeUnits[ parsedUnit.unit ] ) { + return ( + ( relativeUnits[ parsedUnit.unit ] * parsedUnit.value ).toFixed( + 0 + ) + 'px' + ); + } + + if ( absoluteUnits[ parsedUnit.unit ] ) { + return ( + ( absoluteUnits[ parsedUnit.unit ] * parsedUnit.value ).toFixed( + 0 + ) + 'px' + ); + } + + return null; +} +/** + * Returns the px value of a cssUnit. + * + * @param {string} cssUnit + * @param {string} options + * @return {string} returns the cssUnit value in a simple px format. + */ +export function getPxFromCssUnit( cssUnit, options = {} ) { + if ( Number.isFinite( cssUnit ) ) { + return cssUnit.toFixed( 0 ) + 'px'; + } + if ( cssUnit === undefined ) { + return null; + } + let parsedUnit = parseUnit( cssUnit ); + + if ( ! parsedUnit.unit ) { + parsedUnit = parseUnitFunction( cssUnit, options ); + } + + if ( isMathExpression( cssUnit ) && ! parsedUnit.unit ) { + return evalMathExpression( cssUnit ); + } + + return convertParsedUnitToPx( parsedUnit, options ); +} diff --git a/packages/block-editor/src/utils/test/parse-css-unit-to-px.js b/packages/block-editor/src/utils/test/parse-css-unit-to-px.js new file mode 100644 index 0000000000000..90cfac3b55e27 --- /dev/null +++ b/packages/block-editor/src/utils/test/parse-css-unit-to-px.js @@ -0,0 +1,183 @@ +/** + * Internal dependencies + */ +import { getPxFromCssUnit } from '../parse-css-unit-to-px'; + +describe( 'getPxFromCssUnit', () => { + // Absolute units + it( 'test px return px unit', () => { + expect( getPxFromCssUnit( '25px' ) ).toBe( '25px' ); + } ); + + it( 'test numeric float return px unit', () => { + expect( getPxFromCssUnit( '25.5' ) ).toBe( '26px' ); + } ); + + it( 'test cm return px unit', () => { + expect( getPxFromCssUnit( '1cm' ) ).toBe( '38px' ); + } ); + + it( 'test mm return px unit', () => { + expect( getPxFromCssUnit( '10mm' ) ).toBe( '38px' ); + } ); + + it( 'test in return px unit', () => { + expect( getPxFromCssUnit( '1in' ) ).toBe( '96px' ); + } ); + + it( 'test pt return px unit', () => { + expect( getPxFromCssUnit( '12pt' ) ).toBe( '16px' ); + } ); + + it( 'test pc return px unit', () => { + expect( getPxFromCssUnit( '1pc' ) ).toBe( '16px' ); + } ); + + it( 'test Q return px unit', () => { + expect( getPxFromCssUnit( '40Q' ) ).toBe( '38px' ); // 40 Q should be 1 cm + } ); + + // Relative units + it( 'test em return px unit', () => { + expect( getPxFromCssUnit( '2em', { fontSize: 10 } ) ).toBe( '20px' ); + } ); + + it( 'test rem return px unit', () => { + expect( getPxFromCssUnit( '2rem', { fontSize: 10 } ) ).toBe( '20px' ); + } ); + + it( 'test vw return px unit', () => { + expect( getPxFromCssUnit( '20vw', { width: 100 } ) ).toBe( '20px' ); + } ); + + it( 'test vh return px unit', () => { + expect( getPxFromCssUnit( '20vh', { height: 200 } ) ).toBe( '40px' ); + } ); + + it( 'test vmin return px unit', () => { + expect( + getPxFromCssUnit( '20vmin', { height: 200, width: 100 } ) + ).toBe( '20px' ); + } ); + + it( 'test vmax return px unit', () => { + expect( + getPxFromCssUnit( '20vmax', { height: 200, width: 100 } ) + ).toBe( '40px' ); + } ); + + it( 'test lh return px unit', () => { + expect( getPxFromCssUnit( '20lh', { lineHeight: 2 } ) ).toBe( '40px' ); + } ); + + it( 'test % return px unit', () => { + expect( + getPxFromCssUnit( '120%', { + height: 200, + width: 100, + fontSize: 10, + type: 'font', + } ) + ).toBe( '12px' ); + } ); + + // Function units + it( 'test min() return px unit', () => { + expect( getPxFromCssUnit( 'min(20px, 25px)' ) ).toBe( '20px' ); + } ); + + it( 'test min() function with many arguments return px unit', () => { + expect( getPxFromCssUnit( 'min(20px, 9px, 12pt, 25px)' ) ).toBe( + '9px' + ); + } ); + + it( 'test max() return px unit', () => { + expect( getPxFromCssUnit( 'max(20px, 25px)' ) ).toBe( '25px' ); + } ); + + it( 'test clamp() lower return px unit', () => { + expect( getPxFromCssUnit( 'clamp(10px, 9px, 25px)' ) ).toBe( '10px' ); + } ); + + it( 'test clamp() upper return px unit', () => { + expect( getPxFromCssUnit( 'clamp(10px, 35px, 25px)' ) ).toBe( '25px' ); + } ); + + it( 'test clamp() middle return px unit', () => { + expect( getPxFromCssUnit( 'clamp(10px, 15px, 25px)' ) ).toBe( '15px' ); + } ); + + it( 'test nested max min function return px unit', () => { + expect( getPxFromCssUnit( 'min(max(20px,25px), 35px)' ) ).toBe( + '25px' + ); + } ); + + it( 'test nested min max function return px unit', () => { + expect( getPxFromCssUnit( 'max(min(20px,25px), 35px)' ) ).toBe( + '35px' + ); + } ); + + it( 'test calculate function return px unit', () => { + expect( getPxFromCssUnit( '10px + 25px' ) ).toBe( '35px' ); + } ); + + it( 'test calc(10px + 25px) function return px unit', () => { + expect( getPxFromCssUnit( 'calc(10px + 25px)' ) ).toBe( '35px' ); + } ); + + it( 'test calc( number * cssUnit ) return px unit', () => { + expect( getPxFromCssUnit( 'calc( 2 * 20px)' ) ).toBe( '40px' ); + } ); + + it( 'test calc(25px - 10px) function return px unit', () => { + expect( getPxFromCssUnit( 'calc(25px - 10px)' ) ).toBe( '15px' ); + } ); + + it( 'test min(10px + 25px, 55pt) function return px unit', () => { + expect( getPxFromCssUnit( 'min(10px + 25px, 55pt)' ) ).toBe( '35px' ); + } ); + + it( 'test calc(12vw * 10px) function return px unit', () => { + expect( getPxFromCssUnit( 'calc(12vw * 10px)' ) ).toBe( '450px' ); + } ); + + it( 'test calc(42vw / 10px) function return px unit', () => { + expect( getPxFromCssUnit( 'calc(45vw / 10px)' ) ).toBe( '17px' ); + } ); + + it( 'test empty string', () => { + expect( getPxFromCssUnit( '' ) ).toBe( null ); + } ); + + it( 'test undefined string', () => { + expect( getPxFromCssUnit( undefined ) ).toBe( null ); + } ); + it( 'test integer string', () => { + expect( getPxFromCssUnit( 123 ) ).toBe( '123px' ); + } ); + + it( 'test float string', () => { + expect( getPxFromCssUnit( 123.456 ) ).toBe( '123px' ); + } ); + + it( 'test text string', () => { + expect( getPxFromCssUnit( 'abc' ) ).toBe( null ); + } ); + + it( 'test not non function return null', () => { + expect( getPxFromCssUnit( 'abc + num' ) ).toBe( null ); + } ); + + it( 'test not a fishy function return null', () => { + expect( getPxFromCssUnit( 'console.log("howdy"); + 10px' ) ).toBe( + null + ); + } ); + + it( 'test not a typo function return null', () => { + expect( getPxFromCssUnit( 'calc(12vw * 10px' ) ).toBe( null ); + } ); +} ); diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index 709ed1182a250..eccd2681c7d27 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Change + +- Remove the background-colors, foreground-colors, and gradient-colors mixins. + ## 5.0.0 (2021-07-29) ### Breaking Change diff --git a/packages/block-library/package.json b/packages/block-library/package.json index d02fee6ab2f61..4ad410cac248d 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "5.0.0", + "version": "5.0.1", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index c2fe191252a93..22b0ad592eb48 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -20,7 +20,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { audio as icon } from '@wordpress/icons'; import { createBlock } from '@wordpress/blocks'; @@ -157,7 +157,7 @@ function AudioEdit( { checked={ loop } /> <SelectControl - label={ __( 'Preload' ) } + label={ _x( 'Preload', 'noun; Audio block parameter' ) } value={ preload || '' } // `undefined` is required for the preload attribute to be unset. onChange={ ( value ) => @@ -169,7 +169,10 @@ function AudioEdit( { { value: '', label: __( 'Browser default' ) }, { value: 'auto', label: __( 'Auto' ) }, { value: 'metadata', label: __( 'Metadata' ) }, - { value: 'none', label: __( 'None' ) }, + { + value: 'none', + label: _x( 'None', '"Preload" value' ), + }, ] } /> </PanelBody> diff --git a/packages/block-library/src/audio/edit.native.js b/packages/block-library/src/audio/edit.native.js index a46a3b57e46e3..a86abf369bd03 100644 --- a/packages/block-library/src/audio/edit.native.js +++ b/packages/block-library/src/audio/edit.native.js @@ -26,7 +26,7 @@ import { MediaUploadProgress, store as blockEditorStore, } from '@wordpress/block-editor'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { audio as icon, replace } from '@wordpress/icons'; import { useState } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -197,7 +197,10 @@ function AudioEdit( { checked={ loop } /> <SelectControl - label={ __( 'Preload' ) } + label={ _x( + 'Preload', + 'noun; Audio block parameter' + ) } value={ preload || '' } // `undefined` is required for the preload attribute to be unset. onChange={ ( value ) => @@ -209,7 +212,10 @@ function AudioEdit( { { value: '', label: __( 'Browser default' ) }, { value: 'auto', label: __( 'Auto' ) }, { value: 'metadata', label: __( 'Metadata' ) }, - { value: 'none', label: __( 'None' ) }, + { + value: 'none', + label: _x( 'None', '"Preload" value' ), + }, ] } hideCancelButton={ true } /> diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index c0f80bc40ae0d..aa79ee58f67d6 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -7,11 +7,10 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useCallback, useState, useRef } from '@wordpress/element'; +import { useCallback, useEffect, useState, useRef } from '@wordpress/element'; import { Button, ButtonGroup, - KeyboardShortcuts, PanelBody, TextControl, ToolbarButton, @@ -20,7 +19,6 @@ import { import { BlockControls, InspectorControls, - InspectorAdvancedControls, RichText, useBlockProps, __experimentalUseBorderProps as useBorderProps, @@ -28,7 +26,7 @@ import { __experimentalGetSpacingClassesAndStyles as useSpacingProps, __experimentalLinkControl as LinkControl, } from '@wordpress/block-editor'; -import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; +import { displayShortcut, isKeyboardEvent } from '@wordpress/keycodes'; import { link, linkOff } from '@wordpress/icons'; import { createBlock } from '@wordpress/blocks'; @@ -67,88 +65,6 @@ function WidthPanel( { selectedWidth, setAttributes } ) { ); } -function URLPicker( { - isSelected, - url, - setAttributes, - opensInNewTab, - onToggleOpenInNewTab, - anchorRef, -} ) { - const [ isURLPickerOpen, setIsURLPickerOpen ] = useState( false ); - const urlIsSet = !! url; - const urlIsSetandSelected = urlIsSet && isSelected; - const openLinkControl = () => { - setIsURLPickerOpen( true ); - return false; // prevents default behaviour for event - }; - const unlinkButton = () => { - setAttributes( { - url: undefined, - linkTarget: undefined, - rel: undefined, - } ); - setIsURLPickerOpen( false ); - }; - const linkControl = ( isURLPickerOpen || urlIsSetandSelected ) && ( - <Popover - position="bottom center" - onClose={ () => setIsURLPickerOpen( false ) } - anchorRef={ anchorRef?.current } - > - <LinkControl - className="wp-block-navigation-link__inline-link-input" - value={ { url, opensInNewTab } } - onChange={ ( { - url: newURL = '', - opensInNewTab: newOpensInNewTab, - } ) => { - setAttributes( { url: newURL } ); - - if ( opensInNewTab !== newOpensInNewTab ) { - onToggleOpenInNewTab( newOpensInNewTab ); - } - } } - /> - </Popover> - ); - return ( - <> - <BlockControls group="block"> - { ! urlIsSet && ( - <ToolbarButton - name="link" - icon={ link } - title={ __( 'Link' ) } - shortcut={ displayShortcut.primary( 'k' ) } - onClick={ openLinkControl } - /> - ) } - { urlIsSetandSelected && ( - <ToolbarButton - name="link" - icon={ linkOff } - title={ __( 'Unlink' ) } - shortcut={ displayShortcut.primaryShift( 'k' ) } - onClick={ unlinkButton } - isActive={ true } - /> - ) } - </BlockControls> - { isSelected && ( - <KeyboardShortcuts - bindGlobal - shortcuts={ { - [ rawShortcut.primary( 'k' ) ]: openLinkControl, - [ rawShortcut.primaryShift( 'k' ) ]: unlinkButton, - } } - /> - ) } - { linkControl } - </> - ); -} - function ButtonEdit( props ) { const { attributes, @@ -174,35 +90,66 @@ function ButtonEdit( props ) { [ setAttributes ] ); - const onToggleOpenInNewTab = useCallback( - ( value ) => { - const newLinkTarget = value ? '_blank' : undefined; + function onToggleOpenInNewTab( value ) { + const newLinkTarget = value ? '_blank' : undefined; - let updatedRel = rel; - if ( newLinkTarget && ! rel ) { - updatedRel = NEW_TAB_REL; - } else if ( ! newLinkTarget && rel === NEW_TAB_REL ) { - updatedRel = undefined; - } + let updatedRel = rel; + if ( newLinkTarget && ! rel ) { + updatedRel = NEW_TAB_REL; + } else if ( ! newLinkTarget && rel === NEW_TAB_REL ) { + updatedRel = undefined; + } - setAttributes( { - linkTarget: newLinkTarget, - rel: updatedRel, - } ); - }, - [ rel, setAttributes ] - ); + setAttributes( { + linkTarget: newLinkTarget, + rel: updatedRel, + } ); + } - const setButtonText = ( newText ) => { + function setButtonText( newText ) { // Remove anchor tags from button text content. setAttributes( { text: newText.replace( /<\/?a[^>]*>/g, '' ) } ); - }; + } + + function onKeyDown( event ) { + if ( isKeyboardEvent.primary( event, 'k' ) ) { + startEditing( event ); + } else if ( isKeyboardEvent.primaryShift( event, 'k' ) ) { + unlink(); + richTextRef.current?.focus(); + } + } const borderProps = useBorderProps( attributes ); const colorProps = useColorProps( attributes ); const spacingProps = useSpacingProps( attributes ); const ref = useRef(); - const blockProps = useBlockProps( { ref } ); + const richTextRef = useRef(); + const blockProps = useBlockProps( { ref, onKeyDown } ); + + const [ isEditingURL, setIsEditingURL ] = useState( false ); + const isURLSet = !! url; + const opensInNewTab = linkTarget === '_blank'; + + function startEditing( event ) { + event.preventDefault(); + setIsEditingURL( true ); + } + + function unlink() { + setAttributes( { + url: undefined, + linkTarget: undefined, + rel: undefined, + } ); + setIsEditingURL( false ); + } + + useEffect( () => { + if ( ! isSelected ) { + setIsEditingURL( false ); + } + }, [ isSelected ] ); return ( <> @@ -214,6 +161,7 @@ function ButtonEdit( props ) { } ) } > <RichText + ref={ richTextRef } aria-label={ __( 'Button text' ) } placeholder={ placeholder || __( 'Add text…' ) } value={ text } @@ -246,27 +194,71 @@ function ButtonEdit( props ) { identifier="text" /> </div> - <URLPicker - url={ url } - setAttributes={ setAttributes } - isSelected={ isSelected } - opensInNewTab={ linkTarget === '_blank' } - onToggleOpenInNewTab={ onToggleOpenInNewTab } - anchorRef={ ref } - /> + <BlockControls group="block"> + { ! isURLSet && ( + <ToolbarButton + name="link" + icon={ link } + title={ __( 'Link' ) } + shortcut={ displayShortcut.primary( 'k' ) } + onClick={ startEditing } + /> + ) } + { isURLSet && ( + <ToolbarButton + name="link" + icon={ linkOff } + title={ __( 'Unlink' ) } + shortcut={ displayShortcut.primaryShift( 'k' ) } + onClick={ unlink } + isActive={ true } + /> + ) } + </BlockControls> + { isSelected && ( isEditingURL || isURLSet ) && ( + <Popover + position="bottom center" + onClose={ () => { + setIsEditingURL( false ); + richTextRef.current?.focus(); + } } + anchorRef={ ref?.current } + focusOnMount={ isEditingURL ? 'firstElement' : false } + > + <LinkControl + className="wp-block-navigation-link__inline-link-input" + value={ { url, opensInNewTab } } + onChange={ ( { + url: newURL = '', + opensInNewTab: newOpensInNewTab, + } ) => { + setAttributes( { url: newURL } ); + + if ( opensInNewTab !== newOpensInNewTab ) { + onToggleOpenInNewTab( newOpensInNewTab ); + } + } } + onRemove={ () => { + unlink(); + richTextRef.current?.focus(); + } } + forceIsEditingLink={ isEditingURL } + /> + </Popover> + ) } <InspectorControls> <WidthPanel selectedWidth={ width } setAttributes={ setAttributes } /> </InspectorControls> - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> <TextControl label={ __( 'Link rel' ) } value={ rel || '' } onChange={ onSetLinkRel } /> - </InspectorAdvancedControls> + </InspectorControls> </> ); } diff --git a/packages/block-library/src/button/edit.native.js b/packages/block-library/src/button/edit.native.js index a06f5d06adccb..ec9741fe5add4 100644 --- a/packages/block-library/src/button/edit.native.js +++ b/packages/block-library/src/button/edit.native.js @@ -6,7 +6,7 @@ import { View, AccessibilityInfo, Platform, Text } from 'react-native'; * WordPress dependencies */ import { withInstanceId, compose } from '@wordpress/compose'; -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { RichText, InspectorControls, @@ -136,7 +136,10 @@ class ButtonEdit extends Component { }, linkRel: { label: __( 'Link Rel' ), - placeholder: __( 'None' ), + placeholder: _x( + 'None', + 'Link rel attribute value placeholder' + ), }, }; diff --git a/packages/block-library/src/button/style.scss b/packages/block-library/src/button/style.scss index fb85a6cdd6ce2..4b778ed3be541 100644 --- a/packages/block-library/src/button/style.scss +++ b/packages/block-library/src/button/style.scss @@ -14,7 +14,7 @@ $blocks-block__margin: 0.5em; padding: calc(0.667em + 2px) calc(1.333em + 2px); // The extra 2px are added to size solids the same as the outline versions. text-align: center; text-decoration: none; - overflow-wrap: break-word; + word-break: break-word; // overflow-wrap doesn't work well if a link is wrapped in the div, so use word-break here. box-sizing: border-box; &:hover, diff --git a/packages/block-library/src/buttons/block.json b/packages/block-library/src/buttons/block.json index 74eed0a4a0a54..dcd9519af1a10 100644 --- a/packages/block-library/src/buttons/block.json +++ b/packages/block-library/src/buttons/block.json @@ -17,7 +17,8 @@ }, "supports": { "anchor": true, - "align": [ "wide", "full" ] + "align": [ "wide", "full" ], + "__experimentalExposeControlsToChildren": true }, "editorStyle": "wp-block-buttons-editor", "style": "wp-block-buttons" diff --git a/packages/block-library/src/buttons/edit.js b/packages/block-library/src/buttons/edit.js index 424e305bd585e..8cf3c31db5062 100644 --- a/packages/block-library/src/buttons/edit.js +++ b/packages/block-library/src/buttons/edit.js @@ -70,7 +70,7 @@ function ButtonsEdit( { return ( <> - <BlockControls group="block"> + <BlockControls group="block" __experimentalExposeToChildren> <JustifyContentControl allowedControls={ justifyControls } value={ contentJustification } diff --git a/packages/block-library/src/columns/edit.native.js b/packages/block-library/src/columns/edit.native.js index 9bf1ceae7ac99..a7e765b8344a4 100644 --- a/packages/block-library/src/columns/edit.native.js +++ b/packages/block-library/src/columns/edit.native.js @@ -274,7 +274,7 @@ function ColumnsEditContainer( { orientation={ columnsInRow > 1 ? 'horizontal' : undefined } - horizontal={ true } + horizontal={ columnsInRow > 1 } allowedBlocks={ ALLOWED_BLOCKS } contentResizeMode="stretch" onAddBlock={ onAddBlock } diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index bb84ad88a6623..de64661b46b4b 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -12,14 +12,14 @@ @include break-small() { .editor-styles-wrapper .block-editor-block-list__block.wp-block-column:nth-child(even) { - margin-left: $grid-unit-20 * 2; + margin-left: var(--wp--style--block-gap, 2em); } } @include break-medium() { .editor-styles-wrapper .block-editor-block-list__block.wp-block-column:not(:first-child) { - margin-left: $grid-unit-20 * 2; + margin-left: var(--wp--style--block-gap, 2em); } } diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss index aba4c99297a19..acd1d713385bd 100644 --- a/packages/block-library/src/columns/style.scss +++ b/packages/block-library/src/columns/style.scss @@ -43,14 +43,14 @@ // As with mobile styles, this must be important since the Column // assigns its own width as an inline style, which should take effect // starting at `break-medium`. - flex-basis: calc(50% - 1em) !important; + flex-basis: calc(50% - calc(var(--wp--style--block-gap, 2em) / 2)) !important; flex-grow: 0; } // Add space between the multiple columns. Themes can customize this if they wish to work differently. // Only apply this beyond the mobile breakpoint, as there's only a single column on mobile. &:nth-child(even) { - margin-left: 2em; + margin-left: var(--wp--style--block-gap, 2em); } } @@ -74,7 +74,7 @@ // When columns are in a single row, add space before all except the first. &:not(:first-child) { - margin-left: 2em; + margin-left: var(--wp--style--block-gap, 2em); } } } @@ -95,7 +95,7 @@ // When columns are in a single row, add space before all except the first. &:not(:first-child) { - margin-left: 2em; + margin-left: var(--wp--style--block-gap, 2em); } } } diff --git a/packages/block-library/src/common.scss b/packages/block-library/src/common.scss index 6cf9854943376..de0dacc67815e 100644 --- a/packages/block-library/src/common.scss +++ b/packages/block-library/src/common.scss @@ -3,15 +3,9 @@ // the colors override the added specificity by link states such as :hover. :root { - // Background colors. - @include background-colors(); - - // Foreground colors. - @include foreground-colors(); - - // Gradients - @include gradient-colors(); - + @include background-colors-deprecated(); + @include foreground-colors-deprecated(); + @include gradient-colors-deprecated(); } // Font sizes. diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 0870f1b38b0fe..b1919397804a2 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -11,7 +11,6 @@ @import "./freeform/editor.scss"; @import "./gallery/editor.scss"; @import "./group/editor.scss"; -@import "./heading/editor.scss"; @import "./html/editor.scss"; @import "./image/editor.scss"; @import "./latest-posts/editor.scss"; @@ -49,14 +48,9 @@ @import "./term-description/editor.scss"; :root .editor-styles-wrapper { - // Background colors. - @include background-colors(); - - // Foreground colors. - @include foreground-colors(); - - // Gradients - @include gradient-colors(); + @include background-colors-deprecated(); + @include foreground-colors-deprecated(); + @include gradient-colors-deprecated(); } // Font sizes. diff --git a/packages/block-library/src/embed/edit.native.js b/packages/block-library/src/embed/edit.native.js index 623f08752e56d..ac9e1f372be02 100644 --- a/packages/block-library/src/embed/edit.native.js +++ b/packages/block-library/src/embed/edit.native.js @@ -32,9 +32,13 @@ import { import { store as coreStore } from '@wordpress/core-data'; import { View } from '@wordpress/primitives'; +// The inline preview feature will be released progressible, for this reason +// the embed will only be considered previewable for the following providers list. +const PREVIEWABLE_PROVIDERS = [ 'youtube', 'twitter' ]; + const EmbedEdit = ( props ) => { const { - attributes: { providerNameSlug, previewable, responsive, url }, + attributes: { align, providerNameSlug, previewable, responsive, url }, attributes, isSelected, onReplace, @@ -207,6 +211,11 @@ const EmbedEdit = ( props ) => { } = getMergedAttributes(); const className = classnames( classFromPreview, props.className ); + const isProviderPreviewable = + PREVIEWABLE_PROVIDERS.includes( providerNameSlug ) || + // For WordPress embeds, we enable the inline preview for all its providers. + 'wp-embed' === type; + return ( <> { showEmbedPlaceholder ? ( @@ -234,6 +243,7 @@ const EmbedEdit = ( props ) => { /> <View { ...blockProps }> <EmbedPreview + align={ align } className={ className } clientId={ clientId } icon={ icon } @@ -242,7 +252,7 @@ const EmbedEdit = ( props ) => { label={ title } onFocus={ onFocus } preview={ preview } - previewable={ previewable } + previewable={ previewable && isProviderPreviewable } type={ type } url={ url } /> diff --git a/packages/block-library/src/embed/embed-no-preview.native.js b/packages/block-library/src/embed/embed-no-preview.native.js index cd0af7b730851..c76ea855eff5f 100644 --- a/packages/block-library/src/embed/embed-no-preview.native.js +++ b/packages/block-library/src/embed/embed-no-preview.native.js @@ -8,7 +8,7 @@ import { TouchableOpacity, TouchableWithoutFeedback, Text } from 'react-native'; */ import { View } from '@wordpress/primitives'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { useRef, useState } from '@wordpress/element'; import { usePreferredColorSchemeStyle } from '@wordpress/compose'; import { requestPreview } from '@wordpress/react-native-bridge'; @@ -73,11 +73,19 @@ const EmbedNoPreview = ( { label, icon, isSelected, onPress } ) => { postType === 'page' ? __( 'Preview page' ) : __( 'Preview post' ); const comingSoonDescription = postType === 'page' - ? __( - 'We’re working hard on adding support for embed previews. In the meantime, you can preview the embedded content on the page.' + ? sprintf( + // translators: %s: embed block variant's label e.g: "Twitter". + __( + 'We’re working hard on adding support for %s previews. In the meantime, you can preview the embedded content on the page.' + ), + label ) - : __( - 'We’re working hard on adding support for embed previews. In the meantime, you can preview the embedded content on the post.' + : sprintf( + // translators: %s: embed block variant's label e.g: "Twitter". + __( + 'We’re working hard on adding support for %s previews. In the meantime, you can preview the embedded content on the post.' + ), + label ); function onOpenSheet() { @@ -118,7 +126,11 @@ const EmbedNoPreview = ( { label, icon, isSelected, onPress } ) => { <BlockIcon icon={ icon } /> <Text style={ labelStyle }>{ label }</Text> <Text style={ descriptionStyle }> - { __( 'Embed previews not yet available' ) } + { sprintf( + // translators: %s: embed block variant's label e.g: "Twitter". + __( '%s previews not yet available' ), + label + ) } </Text> <Text style={ styles.embed__action }> { previewButtonText.toUpperCase() } @@ -154,7 +166,11 @@ const EmbedNoPreview = ( { label, icon, isSelected, onPress } ) => { /> </View> <Text style={ sheetTitleStyle }> - { __( 'Embed block previews are coming soon' ) } + { sprintf( + // translators: %s: embed block variant's label e.g: "Twitter". + __( '%s block previews are coming soon' ), + label + ) } </Text> <Text style={ sheetDescriptionStyle }> { comingSoonDescription } diff --git a/packages/block-library/src/embed/embed-preview.native.js b/packages/block-library/src/embed/embed-preview.native.js index 2f0cf949d4d0f..0beafe458ccca 100644 --- a/packages/block-library/src/embed/embed-preview.native.js +++ b/packages/block-library/src/embed/embed-preview.native.js @@ -20,8 +20,11 @@ import { SandBox } from '@wordpress/components'; */ import { getPhotoHtml } from './util'; import EmbedNoPreview from './embed-no-preview'; +import WpEmbedPreview from './wp-embed-preview'; +import styles from './styles.scss'; const EmbedPreview = ( { + align, className, clientId, icon, @@ -36,6 +39,12 @@ const EmbedPreview = ( { } ) => { const [ isCaptionSelected, setIsCaptionSelected ] = useState( false ); + const wrapperStyle = styles[ 'embed-preview__wrapper' ]; + const wrapperAlignStyle = + styles[ `embed-preview__wrapper--align-${ align }` ]; + const sandboxAlignStyle = + styles[ `embed-preview__sandbox--align-${ align }` ]; + function accessibilityLabelCreator( caption ) { return isEmpty( caption ) ? /* translators: accessibility text. Empty Embed caption. */ @@ -77,33 +86,35 @@ const EmbedPreview = ( { 'wp-block-embed__wrapper' ); - const embedWrapper = - /* We should render here: <WpEmbedPreview html={ html } /> */ - 'wp-embed' === type ? null : ( - <> - <TouchableWithoutFeedback - onPress={ () => { - if ( onFocus ) { - onFocus(); - } - if ( isCaptionSelected ) { - setIsCaptionSelected( false ); - } - } } + const PreviewContent = 'wp-embed' === type ? WpEmbedPreview : SandBox; + const embedWrapper = ( + <> + <TouchableWithoutFeedback + onPress={ () => { + if ( onFocus ) { + onFocus(); + } + if ( isCaptionSelected ) { + setIsCaptionSelected( false ); + } + } } + > + <View + pointerEvents="box-only" + style={ [ wrapperStyle, wrapperAlignStyle ] } > - <View pointerEvents="box-only"> - <SandBox - html={ html } - title={ iframeTitle } - type={ sandboxClassnames } - providerUrl={ providerUrl } - url={ url } - /> - </View> - </TouchableWithoutFeedback> - </> - ); - + <PreviewContent + html={ html } + title={ iframeTitle } + type={ sandboxClassnames } + providerUrl={ providerUrl } + url={ url } + containerStyle={ sandboxAlignStyle } + /> + </View> + </TouchableWithoutFeedback> + </> + ); return ( <TouchableWithoutFeedback accessible={ ! isSelected } @@ -111,19 +122,16 @@ const EmbedPreview = ( { disabled={ ! isSelected } > <View> - { - // eslint-disable-next-line no-undef - __DEV__ && previewable ? ( - embedWrapper - ) : ( - <EmbedNoPreview - label={ label } - icon={ icon } - isSelected={ isSelected } - onPress={ () => setIsCaptionSelected( false ) } - /> - ) - } + { previewable ? ( + embedWrapper + ) : ( + <EmbedNoPreview + label={ label } + icon={ icon } + isSelected={ isSelected } + onPress={ () => setIsCaptionSelected( false ) } + /> + ) } <BlockCaption accessibilityLabelCreator={ accessibilityLabelCreator } accessible diff --git a/packages/block-library/src/embed/style.scss b/packages/block-library/src/embed/style.scss index f416d1d73c80e..30ab048529ef2 100644 --- a/packages/block-library/src/embed/style.scss +++ b/packages/block-library/src/embed/style.scss @@ -21,6 +21,7 @@ .wp-block-embed { margin: 0 0 1em 0; + overflow-wrap: break-word; // Break long strings of text without spaces so they don't overflow the block. // Supply caption styles to embeds, even if the theme hasn't opted in. // Reason being: the new markup, figcaptions, are not likely to be styled in the majority of existing themes, diff --git a/packages/block-library/src/embed/styles.native.scss b/packages/block-library/src/embed/styles.native.scss index d1fb160a2341a..fade4204a975b 100644 --- a/packages/block-library/src/embed/styles.native.scss +++ b/packages/block-library/src/embed/styles.native.scss @@ -72,6 +72,24 @@ align-items: center; } +.embed-preview__wrapper { + flex: 1; +} + +.embed-preview__wrapper--align-left { + align-items: flex-start; +} +.embed-preview__wrapper--align-right { + align-items: flex-end; +} + +.embed-preview__sandbox--align-left { + max-width: 360px; +} +.embed-preview__sandbox--align-right { + max-width: 360px; +} + .embed-preview__loading--dark { background-color: $background-dark-secondary; } diff --git a/packages/block-library/src/embed/wp-embed-preview.native.js b/packages/block-library/src/embed/wp-embed-preview.native.js new file mode 100644 index 0000000000000..a4b44f322df40 --- /dev/null +++ b/packages/block-library/src/embed/wp-embed-preview.native.js @@ -0,0 +1,80 @@ +/** + * WordPress dependencies + */ +import { memo, useMemo } from '@wordpress/element'; +import { SandBox } from '@wordpress/components'; + +/** + * Checks for WordPress embed events signaling the height change when iframe + * content loads or iframe's window is resized. The event is sent from + * WordPress core via the window.postMessage API. + * + * References: + * window.postMessage: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage + * WordPress core embed-template on load: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L143 + * WordPress core embed-template on resize: https://github.com/WordPress/WordPress/blob/HEAD/wp-includes/js/wp-embed-template.js#L187 + */ +const observeAndResizeJS = ` + ( function() { + if ( ! document.body || ! window.parent ) { + return; + } + + function sendResize( { data: { secret, message, value } = {} } ) { + if ( + [ secret, message, value ].some( + ( attribute ) => ! attribute + ) || + message !== 'height' + ) { + return; + } + + document + .querySelectorAll( 'iframe[data-secret="' + secret + '"' ) + .forEach( ( iframe ) => { + if ( +iframe.height !== value ) { + iframe.height = value; + } + } ); + + // The function postMessage is exposed by the react-native-webview library + // to communicate between React Native and the WebView, in this case, + // we use it for notifying resize changes. + window.ReactNativeWebView.postMessage(JSON.stringify( { + action: 'resize', + height: value, + })); + } + + window.addEventListener( 'message', sendResize ); +} )();`; + +function WpEmbedPreview( { html, ...rest } ) { + const wpEmbedHtml = useMemo( () => { + const doc = new window.DOMParser().parseFromString( html, 'text/html' ); + const iframe = doc.querySelector( 'iframe' ); + + if ( iframe ) { + iframe.removeAttribute( 'style' ); + } + + const blockQuote = doc.querySelector( 'blockquote' ); + + if ( blockQuote ) { + blockQuote.innerHTML = ''; + } + + return doc.body.innerHTML; + }, [ html ] ); + + return ( + <SandBox + customJS={ observeAndResizeJS } + html={ wpEmbedHtml } + { ...rest } + /> + ); +} + +export default memo( WpEmbedPreview ); diff --git a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap index 36a2e8b87915d..440d991192d16 100644 --- a/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/file/test/__snapshots__/edit.native.js.snap @@ -61,6 +61,7 @@ exports[`File block renders file error state without crashing 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} onBackspace={[Function]} @@ -150,6 +151,7 @@ exports[`File block renders file error state without crashing 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} minWidth={40} @@ -259,6 +261,7 @@ exports[`File block renders file without crashing 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} onBackspace={[Function]} @@ -330,6 +333,7 @@ exports[`File block renders file without crashing 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} minWidth={40} diff --git a/packages/block-library/src/file/transforms.js b/packages/block-library/src/file/transforms.js index 30e9808cb2525..3f96667b14745 100644 --- a/packages/block-library/src/file/transforms.js +++ b/packages/block-library/src/file/transforms.js @@ -10,6 +10,7 @@ import { createBlobURL } from '@wordpress/blob'; import { createBlock } from '@wordpress/blocks'; import { select } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import { getFilename } from '@wordpress/url'; const transforms = { from: [ @@ -72,7 +73,8 @@ const transforms = { transform: ( attributes ) => { return createBlock( 'core/file', { href: attributes.url, - fileName: attributes.caption, + fileName: + attributes.caption || getFilename( attributes.url ), textLinkHref: attributes.url, id: attributes.id, anchor: attributes.anchor, diff --git a/packages/block-library/src/gallery/deprecated.scss b/packages/block-library/src/gallery/deprecated.scss index c72fecd994fde..e0503e991e2b3 100644 --- a/packages/block-library/src/gallery/deprecated.scss +++ b/packages/block-library/src/gallery/deprecated.scss @@ -29,27 +29,16 @@ figure { margin: 0; height: 100%; - - // IE doesn't support flex so omit that. - @supports (position: sticky) { - display: flex; - align-items: flex-end; - justify-content: flex-start; - } + display: flex; + align-items: flex-end; + justify-content: flex-start; } img { display: block; max-width: 100%; height: auto; - - // IE doesn't handle cropping, so we need an explicit width here. - width: 100%; - - // IE11 doesn't read rules inside this query. They are applied only to modern browsers. - @supports (position: sticky) { - width: auto; - } + width: auto; } figcaption { @@ -81,16 +70,9 @@ &.is-cropped .blocks-gallery-item { a, img { - // IE11 doesn't support object-fit, so just make sure images aren't skewed. - // The following rules are for all browsers. - width: 100%; - - // IE11 doesn't read rules inside this query. They are applied only to modern browsers. - @supports (position: sticky) { - height: 100%; - flex: 1; - object-fit: cover; - } + height: 100%; + flex: 1; + object-fit: cover; } } diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 3a1fa9c990b95..ec4f274751580 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -24,7 +24,7 @@ import { useBlockProps, } from '@wordpress/block-editor'; import { Platform, useEffect, useMemo } from '@wordpress/element'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { useSelect, useDispatch } from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { View } from '@wordpress/primitives'; @@ -57,7 +57,10 @@ const MAX_COLUMNS = 8; const linkOptions = [ { value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) }, { value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) }, - { value: LINK_DESTINATION_NONE, label: __( 'None' ) }, + { + value: LINK_DESTINATION_NONE, + label: _x( 'None', 'Media item link option' ), + }, ]; const ALLOWED_MEDIA_TYPES = [ 'image' ]; diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss index 32b2b39405cf8..47ebb1bdaa1e8 100644 --- a/packages/block-library/src/gallery/editor.scss +++ b/packages/block-library/src/gallery/editor.scss @@ -16,9 +16,24 @@ figure.wp-block-gallery { flex: 0 0 100%; } - .components-form-file-upload { + > .components-form-file-upload { flex-basis: 100%; } + + .wp-block-image { + .components-notice.is-error { + display: block; + } + .components-notice__content { + margin: 4px 0; + } + .components-notice__dismiss { + position: absolute; + top: 0; + right: 5px; + } + } + // @todo: this deserves a refactor, by being moved to the toolbar. .block-editor-media-placeholder.is-appender { .components-placeholder__label { @@ -30,7 +45,6 @@ figure.wp-block-gallery { } .block-editor-media-placeholder { margin: 0; - height: 100%; &::before { box-shadow: 0 0 0 $border-width $white inset, 0 0 0 3px var(--wp-admin-theme-color) inset; diff --git a/packages/block-library/src/gallery/save.js b/packages/block-library/src/gallery/save.js index bb1fa63a1ba59..77afff7f27984 100644 --- a/packages/block-library/src/gallery/save.js +++ b/packages/block-library/src/gallery/save.js @@ -20,7 +20,7 @@ export default function saveWithInnerBlocks( { attributes } ) { const { caption, columns, imageCrop } = attributes; - const className = classnames( 'blocks-gallery-grid', 'has-nested-images', { + const className = classnames( 'has-nested-images', { [ `columns-${ columns }` ]: columns !== undefined, [ `columns-default` ]: columns === undefined, 'is-cropped': imageCrop, diff --git a/packages/block-library/src/gallery/style.scss b/packages/block-library/src/gallery/style.scss index e1e8300936b60..df7cf0634405e 100644 --- a/packages/block-library/src/gallery/style.scss +++ b/packages/block-library/src/gallery/style.scss @@ -2,7 +2,7 @@ @import "./deprecated.scss"; // Styles for current version of gallery block. -.wp-block-gallery.blocks-gallery-grid.has-nested-images { +.wp-block-gallery.has-nested-images { display: flex; flex-wrap: wrap; // Need bogus :not(#individual-image) to override long :not() @@ -29,33 +29,20 @@ position: relative; margin-top: auto; margin-bottom: auto; - // IE11 doesn't like the "flex-direction: column;" here. - @supports ( position: sticky ) { - flex-direction: column; - } + flex-direction: column; > div, > a { margin: 0; - - // Avoid applying flex styles to IE11. - @supports ( position: sticky ) { - flex-direction: column; - flex-grow: 1; - } + flex-direction: column; + flex-grow: 1; } img { display: block; height: auto; max-width: 100%; - width: 100%; - - // IE doesn't handle cropping, so we need an explicit width here. - // IE11 doesn't read rules inside this query. They are applied only to modern browsers. - @supports ( position: sticky ) { - width: auto; - } + width: auto; } figcaption { @@ -65,12 +52,13 @@ font-size: $default-font-size; left: 0; margin-bottom: 0; - max-height: 100%; + max-height: 60%; overflow: auto; - padding: 40px 10px 9px; + padding: 0 8px 8px; position: absolute; text-align: center; width: 100%; + box-sizing: border-box; img { display: inline; @@ -80,22 +68,15 @@ &.is-style-rounded { > div, > a { - // Not supported in IE11. - @supports ( position: sticky ) { - flex: 1 1 auto; - } + flex: 1 1 auto; } figcaption { + flex: initial; background: none; - // Not supported in IE11. - @supports ( position: sticky ) { - flex: initial; - background: none; - color: inherit; - margin: 0; - padding: 10px 10px 9px; - position: relative; - } + color: inherit; + margin: 0; + padding: 10px 10px 9px; + position: relative; } } } @@ -127,24 +108,15 @@ align-self: inherit; > div:not(.components-drop-zone), > a { - display: block; // Thanks to IE11 not supporting object-fit fall back to display: block. - - // Without IE11 object-fit support "display: flex;" here causes distortion of aspect ratio. - @supports ( position: sticky ) { - display: flex; - } + display: flex; } a, img { width: 100%; - - // IE11 doesn't read rules inside this query. They are applied only to modern browsers. - @supports ( position: sticky ) { - flex: 1 0 0%; - height: 100%; - object-fit: cover; - } + flex: 1 0 0%; + height: 100%; + object-fit: cover; } } diff --git a/packages/block-library/src/gallery/use-get-media.js b/packages/block-library/src/gallery/use-get-media.js index 597b112a8af3d..2d70119e4aa22 100644 --- a/packages/block-library/src/gallery/use-get-media.js +++ b/packages/block-library/src/gallery/use-get-media.js @@ -5,23 +5,26 @@ import { useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +/** + * Retrieves the extended media info for each gallery image from the store. This is used to + * determine which image size options are available for the current gallery. + * + * @param {Array} innerBlockImages An array of the innerBlock images currently in the gallery. + * + * @return {Array} An array of media info options for each gallery image. + */ export default function useGetMedia( innerBlockImages ) { const [ currentImageMedia, setCurrentImageMedia ] = useState( [] ); const imageMedia = useSelect( ( select ) => { - if ( - ! innerBlockImages?.length || - innerBlockImages.some( - ( imageBlock ) => ! imageBlock.attributes.id - ) - ) { + if ( ! innerBlockImages?.length ) { return currentImageMedia; } - const imageIds = innerBlockImages.map( - ( imageBlock ) => imageBlock.attributes.id - ); + const imageIds = innerBlockImages + .map( ( imageBlock ) => imageBlock.attributes.id ) + .filter( ( id ) => id !== undefined ); if ( imageIds.length === 0 ) { return currentImageMedia; diff --git a/packages/block-library/src/gallery/use-get-new-images.js b/packages/block-library/src/gallery/use-get-new-images.js index 67578a5cb9941..056cf1d2ff6ba 100644 --- a/packages/block-library/src/gallery/use-get-new-images.js +++ b/packages/block-library/src/gallery/use-get-new-images.js @@ -3,6 +3,16 @@ */ import { useMemo, useState } from '@wordpress/element'; +/** + * Keeps track of images already in the gallery to allow new innerBlocks to be identified. This + * is required so default gallery attributes can be applied without overwriting any custom + * attributes applied to existing images. + * + * @param {Array} images Basic image block data taken from current gallery innerBlock + * @param {Array} imageData The related image data for each of the current gallery images. + * + * @return {Array} An array of any new images that have been added to the gallery. + */ export default function useGetNewImages( images, imageData ) { const [ currentImages, setCurrentImages ] = useState( [] ); diff --git a/packages/block-library/src/gallery/use-image-sizes.js b/packages/block-library/src/gallery/use-image-sizes.js index 5b406adab4cc9..877bacb67dd13 100644 --- a/packages/block-library/src/gallery/use-image-sizes.js +++ b/packages/block-library/src/gallery/use-image-sizes.js @@ -8,6 +8,16 @@ import { get, some } from 'lodash'; */ import { useMemo } from '@wordpress/element'; +/** + * Calculates the image sizes that are avaible for the current gallery images in order to + * populate the 'Image size' selector. + * + * @param {Array} images Basic image block data taken from current gallery innerBlock + * @param {boolean} isSelected Is the block currently selected in the editor. + * @param {Function} getSettings Block editor store selector. + * + * @return {Array} An array of image size options. + */ export default function useImageSizes( images, isSelected, getSettings ) { return useMemo( () => getImageSizing(), [ images, isSelected ] ); diff --git a/packages/block-library/src/gallery/use-short-code-transform.js b/packages/block-library/src/gallery/use-short-code-transform.js index 3843df9d5a642..bc25111910408 100644 --- a/packages/block-library/src/gallery/use-short-code-transform.js +++ b/packages/block-library/src/gallery/use-short-code-transform.js @@ -9,6 +9,14 @@ import { every } from 'lodash'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +/** + * Shortcode transforms don't currently have a tranform method and so can't use a selector to + * retrieve the data for each image being transformer, so this selector handle this post transformation. + * + * @param {Array} shortCodeTransforms An array of image data passed from the shortcode transform. + * + * @return {Array} An array of extended image data objects for each of the shortcode transform images. + */ export default function useShortCodeTransform( shortCodeTransforms ) { const newImageData = useSelect( ( select ) => { diff --git a/packages/block-library/src/gallery/v1/tiles.native.js b/packages/block-library/src/gallery/v1/tiles.native.js index c5ce52b4ba24b..30126ce872593 100644 --- a/packages/block-library/src/gallery/v1/tiles.native.js +++ b/packages/block-library/src/gallery/v1/tiles.native.js @@ -23,7 +23,8 @@ function Tiles( props ) { const lastRow = Math.floor( lastTile / columns ); const wrappedChildren = Children.map( children, ( child, index ) => { - /** Since we don't have `calc()`, we must calculate our spacings here in + /** + * Since we don't have `calc()`, we must calculate our spacings here in * order to preserve even spacing between tiles and equal width for tiles * in a given row. * diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 5628ea0105ce8..f30a7482bc3d8 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -5,7 +5,7 @@ import { useSelect } from '@wordpress/data'; import { InnerBlocks, useBlockProps, - InspectorAdvancedControls, + InspectorControls, __experimentalUseInnerBlocksProps as useInnerBlocksProps, useSetting, store as blockEditorStore, @@ -28,10 +28,12 @@ function GroupEdit( { attributes, setAttributes, clientId } ) { const defaultLayout = useSetting( 'layout' ) || {}; const { tagName: TagName = 'div', templateLock, layout = {} } = attributes; const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const { type = 'default' } = usedLayout; + const layoutSupportEnabled = themeSupportsLayout || type !== 'default'; const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( - themeSupportsLayout + layoutSupportEnabled ? blockProps : { className: 'wp-block-group__inner-container' }, { @@ -39,13 +41,13 @@ function GroupEdit( { attributes, setAttributes, clientId } ) { renderAppender: hasInnerBlocks ? undefined : InnerBlocks.ButtonBlockAppender, - __experimentalLayout: themeSupportsLayout ? usedLayout : undefined, + __experimentalLayout: layoutSupportEnabled ? usedLayout : undefined, } ); return ( <> - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> <SelectControl label={ __( 'HTML element' ) } options={ [ @@ -62,11 +64,11 @@ function GroupEdit( { attributes, setAttributes, clientId } ) { setAttributes( { tagName: value } ) } /> - </InspectorAdvancedControls> - { themeSupportsLayout && <TagName { ...innerBlocksProps } /> } + </InspectorControls> + { layoutSupportEnabled && <TagName { ...innerBlocksProps } /> } { /* Ideally this is not needed but it's there for backward compatibility reason to keep this div for themes that might rely on its presence */ } - { ! themeSupportsLayout && ( + { ! layoutSupportEnabled && ( <TagName { ...blockProps }> <div { ...innerBlocksProps } /> </TagName> diff --git a/packages/block-library/src/group/index.js b/packages/block-library/src/group/index.js index dac9a09b3cb1f..e3b9d887d95f8 100644 --- a/packages/block-library/src/group/index.js +++ b/packages/block-library/src/group/index.js @@ -12,6 +12,7 @@ import deprecated from './deprecated'; import edit from './edit'; import metadata from './block.json'; import save from './save'; +import variations from './variations'; const { name } = metadata; @@ -135,4 +136,5 @@ export const settings = { edit, save, deprecated, + variations, }; diff --git a/packages/block-library/src/group/variations.js b/packages/block-library/src/group/variations.js new file mode 100644 index 0000000000000..76de042661f38 --- /dev/null +++ b/packages/block-library/src/group/variations.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const variations = [ + { + name: 'group-row', + title: __( 'Row' ), + description: __( 'Blocks shown in a row.' ), + attributes: { layout: { type: 'flex' } }, + scope: [ 'inserter' ], + isActive: ( blockAttributes ) => + blockAttributes.layout?.type === 'flex', + }, +]; + +export default variations; diff --git a/packages/block-library/src/heading/editor.scss b/packages/block-library/src/heading/editor.scss deleted file mode 100644 index a1af2ba370244..0000000000000 --- a/packages/block-library/src/heading/editor.scss +++ /dev/null @@ -1,20 +0,0 @@ -// Remove padding in heading level control popover since the toolbar buttons already have padding. -.block-library-heading-level-dropdown .components-popover__content { - // TODO: Find a less hardcoded way of doing this. `max-content` works on - // Chromium, but it results in a scrollbar on Safari, and isn't supported - // at all in IE11. - min-width: 230px; - - > div { - padding: 0; - } -} - -// The dropdown already has a border, so we can remove the one on the heading -// level toolbar. -.block-library-heading-level-toolbar { - border: none; - .components-toolbar-group { - flex-wrap: nowrap; - } -} diff --git a/packages/block-library/src/heading/heading-level-dropdown.js b/packages/block-library/src/heading/heading-level-dropdown.js index 84d00181b7129..592787b5fa9dd 100644 --- a/packages/block-library/src/heading/heading-level-dropdown.js +++ b/packages/block-library/src/heading/heading-level-dropdown.js @@ -1,14 +1,8 @@ /** * WordPress dependencies */ -import { - Dropdown, - Toolbar, - ToolbarButton, - ToolbarGroup, -} from '@wordpress/components'; +import { ToolbarDropdownMenu } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { DOWN } from '@wordpress/keycodes'; /** * Internal dependencies @@ -19,7 +13,6 @@ const HEADING_LEVELS = [ 1, 2, 3, 4, 5, 6 ]; const POPOVER_PROPS = { className: 'block-library-heading-level-dropdown', - isAlternate: true, }; /** @typedef {import('@wordpress/element').WPComponent} WPComponent */ @@ -43,58 +36,33 @@ const POPOVER_PROPS = { */ export default function HeadingLevelDropdown( { selectedLevel, onChange } ) { return ( - <Dropdown + <ToolbarDropdownMenu popoverProps={ POPOVER_PROPS } - renderToggle={ ( { onToggle, isOpen } ) => { - const openOnArrowDown = ( event ) => { - if ( ! isOpen && event.keyCode === DOWN ) { - event.preventDefault(); - onToggle(); - } - }; + icon={ <HeadingLevelIcon level={ selectedLevel } /> } + label={ __( 'Change heading level' ) } + controls={ HEADING_LEVELS.map( ( targetLevel ) => { + { + const isActive = targetLevel === selectedLevel; - return ( - <ToolbarButton - aria-expanded={ isOpen } - aria-haspopup="true" - icon={ <HeadingLevelIcon level={ selectedLevel } /> } - label={ __( 'Change heading level' ) } - onClick={ onToggle } - onKeyDown={ openOnArrowDown } - showTooltip - /> - ); - } } - renderContent={ () => ( - <Toolbar - className="block-library-heading-level-toolbar" - label={ __( 'Change heading level' ) } - > - <ToolbarGroup - isCollapsed={ false } - controls={ HEADING_LEVELS.map( ( targetLevel ) => { - const isActive = targetLevel === selectedLevel; - return { - icon: ( - <HeadingLevelIcon - level={ targetLevel } - isPressed={ isActive } - /> - ), - title: sprintf( - // translators: %s: heading level e.g: "1", "2", "3" - __( 'Heading %d' ), - targetLevel - ), - isActive, - onClick() { - onChange( targetLevel ); - }, - }; - } ) } - /> - </Toolbar> - ) } + return { + icon: ( + <HeadingLevelIcon + level={ targetLevel } + isPressed={ isActive } + /> + ), + label: sprintf( + // translators: %s: heading level e.g: "1", "2", "3" + __( 'Heading %d' ), + targetLevel + ), + isActive, + onClick() { + onChange( targetLevel ); + }, + }; + } + } ) } /> ); } diff --git a/packages/block-library/src/heading/style.scss b/packages/block-library/src/heading/style.scss index 31a6989c39c81..e07cdb2f11193 100644 --- a/packages/block-library/src/heading/style.scss +++ b/packages/block-library/src/heading/style.scss @@ -4,6 +4,9 @@ h3, h4, h5, h6 { + // Break long strings of text without spaces so they don't overflow the block. + overflow-wrap: break-word; + &.has-background { padding: $block-bg-padding--v $block-bg-padding--h; } diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 3a02291ea0c95..7f29a8d1078c5 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -246,7 +246,7 @@ export function ImageEdit( { } ); } - const isTemp = isTemporaryImage( id, url ); + let isTemp = isTemporaryImage( id, url ); // Upload a temporary image on mount. useEffect( () => { @@ -264,6 +264,7 @@ export function ImageEdit( { }, allowedTypes: ALLOWED_MEDIA_TYPES, onError: ( message ) => { + isTemp = false; noticeOperations.createErrorNotice( message ); setAttributes( { src: undefined, diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 54174c3607a74..1233abbcdca60 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -42,7 +42,7 @@ import { BlockStyles, store as blockEditorStore, } from '@wordpress/block-editor'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _x, sprintf } from '@wordpress/i18n'; import { getProtocol, hasQueryArg } from '@wordpress/url'; import { doAction, hasAction } from '@wordpress/hooks'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; @@ -121,7 +121,10 @@ export class ImageEdit extends Component { }, linkRel: { label: __( 'Link Rel' ), - placeholder: __( 'None' ), + placeholder: _x( + 'None', + 'Link rel attribute value placeholder' + ), }, }; } diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 54b552186e7ff..dedd8786534be 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { get, filter, map, last, pick, includes } from 'lodash'; +import { get, filter, map, pick, includes } from 'lodash'; /** * WordPress dependencies @@ -21,7 +21,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { BlockControls, InspectorControls, - InspectorAdvancedControls, RichText, __experimentalImageSizeControl as ImageSizeControl, __experimentalImageURLInputUI as ImageURLInputUI, @@ -31,7 +30,7 @@ import { } from '@wordpress/block-editor'; import { useEffect, useState, useRef } from '@wordpress/element'; import { __, sprintf, isRTL } from '@wordpress/i18n'; -import { getPath } from '@wordpress/url'; +import { getFilename } from '@wordpress/url'; import { createBlock, switchToBlockType } from '@wordpress/blocks'; import { crop, overlayText, upload } from '@wordpress/icons'; import { store as noticesStore } from '@wordpress/notices'; @@ -50,13 +49,6 @@ import { isExternalImage } from './edit'; */ import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants'; -function getFilename( url ) { - const path = getPath( url ); - if ( path ) { - return last( path.split( '/' ) ); - } -} - export default function Image( { temporaryURL, attributes: { @@ -371,7 +363,7 @@ export default function Image( { /> </PanelBody> </InspectorControls> - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> <TextControl label={ __( 'Title attribute' ) } value={ title || '' } @@ -389,7 +381,7 @@ export default function Image( { </> } /> - </InspectorAdvancedControls> + </InspectorControls> </> ); diff --git a/packages/block-library/src/list/style.scss b/packages/block-library/src/list/style.scss index 04f579042196f..2bc29a349e4f4 100644 --- a/packages/block-library/src/list/style.scss +++ b/packages/block-library/src/list/style.scss @@ -1,4 +1,9 @@ -ol.has-background, -ul.has-background { - padding: $block-bg-padding--v $block-bg-padding--h; +ol, +ul { + // Break long strings of text without spaces so they don't overflow the block. + overflow-wrap: break-word; + + &.has-background { + padding: $block-bg-padding--v $block-bg-padding--h; + } } diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 9df12f2681bc5..fce0a339b2e25 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -10,7 +10,6 @@ import { escape } from 'lodash'; import { createBlock } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; import { - KeyboardShortcuts, PanelBody, Popover, TextControl, @@ -18,7 +17,7 @@ import { ToolbarButton, ToolbarGroup, } from '@wordpress/components'; -import { rawShortcut, displayShortcut } from '@wordpress/keycodes'; +import { displayShortcut, isKeyboardEvent, ENTER } from '@wordpress/keycodes'; import { __, sprintf } from '@wordpress/i18n'; import { BlockControls, @@ -292,7 +291,10 @@ export default function NavigationLinkEdit( { }; const { showSubmenuIcon } = context; const { saveEntityRecord } = useDispatch( coreStore ); - const { insertBlock } = useDispatch( blockEditorStore ); + const { + insertBlock, + __unstableMarkNextChangeAsNotPersistent, + } = useDispatch( blockEditorStore ); const [ isLinkOpen, setIsLinkOpen ] = useState( false ); const listItemRef = useRef( null ); const isDraggingWithin = useIsDraggingWithin( listItemRef ); @@ -355,8 +357,14 @@ export default function NavigationLinkEdit( { [ clientId ] ); - // Store the colors from context as attributes for rendering - useEffect( () => setAttributes( { isTopLevelLink } ), [ isTopLevelLink ] ); + useEffect( () => { + // This side-effect should not create an undo level as those should + // only be created via user interactions. Mark this change as + // not persistent to avoid undo level creation. + // See https://github.com/WordPress/gutenberg/issues/34564. + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { isTopLevelLink } ); + }, [ isTopLevelLink ] ); /** * Insert a link block when submenu is added. @@ -467,6 +475,15 @@ export default function NavigationLinkEdit( { customBackgroundColor, } = getColors( context, ! isTopLevelLink ); + function onKeyDown( event ) { + if ( + isKeyboardEvent.primary( event, 'k' ) || + ( ! url && event.keyCode === ENTER ) + ) { + setIsLinkOpen( true ); + } + } + const blockProps = useBlockProps( { ref: listItemRef, className: classnames( 'wp-block-navigation-item', { @@ -486,6 +503,7 @@ export default function NavigationLinkEdit( { color: ! textColor && customTextColor, backgroundColor: ! backgroundColor && customBackgroundColor, }, + onKeyDown, } ); if ( ! url ) { @@ -566,13 +584,6 @@ export default function NavigationLinkEdit( { <Fragment> <BlockControls> <ToolbarGroup> - <KeyboardShortcuts - bindGlobal - shortcuts={ { - [ rawShortcut.primary( 'k' ) ]: () => - setIsLinkOpen( true ), - } } - /> <ToolbarButton name="link" icon={ linkIcon } @@ -626,12 +637,6 @@ export default function NavigationLinkEdit( { { /* eslint-enable */ } { ! url ? ( <div className="wp-block-navigation-link__placeholder-text"> - <KeyboardShortcuts - shortcuts={ { - enter: () => - isSelected && setIsLinkOpen( true ), - } } - /> { missingText } </div> ) : ( @@ -672,12 +677,6 @@ export default function NavigationLinkEdit( { onClose={ () => setIsLinkOpen( false ) } anchorRef={ listItemRef.current } > - <KeyboardShortcuts - bindGlobal - shortcuts={ { - escape: () => setIsLinkOpen( false ), - } } - /> <LinkControl className="wp-block-navigation-link__inline-link-input" value={ link } diff --git a/packages/block-library/src/navigation-link/editor.scss b/packages/block-library/src/navigation-link/editor.scss index a720088d055dd..48d71e40d7150 100644 --- a/packages/block-library/src/navigation-link/editor.scss +++ b/packages/block-library/src/navigation-link/editor.scss @@ -1,5 +1,5 @@ // Normalize Link and edit containers, to look mostly the same. -.wp-block-navigation-link__container { +.wp-block-navigation__submenu-container { border-radius: 0; // Make it the same height as the appender to prevent a jump. @@ -15,25 +15,26 @@ .wp-block-navigation .has-child { cursor: pointer; - .submenu-container, - .wp-block-navigation-link__container { - z-index: z-index(".has-child .wp-block-navigation-link__container"); + .wp-block-navigation__submenu-container { + z-index: z-index(".has-child .wp-block-navigation__submenu-container"); } &:hover { - .submenu-container, - .wp-block-navigation-link__container { - z-index: z-index(".has-child:hover .wp-block-navigation-link__container"); + .wp-block-navigation__submenu-container { + z-index: z-index(".has-child:hover .wp-block-navigation__submenu-container"); } } // Show on editor selected, even if on frontend it only stays open on focus-within. &.is-selected, &.has-child-selected { - > .wp-block-navigation-link__container { + > .wp-block-navigation__submenu-container { // We use important here because if the parent block is selected and submenus are present, they should always be visible. visibility: visible !important; opacity: 1 !important; + min-width: 200px !important; + height: auto !important; + width: auto !important; } } } @@ -43,12 +44,12 @@ * Navigation Items. */ -.wp-block-navigation-link { - .wp-block-navigation-link__container { +.wp-block-navigation-item { + .wp-block-navigation__submenu-container { display: block; } - .wp-block-navigation-link__content { + .wp-block-navigation-item__content { cursor: text; } @@ -96,7 +97,7 @@ } // This needs extra specificity. - &.wp-block-navigation-link__content { + &.wp-block-navigation-item__content { cursor: pointer; } } diff --git a/packages/block-library/src/navigation/block-navigation-list.js b/packages/block-library/src/navigation/block-navigation-list.js index 2aee258574605..814a56b20f4fd 100644 --- a/packages/block-library/src/navigation/block-navigation-list.js +++ b/packages/block-library/src/navigation/block-navigation-list.js @@ -6,6 +6,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; +import { useRef, useEffect, useState } from '@wordpress/element'; export default function BlockNavigationList( { clientId, @@ -17,13 +18,21 @@ export default function BlockNavigationList( { [ clientId ] ); + const listViewRef = useRef(); + const [ minHeight, setMinHeight ] = useState( 300 ); + useEffect( () => { + setMinHeight( listViewRef?.current?.clientHeight ?? 300 ); + }, [] ); + return ( - <ListView - blocks={ blocks } - showAppender - showBlockMovers - showNestedBlocks - __experimentalFeatures={ __experimentalFeatures } - /> + <div style={ { minHeight } }> + <ListView + ref={ listViewRef } + blocks={ blocks } + showBlockMovers + showNestedBlocks + __experimentalFeatures={ __experimentalFeatures } + /> + </div> ); } diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 79da72eb86a2f..98663a8075b23 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -8,7 +8,8 @@ "textdomain": "default", "attributes": { "orientation": { - "type": "string" + "type": "string", + "default": "horizontal" }, "textColor": { "type": "string" diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js index 0f55e958e5b97..bdab02b4b7c80 100644 --- a/packages/block-library/src/navigation/edit.js +++ b/packages/block-library/src/navigation/edit.js @@ -46,6 +46,8 @@ const ALLOWED_BLOCKS = [ 'core/page-list', 'core/spacer', 'core/home-link', + 'core/site-title', + 'core/site-logo', ]; const LAYOUT = { @@ -101,6 +103,7 @@ function Navigation( { hasSubmenuIndicatorSetting = true, hasItemJustificationControls = true, hasColorSettings = true, + customPlaceholder: CustomPlaceholder = null, } ) { const [ isPlaceholderShown, setIsPlaceholderShown ] = useState( ! hasExistingNavItems @@ -148,20 +151,24 @@ function Navigation( { }, { allowedBlocks: ALLOWED_BLOCKS, - orientation: attributes.orientation || 'horizontal', + orientation: attributes.orientation, renderAppender: ( isImmediateParentOfSelectedBlock && ! selectedBlockHasDescendants ) || isSelected ? InnerBlocks.DefaultAppender : false, - __experimentalCaptureToolbars: true, + // Ensure block toolbar is not too far removed from item + // being edited when in vertical mode. + // see: https://github.com/WordPress/gutenberg/pull/34615. + __experimentalCaptureToolbars: + attributes.orientation !== 'vertical', // Template lock set to false here so that the Nav // Block on the experimental menus screen does not // inherit templateLock={ 'all' }. templateLock: false, __experimentalLayout: LAYOUT, - placeholder, + placeholder: ! CustomPlaceholder ? placeholder : undefined, } ); @@ -198,9 +205,13 @@ function Navigation( { } ); if ( isPlaceholderShown ) { + const PlaceholderComponent = CustomPlaceholder + ? CustomPlaceholder + : NavigationPlaceholder; + return ( <div { ...blockProps }> - <NavigationPlaceholder + <PlaceholderComponent onCreate={ ( blocks, selectNavigationBlock ) => { setIsPlaceholderShown( false ); updateInnerBlocks( blocks ); diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index fff28e42151ab..ba4212cf67777 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -11,6 +11,13 @@ margin-left: 0; padding-left: 0; } + + // Revert any left/right margins. + // This also makes it work with classic theme auto margins. + .wp-block-navigation-item.wp-block { + margin-left: revert; + margin-right: revert; + } } // This element is inline on the frontend but needs to be editable, therefore inline-block, in the editor. @@ -31,13 +38,13 @@ // Low specificity default to ensure background color applies to submenus. .wp-block-navigation__container, -.wp-block-navigation-link { +.wp-block-navigation-item { background-color: inherit; } // Only show the flyout on hover if the parent menu item is selected. .wp-block-navigation:not(.is-selected):not(.has-child-selected) .has-child:hover { - > .wp-block-navigation-link__container { + > .wp-block-navigation__submenu-container { opacity: 0; visibility: hidden; } @@ -47,7 +54,7 @@ .has-child { &.is-selected, &.has-child-selected { - > .wp-block-navigation-link__container { + > .wp-block-navigation__submenu-container { display: flex; opacity: 1; visibility: visible; @@ -58,7 +65,7 @@ // Show a submenu when generally dragging (is-dragging-components-draggable) if that // submenu has children (has-child) and is being dragged within (is-dragging-within). .is-dragging-components-draggable .has-child.is-dragging-within { - > .wp-block-navigation-link__container { + > .wp-block-navigation__submenu-container { opacity: 1; visibility: visible; } @@ -215,7 +222,7 @@ $color-control-label-height: 20px; // Style skeleton elements to mostly match the metrics of actual menu items. // Needs specificity. - .wp-block-navigation-link.wp-block-navigation-link { + .wp-block-navigation-item.wp-block-navigation-item { position: relative; min-width: 72px; @@ -237,7 +244,7 @@ $color-control-label-height: 20px; } - .wp-block-navigation-link.wp-block-navigation-link, + .wp-block-navigation-item.wp-block-navigation-item, .wp-block-navigation-placeholder__preview-search-icon { opacity: 0.3; } @@ -250,6 +257,7 @@ $color-control-label-height: 20px; width: 0; overflow: hidden; flex-wrap: nowrap; + flex: 0; } // Hide entirely when vertical. diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index df93471feecbc..68246111ffe0e 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -256,15 +256,19 @@ function render_block_core_navigation( $attributes, $content, $block ) { $inner_blocks_html = ''; $is_list_open = false; foreach ( $inner_blocks as $inner_block ) { - if ( ( 'core/navigation-link' === $inner_block->name || 'core/home-link' === $inner_block->name ) && ! $is_list_open ) { + if ( ( 'core/navigation-link' === $inner_block->name || 'core/home-link' === $inner_block->name || 'core/site-title' === $inner_block->name || 'core/site-logo' === $inner_block->name ) && ! $is_list_open ) { $is_list_open = true; $inner_blocks_html .= '<ul class="wp-block-navigation__container">'; } - if ( 'core/navigation-link' !== $inner_block->name && 'core/home-link' !== $inner_block->name && $is_list_open ) { + if ( 'core/navigation-link' !== $inner_block->name && 'core/home-link' !== $inner_block->name && 'core/site-title' !== $inner_block->name && 'core/site-logo' !== $inner_block->name && $is_list_open ) { $is_list_open = false; $inner_blocks_html .= '</ul>'; } - $inner_blocks_html .= $inner_block->render(); + if ( 'core/site-title' === $inner_block->name || 'core/site-logo' === $inner_block->name ) { + $inner_blocks_html .= '<li class="wp-block-navigation-item">' . $inner_block->render() . '</li>'; + } else { + $inner_blocks_html .= $inner_block->render(); + } } if ( $is_list_open ) { diff --git a/packages/block-library/src/navigation/placeholder-preview.js b/packages/block-library/src/navigation/placeholder-preview.js index fff5f80b39f06..a8c1ae6bdd451 100644 --- a/packages/block-library/src/navigation/placeholder-preview.js +++ b/packages/block-library/src/navigation/placeholder-preview.js @@ -6,9 +6,9 @@ import { Icon, search } from '@wordpress/icons'; const PlaceholderPreview = () => { return ( <ul className="wp-block-navigation-placeholder__preview wp-block-navigation__container"> - <li className="wp-block-navigation-link">​</li> - <li className="wp-block-navigation-link">​</li> - <li className="wp-block-navigation-link">​</li> + <li className="wp-block-navigation-item">​</li> + <li className="wp-block-navigation-item">​</li> + <li className="wp-block-navigation-item">​</li> <li className="wp-block-navigation-placeholder__preview-search-icon"> <Icon icon={ search } /> </li> diff --git a/packages/block-library/src/navigation/placeholder.js b/packages/block-library/src/navigation/placeholder.js index a9e0e589910ce..ce1c14533894f 100644 --- a/packages/block-library/src/navigation/placeholder.js +++ b/packages/block-library/src/navigation/placeholder.js @@ -48,7 +48,7 @@ function NavigationPlaceholder( { onCreate }, ref ) { const { innerBlocks: blocks } = menuItemsToBlocks( menuItems ); const selectNavigationBlock = true; onCreate( blocks, selectNavigationBlock ); - } ); + }, [ menuItems, menuItemsToBlocks, onCreate ] ); const onCreateFromMenu = () => { // If we have menu items, create the block right away. diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 2d3671c847a6c..379ac1ee403e7 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -1,8 +1,11 @@ // Navigation block and menu item styles. -// This also styles navigation links inside the Page List block, -// as that block is meant to behave as menu items when leveraged. -// The CSS lives here so that it is output even if you only use a -// Page List block inside your navigation block. +// These styles also affect the Page List block when used inside your navigation block. +// +// Classes: +// - .wp-block-navigation__submenu-container targets submenu containers. +// - .wp-block-navigation-item targets the menu item itself. +// - .wp-block-navigation-item__content targets the link inside a menu item. +// - .wp-block-navigation__submenu-icon targets the chevron icon indicating submenus. .wp-block-navigation { position: relative; @@ -27,13 +30,12 @@ } // Menu item container. - .wp-block-pages-list__item, - .wp-block-navigation-link { + .wp-block-navigation-item { display: flex; align-items: center; position: relative; - .wp-block-navigation-link__container:empty { + .wp-block-navigation__submenu-container:empty { display: none; } } @@ -50,9 +52,8 @@ // Force links to inherit text decoration applied to navigation block. // The extra selector adds specificity to ensure it works when user-set. &[style*="text-decoration"] { - .wp-block-pages-list__item, - .wp-block-navigation-link__container, - .wp-block-navigation-link { + .wp-block-navigation-item, + .wp-block-navigation__submenu-container { text-decoration: inherit; } @@ -78,12 +79,14 @@ } // Submenu indicator. - .wp-block-page-list__submenu-icon, - .wp-block-navigation-link__submenu-icon { + .wp-block-navigation__submenu-icon { + align-self: center; height: inherit; + line-height: 0; margin-left: 6px; svg { + display: inline-block; stroke: currentColor; } } @@ -96,7 +99,7 @@ // We use :where to keep specificity minimal, yet still scope it to only the navigation block. // That way if padding is set in theme.json, it still wins. // https://css-tricks.com/almanac/selectors/w/where/ - :where(.submenu-container, .wp-block-navigation-link__container) { + :where(.wp-block-navigation__submenu-container) { background-color: inherit; color: inherit; position: absolute; @@ -104,23 +107,24 @@ display: flex; flex-direction: column; align-items: normal; - min-width: 200px; // Hide until hover or focus within. opacity: 0; transition: opacity 0.1s linear; visibility: hidden; + // Don't take up space when the menu is collapsed. + width: 0; + height: 0; + // Submenu items. - > .wp-block-pages-list__item, - > .wp-block-navigation-link { + > .wp-block-navigation-item { > a { display: flex; flex-grow: 1; // Right-align the chevron in submenus. - .wp-block-page-list__submenu-icon, - .wp-block-navigation-link__submenu-icon { + .wp-block-navigation__submenu-icon { margin-right: 0; margin-left: auto; } @@ -140,8 +144,7 @@ // Nested submenus sit to the left on large breakpoints. // On smaller breakpoints, they open vertically, accordion-style. @include break-medium { - .submenu-container, - .wp-block-navigation-link__container { + .wp-block-navigation__submenu-container { left: 100%; top: -1px; // Border width. @@ -158,8 +161,7 @@ } // Reset the submenu indicator for horizontal flyouts. - .wp-block-page-list__submenu-icon svg, - .wp-block-navigation-link__submenu-icon svg { + .wp-block-navigation__submenu-icon svg { transform: rotate(-90deg); } } @@ -171,47 +173,32 @@ // Custom menu items. // Show submenus on hover. - &:hover > .wp-block-navigation-link__container { + &:hover > .wp-block-navigation__submenu-container { visibility: visible; opacity: 1; + width: auto; + height: auto; + min-width: 200px; } // Keep submenus open when focus is within. - &:focus-within > .wp-block-navigation-link__container { + &:focus-within > .wp-block-navigation__submenu-container { visibility: visible; opacity: 1; - } - - // Page list menu items. - &:hover { - cursor: pointer; - - > .submenu-container { - visibility: visible; - opacity: 1; - } - } - - &:focus-within { - cursor: pointer; - - > .submenu-container { - visibility: visible; - opacity: 1; - } + width: auto; + height: auto; + min-width: 200px; } } // Submenu indentation when there's a background. -.wp-block-navigation.has-background .has-child .submenu-container, -.wp-block-navigation.has-background .has-child .wp-block-navigation-link__container { +.wp-block-navigation.has-background .has-child .wp-block-navigation__submenu-container { left: 0; top: 100%; // There's no border on submenus when there are backgrounds. @include break-medium { - .submenu-container, - .wp-block-navigation-link__container { + .wp-block-navigation__submenu-container { left: 100%; top: 0; } @@ -221,19 +208,12 @@ /** * Margins - * @todo: refactor this to use gap. */ // Menu items with no background. -.wp-block-page-list, -.wp-block-page-list > .wp-block-pages-list__item, -.wp-block-navigation__container > .wp-block-navigation-link { - margin: 0 2em 0 0; - - // Margin of right-most margin should be zero, for right aligned or justified items. - &:last-child { - margin-right: 0; - } +.wp-block-navigation .wp-block-page-list, +.wp-block-navigation__container { + gap: 0.5em 2em; } // Menu items with background. @@ -241,15 +221,9 @@ // That way if padding is set in theme.json, it still wins. // https://css-tricks.com/almanac/selectors/w/where/ .wp-block-navigation:where(.has-background) { - .wp-block-page-list, - .wp-block-page-list > .wp-block-pages-list__item, - .wp-block-navigation__container > .wp-block-navigation-link { - margin: 0 0.5em 0 0; - - // Don't show right margin on the last child. - &:last-child { - margin: 0; - } + .wp-block-navigation .wp-block-page-list, + .wp-block-navigation__container { + gap: 0 0.5em; } } @@ -274,7 +248,7 @@ } // Provide a default padding for submenus who should always have some, regardless of the top level menu items. -.wp-block-navigation :where(.submenu-container, .wp-block-navigation-link__container) a { +.wp-block-navigation :where(.wp-block-navigation__submenu-container) a { padding: 0.5em 1em; } @@ -292,14 +266,12 @@ .wp-block-navigation.items-justified-right > .wp-block-navigation__container .has-child { // First submenu. - .submenu-container, - .wp-block-navigation-link__container { + .wp-block-navigation__submenu-container { left: auto; right: 0; // Nested submenus. - .submenu-container, - .wp-block-navigation-link__container { + .wp-block-navigation__submenu-container { left: auto; right: 100%; } @@ -308,8 +280,7 @@ // Default background and font color. .wp-block-navigation:not(.has-background) { - .submenu-container, // This target items created by the Page List block. - .wp-block-navigation__container .wp-block-navigation-link__container { + .wp-block-navigation__submenu-container { // Set a background color for submenus so that they're not transparent. // NOTE TO DEVS - if refactoring this code, please double-check that // submenus have a default background color, this feature has regressed @@ -338,13 +309,15 @@ // Horizontal layout display: flex; flex-wrap: wrap; + flex: 1; +} - // Vertical layout - .is-vertical & { - display: block; - flex-direction: column; - align-items: flex-start; - } +// Vertical layout +.is-vertical .wp-block-page-list, // Page list. +.is-vertical .wp-block-navigation__container { + display: block; + flex-direction: column; + align-items: flex-start; } // Justification. @@ -359,6 +332,7 @@ .items-justified-space-between .wp-block-navigation__container { justify-content: space-between; + flex: 1; } // Vertical justification. @@ -369,9 +343,7 @@ .is-vertical.items-justified-right > ul { align-items: flex-end; - .wp-block-navigation-link, - .wp-block-pages-list__item { - margin-right: 0; + .wp-block-navigation-item { justify-content: flex-end; } } @@ -427,10 +399,8 @@ // Remove background colors for items inside the overlay menu. // Has to be !important to override global styles. // @todo: We should revisit this so that the overlay colors can be applied, instead of the background. - .wp-block-pages-list__item .submenu-container, - .wp-block-navigation-link .wp-block-navigation-link__container, - .wp-block-pages-list__item, - .wp-block-navigation-link { + .wp-block-navigation-item .wp-block-navigation__submenu-container, + .wp-block-navigation-item { color: inherit !important; background: transparent !important; } @@ -441,6 +411,7 @@ display: flex; flex-direction: row; position: relative; + z-index: 2; background-color: inherit; .wp-block-navigation__responsive-container-close { @@ -450,8 +421,7 @@ &.is-menu-open { // Override breakpoint-inherited submenu rules. - .submenu-container.submenu-container.submenu-container.submenu-container, - .wp-block-navigation-link__container.wp-block-navigation-link__container.wp-block-navigation-link__container.wp-block-navigation-link__container { + .wp-block-navigation__submenu-container.wp-block-navigation__submenu-container.wp-block-navigation__submenu-container.wp-block-navigation__submenu-container { left: 0; } } @@ -531,23 +501,24 @@ // Always show submenus fully expanded inside the modal menu. .wp-block-navigation .wp-block-navigation__responsive-container.is-menu-open { - .wp-block-page-list__submenu-icon, - .wp-block-navigation-link__submenu-icon { + .wp-block-navigation__submenu-icon { display: none; } .has-child .submenu-container, - .has-child .wp-block-navigation-link__container { + .has-child .wp-block-navigation__submenu-container { position: relative; opacity: 1; visibility: visible; + height: auto; + width: auto; padding: 0 0 0 32px; border: none; } - .wp-block-navigation-link, - .wp-block-pages-list__item { + .wp-block-navigation-item, + .wp-block-page-list { flex-direction: column; align-items: flex-start; } diff --git a/packages/block-library/src/navigation/use-block-navigator.js b/packages/block-library/src/navigation/use-block-navigator.js index 479a54c24e216..0f4f89ffd1998 100644 --- a/packages/block-library/src/navigation/use-block-navigator.js +++ b/packages/block-library/src/navigation/use-block-navigator.js @@ -30,6 +30,7 @@ export default function useBlockNavigator( clientId, __experimentalFeatures ) { onRequestClose={ () => { setIsNavigationListOpen( false ); } } + shouldCloseOnClickOutside={ false } > <BlockNavigationList clientId={ clientId } diff --git a/packages/block-library/src/navigation/variations.js b/packages/block-library/src/navigation/variations.js index 307fa8c0c1204..c17813f953211 100644 --- a/packages/block-library/src/navigation/variations.js +++ b/packages/block-library/src/navigation/variations.js @@ -6,18 +6,17 @@ import { __ } from '@wordpress/i18n'; const variations = [ { name: 'horizontal', - isDefault: true, title: __( 'Navigation (horizontal)' ), description: __( 'Links shown in a row.' ), attributes: { orientation: 'horizontal' }, - scope: [ 'inserter', 'transform' ], + scope: [ 'transform' ], }, { name: 'vertical', title: __( 'Navigation (vertical)' ), description: __( 'Links shown in a column.' ), attributes: { orientation: 'vertical' }, - scope: [ 'inserter', 'transform' ], + scope: [ 'transform' ], }, ]; diff --git a/packages/block-library/src/page-list/edit.js b/packages/block-library/src/page-list/edit.js index 9818a320c326e..58c0e1b6a2853 100644 --- a/packages/block-library/src/page-list/edit.js +++ b/packages/block-library/src/page-list/edit.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies @@ -70,11 +69,6 @@ export default function PageListEdit( { context.customOverlayBackgroundColor, ] ); - useEffect( () => { - const isNavigationChild = isEmpty( context ) ? false : true; - setAttributes( { isNavigationChild } ); - }, [] ); - const { textColor, backgroundColor, showSubmenuIcon, style } = context || {}; @@ -105,6 +99,10 @@ export default function PageListEdit( { [ clientId ] ); + useEffect( () => { + setAttributes( { isNavigationChild: isParentNavigation } ); + }, [] ); + useEffect( () => { if ( isParentNavigation ) { apiFetch( { @@ -127,6 +125,12 @@ export default function PageListEdit( { const openModal = () => setOpen( true ); const closeModal = () => setOpen( false ); + // Update parent status before component first renders. + const attributesWithParentStatus = { + ...attributes, + isNavigationChild: isParentNavigation, + }; + return ( <> { allowConvertToLinks && ( @@ -145,7 +149,7 @@ export default function PageListEdit( { <div { ...blockProps }> <ServerSideRender block="core/page-list" - attributes={ attributes } + attributes={ attributesWithParentStatus } /> </div> </> diff --git a/packages/block-library/src/page-list/style.scss b/packages/block-library/src/page-list/style.scss index 8b2355c8f9740..806c075319eaa 100644 --- a/packages/block-library/src/page-list/style.scss +++ b/packages/block-library/src/page-list/style.scss @@ -8,7 +8,7 @@ // Menu items generated by the page list do not get `has-[x]-background-color`, // and must therefore inherit from the parent. - .wp-block-pages-list__item { + .wp-block-navigation-item { background-color: inherit; } diff --git a/packages/block-library/src/paragraph/style.scss b/packages/block-library/src/paragraph/style.scss index eb320b1853c2a..23e4de9c320ca 100644 --- a/packages/block-library/src/paragraph/style.scss +++ b/packages/block-library/src/paragraph/style.scss @@ -28,6 +28,11 @@ font-style: normal; } +p { + // Break long strings of text without spaces so they don't overflow the block. + overflow-wrap: break-word; +} + // Prevent the dropcap from breaking out of the box when a background is applied. p.has-drop-cap.has-background { overflow: hidden; diff --git a/packages/block-library/src/post-author/block.json b/packages/block-library/src/post-author/block.json index f82c5662abeae..4839069ed4a1b 100644 --- a/packages/block-library/src/post-author/block.json +++ b/packages/block-library/src/post-author/block.json @@ -33,7 +33,8 @@ }, "color": { "gradients": true, - "link": true + "link": true, + "__experimentalDuotone": ".wp-block-post-author__avatar img" } }, "editorStyle": "wp-block-post-author-editor", diff --git a/packages/block-library/src/post-template/index.js b/packages/block-library/src/post-template/index.js index 5e3bba213228b..ace8add51d328 100644 --- a/packages/block-library/src/post-template/index.js +++ b/packages/block-library/src/post-template/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { loop } from '@wordpress/icons'; +import { layout } from '@wordpress/icons'; /** * Internal dependencies @@ -14,7 +14,7 @@ const { name } = metadata; export { metadata, name }; export const settings = { - icon: loop, + icon: layout, edit, save, }; diff --git a/packages/block-library/src/post-terms/edit.js b/packages/block-library/src/post-terms/edit.js index 4a939e2af579c..4ce94235615a4 100644 --- a/packages/block-library/src/post-terms/edit.js +++ b/packages/block-library/src/post-terms/edit.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; */ import { AlignmentToolbar, - InspectorAdvancedControls, + InspectorControls, BlockControls, Warning, useBlockProps, @@ -79,7 +79,7 @@ export default function PostTermsEdit( { } } /> </BlockControls> - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> <TextControl autoComplete="off" label={ __( 'Separator' ) } @@ -89,7 +89,7 @@ export default function PostTermsEdit( { } } help={ __( 'Enter character(s) used to separate terms.' ) } /> - </InspectorAdvancedControls> + </InspectorControls> <div { ...blockProps }> { isLoading && <Spinner /> } { ! isLoading && diff --git a/packages/block-library/src/pullquote/style.scss b/packages/block-library/src/pullquote/style.scss index 3466ce56fa6c0..e1c98fa5ec0c4 100644 --- a/packages/block-library/src/pullquote/style.scss +++ b/packages/block-library/src/pullquote/style.scss @@ -2,6 +2,7 @@ margin: 0 0 1em 0; padding: 3em 0; text-align: center; // Default text-alignment where the `textAlign` attribute value isn't specified. + overflow-wrap: break-word; // Break long strings of text without spaces so they don't overflow the block. p, blockquote, diff --git a/packages/block-library/src/query-pagination-next/block.json b/packages/block-library/src/query-pagination-next/block.json index afc69d6e14a3b..f7d4850413222 100644 --- a/packages/block-library/src/query-pagination-next/block.json +++ b/packages/block-library/src/query-pagination-next/block.json @@ -11,7 +11,7 @@ "type": "string" } }, - "usesContext": [ "queryId", "query" ], + "usesContext": [ "queryId", "query", "paginationArrow" ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-next/edit.js b/packages/block-library/src/query-pagination-next/edit.js index 4e48c0bcecaa6..d91f3d7e0ba30 100644 --- a/packages/block-library/src/query-pagination-next/edit.js +++ b/packages/block-library/src/query-pagination-next/edit.js @@ -4,19 +4,41 @@ import { __ } from '@wordpress/i18n'; import { useBlockProps, PlainText } from '@wordpress/block-editor'; +const arrowMap = { + none: '', + arrow: '→', + chevron: '»', +}; + export default function QueryPaginationNextEdit( { attributes: { label }, setAttributes, + context: { paginationArrow }, } ) { + const displayArrow = arrowMap[ paginationArrow ]; return ( - <PlainText - __experimentalVersion={ 2 } - tagName="a" - aria-label={ __( 'Next page link' ) } - placeholder={ __( 'Next Page' ) } - value={ label } - onChange={ ( newLabel ) => setAttributes( { label: newLabel } ) } + <a + href="#pagination-next-pseudo-link" + onClick={ ( event ) => event.preventDefault() } { ...useBlockProps() } - /> + > + <PlainText + __experimentalVersion={ 2 } + tagName="span" + aria-label={ __( 'Next page link' ) } + placeholder={ __( 'Next Page' ) } + value={ label } + onChange={ ( newLabel ) => + setAttributes( { label: newLabel } ) + } + /> + { displayArrow && ( + <span + className={ `wp-block-query-pagination-next-arrow is-arrow-${ paginationArrow }` } + > + { displayArrow } + </span> + ) } + </a> ); } diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php index 7b0b4051f1aac..d091e1c6bbc0f 100644 --- a/packages/block-library/src/query-pagination-next/index.php +++ b/packages/block-library/src/query-pagination-next/index.php @@ -22,7 +22,11 @@ function render_block_core_query_pagination_next( $attributes, $content, $block $wrapper_attributes = get_block_wrapper_attributes(); $default_label = __( 'Next Page' ); $label = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? $attributes['label'] : $default_label; - $content = ''; + $pagination_arrow = get_query_pagination_arrow( $block, true ); + if ( $pagination_arrow ) { + $label .= $pagination_arrow; + } + $content = ''; // Check if the pagination is for Query that inherits the global context. if ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ) { diff --git a/packages/block-library/src/query-pagination-previous/block.json b/packages/block-library/src/query-pagination-previous/block.json index 78a53867d0b7a..c3a05cc202d30 100644 --- a/packages/block-library/src/query-pagination-previous/block.json +++ b/packages/block-library/src/query-pagination-previous/block.json @@ -11,7 +11,7 @@ "type": "string" } }, - "usesContext": [ "queryId", "query" ], + "usesContext": [ "queryId", "query", "paginationArrow" ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-previous/edit.js b/packages/block-library/src/query-pagination-previous/edit.js index 2c0bf33005de8..c695a453ce1e3 100644 --- a/packages/block-library/src/query-pagination-previous/edit.js +++ b/packages/block-library/src/query-pagination-previous/edit.js @@ -4,19 +4,41 @@ import { __ } from '@wordpress/i18n'; import { useBlockProps, PlainText } from '@wordpress/block-editor'; +const arrowMap = { + none: '', + arrow: '←', + chevron: '«', +}; + export default function QueryPaginationPreviousEdit( { attributes: { label }, setAttributes, + context: { paginationArrow }, } ) { + const displayArrow = arrowMap[ paginationArrow ]; return ( - <PlainText - __experimentalVersion={ 2 } - tagName="a" - aria-label={ __( 'Previous page link' ) } - placeholder={ __( 'Previous Page' ) } - value={ label } - onChange={ ( newLabel ) => setAttributes( { label: newLabel } ) } + <a + href="#pagination-previous-pseudo-link" + onClick={ ( event ) => event.preventDefault() } { ...useBlockProps() } - /> + > + { displayArrow && ( + <span + className={ `wp-block-query-pagination-previous-arrow is-arrow-${ paginationArrow }` } + > + { displayArrow } + </span> + ) } + <PlainText + __experimentalVersion={ 2 } + tagName="span" + aria-label={ __( 'Previous page link' ) } + placeholder={ __( 'Previous Page' ) } + value={ label } + onChange={ ( newLabel ) => + setAttributes( { label: newLabel } ) + } + /> + </a> ); } diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index ac319d0be4dbf..47506496722d8 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -21,7 +21,11 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl $wrapper_attributes = get_block_wrapper_attributes(); $default_label = __( 'Previous Page' ); $label = isset( $attributes['label'] ) && ! empty( $attributes['label'] ) ? $attributes['label'] : $default_label; - $content = ''; + $pagination_arrow = get_query_pagination_arrow( $block, false ); + if ( $pagination_arrow ) { + $label = $pagination_arrow . $label; + } + $content = ''; // Check if the pagination is for Query that inherits the global context // and handle appropriately. if ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ) { diff --git a/packages/block-library/src/query-pagination/block.json b/packages/block-library/src/query-pagination/block.json index c1de24977f37f..45b7a42bffbad 100644 --- a/packages/block-library/src/query-pagination/block.json +++ b/packages/block-library/src/query-pagination/block.json @@ -6,7 +6,16 @@ "parent": [ "core/query" ], "description": "Displays a paginated navigation to next/previous set of posts, when applicable.", "textdomain": "default", + "attributes": { + "paginationArrow": { + "type": "string", + "default": "none" + } + }, "usesContext": [ "queryId", "query" ], + "providesContext": { + "paginationArrow": "paginationArrow" + }, "supports": { "align": true, "reusable": false, diff --git a/packages/block-library/src/query-pagination/edit.js b/packages/block-library/src/query-pagination/edit.js index 47ac55f30693a..c3f4f53f5f2b3 100644 --- a/packages/block-library/src/query-pagination/edit.js +++ b/packages/block-library/src/query-pagination/edit.js @@ -1,10 +1,20 @@ /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { + InspectorControls, useBlockProps, __experimentalUseInnerBlocksProps as useInnerBlocksProps, + store as blockEditorStore, } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { PanelBody } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { QueryPaginationArrowControls } from './query-pagination-arrow-controls'; const TEMPLATE = [ [ 'core/query-pagination-previous' ], @@ -12,7 +22,25 @@ const TEMPLATE = [ [ 'core/query-pagination-next' ], ]; -export default function QueryPaginationEdit() { +export default function QueryPaginationEdit( { + attributes: { paginationArrow }, + setAttributes, + clientId, +} ) { + const hasNextPreviousBlocks = useSelect( ( select ) => { + const { getBlocks } = select( blockEditorStore ); + const innerBlocks = getBlocks( clientId ); + /** + * Show the `paginationArrow` control only if a + * `QueryPaginationNext/Previous` block exists. + */ + return innerBlocks?.find( ( innerBlock ) => { + return [ + 'core/query-pagination-next', + 'core/query-pagination-previous', + ].includes( innerBlock.name ); + } ); + }, [] ); const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( blockProps, { template: TEMPLATE, @@ -23,5 +51,21 @@ export default function QueryPaginationEdit() { ], orientation: 'horizontal', } ); - return <div { ...innerBlocksProps } />; + return ( + <> + { hasNextPreviousBlocks && ( + <InspectorControls> + <PanelBody title={ __( 'Settings' ) }> + <QueryPaginationArrowControls + value={ paginationArrow } + onChange={ ( value ) => { + setAttributes( { paginationArrow: value } ); + } } + /> + </PanelBody> + </InspectorControls> + ) } + <div { ...innerBlocksProps } /> + </> + ); } diff --git a/packages/block-library/src/query-pagination/query-pagination-arrow-controls.js b/packages/block-library/src/query-pagination/query-pagination-arrow-controls.js new file mode 100644 index 0000000000000..28ae36ed41572 --- /dev/null +++ b/packages/block-library/src/query-pagination/query-pagination-arrow-controls.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, +} from '@wordpress/components'; + +export function QueryPaginationArrowControls( { value, onChange } ) { + return ( + <ToggleGroupControl + label={ __( 'Arrow' ) } + value={ value } + onChange={ onChange } + help={ __( + 'A decorative arrow appended to the next and previous page link.' + ) } + isBlock + > + <ToggleGroupControlOption + value="none" + label={ _x( + 'None', + 'Arrow option for Query Pagination Next/Previous blocks' + ) } + /> + <ToggleGroupControlOption + value="arrow" + label={ _x( + 'Arrow', + 'Arrow option for Query Pagination Next/Previous blocks' + ) } + /> + <ToggleGroupControlOption + value="chevron" + label={ _x( + 'Chevron', + 'Arrow option for Query Pagination Next/Previous blocks' + ) } + /> + </ToggleGroupControl> + ); +} diff --git a/packages/block-library/src/query-pagination/style.scss b/packages/block-library/src/query-pagination/style.scss index e140863a7d981..5a4a950757cd7 100644 --- a/packages/block-library/src/query-pagination/style.scss +++ b/packages/block-library/src/query-pagination/style.scss @@ -18,4 +18,22 @@ $pagination-margin: 0.5em; margin-right: 0; } } + .wp-block-query-pagination-previous-arrow { + margin-right: 1ch; + display: inline-block; // Needed so that the transform works. + // chevron(`»`) symbol doesn't need the mirroring by us. + &:not(.is-arrow-chevron) { + // Flip for RTL. + transform: scaleX(1) #{"/*rtl:scaleX(-1);*/"}; // This points the arrow right for LTR and left for RTL. + } + } + .wp-block-query-pagination-next-arrow { + margin-left: 1ch; + display: inline-block; // Needed so that the transform works. + // chevron(`»`) symbol doesn't need the mirroring by us. + &:not(.is-arrow-chevron) { + // Flip for RTL. + transform: scaleX(1) #{"/*rtl:scaleX(-1);*/"}; // This points the arrow right for LTR and left for RTL. + } + } } diff --git a/packages/block-library/src/query/edit/index.js b/packages/block-library/src/query/edit/index.js index e297534ed8f79..b08bd12eba31b 100644 --- a/packages/block-library/src/query/edit/index.js +++ b/packages/block-library/src/query/edit/index.js @@ -7,7 +7,7 @@ import { useInstanceId } from '@wordpress/compose'; import { useEffect } from '@wordpress/element'; import { BlockControls, - InspectorAdvancedControls, + InspectorControls, useBlockProps, useSetting, store as blockEditorStore, @@ -104,7 +104,7 @@ export function QueryContent( { attributes, setAttributes } ) { setDisplayLayout={ updateDisplayLayout } /> </BlockControls> - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> <SelectControl label={ __( 'HTML element' ) } options={ [ @@ -118,7 +118,7 @@ export function QueryContent( { attributes, setAttributes } ) { setAttributes( { tagName: value } ) } /> - </InspectorAdvancedControls> + </InspectorControls> <TagName { ...innerBlocksProps } /> </> ); diff --git a/packages/block-library/src/quote/style.scss b/packages/block-library/src/quote/style.scss index e6a6dfea5a485..e10ccb18bec7c 100644 --- a/packages/block-library/src/quote/style.scss +++ b/packages/block-library/src/quote/style.scss @@ -1,4 +1,6 @@ .wp-block-quote { + overflow-wrap: break-word; // Break long strings of text without spaces so they don't overflow the block. + &.is-style-large, &.is-large { margin-bottom: 1em; diff --git a/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap index 25ce13f150a1d..6030decd6fcf5 100644 --- a/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/search/test/__snapshots__/edit.native.js.snap @@ -23,6 +23,7 @@ exports[`Search Block renders block with button inside option 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} onBackspace={[Function]} @@ -126,6 +127,7 @@ exports[`Search Block renders block with button inside option 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} minWidth={75} @@ -196,6 +198,7 @@ exports[`Search Block renders block with icon button option matches snapshot 1`] disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} onBackspace={[Function]} @@ -356,6 +359,7 @@ exports[`Search Block renders block with label hidden matches snapshot 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} minWidth={75} @@ -426,6 +430,7 @@ exports[`Search Block renders with default configuration matches snapshot 1`] = disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} onBackspace={[Function]} @@ -529,6 +534,7 @@ exports[`Search Block renders with default configuration matches snapshot 1`] = disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} minWidth={75} @@ -599,6 +605,7 @@ exports[`Search Block renders with no-button option matches snapshot 1`] = ` disableEditingMenu={false} focusable={true} fontFamily="serif" + fontSize={16} isMultiline={false} maxImagesWidth={200} onBackspace={[Function]} diff --git a/packages/block-library/src/site-logo/block.json b/packages/block-library/src/site-logo/block.json index c40cfcdd2bab0..b2adec6911dc5 100644 --- a/packages/block-library/src/site-logo/block.json +++ b/packages/block-library/src/site-logo/block.json @@ -3,7 +3,7 @@ "name": "core/site-logo", "title": "Site Logo", "category": "layout", - "description": "Useful for displaying a graphic mark, design, or symbol to represent the site. Once a site logo is set, it can be reused in different places and templates. It should not be confused with the site icon, which is the small image used in the dashboard, browser tabs, public search results, etc, to help recognize a site.", + "description": "Display a graphic to represent this site. Update the block, and the changes apply everywhere it’s used. This is different than the site icon, which is the smaller image visible in your dashboard, browser tabs, etc used to help others recognize this site.", "textdomain": "default", "attributes": { "align": { diff --git a/packages/block-library/src/site-title/edit/index.js b/packages/block-library/src/site-title/edit/index.js index 144552847996d..f600235a69869 100644 --- a/packages/block-library/src/site-title/edit/index.js +++ b/packages/block-library/src/site-title/edit/index.js @@ -52,7 +52,7 @@ export default function SiteTitleEdit( { tagName="a" aria-label={ __( 'Site title text' ) } placeholder={ __( 'Write site title…' ) } - value={ title || readOnlyTitle } + value={ title } onChange={ setTitle } allowedFormats={ [] } disableLineBreaks diff --git a/packages/block-library/src/social-links/block.json b/packages/block-library/src/social-links/block.json index b559ae04fa8a5..b9c8eddbdc4d6 100644 --- a/packages/block-library/src/social-links/block.json +++ b/packages/block-library/src/social-links/block.json @@ -40,7 +40,15 @@ }, "supports": { "align": [ "left", "center", "right" ], - "anchor": true + "anchor": true, + "__experimentalExposeControlsToChildren": true, + "__experimentalLayout": { + "allowSwitching": false, + "allowInheriting": false, + "default": { + "type": "flex" + } + } }, "styles": [ { "name": "default", "label": "Default", "isDefault": true }, diff --git a/packages/block-library/src/social-links/deprecated.js b/packages/block-library/src/social-links/deprecated.js index e8615282201f5..8f1ae2c42a68e 100644 --- a/packages/block-library/src/social-links/deprecated.js +++ b/packages/block-library/src/social-links/deprecated.js @@ -8,8 +8,101 @@ import classNames from 'classnames'; */ import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; +/** + * The specific handling by `className` below is needed because `itemsJustification` + * was introduced in https://github.com/WordPress/gutenberg/pull/28980/files and wasn't + * declared in block.json. + * + * @param {Object} attributes Block's attributes. + */ +const migrateWithLayout = ( attributes ) => { + if ( !! attributes.layout ) { + return attributes; + } + const { className } = attributes; + // Matches classes with `items-justified-` prefix. + const prefix = `items-justified-`; + const justifiedItemsRegex = new RegExp( `\\b${ prefix }[^ ]*[ ]?\\b`, 'g' ); + const newAttributes = { + ...attributes, + className: className?.replace( justifiedItemsRegex, '' ).trim(), + }; + /** + * Add `layout` prop only if `justifyContent` is defined, for backwards + * compatibility. In other cases the block's default layout will be used. + * Also noting that due to the missing attribute, it's possible for a block + * to have more than one of `justified` classes. + */ + const justifyContent = className + ?.match( justifiedItemsRegex )?.[ 0 ] + ?.trim(); + if ( justifyContent ) { + Object.assign( newAttributes, { + layout: { + type: 'flex', + justifyContent: justifyContent.slice( prefix.length ), + }, + } ); + } + return newAttributes; +}; + // Social Links block deprecations. const deprecated = [ + // Implement `flex` layout. + { + attributes: { + iconColor: { + type: 'string', + }, + customIconColor: { + type: 'string', + }, + iconColorValue: { + type: 'string', + }, + iconBackgroundColor: { + type: 'string', + }, + customIconBackgroundColor: { + type: 'string', + }, + iconBackgroundColorValue: { + type: 'string', + }, + openInNewTab: { + type: 'boolean', + default: false, + }, + size: { + type: 'string', + }, + }, + isEligible: ( { layout } ) => ! layout, + migrate: migrateWithLayout, + save( props ) { + const { + attributes: { + iconBackgroundColorValue, + iconColorValue, + itemsJustification, + size, + }, + } = props; + + const className = classNames( size, { + 'has-icon-color': iconColorValue, + 'has-icon-background-color': iconBackgroundColorValue, + [ `items-justified-${ itemsJustification }` ]: itemsJustification, + } ); + + return ( + <ul { ...useBlockProps.save( { className } ) }> + <InnerBlocks.Content /> + </ul> + ); + }, + }, // V1. Remove CSS variable use for colors. { attributes: { @@ -46,6 +139,7 @@ const deprecated = [ align: [ 'left', 'center', 'right' ], anchor: true, }, + migrate: migrateWithLayout, save: ( props ) => { const { attributes: { diff --git a/packages/block-library/src/social-links/edit.js b/packages/block-library/src/social-links/edit.js index 089dd24824b28..3b3214be223ff 100644 --- a/packages/block-library/src/social-links/edit.js +++ b/packages/block-library/src/social-links/edit.js @@ -6,15 +6,13 @@ import classNames from 'classnames'; /** * WordPress dependencies */ - +import { getBlockSupport } from '@wordpress/blocks'; import { Fragment, useEffect } from '@wordpress/element'; - import { BlockControls, __experimentalUseInnerBlocksProps as useInnerBlocksProps, useBlockProps, InspectorControls, - JustifyContentControl, ContrastChecker, PanelColorSettings, withColors, @@ -38,8 +36,17 @@ const sizeOptions = [ { name: __( 'Huge' ), value: 'has-huge-icon-size' }, ]; +const getDefaultBlockLayout = ( blockTypeOrName ) => { + const layoutBlockSupportConfig = getBlockSupport( + blockTypeOrName, + '__experimentalLayout' + ); + return layoutBlockSupportConfig?.default; +}; + export function SocialLinksEdit( props ) { const { + name, attributes, iconBackgroundColor, iconColor, @@ -52,10 +59,11 @@ export function SocialLinksEdit( props ) { const { iconBackgroundColorValue, iconColorValue, - itemsJustification, openInNewTab, size, + layout, } = attributes; + const usedLayout = layout || getDefaultBlockLayout( name ); // Remove icon background color if logos only style selected. const logosOnly = @@ -93,16 +101,15 @@ export function SocialLinksEdit( props ) { 'has-icon-color': iconColor.color || iconColorValue, 'has-icon-background-color': iconBackgroundColor.color || iconBackgroundColorValue, - [ `items-justified-${ itemsJustification }` ]: itemsJustification, } ); const blockProps = useBlockProps( { className } ); const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: ALLOWED_BLOCKS, - orientation: 'horizontal', placeholder: isSelected ? SelectedSocialPlaceholder : SocialPlaceholder, templateLock: false, __experimentalAppenderTagName: 'li', + __experimentalLayout: usedLayout, } ); const POPOVER_PROPS = { @@ -111,24 +118,6 @@ export function SocialLinksEdit( props ) { return ( <Fragment> - <BlockControls group="block"> - <JustifyContentControl - allowedControls={ [ - 'left', - 'center', - 'right', - 'space-between', - ] } - value={ itemsJustification } - onChange={ ( value ) => - setAttributes( { itemsJustification: value } ) - } - popoverProps={ { - position: 'bottom right', - isAlternate: true, - } } - /> - </BlockControls> <BlockControls group="other"> <ToolbarDropdownMenu label={ __( 'Size' ) } diff --git a/packages/block-library/src/social-links/save.js b/packages/block-library/src/social-links/save.js index bbf8edebb7b1a..7ed90959cc9cd 100644 --- a/packages/block-library/src/social-links/save.js +++ b/packages/block-library/src/social-links/save.js @@ -10,18 +10,12 @@ import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; export default function save( props ) { const { - attributes: { - iconBackgroundColorValue, - iconColorValue, - itemsJustification, - size, - }, + attributes: { iconBackgroundColorValue, iconColorValue, size }, } = props; const className = classNames( size, { 'has-icon-color': iconColorValue, 'has-icon-background-color': iconBackgroundColorValue, - [ `items-justified-${ itemsJustification }` ]: itemsJustification, } ); return ( diff --git a/packages/block-library/src/social-links/style.scss b/packages/block-library/src/social-links/style.scss index eee1000a913f2..1e155f7f3c8e2 100644 --- a/packages/block-library/src/social-links/style.scss +++ b/packages/block-library/src/social-links/style.scss @@ -1,6 +1,4 @@ .wp-block-social-links { - display: flex; - flex-wrap: wrap; padding-left: 0; padding-right: 0; // Some themes set text-indent on all <ul> @@ -16,13 +14,7 @@ box-shadow: none; } - // Vertically balance the margin of each icon. .wp-social-link { - // This needs specificity to override some themes. - &.wp-social-link.wp-social-link { - margin: 4px 8px 4px 0; - } - // By setting the font size, we can scale icons and paddings consistently based on that. // This also allows themes to override this, if need be. a { diff --git a/packages/block-library/src/table/block.json b/packages/block-library/src/table/block.json index 01e5c58520d55..f4723331098bc 100644 --- a/packages/block-library/src/table/block.json +++ b/packages/block-library/src/table/block.json @@ -3,7 +3,7 @@ "name": "core/table", "title": "Table", "category": "text", - "description": "Insert a table — perfect for sharing charts and data.", + "description": "Create structured content in rows and columns to display information.", "textdomain": "default", "attributes": { "hasFixedLayout": { diff --git a/packages/block-library/src/template-part/edit/advanced-controls.js b/packages/block-library/src/template-part/edit/advanced-controls.js index 9086b307b1a83..26393db2d7585 100644 --- a/packages/block-library/src/template-part/edit/advanced-controls.js +++ b/packages/block-library/src/template-part/edit/advanced-controls.js @@ -4,7 +4,7 @@ import { useEntityProp } from '@wordpress/core-data'; import { SelectControl, TextControl } from '@wordpress/components'; import { sprintf, __ } from '@wordpress/i18n'; -import { InspectorAdvancedControls } from '@wordpress/block-editor'; +import { InspectorControls } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; export function TemplatePartAdvancedControls( { @@ -44,7 +44,7 @@ export function TemplatePartAdvancedControls( { }, [] ); return ( - <InspectorAdvancedControls> + <InspectorControls __experimentalGroup="advanced"> { isEntityAvailable && ( <> <TextControl @@ -87,6 +87,6 @@ export function TemplatePartAdvancedControls( { value={ tagName || '' } onChange={ ( value ) => setAttributes( { tagName: value } ) } /> - </InspectorAdvancedControls> + </InspectorControls> ); } diff --git a/packages/block-library/src/video/edit-common-settings.js b/packages/block-library/src/video/edit-common-settings.js index 74067cbd7dd8d..5cd74c41bec92 100644 --- a/packages/block-library/src/video/edit-common-settings.js +++ b/packages/block-library/src/video/edit-common-settings.js @@ -1,14 +1,14 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { ToggleControl, SelectControl } from '@wordpress/components'; import { useMemo, useCallback, Platform } from '@wordpress/element'; const options = [ { value: 'auto', label: __( 'Auto' ) }, { value: 'metadata', label: __( 'Metadata' ) }, - { value: 'none', label: __( 'None' ) }, + { value: 'none', label: _x( 'None', 'Preload value' ) }, ]; const VideoSettings = ( { setAttributes, attributes } ) => { diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index 658640158bd4d..d678b2ff21625 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -94,12 +94,22 @@ function VideoEdit( { // in this case there was an error // previous attributes should be removed // because they may be temporary blob urls - setAttributes( { src: undefined, id: undefined } ); + setAttributes( { + src: undefined, + id: undefined, + poster: undefined, + } ); return; } + // sets the block's attribute and updates the edit component from the // selected media - setAttributes( { src: media.url, id: media.id } ); + setAttributes( { + src: media.url, + id: media.id, + poster: + media.image?.src !== media.icon ? media.image?.src : undefined, + } ); } function onSelectURL( newSrc ) { @@ -112,7 +122,7 @@ function VideoEdit( { onReplace( embedBlock ); return; } - setAttributes( { src: newSrc, id: undefined } ); + setAttributes( { src: newSrc, id: undefined, poster: undefined } ); } } @@ -151,10 +161,10 @@ function VideoEdit( { } function onRemovePoster() { - setAttributes( { poster: '' } ); + setAttributes( { poster: undefined } ); // Move focus back to the Media Upload button. - this.posterImageButton.current.focus(); + posterImageButton.current.focus(); } const videoPosterDescription = `video-block__poster-image-description-${ instanceId }`; diff --git a/packages/block-library/src/video/tracks-editor.js b/packages/block-library/src/video/tracks-editor.js index 04f3f04f73cd0..9fcbbd5d4581c 100644 --- a/packages/block-library/src/video/tracks-editor.js +++ b/packages/block-library/src/video/tracks-editor.js @@ -24,6 +24,7 @@ import { import { upload, media } from '@wordpress/icons'; import { useSelect } from '@wordpress/data'; import { useState } from '@wordpress/element'; +import { getFilename } from '@wordpress/url'; const ALLOWED_TYPES = [ 'text/vtt' ]; @@ -99,9 +100,7 @@ function TrackList( { tracks, onEditPress } ) { function SingleTrackEditor( { track, onChange, onClose, onRemove } ) { const { src = '', label = '', srcLang = '', kind = DEFAULT_KIND } = track; - const fileName = src.startsWith( 'blob:' ) - ? '' - : src.substring( src.lastIndexOf( '/' ) + 1 ); + const fileName = src.startsWith( 'blob:' ) ? '' : getFilename( src ) || ''; return ( <NavigableMenu> <div className="block-library-video-tracks-editor__single-track-editor"> diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 79f2d8aaee271..486282484503c 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Backward Compatibility + +- Register a block even when an invalid value provided for the icon setting ([#34350](https://github.com/WordPress/gutenberg/pull/34350)). + +### New API + +- The `isMatch` callback on block transforms now receives the block object (or block objects if `isMulti` is `true`) as its second argument. + ## 11.0.0 (2021-07-29) ### Breaking Change diff --git a/packages/blocks/README.md b/packages/blocks/README.md index ba65fdd0ac183..aabe50096d62c 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -142,7 +142,7 @@ add_action( 'enqueue_block_editor_assets', 'random_image_enqueue_block_editor_as return el( 'form', - Object.assing( blockProps, { onSubmit: setCategory } ), + Object.assign( blockProps, { onSubmit: setCategory } ), children ); }, diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 74c285fa650ab..e7c759dbe5913 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "11.0.0", + "version": "11.0.1", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -40,7 +40,6 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", - "@wordpress/icons": "file:../icons", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/shortcode": "file:../shortcode", "hpq": "^1.3.0", diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 18342aa916256..2ca489dd609f5 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -1,3 +1,5 @@ +export const BLOCK_ICON_DEFAULT = 'block-default'; + /** * Array of valid keys in a block type settings deprecation object. * @@ -24,7 +26,8 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, backgroundColor: { value: [ 'color', 'background' ], - support: [ 'color' ], + support: [ 'color', 'background' ], + requiresOptOut: true, }, borderColor: { value: [ 'border', 'color' ], @@ -50,7 +53,8 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, color: { value: [ 'color', 'text' ], - support: [ 'color' ], + support: [ 'color', 'text' ], + requiresOptOut: true, }, linkColor: { value: [ 'elements', 'link', 'color', 'text' ], @@ -106,10 +110,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, letterSpacing: { value: [ 'typography', 'letterSpacing' ], - support: [ '__experimentalLetterSpacing' ], + support: [ 'typography', '__experimentalLetterSpacing' ], }, '--wp--style--block-gap': { value: [ 'spacing', 'blockGap' ], + support: [ 'spacing', 'blockGap' ], }, }; @@ -122,3 +127,10 @@ export const __EXPERIMENTAL_ELEMENTS = { h5: 'h5', h6: 'h6', }; + +export const __EXPERIMENTAL_PATHS_WITH_MERGE = { + 'color.gradients': true, + 'color.palette': true, + 'typography.fontFamilies': true, + 'typography.fontSizes': true, +}; diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js index 5b1239c46fc7c..3013a3d9f79e5 100644 --- a/packages/blocks/src/api/factory.js +++ b/packages/blocks/src/api/factory.js @@ -224,7 +224,8 @@ const isPossibleTransformForSource = ( transform, direction, blocks ) => { const attributes = transform.isMultiBlock ? blocks.map( ( block ) => block.attributes ) : sourceBlock.attributes; - if ( ! transform.isMatch( attributes ) ) { + const block = transform.isMultiBlock ? blocks : sourceBlock; + if ( ! transform.isMatch( attributes, block ) ) { return false; } } diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 394fddaa5de6e..e567cde21e74b 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -159,4 +159,5 @@ export { default as node } from './node'; export { __EXPERIMENTAL_STYLE_PROPERTY, __EXPERIMENTAL_ELEMENTS, + __EXPERIMENTAL_PATHS_WITH_MERGE, } from './constants'; diff --git a/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js b/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js index cd1e45d077e4d..d1e3a3e43439a 100644 --- a/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js +++ b/packages/blocks/src/api/raw-handling/phrasing-content-reducer.js @@ -56,5 +56,18 @@ export default function phrasingContentReducer( node, doc ) { node.removeAttribute( 'target' ); node.removeAttribute( 'rel' ); } + + // Saves anchor elements name attribute as id + if ( node.name && ! node.id ) { + node.id = node.name; + } + + // Keeps id only if there is an internal link pointing to it + if ( + node.id && + ! node.ownerDocument.querySelector( `[href="#${ node.id }"]` ) + ) { + node.removeAttribute( 'id' ); + } } } diff --git a/packages/blocks/src/api/raw-handling/readme.md b/packages/blocks/src/api/raw-handling/readme.md index eb484450758c8..6c2be6929a597 100644 --- a/packages/blocks/src/api/raw-handling/readme.md +++ b/packages/blocks/src/api/raw-handling/readme.md @@ -4,22 +4,24 @@ This folder contains all paste specific logic (filters, converters, normalisers. ## Support table -| Source | Formatting | Headings | Lists | Image | Separator | Table | -| ---------------- | ---------- | -------- | ----- | ----- | --------- | ----- | -| Google Docs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Apple Pages | ✓ | ✘ [1] | ✓ | ✘ [1] | n/a | ✓ | -| MS Word | ✓ | ✓ | ✓ | ✘ [2] | n/a | ✓ | -| MS Word Online | ✓ | ✘ [3] | ✓ | ✓ | n/a | ✓ | -| Evernote | ✓ | ✘ [4] | ✓ | ✓ | ✓ | ✓ | -| Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Legacy WordPress | ✓ | ✓ | ✓ | … [5] | ✓ | ✓ | -| Web | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Source | Formatting | Headings | Lists | Image | Separator | Table | Footnotes, endnotes | +| ---------------- | ---------- | -------- | ----- | ----- | --------- | ----- | ------------------- | +| Google Docs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ [1] | +| Apple Pages | ✓ | ✘ [2] | ✓ | ✘ [2] | n/a | ✓ | ✘ [1] | +| MS Word | ✓ | ✓ | ✓ | ✘ [3] | n/a | ✓ | ✓ | +| MS Word Online | ✓ | ✘ [4] | ✓ | ✓ | n/a | ✓ | ✘ [1] | +| LibreOffice | ✓ | ✓ | ✓ | ✘ [3] | ✓ | ✓ | ✓ | +| Evernote | ✓ | ✘ [5] | ✓ | ✓ | ✓ | ✓ | n/a | +| Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | +| Legacy WordPress | ✓ | ✓ | ✓ | … [6] | ✓ | ✓ | n/a | +| Web | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | n/a | -1. Apple Pages does not pass heading and image information. -2. MS Word only provides a local file path, which cannot be accessed in JavaScript for security reasons. Image placeholders will be provided instead. Single images, however, _can_ be copied and pasted without any problem. -3. Still to do for MS Word Online. -4. Evernote does not have headings. -5. For caption and gallery shortcodes, see #2874. +1. Google Docs, Apple Pages and MS Word online don't pass footnote nor endnote information. +2. Apple Pages does not pass heading and image information. +3. MS Word and LibreOffice only provide a local file path, which cannot be accessed in JavaScript for security reasons. Image placeholders will be provided instead. Single images, however, _can_ be copied and pasted without any problem. +4. Still to do for MS Word Online. +5. Evernote does not have headings. +6. For caption and gallery shortcodes, see #2874. ## Other notable capabilities diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index bd400e148a6eb..09033b2ef4beb 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -25,14 +25,13 @@ import { import { applyFilters } from '@wordpress/hooks'; import { select, dispatch } from '@wordpress/data'; import { _x } from '@wordpress/i18n'; -import { blockDefault } from '@wordpress/icons'; /** * Internal dependencies */ import i18nBlockSchema from './i18n-block.json'; import { isValidIcon, normalizeIconObject } from './utils'; -import { DEPRECATED_ENTRY_KEYS } from './constants'; +import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from './constants'; import { store as blocksStore } from '../store'; /** @@ -265,7 +264,7 @@ export function registerBlockType( blockNameOrMetadata, settings ) { settings = { name, - icon: blockDefault, + icon: BLOCK_ICON_DEFAULT, keywords: [], attributes: {}, providesContext: {}, diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index c88828b9cda79..a5e8e679b4583 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -18,7 +18,6 @@ import { getBlockType, getFreeformContentHandlerName, getUnregisteredTypeHandlerName, - hasBlockSupport, } from './registration'; import { isUnmodifiedDefaultBlock, normalizeBlockType } from './utils'; import BlockContentProvider from '../block-content-provider'; @@ -118,14 +117,10 @@ export function getSaveElement( let element = save( { attributes, innerBlocks } ); - const hasLightBlockWrapper = - blockType.apiVersion > 1 || - hasBlockSupport( blockType, 'lightBlockWrapper', false ); - if ( isObject( element ) && hasFilter( 'blocks.getSaveContent.extraProps' ) && - ! hasLightBlockWrapper + ! ( blockType.apiVersion > 1 ) ) { /** * Filters the props applied to the block save result element. diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js index fc38d753e997b..3b4d8671f6e1b 100644 --- a/packages/blocks/src/api/test/factory.js +++ b/packages/blocks/src/api/test/factory.js @@ -979,7 +979,7 @@ describe( 'block factory', () => { expect( availableBlocks ).toEqual( [] ); } ); - it( 'for a non multiblock transform, the isMatch function receives the source block’s attributes object as its first argument', () => { + it( 'for a non multiblock transform, the isMatch function receives the source block’s attributes object and the block object as its arguments', () => { const isMatch = jest.fn(); registerBlockType( 'core/updated-text-block', { @@ -1010,10 +1010,10 @@ describe( 'block factory', () => { getPossibleBlockTransformations( [ block ] ); - expect( isMatch ).toHaveBeenCalledWith( { value: 'ribs' } ); + expect( isMatch ).toHaveBeenCalledWith( { value: 'ribs' }, block ); } ); - it( 'for a multiblock transform, the isMatch function receives an array containing every source block’s attributes as its first argument', () => { + it( 'for a multiblock transform, the isMatch function receives an array containing every source block’s attributes and an array of source blocks as its arguments', () => { const isMatch = jest.fn(); registerBlockType( 'core/updated-text-block', { @@ -1049,10 +1049,10 @@ describe( 'block factory', () => { getPossibleBlockTransformations( [ meatBlock, cheeseBlock ] ); - expect( isMatch ).toHaveBeenCalledWith( [ - { value: 'ribs' }, - { value: 'halloumi' }, - ] ); + expect( isMatch ).toHaveBeenCalledWith( + [ { value: 'ribs' }, { value: 'halloumi' } ], + [ meatBlock, cheeseBlock ] + ); } ); describe( 'wildcard block transforms', () => { diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index 49851544dd5ae..4b3d2864a0b22 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -10,7 +10,6 @@ import { noop, get, omit, pick } from 'lodash'; */ import { addFilter, removeAllFilters, removeFilter } from '@wordpress/hooks'; import { select } from '@wordpress/data'; -import { blockDefault as blockIcon } from '@wordpress/icons'; /** * Internal dependencies @@ -36,7 +35,7 @@ import { serverSideBlockDefinitions, unstable__bootstrapServerSideBlockDefinitions, // eslint-disable-line camelcase } from '../registration'; -import { DEPRECATED_ENTRY_KEYS } from '../constants'; +import { BLOCK_ICON_DEFAULT, DEPRECATED_ENTRY_KEYS } from '../constants'; import { store as blocksStore } from '../../store'; describe( 'blocks', () => { @@ -121,9 +120,7 @@ describe( 'blocks', () => { expect( console ).not.toHaveErrored(); expect( block ).toEqual( { name: 'my-plugin/fancy-block-4', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -268,9 +265,7 @@ describe( 'blocks', () => { name: 'core/test-block-with-defaults', title: 'block title', category: 'text', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -301,9 +296,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: { ok: { type: 'boolean', @@ -338,9 +331,7 @@ describe( 'blocks', () => { name: blockName, save: expect.any( Function ), title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -371,9 +362,7 @@ describe( 'blocks', () => { name: blockName, save: expect.any( Function ), title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: { fontSize: 'fontSize', @@ -410,9 +399,7 @@ describe( 'blocks', () => { save: expect.any( Function ), title: 'block title', category: 'widgets', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -636,9 +623,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -723,7 +708,7 @@ describe( 'blocks', () => { ...omit( { name, - icon: blockIcon, + icon: BLOCK_ICON_DEFAULT, attributes: {}, providesContext: {}, usesContext: [], @@ -960,9 +945,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -979,9 +962,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -1059,9 +1040,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -1085,9 +1064,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -1118,9 +1095,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], @@ -1135,9 +1110,7 @@ describe( 'blocks', () => { save: noop, category: 'text', title: 'block title', - icon: { - src: blockIcon, - }, + icon: { src: BLOCK_ICON_DEFAULT }, attributes: {}, providesContext: {}, usesContext: [], diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index d6b588668b0ed..653676a85fd70 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -14,6 +14,7 @@ import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies */ +import { BLOCK_ICON_DEFAULT } from './constants'; import { getBlockType, getDefaultBlockName } from './registration'; import { createBlock } from './factory'; @@ -89,6 +90,7 @@ export function isValidIcon( icon ) { * @return {WPBlockTypeIconDescriptor} Object describing the icon. */ export function normalizeIconObject( icon ) { + icon = icon || BLOCK_ICON_DEFAULT; if ( isValidIcon( icon ) ) { return { src: icon }; } diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 2ed797b1a56a7..4d943b62a9d92 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,18 +2,33 @@ ## Unreleased +### Breaking Change + +- Removed a min-width from the `DropdownMenu` component, allowing the menu to accommodate thin contents like vertical tools menus ([#33995](https://github.com/WordPress/gutenberg/pull/33995)). + ### Bug Fix -- Listen to `resize` events correctly in `useBreakpointIndex`. This hook is used in `useResponsiveValue` and consequently in the `Flex` and `Grid` components ([#33902](https://github.com/WordPress/gutenberg/pull/33902)) - Fixed RTL styles in `Flex` component ([#33729](https://github.com/WordPress/gutenberg/pull/33729)). +- Fixed unit test errors caused by `CSS.supports` being called in a non-browser environment ([#34572](https://github.com/WordPress/gutenberg/pull/34572)). +- Fixed `ToggleGroupControl`'s backdrop not updating when changing the `isAdaptiveWidth` property ([#34595](https://github.com/WordPress/gutenberg/pull/34595)). + +### Internal + +- Renamed `PolymorphicComponent*` types to `WordPressComponent*` ([#34330](https://github.com/WordPress/gutenberg/pull/34330)) + +## 16.0.0 (2021-08-23) ### Breaking Change -- Updated the visual styles of the RangeControl component ([#33824](https://github.com/WordPress/gutenberg/pull/33824)) +- Updated the visual styles of the RangeControl component ([#33824](https://github.com/WordPress/gutenberg/pull/33824)). ### New Feature -- Add `hideLabelFromVision` prop to `RangeControl` ([#33714](https://github.com/WordPress/gutenberg/pull/33714)) +- Add `hideLabelFromVision` prop to `RangeControl` ([#33714](https://github.com/WordPress/gutenberg/pull/33714)). + +### Bug Fix + +- Listen to `resize` events correctly in `useBreakpointIndex`. This hook is used in `useResponsiveValue` and consequently in the `Flex` and `Grid` components ([#33902](https://github.com/WordPress/gutenberg/pull/33902)) ## 15.0.0 (2021-07-29) diff --git a/packages/components/package.json b/packages/components/package.json index d37128ced84e0..72645f2d26d86 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "15.0.0", + "version": "16.0.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/src/alignment-matrix-control/README.md b/packages/components/src/alignment-matrix-control/README.md index d8470d905a1ae..e041cb7f83920 100644 --- a/packages/components/src/alignment-matrix-control/README.md +++ b/packages/components/src/alignment-matrix-control/README.md @@ -5,14 +5,61 @@ AlignmentMatrixControl components enable adjustments to horizontal and vertical ## Usage ```jsx -import { AlignmentMatrixControl } from '@wordpress/components'; +import { __experimentalAlignmentMatrixControl as AlignmentMatrixControl } from '@wordpress/components'; import { useState } from '@wordpress/element'; const Example = () => { - const [ alignment, setAlignment ] = useState( 'center center' ); + const [alignment, setAlignment] = useState('center center'); return ( - <AlignmentMatrixControl value={ alignment } onChange={ setAlignment } /> + <AlignmentMatrixControl + value={alignment} + onChange={(newAlignment) => setAlignment(newAlignment)} + /> ); }; ``` + +## Props + +The component accepts the following props: +### className + +The class that will be added with "component-alignment-matrix-control" to the classes of the wrapper <Composite/> component. +If no className is passed only "component-alignment-matrix-control" is used. + +- Type: `String` +- Required: No + +### id + +Unique ID for the component. +- Type: `String` +- Required: No +### label + +If provided, sets the aria-label attribute of the wrapper <Composite/> component. + +- Type: `String` +- Required: No +- Default: `Alignment Matrix Control` +### defaultValue + +If provided, sets the default alignment value. +- Type: `String` +- Required: No +- Default: `center center` + +### onChange + +A function that receives the updated alignment value. + +- Type: `function` +- Required: No +- Default: `noop` +### width + +If provided, sets the width of the wrapper <Composite/> component. + - Type: `Number` + - Required: No + - Default: `92` diff --git a/packages/components/src/animate/index.js b/packages/components/src/animate/index.js index 95fba645fd178..1ef1e272dbb45 100644 --- a/packages/components/src/animate/index.js +++ b/packages/components/src/animate/index.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; * @typedef {'left' | 'right'} SlideInOrigin * @typedef {{ type: 'appear'; origin?: AppearOrigin }} AppearOptions * @typedef {{ type: 'slide-in'; origin?: SlideInOrigin }} SlideInOptions - * @typedef {{ type: 'loading'; }} LoadingOptions + * @typedef {{ type: 'loading' }} LoadingOptions * @typedef {AppearOptions | SlideInOptions | LoadingOptions} GetAnimateOptions */ diff --git a/packages/components/src/base-field/hook.js b/packages/components/src/base-field/hook.js index 14cb90c9e97eb..e0eaf01dc5c9c 100644 --- a/packages/components/src/base-field/hook.js +++ b/packages/components/src/base-field/hook.js @@ -23,7 +23,7 @@ import { useCx } from '../utils/hooks/use-cx'; /** @typedef {import('../flex/types').FlexProps & OwnProps} Props */ /** - * @param {import('../ui/context').PolymorphicComponentProps<Props, 'div'>} props + * @param {import('../ui/context').WordPressComponentProps<Props, 'div'>} props */ export function useBaseField( props ) { const { diff --git a/packages/components/src/card/card-body/component.js b/packages/components/src/card/card-body/component.js index 455ddc85ef5b2..73e2e337b4beb 100644 --- a/packages/components/src/card/card-body/component.js +++ b/packages/components/src/card/card-body/component.js @@ -7,8 +7,8 @@ import { View } from '../../view'; import { useCardBody } from './hook'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').BodyProps, 'div'>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').BodyProps, 'div'>} props + * @param {import('react').Ref<any>} forwardedRef */ function CardBody( props, forwardedRef ) { const { isScrollable, ...otherProps } = useCardBody( props ); diff --git a/packages/components/src/card/card-body/hook.js b/packages/components/src/card/card-body/hook.js index f7fdab10a0304..2e62181b60984 100644 --- a/packages/components/src/card/card-body/hook.js +++ b/packages/components/src/card/card-body/hook.js @@ -11,7 +11,7 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').BodyProps, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').BodyProps, 'div'>} props */ export function useCardBody( props ) { const { diff --git a/packages/components/src/card/card-divider/component.js b/packages/components/src/card/card-divider/component.js index d3cce90bc02ba..058cc341633c2 100644 --- a/packages/components/src/card/card-divider/component.js +++ b/packages/components/src/card/card-divider/component.js @@ -6,8 +6,8 @@ import { Divider } from '../../divider'; import { useCardDivider } from './hook'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../../divider').DividerProps, 'hr', false>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../../divider').DividerProps, 'hr', false>} props + * @param {import('react').Ref<any>} forwardedRef */ function CardDivider( props, forwardedRef ) { const dividerProps = useCardDivider( props ); diff --git a/packages/components/src/card/card-divider/hook.js b/packages/components/src/card/card-divider/hook.js index 06961607c7884..091f99a12d079 100644 --- a/packages/components/src/card/card-divider/hook.js +++ b/packages/components/src/card/card-divider/hook.js @@ -11,7 +11,7 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../../divider').DividerProps, 'hr', false>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../../divider').DividerProps, 'hr', false>} props */ export function useCardDivider( props ) { const { className, ...otherProps } = useContextSystem( diff --git a/packages/components/src/card/card-footer/component.js b/packages/components/src/card/card-footer/component.js index b5ef215eb3964..0b0fa967a7d6c 100644 --- a/packages/components/src/card/card-footer/component.js +++ b/packages/components/src/card/card-footer/component.js @@ -6,8 +6,8 @@ import { Flex } from '../../flex'; import { useCardFooter } from './hook'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FooterProps, 'div'>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FooterProps, 'div'>} props + * @param {import('react').Ref<any>} forwardedRef */ function CardFooter( props, forwardedRef ) { const footerProps = useCardFooter( props ); diff --git a/packages/components/src/card/card-footer/hook.js b/packages/components/src/card/card-footer/hook.js index 772d0fc175b40..9c5ba8da75532 100644 --- a/packages/components/src/card/card-footer/hook.js +++ b/packages/components/src/card/card-footer/hook.js @@ -11,7 +11,7 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FooterProps, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FooterProps, 'div'>} props */ export function useCardFooter( props ) { const { diff --git a/packages/components/src/card/card-header/component.js b/packages/components/src/card/card-header/component.js index 621448d82856a..d08a2e3438ea1 100644 --- a/packages/components/src/card/card-header/component.js +++ b/packages/components/src/card/card-header/component.js @@ -6,8 +6,8 @@ import { Flex } from '../../flex'; import { useCardHeader } from './hook'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').HeaderProps, 'div'>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').HeaderProps, 'div'>} props + * @param {import('react').Ref<any>} forwardedRef */ function CardHeader( props, forwardedRef ) { const headerProps = useCardHeader( props ); diff --git a/packages/components/src/card/card-header/hook.js b/packages/components/src/card/card-header/hook.js index 56a9b99972cab..3c72511d6bff4 100644 --- a/packages/components/src/card/card-header/hook.js +++ b/packages/components/src/card/card-header/hook.js @@ -11,7 +11,7 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').HeaderProps, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').HeaderProps, 'div'>} props */ export function useCardHeader( props ) { const { diff --git a/packages/components/src/card/card-media/hook.js b/packages/components/src/card/card-media/hook.js index 266e112090af1..ec42aabc09155 100644 --- a/packages/components/src/card/card-media/hook.js +++ b/packages/components/src/card/card-media/hook.js @@ -11,7 +11,7 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<{ children: import('react').ReactNode }, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<{ children: import('react').ReactNode }, 'div'>} props */ export function useCardMedia( props ) { const { className, ...otherProps } = useContextSystem( props, 'CardMedia' ); diff --git a/packages/components/src/card/card/component.js b/packages/components/src/card/card/component.js index 9f8bcbde6d9bf..45afbc28de136 100644 --- a/packages/components/src/card/card/component.js +++ b/packages/components/src/card/card/component.js @@ -20,8 +20,8 @@ import CONFIG from '../../utils/config-values'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').Props, 'div'>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').Props, 'div'>} props + * @param {import('react').Ref<any>} forwardedRef */ function Card( props, forwardedRef ) { const { diff --git a/packages/components/src/card/card/hook.js b/packages/components/src/card/card/hook.js index b0e2d1a32325d..d5a5cf0b47841 100644 --- a/packages/components/src/card/card/hook.js +++ b/packages/components/src/card/card/hook.js @@ -13,10 +13,10 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').Props, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').Props, 'div'>} props */ function useDeprecatedProps( { elevation, isElevated, ...otherProps } ) { - /**@type {import('../../ui/context').PolymorphicComponentProps<import('../types').Props, 'div'>} */ + /**@type {import('../../ui/context').WordPressComponentProps<import('../types').Props, 'div'>} */ const propsToReturn = { ...otherProps, }; @@ -40,7 +40,7 @@ function useDeprecatedProps( { elevation, isElevated, ...otherProps } ) { } /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').Props, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').Props, 'div'>} props */ export function useCard( props ) { const { diff --git a/packages/components/src/color-palette/style.native.scss b/packages/components/src/color-palette/style.native.scss index 3343dda21303a..f398def870d1b 100644 --- a/packages/components/src/color-palette/style.native.scss +++ b/packages/components/src/color-palette/style.native.scss @@ -40,5 +40,5 @@ .customTextAndroid { letter-spacing: 1.25; - font-weight: medium; + font-weight: 500; } diff --git a/packages/components/src/combobox-control/index.js b/packages/components/src/combobox-control/index.js index 5f5eb4ef2ad3d..9d8675aa1095f 100644 --- a/packages/components/src/combobox-control/index.js +++ b/packages/components/src/combobox-control/index.js @@ -62,6 +62,7 @@ function ComboboxControl( { currentOption || null ); const [ isExpanded, setIsExpanded ] = useState( false ); + const [ inputHasFocus, setInputHasFocus ] = useState( false ); const [ inputValue, setInputValue ] = useState( '' ); const inputContainer = useRef(); @@ -139,7 +140,12 @@ function ComboboxControl( { } }; + const onBlur = () => { + setInputHasFocus( false ); + }; + const onFocus = () => { + setInputHasFocus( true ); setIsExpanded( true ); onFilterValueChange( '' ); setInputValue( '' ); @@ -153,7 +159,9 @@ function ComboboxControl( { const text = event.value; setInputValue( text ); onFilterValueChange( text ); - setIsExpanded( true ); + if ( inputHasFocus ) { + setIsExpanded( true ); + } }; const handleOnReset = () => { @@ -228,6 +236,7 @@ function ComboboxControl( { : null } onFocus={ onFocus } + onBlur={ onBlur } isExpanded={ isExpanded } selectedSuggestionIndex={ matchingSuggestions.indexOf( selectedSuggestion diff --git a/packages/components/src/custom-gradient-bar/control-points.js b/packages/components/src/custom-gradient-bar/control-points.js index e5edfc8882014..fc63c5896a587 100644 --- a/packages/components/src/custom-gradient-bar/control-points.js +++ b/packages/components/src/custom-gradient-bar/control-points.js @@ -10,6 +10,7 @@ import { useInstanceId } from '@wordpress/compose'; import { useEffect, useRef, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { plus } from '@wordpress/icons'; +import { LEFT, RIGHT } from '@wordpress/keycodes'; /** * Internal dependencies @@ -17,7 +18,6 @@ import { plus } from '@wordpress/icons'; import Button from '../button'; import ColorPicker from '../color-picker'; import Dropdown from '../dropdown'; -import KeyboardShortcuts from '../keyboard-shortcuts'; import { VisuallyHidden } from '../visually-hidden'; import { @@ -36,46 +36,11 @@ import { KEYBOARD_CONTROL_POINT_VARIATION, } from './constants'; -function ControlPointKeyboardMove( { value: position, onChange, children } ) { - const shortcuts = { - right( event ) { - // Stop propagation of the key press event to avoid focus moving - // to another editor area. - event.stopPropagation(); - const newPosition = clampPercent( - position + KEYBOARD_CONTROL_POINT_VARIATION - ); - onChange( newPosition ); - }, - left( event ) { - // Stop propagation of the key press event to avoid focus moving - // to another editor area. - event.stopPropagation(); - const newPosition = clampPercent( - position - KEYBOARD_CONTROL_POINT_VARIATION - ); - onChange( newPosition ); - }, - }; - - return ( - <KeyboardShortcuts shortcuts={ shortcuts }> - { children } - </KeyboardShortcuts> - ); -} - -function ControlPointButton( { - isOpen, - position, - color, - onChange, - ...additionalProps -} ) { +function ControlPointButton( { isOpen, position, color, ...additionalProps } ) { const instanceId = useInstanceId( ControlPointButton ); const descriptionId = `components-custom-gradient-picker__control-point-button-description-${ instanceId }`; return ( - <ControlPointKeyboardMove value={ position } onChange={ onChange }> + <> <Button aria-label={ sprintf( // translators: %1$s: gradient position e.g: 70, %2$s: gradient color code e.g: rgb(52,121,151). @@ -104,7 +69,7 @@ function ControlPointButton( { 'Use your left or right arrow keys or drag and drop with the mouse to change the gradient position. Press the button to change the color or remove the control point.' ) } </VisuallyHidden> - </ControlPointKeyboardMove> + </> ); } @@ -208,18 +173,40 @@ function ControlPoints( { ); } } } + onKeyDown={ ( event ) => { + if ( event.keyCode === LEFT ) { + // Stop propagation of the key press event to avoid focus moving + // to another editor area. + event.stopPropagation(); + onChange( + updateControlPointPosition( + controlPoints, + index, + clampPercent( + point.position - + KEYBOARD_CONTROL_POINT_VARIATION + ) + ) + ); + } else if ( event.keyCode === RIGHT ) { + // Stop propagation of the key press event to avoid focus moving + // to another editor area. + event.stopPropagation(); + onChange( + updateControlPointPosition( + controlPoints, + index, + clampPercent( + point.position + + KEYBOARD_CONTROL_POINT_VARIATION + ) + ) + ); + } + } } isOpen={ isOpen } position={ point.position } color={ point.color } - onChange={ ( newPosition ) => { - onChange( - updateControlPointPosition( - controlPoints, - index, - newPosition - ) - ); - } } /> ) } renderContent={ ( { onClose } ) => ( diff --git a/packages/components/src/custom-select-control/index.js b/packages/components/src/custom-select-control/index.js index c78c7f715e868..0c40f485594e7 100644 --- a/packages/components/src/custom-select-control/index.js +++ b/packages/components/src/custom-select-control/index.js @@ -75,14 +75,24 @@ export default function CustomSelectControl( { items, itemToString, onSelectedItemChange, - selectedItem: _selectedItem, + ...( typeof _selectedItem !== 'undefined' && _selectedItem !== null + ? { selectedItem: _selectedItem } + : undefined ), stateReducer, } ); - const controlDescribedBy = describedBy - ? describedBy - : // translators: %s: The selected option. - sprintf( __( 'Currently selected: %s' ), selectedItem.name ); + function getDescribedBy() { + if ( describedBy ) { + return describedBy; + } + + if ( ! selectedItem ) { + return __( 'No selection' ); + } + + // translators: %s: The selected option. + return sprintf( __( 'Currently selected: %s' ), selectedItem.name ); + } const menuProps = getMenuProps( { className: 'components-custom-select-control__menu', @@ -127,7 +137,7 @@ export default function CustomSelectControl( { 'aria-labelledby': undefined, className: 'components-custom-select-control__button', isSmall: true, - describedBy: controlDescribedBy, + describedBy: getDescribedBy(), } ) } > { itemToString( selectedItem ) } diff --git a/packages/components/src/date-time/style.scss b/packages/components/src/date-time/style.scss index 69f5eadb9acf9..5eac3985921e4 100644 --- a/packages/components/src/date-time/style.scss +++ b/packages/components/src/date-time/style.scss @@ -12,6 +12,7 @@ .components-datetime__calendar-help { padding: $grid-unit-20; + min-width: 260px; h4 { margin: 0; @@ -47,6 +48,10 @@ margin-top: 0; margin-bottom: 0; } + + .components-button:focus { + z-index: z-index(".components-button {:focus or .is-primary}"); + } } .components-datetime__date { diff --git a/packages/components/src/dimension-control/sizes.js b/packages/components/src/dimension-control/sizes.js index 4a77ef25ff773..55dcff9698d4e 100644 --- a/packages/components/src/dimension-control/sizes.js +++ b/packages/components/src/dimension-control/sizes.js @@ -10,7 +10,7 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { _x } from '@wordpress/i18n'; /** * Finds the correct size object from the provided sizes @@ -26,23 +26,23 @@ export const findSizeBySlug = ( sizes, slug ) => export default [ { - name: __( 'None' ), + name: _x( 'None', 'Size of a UI element' ), slug: 'none', }, { - name: __( 'Small' ), + name: _x( 'Small', 'Size of a UI element' ), slug: 'small', }, { - name: __( 'Medium' ), + name: _x( 'Medium', 'Size of a UI element' ), slug: 'medium', }, { - name: __( 'Large' ), + name: _x( 'Large', 'Size of a UI element' ), slug: 'large', }, { - name: __( 'Extra Large' ), + name: _x( 'Extra Large', 'Size of a UI element' ), slug: 'xlarge', }, ]; diff --git a/packages/components/src/divider/component.tsx b/packages/components/src/divider/component.tsx index 7204132d707ce..085d2f5f012b7 100644 --- a/packages/components/src/divider/component.tsx +++ b/packages/components/src/divider/component.tsx @@ -9,13 +9,16 @@ import type { Ref } from 'react'; /** * Internal dependencies */ -import { contextConnect, useContextSystem } from '../ui/context'; -import type { PolymorphicComponentProps } from '../ui/context'; +import { + contextConnect, + useContextSystem, + WordPressComponentProps, +} from '../ui/context'; import { DividerView } from './styles'; import type { Props } from './types'; function Divider( - props: PolymorphicComponentProps< Props, 'hr', false >, + props: WordPressComponentProps< Props, 'hr', false >, forwardedRef: Ref< any > ) { const contextProps = useContextSystem( props, 'Divider' ); diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.js index 9f734f31a317c..f2ae27cd91e7d 100644 --- a/packages/components/src/dropdown-menu/index.js +++ b/packages/components/src/dropdown-menu/index.js @@ -173,9 +173,11 @@ function DropdownMenu( { indexOfSet > 0 && indexOfControl === 0, 'is-active': control.isActive, + 'is-icon-only': ! control.title, } ) } icon={ control.icon } + label={ control.label } aria-checked={ control.role === 'menuitemcheckbox' || control.role === 'menuitemradio' diff --git a/packages/components/src/dropdown-menu/style.scss b/packages/components/src/dropdown-menu/style.scss index 958ac9b5a3cae..64303ea9049eb 100644 --- a/packages/components/src/dropdown-menu/style.scss +++ b/packages/components/src/dropdown-menu/style.scss @@ -44,6 +44,11 @@ width: $button-size-small; height: $button-size-small; } + + // If menu items are icon-only, make them stretch only to the icon size. + &.is-icon-only { + width: auto; + } } .components-menu-item__button, @@ -56,25 +61,25 @@ } .components-menu-group { - padding: $grid-unit-15; + padding: $grid-unit-10; margin-top: 0; margin-bottom: 0; - margin-left: -$grid-unit-15; - margin-right: -$grid-unit-15; + margin-left: -$grid-unit-10; + margin-right: -$grid-unit-10; &:first-child { - margin-top: -$grid-unit-15; + margin-top: -$grid-unit-10; } &:last-child { - margin-bottom: -$grid-unit-15; + margin-bottom: -$grid-unit-10; } } .components-menu-group + .components-menu-group { margin-top: 0; border-top: $border-width solid $gray-400; - padding: $grid-unit-15; + padding: $grid-unit-10; .is-alternate & { border-color: $gray-900; diff --git a/packages/components/src/dropdown/style.scss b/packages/components/src/dropdown/style.scss index 3100a67443698..edb425b74a352 100644 --- a/packages/components/src/dropdown/style.scss +++ b/packages/components/src/dropdown/style.scss @@ -4,7 +4,7 @@ .components-dropdown__content { .components-popover__content > div { - padding: $grid-unit-15; + padding: $grid-unit-10; } [role="menuitem"] { diff --git a/packages/components/src/duotone-picker/README.md b/packages/components/src/duotone-picker/README.md index 16d58b016b0c5..51c12cf49ad32 100644 --- a/packages/components/src/duotone-picker/README.md +++ b/packages/components/src/duotone-picker/README.md @@ -7,15 +7,15 @@ import { DuotonePicker, DuotoneSwatch } from '@wordpress/components'; import { useState } from '@wordpress/element'; const DUOTONE_PALETTE = [ - { colors: [ '#8c00b7', '#fcff41' ] name: 'Purple and yellow' slug: 'purple-yellow' }, - { colors: [ '#000097', '#ff4747' ] name: 'Blue and red' slug: 'blue-red' }, + { colors: [ '#8c00b7', '#fcff41' ], name: 'Purple and yellow', slug: 'purple-yellow' }, + { colors: [ '#000097', '#ff4747' ], name: 'Blue and red', slug: 'blue-red' }, ]; const COLOR_PALETTE = [ - { colors: [ '#ff4747' ] name: 'Red' slug: 'red' }, - { colors: [ '#fcff41' ] name: 'Yellow' slug: 'yellow' }, - { colors: [ '#000097' ] name: 'Blue' slug: 'blue' }, - { colors: [ '#8c00b7' ] name: 'Purple' slug: 'purple' }, + { color: '#ff4747', name: 'Red', slug: 'red' }, + { color: '#fcff41', name: 'Yellow', slug: 'yellow' }, + { color: '#000097', name: 'Blue', slug: 'blue' }, + { color: '#8c00b7', name: 'Purple', slug: 'purple' }, ]; const Example = () => { diff --git a/packages/components/src/elevation/hook.js b/packages/components/src/elevation/hook.js index 24f31fef0f753..9256a2b2d99ba 100644 --- a/packages/components/src/elevation/hook.js +++ b/packages/components/src/elevation/hook.js @@ -30,7 +30,7 @@ export function getBoxShadow( value ) { } /** - * @param {import('../ui/context').PolymorphicComponentProps<import('./types').Props, 'div'>} props + * @param {import('../ui/context').WordPressComponentProps<import('./types').Props, 'div'>} props */ export function useElevation( props ) { const { diff --git a/packages/components/src/flex/flex-block/hook.js b/packages/components/src/flex/flex-block/hook.js index 82d0e2abb6e9d..deed81d8f28f7 100644 --- a/packages/components/src/flex/flex-block/hook.js +++ b/packages/components/src/flex/flex-block/hook.js @@ -5,7 +5,7 @@ import { useContextSystem } from '../../ui/context'; import { useFlexItem } from '../flex-item'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FlexBlockProps, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FlexBlockProps, 'div'>} props */ export function useFlexBlock( props ) { const otherProps = useContextSystem( props, 'FlexBlock' ); diff --git a/packages/components/src/flex/flex-item/hook.js b/packages/components/src/flex/flex-item/hook.js index 136661aa7dcfb..2c38e550db32c 100644 --- a/packages/components/src/flex/flex-item/hook.js +++ b/packages/components/src/flex/flex-item/hook.js @@ -12,7 +12,7 @@ import * as styles from '../styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FlexItemProps, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FlexItemProps, 'div'>} props */ export function useFlexItem( props ) { const { diff --git a/packages/components/src/flex/flex/component.js b/packages/components/src/flex/flex/component.js index 70c4e448985e0..31983e0bc4871 100644 --- a/packages/components/src/flex/flex/component.js +++ b/packages/components/src/flex/flex/component.js @@ -7,8 +7,8 @@ import { FlexContext } from './../context'; import { View } from '../../view'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FlexProps, 'div'>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FlexProps, 'div'>} props + * @param {import('react').Ref<any>} forwardedRef */ function Flex( props, forwardedRef ) { const { children, isColumn, ...otherProps } = useFlex( props ); diff --git a/packages/components/src/flex/flex/hook.js b/packages/components/src/flex/flex/hook.js index 35c3c7b10de23..d0826099ec112 100644 --- a/packages/components/src/flex/flex/hook.js +++ b/packages/components/src/flex/flex/hook.js @@ -20,8 +20,8 @@ import { useCx, rtl } from '../../utils'; /** * - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FlexProps, 'div'>} props - * @return {import('../../ui/context').PolymorphicComponentProps<import('../types').FlexProps, 'div'>} Props with the deprecated props removed. + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FlexProps, 'div'>} props + * @return {import('../../ui/context').WordPressComponentProps<import('../types').FlexProps, 'div'>} Props with the deprecated props removed. */ function useDeprecatedProps( { isReversed, ...otherProps } ) { if ( typeof isReversed !== 'undefined' ) { @@ -39,7 +39,7 @@ function useDeprecatedProps( { isReversed, ...otherProps } ) { } /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').FlexProps, 'div'>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').FlexProps, 'div'>} props */ export function useFlex( props ) { const { diff --git a/packages/components/src/flyout/flyout-content/component.js b/packages/components/src/flyout/flyout-content/component.js index 9c0b41124d317..943fe0d60c81f 100644 --- a/packages/components/src/flyout/flyout-content/component.js +++ b/packages/components/src/flyout/flyout-content/component.js @@ -7,8 +7,8 @@ import { contextConnect, useContextSystem } from '../../ui/context'; /** * - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').ContentProps, 'div', false>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').ContentProps, 'div', false>} props + * @param {import('react').Ref<any>} forwardedRef */ function FlyoutContent( props, forwardedRef ) { const { diff --git a/packages/components/src/flyout/flyout/component.js b/packages/components/src/flyout/flyout/component.js index e1d8de1388856..6d9c4a46153ea 100644 --- a/packages/components/src/flyout/flyout/component.js +++ b/packages/components/src/flyout/flyout/component.js @@ -21,8 +21,8 @@ import { useFlyout } from './hook'; /** * - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').Props, 'div', false>} props - * @param {import('react').Ref<any>} forwardedRef + * @param {import('../../ui/context').WordPressComponentProps<import('../types').Props, 'div', false>} props + * @param {import('react').Ref<any>} forwardedRef */ function Flyout( props, forwardedRef ) { const { diff --git a/packages/components/src/flyout/flyout/hook.js b/packages/components/src/flyout/flyout/hook.js index 4cdb029901975..d917c1011f4b9 100644 --- a/packages/components/src/flyout/flyout/hook.js +++ b/packages/components/src/flyout/flyout/hook.js @@ -10,7 +10,7 @@ import { usePopoverState } from 'reakit'; import { useContextSystem } from '../../ui/context'; /** - * @param {import('../../ui/context').PolymorphicComponentProps<import('../types').Props, 'div', false>} props + * @param {import('../../ui/context').WordPressComponentProps<import('../types').Props, 'div', false>} props */ export function useFlyout( props ) { const { diff --git a/packages/components/src/focal-point-picker/controls.js b/packages/components/src/focal-point-picker/controls.js index ae375a1bc32ce..36991e0560413 100644 --- a/packages/components/src/focal-point-picker/controls.js +++ b/packages/components/src/focal-point-picker/controls.js @@ -60,7 +60,7 @@ function UnitControl( props ) { return ( <BaseUnitControl className="focal-point-picker__controls-position-unit-control" - labelPosition="side" + labelPosition="top" max={ TEXTCONTROL_MAX } min={ TEXTCONTROL_MIN } unit="%" diff --git a/packages/components/src/font-size-picker/index.native.js b/packages/components/src/font-size-picker/index.native.js new file mode 100644 index 0000000000000..1da82363c4789 --- /dev/null +++ b/packages/components/src/font-size-picker/index.native.js @@ -0,0 +1,175 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { useNavigation } from '@react-navigation/native'; +import { useState } from '@wordpress/element'; +import { Icon, chevronRight, check } from '@wordpress/icons'; +import { __, sprintf } from '@wordpress/i18n'; +import { BottomSheet } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { default as UnitControl, useCustomUnits } from '../unit-control'; +import styles from './style.scss'; + +function FontSizePicker( { + fontSizes = [], + disableCustomFontSizes = false, + onChange, + value: selectedValue, +} ) { + const [ showSubSheet, setShowSubSheet ] = useState( false ); + const navigation = useNavigation(); + + const onChangeValue = ( value ) => { + return () => { + goBack(); + onChange( value ); + }; + }; + + const selectedOption = fontSizes.find( + ( option ) => option.size === selectedValue + ) ?? { name: 'Custom' }; + + const goBack = () => { + setShowSubSheet( false ); + navigation.goBack(); + }; + + const openSubSheet = () => { + navigation.navigate( BottomSheet.SubSheet.screenName ); + setShowSubSheet( true ); + }; + const label = __( 'Font Size' ); + + const units = useCustomUnits( { + availableUnits: [ 'px', 'em', 'rem' ], + } ); + + return ( + <BottomSheet.SubSheet + navigationButton={ + <BottomSheet.Cell + label={ label } + separatorType="none" + value={ + selectedValue + ? sprintf( + // translators: %1$s: Select control font size name e.g. Small, %2$s: Select control font size e.g. 12px + __( '%1$s (%2$s)' ), + selectedOption.name, + selectedValue + ) + : __( 'Default' ) + } + onPress={ openSubSheet } + accessibilityRole={ 'button' } + accessibilityLabel={ selectedOption.name } + accessibilityHint={ sprintf( + // translators: %s: Select control button label e.g. Small + __( 'Navigates to select %s' ), + selectedOption.name + ) } + > + <Icon icon={ chevronRight }></Icon> + </BottomSheet.Cell> + } + showSheet={ showSubSheet } + > + <> + <BottomSheet.NavBar> + <BottomSheet.NavBar.BackButton onPress={ goBack } /> + <BottomSheet.NavBar.Heading> + { label } + </BottomSheet.NavBar.Heading> + </BottomSheet.NavBar> + <View style={ styles[ 'components-font-size-picker' ] }> + <BottomSheet.Cell + customActionButton + separatorType="none" + label={ __( 'Default' ) } + onPress={ onChangeValue( undefined ) } + leftAlign={ true } + key={ 'default' } + accessibilityRole={ 'button' } + accessibilityLabel={ __( 'Selected: Default' ) } + accessibilityHint={ __( + 'Double tap to select default font size' + ) } + > + <View> + { selectedValue === undefined && ( + <Icon icon={ check }></Icon> + ) } + </View> + </BottomSheet.Cell> + { fontSizes.map( ( item, index ) => { + // Only display a choice that we can currenly select. + if ( ! parseFloat( item.size ) ) { + return null; + } + return ( + <BottomSheet.Cell + customActionButton + separatorType="none" + label={ item.name } + subLabel={ item.size } + onPress={ onChangeValue( item.size ) } + leftAlign={ true } + key={ index } + accessibilityRole={ 'button' } + accessibilityLabel={ + item.size === selectedValue + ? sprintf( + // translators: %s: Select font size option value e.g: "Selected: Large". + __( 'Selected: %s' ), + item.name + ) + : item.name + } + accessibilityHint={ __( + 'Double tap to select font size' + ) } + > + <View> + { item.size === selectedValue && ( + <Icon icon={ check }></Icon> + ) } + </View> + </BottomSheet.Cell> + ); + } ) } + { ! disableCustomFontSizes && ( + <UnitControl + label={ __( 'Custom' ) } + min={ 0 } + max={ 200 } + step={ 1 } + value={ selectedValue } + onChange={ ( nextSize ) => { + if ( + 0 === parseFloat( nextSize ) || + ! nextSize + ) { + onChange( undefined ); + } else { + onChange( nextSize ); + } + } } + units={ units } + /> + ) } + </View> + </> + </BottomSheet.SubSheet> + ); +} + +export default FontSizePicker; diff --git a/packages/components/src/font-size-picker/style.native.scss b/packages/components/src/font-size-picker/style.native.scss new file mode 100644 index 0000000000000..746e62b5bb361 --- /dev/null +++ b/packages/components/src/font-size-picker/style.native.scss @@ -0,0 +1,6 @@ +.components-font-size-picker { + padding: 0 $block-edge-to-content; +} +.components-font-size-picker__font-size { + font-size: 11px; +} diff --git a/packages/components/src/grid/hook.js b/packages/components/src/grid/hook.js index f2c80c49bd745..b38b609f99b48 100644 --- a/packages/components/src/grid/hook.js +++ b/packages/components/src/grid/hook.js @@ -18,7 +18,7 @@ import CONFIG from '../utils/config-values'; import { useCx } from '../utils/hooks/use-cx'; /** - * @param {import('../ui/context').PolymorphicComponentProps<import('./types').Props, 'div'>} props + * @param {import('../ui/context').WordPressComponentProps<import('./types').Props, 'div'>} props */ export default function useGrid( props ) { const { diff --git a/packages/components/src/guide/index.js b/packages/components/src/guide/index.js index dc0a4f22613e5..bc29f5ad274f4 100644 --- a/packages/components/src/guide/index.js +++ b/packages/components/src/guide/index.js @@ -9,12 +9,12 @@ import classnames from 'classnames'; import { useState, useEffect, Children } from '@wordpress/element'; import deprecated from '@wordpress/deprecated'; import { __ } from '@wordpress/i18n'; +import { LEFT, RIGHT } from '@wordpress/keycodes'; /** * Internal dependencies */ import Modal from '../modal'; -import KeyboardShortcuts from '../keyboard-shortcuts'; import Button from '../button'; import PageControl from './page-control'; import FinishButton from './finish-button'; @@ -66,15 +66,14 @@ export default function Guide( { className={ classnames( 'components-guide', className ) } contentLabel={ contentLabel } onRequestClose={ onFinish } + onKeyDown={ ( event ) => { + if ( event.keyCode === LEFT ) { + goBack(); + } else if ( event.keyCode === RIGHT ) { + goForward(); + } + } } > - <KeyboardShortcuts - key={ currentPage } - shortcuts={ { - left: goBack, - right: goForward, - } } - /> - <div className="components-guide__container"> <div className="components-guide__page"> { pages[ currentPage ].image } diff --git a/packages/components/src/h-stack/hook.js b/packages/components/src/h-stack/hook.js index ceadb162e307b..dd4795776f953 100644 --- a/packages/components/src/h-stack/hook.js +++ b/packages/components/src/h-stack/hook.js @@ -8,7 +8,7 @@ import { getValidChildren } from '../ui/utils/get-valid-children'; /** * - * @param {import('../ui/context').PolymorphicComponentProps<import('./types').Props, 'div'>} props + * @param {import('../ui/context').WordPressComponentProps<import('./types').Props, 'div'>} props */ export function useHStack( props ) { const { diff --git a/packages/components/src/heading/hook.ts b/packages/components/src/heading/hook.ts index c967b9f0d4ac7..238e4e244c0f1 100644 --- a/packages/components/src/heading/hook.ts +++ b/packages/components/src/heading/hook.ts @@ -1,8 +1,7 @@ /** * Internal dependencies */ -import { useContextSystem } from '../ui/context'; -import type { PolymorphicComponentProps } from '../ui/context'; +import { useContextSystem, WordPressComponentProps } from '../ui/context'; import type { Props as TextProps } from '../text/types'; import { useText } from '../text'; import { getHeadingFontSize } from '../ui/utils/font-size'; @@ -50,7 +49,7 @@ export interface HeadingProps extends Omit< TextProps, 'size' > { } export function useHeading( - props: PolymorphicComponentProps< HeadingProps, 'h1' > + props: WordPressComponentProps< HeadingProps, 'h1' > ) { const { as: asProp, level = 2, ...otherProps } = useContextSystem( props, diff --git a/packages/components/src/higher-order/navigate-regions/index.js b/packages/components/src/higher-order/navigate-regions/index.js index db9fc877e9467..ca8cc47361d13 100644 --- a/packages/components/src/higher-order/navigate-regions/index.js +++ b/packages/components/src/higher-order/navigate-regions/index.js @@ -1,19 +1,39 @@ /** * WordPress dependencies */ -import { useCallback, useState, useRef, useEffect } from '@wordpress/element'; +import { useState, useRef } from '@wordpress/element'; import { createHigherOrderComponent, - useKeyboardShortcut, + useRefEffect, + useMergeRefs, } from '@wordpress/compose'; -import { rawShortcut } from '@wordpress/keycodes'; +import { isKeyboardEvent } from '@wordpress/keycodes'; const defaultShortcuts = { - previous: [ 'ctrl+shift+`', rawShortcut.access( 'p' ) ], - next: [ 'ctrl+`', rawShortcut.access( 'n' ) ], + previous: [ + { + modifier: 'ctrlShift', + character: '`', + }, + { + modifier: 'access', + character: 'p', + }, + ], + next: [ + { + modifier: 'ctrl', + character: '`', + }, + { + modifier: 'access', + character: 'n', + }, + ], }; -export function useNavigateRegions( ref, shortcuts = defaultShortcuts ) { +export function useNavigateRegions( shortcuts = defaultShortcuts ) { + const ref = useRef(); const [ isFocusingRegions, setIsFocusingRegions ] = useState( false ); function focusRegion( offset ) { @@ -37,42 +57,48 @@ export function useNavigateRegions( ref, shortcuts = defaultShortcuts ) { nextRegion.focus(); setIsFocusingRegions( true ); } - const focusPrevious = useCallback( () => focusRegion( -1 ), [] ); - const focusNext = useCallback( () => focusRegion( 1 ), [] ); - useKeyboardShortcut( shortcuts.previous, focusPrevious, { - bindGlobal: true, - } ); - useKeyboardShortcut( shortcuts.next, focusNext, { bindGlobal: true } ); + const clickRef = useRefEffect( + ( element ) => { + function onClick() { + setIsFocusingRegions( false ); + } - useEffect( () => { - function onClick() { - setIsFocusingRegions( false ); - } - - ref.current.addEventListener( 'click', onClick ); + element.addEventListener( 'click', onClick ); - return () => { - ref.current?.removeEventListener( 'click', onClick ); - }; - }, [ setIsFocusingRegions ] ); + return () => { + element.removeEventListener( 'click', onClick ); + }; + }, + [ setIsFocusingRegions ] + ); - if ( ! isFocusingRegions ) { - return; - } - - return 'is-focusing-regions'; + return { + ref: useMergeRefs( [ ref, clickRef ] ), + className: isFocusingRegions ? 'is-focusing-regions' : '', + onKeyDown( event ) { + if ( + shortcuts.previous.some( ( { modifier, character } ) => { + return isKeyboardEvent[ modifier ]( event, character ); + } ) + ) { + focusRegion( -1 ); + } else if ( + shortcuts.next.some( ( { modifier, character } ) => { + return isKeyboardEvent[ modifier ]( event, character ); + } ) + ) { + focusRegion( 1 ); + } + }, + }; } export default createHigherOrderComponent( - ( Component ) => ( { shortcuts, ...props } ) => { - const ref = useRef(); - const className = useNavigateRegions( ref, shortcuts ); - return ( - <div ref={ ref } className={ className }> - <Component { ...props } /> - </div> - ); - }, + ( Component ) => ( { shortcuts, ...props } ) => ( + <div { ...useNavigateRegions( shortcuts ) }> + <Component { ...props } /> + </div> + ), 'navigateRegions' ); diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index e8aef964cc1a6..626c3f03da0ba 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -30,6 +30,7 @@ export { Fill, Provider as SlotFillProvider, } from './slot-fill'; +export { default as FontSizePicker } from './font-size-picker'; // Intentionally called after slot-fill. export { default as __experimentalStyleProvider } from './style-provider'; export { default as BaseControl } from './base-control'; export { default as TextareaControl } from './textarea-control'; diff --git a/packages/components/src/input-control/input-field.tsx b/packages/components/src/input-control/input-field.tsx index 02455733de03c..88596a8853e07 100644 --- a/packages/components/src/input-control/input-field.tsx +++ b/packages/components/src/input-control/input-field.tsx @@ -22,7 +22,7 @@ import { UP, DOWN, ENTER } from '@wordpress/keycodes'; /** * Internal dependencies */ -import type { PolymorphicComponentProps } from '../ui/context'; +import type { WordPressComponentProps } from '../ui/context'; import { useDragCursor } from './utils'; import { Input } from './styles/input-control-styles'; import { useInputControlStateReducer } from './reducer/reducer'; @@ -53,7 +53,7 @@ function InputField( value: valueProp, type, ...props - }: PolymorphicComponentProps< InputFieldProps, 'input', false >, + }: WordPressComponentProps< InputFieldProps, 'input', false >, ref: Ref< HTMLInputElement > ) { const { diff --git a/packages/components/src/input-control/label.tsx b/packages/components/src/input-control/label.tsx index 537c335373b5d..ea214707b012b 100644 --- a/packages/components/src/input-control/label.tsx +++ b/packages/components/src/input-control/label.tsx @@ -3,7 +3,7 @@ */ import { VisuallyHidden } from '../visually-hidden'; import { Label as BaseLabel } from './styles/input-control-styles'; -import type { PolymorphicComponentProps } from '../ui/context'; +import type { WordPressComponentProps } from '../ui/context'; import type { InputControlLabelProps } from './types'; export default function Label( { @@ -11,7 +11,7 @@ export default function Label( { hideLabelFromVision, htmlFor, ...props -}: PolymorphicComponentProps< InputControlLabelProps, 'label', false > ) { +}: WordPressComponentProps< InputControlLabelProps, 'label', false > ) { if ( ! children ) return null; if ( hideLabelFromVision ) { diff --git a/packages/components/src/input-control/reducer/actions.ts b/packages/components/src/input-control/reducer/actions.ts index 652e7211e251e..66ca918de7707 100644 --- a/packages/components/src/input-control/reducer/actions.ts +++ b/packages/components/src/input-control/reducer/actions.ts @@ -44,10 +44,7 @@ export type DragEndAction = Action< typeof DRAG_END, DragProps >; export type DragAction = Action< typeof DRAG, DragProps >; export type ResetAction = Action< typeof RESET, Partial< ValuePayload > >; export type UpdateAction = Action< typeof UPDATE, ValuePayload >; -export type InvalidateAction = Action< - typeof INVALIDATE, - { error: Error | null } ->; +export type InvalidateAction = Action< typeof INVALIDATE, { error: unknown } >; export type ChangeEventAction = | ChangeAction diff --git a/packages/components/src/input-control/reducer/reducer.ts b/packages/components/src/input-control/reducer/reducer.ts index 1aad3f8d3e672..362739e758a42 100644 --- a/packages/components/src/input-control/reducer/reducer.ts +++ b/packages/components/src/input-control/reducer/reducer.ts @@ -213,7 +213,7 @@ export function useInputControlStateReducer( * Actions for the reducer */ const change = createChangeEvent( actions.CHANGE ); - const invalidate = ( error: Error, event: SyntheticEvent ) => + const invalidate = ( error: unknown, event: SyntheticEvent ) => dispatch( { type: actions.INVALIDATE, payload: { error, event } } ); const reset = createChangeEvent( actions.RESET ); const commit = createChangeEvent( actions.COMMIT ); diff --git a/packages/components/src/input-control/reducer/state.ts b/packages/components/src/input-control/reducer/state.ts index 9fcbfad2d1ae2..455cce5f00f1d 100644 --- a/packages/components/src/input-control/reducer/state.ts +++ b/packages/components/src/input-control/reducer/state.ts @@ -11,7 +11,7 @@ import type { InputAction } from './actions'; export interface InputState { _event: Event | {}; - error: Error | null; + error: unknown; initialValue?: string; isDirty: boolean; isDragEnabled: boolean; diff --git a/packages/components/src/input-control/styles/input-control-styles.tsx b/packages/components/src/input-control/styles/input-control-styles.tsx index bbd518ccb4822..c6471fa96ead5 100644 --- a/packages/components/src/input-control/styles/input-control-styles.tsx +++ b/packages/components/src/input-control/styles/input-control-styles.tsx @@ -9,7 +9,7 @@ import type { CSSProperties, ReactNode } from 'react'; /** * Internal dependencies */ -import type { PolymorphicComponentProps } from '../../ui/context'; +import type { WordPressComponentProps } from '../../ui/context'; import { Flex, FlexItem } from '../../flex'; import { Text } from '../../text'; import { COLORS, rtl } from '../../utils'; @@ -251,7 +251,7 @@ const BaseLabel = styled( Text )< { labelPosition?: LabelPosition } >` `; export const Label = ( - props: PolymorphicComponentProps< + props: WordPressComponentProps< { labelPosition?: LabelPosition; children: ReactNode }, 'label', false diff --git a/packages/components/src/input-control/types.ts b/packages/components/src/input-control/types.ts index e166ffd6219ba..971075e56a5ee 100644 --- a/packages/components/src/input-control/types.ts +++ b/packages/components/src/input-control/types.ts @@ -15,7 +15,7 @@ import type { useDrag } from 'react-use-gesture'; */ import type { StateReducer } from './reducer/state'; import type { FlexProps } from '../flex/types'; -import type { PolymorphicComponentProps } from '../ui/context'; +import type { WordPressComponentProps } from '../ui/context'; export type LabelPosition = 'top' | 'bottom' | 'side' | 'edge'; @@ -65,9 +65,9 @@ export interface InputBaseProps extends BaseProps, FlexProps { export interface InputControlProps extends Omit< InputBaseProps, 'children' | 'isFocused' >, /** - * The `prefix` prop in `PolymorphicComponentProps< InputFieldProps, 'input', false >` comes from the + * The `prefix` prop in `WordPressComponentProps< InputFieldProps, 'input', false >` comes from the * `HTMLInputAttributes` and clashes with the one from `InputBaseProps`. So we have to omit it from - * `PolymorphicComponentProps< InputFieldProps, 'input', false >` in order that `InputBaseProps[ 'prefix' ]` + * `WordPressComponentProps< InputFieldProps, 'input', false >` in order that `InputBaseProps[ 'prefix' ]` * be the only prefix prop. Otherwise it tries to do a union of the two prefix properties and you end up * with an unresolvable type. * @@ -75,7 +75,7 @@ export interface InputControlProps * for InputField are passed through. */ Omit< - PolymorphicComponentProps< InputFieldProps, 'input', false >, + WordPressComponentProps< InputFieldProps, 'input', false >, 'stateReducer' | 'prefix' | 'isFocused' | 'setIsFocused' > { __unstableStateReducer?: InputFieldProps[ 'stateReducer' ]; diff --git a/packages/components/src/item-group/item-group/component.tsx b/packages/components/src/item-group/item-group/component.tsx index 2b77ec041e055..e58eae897658c 100644 --- a/packages/components/src/item-group/item-group/component.tsx +++ b/packages/components/src/item-group/item-group/component.tsx @@ -7,14 +7,14 @@ import type { Ref } from 'react'; /** * Internal dependencies */ -import { contextConnect, PolymorphicComponentProps } from '../../ui/context'; +import { contextConnect, WordPressComponentProps } from '../../ui/context'; import { useItemGroup } from './hook'; import { ItemGroupContext, useItemGroupContext } from '../context'; import { View } from '../../view'; import type { ItemGroupProps } from '../types'; function ItemGroup( - props: PolymorphicComponentProps< ItemGroupProps, 'div' >, + props: WordPressComponentProps< ItemGroupProps, 'div' >, forwardedRef: Ref< any > ) { const { diff --git a/packages/components/src/item-group/item-group/hook.ts b/packages/components/src/item-group/item-group/hook.ts index 90bb63bae3959..f8ec3740e721c 100644 --- a/packages/components/src/item-group/item-group/hook.ts +++ b/packages/components/src/item-group/item-group/hook.ts @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { useContextSystem, PolymorphicComponentProps } from '../../ui/context'; +import { useContextSystem, WordPressComponentProps } from '../../ui/context'; /** * Internal dependencies @@ -11,7 +11,7 @@ import { useCx } from '../../utils/hooks/use-cx'; import type { ItemGroupProps } from '../types'; export function useItemGroup( - props: PolymorphicComponentProps< ItemGroupProps, 'div' > + props: WordPressComponentProps< ItemGroupProps, 'div' > ) { const { className, diff --git a/packages/components/src/item-group/item/component.tsx b/packages/components/src/item-group/item/component.tsx index 6a0eac90a93c9..18f6ae0eb9953 100644 --- a/packages/components/src/item-group/item/component.tsx +++ b/packages/components/src/item-group/item/component.tsx @@ -9,11 +9,11 @@ import type { Ref } from 'react'; */ import type { ItemProps } from '../types'; import { useItem } from './hook'; -import { contextConnect, PolymorphicComponentProps } from '../../ui/context'; +import { contextConnect, WordPressComponentProps } from '../../ui/context'; import { View } from '../../view'; function Item( - props: PolymorphicComponentProps< ItemProps, 'div' >, + props: WordPressComponentProps< ItemProps, 'div' >, forwardedRef: Ref< any > ) { const { role, wrapperClassName, ...otherProps } = useItem( props ); diff --git a/packages/components/src/item-group/item/hook.ts b/packages/components/src/item-group/item/hook.ts index ac7ad6356ceb3..2ff7574751098 100644 --- a/packages/components/src/item-group/item/hook.ts +++ b/packages/components/src/item-group/item/hook.ts @@ -7,15 +7,13 @@ import type { ElementType } from 'react'; /** * Internal dependencies */ -import { useContextSystem, PolymorphicComponentProps } from '../../ui/context'; +import { useContextSystem, WordPressComponentProps } from '../../ui/context'; import * as styles from '../styles'; import { useItemGroupContext } from '../context'; import { useCx } from '../../utils/hooks/use-cx'; import type { ItemProps } from '../types'; -export function useItem( - props: PolymorphicComponentProps< ItemProps, 'div' > -) { +export function useItem( props: WordPressComponentProps< ItemProps, 'div' > ) { const { isAction = false, as: asProp, diff --git a/packages/components/src/menu-group/style.scss b/packages/components/src/menu-group/style.scss index 202bd74a469c1..d9412c504940b 100644 --- a/packages/components/src/menu-group/style.scss +++ b/packages/components/src/menu-group/style.scss @@ -18,4 +18,5 @@ text-transform: uppercase; font-size: 11px; font-weight: 500; + white-space: nowrap; } diff --git a/packages/components/src/menu-item/style.scss b/packages/components/src/menu-item/style.scss index 0f5badd4ae355..69e5c6f50fd7e 100644 --- a/packages/components/src/menu-item/style.scss +++ b/packages/components/src/menu-item/style.scss @@ -2,6 +2,15 @@ .components-menu-item__button.components-button { width: 100%; + &[role="menuitemradio"], + &[role="menuitemcheckbox"] { + .components-menu-item__item:only-child { + // Ensure unchecked items have clearance for consistency + // with checked items containing an icon or shortcut. + padding-right: $grid-unit-60; + } + } + .components-menu-items__item-icon { margin-right: -2px; // This optically balances the icon. margin-left: $grid-unit-30; @@ -43,7 +52,10 @@ } .components-menu-item__item { + // Provide a minimum width for text items in menus. white-space: nowrap; + min-width: 160px; + margin-right: auto; display: inline-flex; align-items: center; diff --git a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js index 5f2ec0aee7997..bc4a828dd9bdf 100644 --- a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js +++ b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js @@ -70,10 +70,12 @@ const BottomSheetSelectControl = ( { showSheet={ showSubSheet } > <> - <BottomSheet.NavigationHeader - screen={ label } - leftButtonOnPress={ goBack } - /> + <BottomSheet.NavBar> + <BottomSheet.NavBar.BackButton onPress={ goBack } /> + <BottomSheet.NavBar.Heading> + { label } + </BottomSheet.NavBar.Heading> + </BottomSheet.NavBar> <View style={ styles.selectControl }> { items.map( ( item, index ) => ( <BottomSheet.Cell diff --git a/packages/components/src/mobile/bottom-sheet-text-control/index.native.js b/packages/components/src/mobile/bottom-sheet-text-control/index.native.js index c3bcb337d9ad5..820f8e16da296 100644 --- a/packages/components/src/mobile/bottom-sheet-text-control/index.native.js +++ b/packages/components/src/mobile/bottom-sheet-text-control/index.native.js @@ -42,8 +42,6 @@ const BottomSheetTextControl = ( { setShowSubSheet( true ); }; - const [ value, onChangeText ] = useState( initialValue ); - const horizontalBorderStyle = usePreferredColorSchemeStyle( styles.horizontalBorder, styles.horizontalBorderDark @@ -70,16 +68,17 @@ const BottomSheetTextControl = ( { showSheet={ showSubSheet } > <> - <BottomSheet.NavigationHeader - screen={ label } - leftButtonOnPress={ goBack } - /> + <BottomSheet.NavBar> + <BottomSheet.NavBar.BackButton onPress={ goBack } /> + <BottomSheet.NavBar.Heading> + { label } + </BottomSheet.NavBar.Heading> + </BottomSheet.NavBar> <PanelBody style={ horizontalBorderStyle }> <TextInput label={ label } - onChangeText={ ( text ) => onChangeText( text ) } - onChange={ onChange( value ) } - value={ value } + onChangeText={ ( text ) => onChange( text ) } + defaultValue={ initialValue } multiline={ true } placeholder={ placeholder } placeholderTextColor={ '#87a6bc' } diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js index ee5b7d0ace4c4..136e4daef1b22 100644 --- a/packages/components/src/mobile/bottom-sheet/cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/cell.native.js @@ -101,6 +101,7 @@ class BottomSheetCell extends Component { onPress, onLongPress, label, + subLabel, value, valuePlaceholder = '', icon, @@ -147,6 +148,11 @@ class BottomSheetCell extends Component { ? cellLabelStyle : defaultMissingIconAndValue; + const defaultSubLabelStyleText = getStylesFromColorScheme( + styles.cellSubLabelText, + styles.cellSubLabelTextDark + ); + const drawSeparator = ( separatorType && separatorType !== 'none' ) || separatorStyle === undefined; @@ -366,7 +372,22 @@ class BottomSheetCell extends Component { /> </View> ) } - { label && ( + { subLabel && label && ( + <View> + <Text + style={ [ + defaultLabelStyle, + labelStyle, + ] } + > + { label } + </Text> + <Text style={ defaultSubLabelStyleText }> + { subLabel } + </Text> + </View> + ) } + { ! subLabel && label && ( <Text style={ [ defaultLabelStyle, labelStyle ] } > diff --git a/packages/components/src/mobile/bottom-sheet/footer-message-link/footer-message-link.native.js b/packages/components/src/mobile/bottom-sheet/footer-message-link/footer-message-link.native.js index cdd478a958ef0..87a9ae0813fd0 100644 --- a/packages/components/src/mobile/bottom-sheet/footer-message-link/footer-message-link.native.js +++ b/packages/components/src/mobile/bottom-sheet/footer-message-link/footer-message-link.native.js @@ -5,21 +5,22 @@ import { Text, Linking } from 'react-native'; /** * WordPress dependencies */ -import { withPreferredColorScheme } from '@wordpress/compose'; +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; /** * Internal dependencies */ import styles from './styles.scss'; function FooterMessageLink( { href, value } ) { + const textStyle = usePreferredColorSchemeStyle( + styles.footerMessageLink, + styles.footerMessageLinkDark + ); return ( - <Text - style={ styles.footerMessageLink } - onPress={ () => Linking.openURL( href ) } - > + <Text style={ textStyle } onPress={ () => Linking.openURL( href ) }> { value } </Text> ); } -export default withPreferredColorScheme( FooterMessageLink ); +export default FooterMessageLink; diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index b481e67dd2f01..777a17ecb2592 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -42,7 +42,7 @@ import NavigationScreen from './bottom-sheet-navigation/navigation-screen'; import NavigationContainer from './bottom-sheet-navigation/navigation-container'; import KeyboardAvoidingView from './keyboard-avoiding-view'; import BottomSheetSubSheet from './sub-sheet'; -import NavigationHeader from './navigation-header'; +import NavBar from './nav-bar'; import { BottomSheetProvider } from './bottom-sheet-context'; const DEFAULT_LAYOUT_ANIMATION = LayoutAnimation.Presets.easeInEaseOut; @@ -485,6 +485,16 @@ class BottomSheet extends Component { </> ); + const showDragIndicator = () => { + // if iOS or not fullscreen show the drag indicator + if ( Platform.OS === 'ios' || ! this.state.isFullScreen ) { + return true; + } + + // Otherwise check the allowDragIndicator + return this.props.allowDragIndicator; + }; + return ( <Modal isVisible={ isVisible } @@ -536,7 +546,7 @@ class BottomSheet extends Component { style={ styles.header } onLayout={ this.onHeaderLayout } > - { ! ( Platform.OS === 'android' && isFullScreen ) && ( + { showDragIndicator() && ( <View style={ styles.dragIndicator } /> ) } { ! hideHeader && getHeader() } @@ -599,7 +609,7 @@ ThemedBottomSheet.getWidth = getWidth; ThemedBottomSheet.Button = Button; ThemedBottomSheet.Cell = Cell; ThemedBottomSheet.SubSheet = BottomSheetSubSheet; -ThemedBottomSheet.NavigationHeader = NavigationHeader; +ThemedBottomSheet.NavBar = NavBar; ThemedBottomSheet.CyclePickerCell = CyclePickerCell; ThemedBottomSheet.PickerCell = PickerCell; ThemedBottomSheet.SwitchCell = SwitchCell; diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/README.md b/packages/components/src/mobile/bottom-sheet/nav-bar/README.md new file mode 100644 index 0000000000000..76f1c19b14501 --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/README.md @@ -0,0 +1,61 @@ +# BottomSheet Header + +BottomSheet Header components provide styled elements for composing header UI within a `BottomSheet`. + +## Usage + +```jsx +import { BottomSheet } from '@wordpress/components'; + +export default = () => ( + <BottomSheet> + <BottomSheet.NavBar> + <BottomSheet.NavBar.BackButton onPress={ () => {} } /> + <BottomSheet.NavBar.Title>A Sheet Title</BottomSheet.NavBar.Title> + <BottomSheet.NavBar.ApplyButton onPress={ () => {} } /> + </BottomSheet.NavBar> + </BottomSheet> +); +``` + +## BottomSheet.NavBar + +Provides structural styles for left-center-right layout for header UI. + +## BottomSheet.NavBar.Title + +Displays a styled title for a bottom sheet. + +## BottomSheet.NavBar.ApplyButton + +Displays a styled button to apply settings of bottom sheet controls. + +### Props + +#### onPress + +Callback invoked once the button is pressed. + +## BottomSheet.NavBar.BackButton + +Displays a styled button to navigate backwards from a bottom sheet. + +### Props + +#### onPress + +Callback invoked once the button is pressed. + +## BottomSheet.NavBar.DismissButton + +Displays a styled button to dismiss a full screen bottom sheet. + +### Props + +#### onPress + +Callback invoked once the button is pressed. + +#### iosText + +Used to display iOS text if different from "Cancel". \ No newline at end of file diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/action-button.native.js b/packages/components/src/mobile/bottom-sheet/nav-bar/action-button.native.js new file mode 100644 index 0000000000000..6b11f30959fe5 --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/action-button.native.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { View, TouchableWithoutFeedback } from 'react-native'; + +/** + * Internal dependencies + */ +import styles from './styles.scss'; + +// Action button component is used by both Back and Apply Button componenets. +function ActionButton( { + onPress, + accessibilityLabel, + accessibilityHint, + children, +} ) { + return ( + <TouchableWithoutFeedback + onPress={ onPress } + accessibilityRole={ 'button' } + accessibilityLabel={ accessibilityLabel } + accessibilityHint={ accessibilityHint } + > + <View style={ styles[ 'action-button' ] }>{ children }</View> + </TouchableWithoutFeedback> + ); +} + +export default ActionButton; diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/apply-button.native.js b/packages/components/src/mobile/bottom-sheet/nav-bar/apply-button.native.js new file mode 100644 index 0000000000000..a5362f1a67a09 --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/apply-button.native.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { View, Text, Platform } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Icon, check } from '@wordpress/icons'; +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import styles from './styles.scss'; +import ActionButton from './action-button'; + +function ApplyButton( { onPress } ) { + const buttonTextStyle = usePreferredColorSchemeStyle( + styles[ 'button-text' ], + styles[ 'button-text-dark' ] + ); + + const applyButtonStyle = usePreferredColorSchemeStyle( + styles[ 'apply-button-icon' ], + styles[ 'apply-button-icon-dark' ] + ); + + return ( + <View style={ styles[ 'apply-button' ] }> + <ActionButton + onPress={ onPress } + accessibilityLabel={ __( 'Apply' ) } + accessibilityHint={ __( 'Applies the setting' ) } + > + { Platform.OS === 'ios' ? ( + <Text style={ buttonTextStyle } maxFontSizeMultiplier={ 2 }> + { __( 'Apply' ) } + </Text> + ) : ( + <Icon + icon={ check } + size={ 24 } + style={ applyButtonStyle } + /> + ) } + </ActionButton> + </View> + ); +} + +export default ApplyButton; diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/back-button.native.js b/packages/components/src/mobile/bottom-sheet/nav-bar/back-button.native.js new file mode 100644 index 0000000000000..859eba98b9604 --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/back-button.native.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import { View, Platform, Text } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Icon, arrowLeft, close } from '@wordpress/icons'; +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import styles from './styles.scss'; +import ActionButton from './action-button'; +import chevronBack from './../chevron-back'; + +function Button( { onPress, icon, text } ) { + const buttonTextStyle = usePreferredColorSchemeStyle( + styles[ 'button-text' ], + styles[ 'button-text-dark' ] + ); + + return ( + <View style={ styles[ 'back-button' ] }> + <ActionButton + onPress={ onPress } + accessibilityLabel={ __( 'Go back' ) } + accessibilityHint={ __( + 'Navigates to the previous content sheet' + ) } + > + { icon } + { text && ( + <Text style={ buttonTextStyle } maxFontSizeMultiplier={ 2 }> + { text } + </Text> + ) } + </ActionButton> + </View> + ); +} + +function BackButton( { onPress } ) { + const chevronLeftStyle = usePreferredColorSchemeStyle( + styles[ 'chevron-left-icon' ], + styles[ 'chevron-left-icon-dark' ] + ); + const arrowLeftStyle = usePreferredColorSchemeStyle( + styles[ 'arrow-left-icon' ], + styles[ 'arrow-right-icon-dark' ] + ); + + let backIcon; + let backText; + + if ( Platform.OS === 'ios' ) { + backIcon = ( + <Icon icon={ chevronBack } size={ 21 } style={ chevronLeftStyle } /> + ); + backText = __( 'Back' ); + } else { + backIcon = ( + <Icon icon={ arrowLeft } size={ 24 } style={ arrowLeftStyle } /> + ); + } + + return <Button onPress={ onPress } icon={ backIcon } text={ backText } />; +} + +function DismissButton( { onPress, iosText } ) { + const arrowLeftStyle = usePreferredColorSchemeStyle( + styles[ 'arrow-left-icon' ], + styles[ 'arrow-right-icon-dark' ] + ); + + let backIcon; + let backText; + + if ( Platform.OS === 'ios' ) { + backText = iosText ? iosText : __( 'Cancel' ); + } else { + backIcon = <Icon icon={ close } size={ 24 } style={ arrowLeftStyle } />; + } + + return <Button onPress={ onPress } icon={ backIcon } text={ backText } />; +} + +Button.Back = BackButton; +Button.Dismiss = DismissButton; // Cancel or Close Button + +export default Button; diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/heading.native.js b/packages/components/src/mobile/bottom-sheet/nav-bar/heading.native.js new file mode 100644 index 0000000000000..9bbae77dbde5e --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/heading.native.js @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { Text } from 'react-native'; + +/** + * WordPress dependencies + */ +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import styles from './styles.scss'; + +function Heading( { children } ) { + const headingStyle = usePreferredColorSchemeStyle( + styles.heading, + styles[ 'heading-dark' ] + ); + + return ( + <Text + accessibilityRole="header" + style={ headingStyle } + maxFontSizeMultiplier={ 3 } + > + { children } + </Text> + ); +} + +export default Heading; diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/index.native.js b/packages/components/src/mobile/bottom-sheet/nav-bar/index.native.js new file mode 100644 index 0000000000000..57b4c23d4b49a --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/index.native.js @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * Internal dependencies + */ +import ApplyButton from './apply-button'; +import Button from './back-button'; +import Heading from './heading'; +import styles from './styles.scss'; +function NavBar( { children } ) { + return <View style={ styles[ 'nav-bar' ] }>{ children }</View>; +} + +NavBar.ApplyButton = ApplyButton; +NavBar.BackButton = Button.Back; +NavBar.DismissButton = Button.Dismiss; + +NavBar.Heading = Heading; + +export default NavBar; diff --git a/packages/components/src/mobile/bottom-sheet/nav-bar/styles.native.scss b/packages/components/src/mobile/bottom-sheet/nav-bar/styles.native.scss new file mode 100644 index 0000000000000..a6fe92ea37626 --- /dev/null +++ b/packages/components/src/mobile/bottom-sheet/nav-bar/styles.native.scss @@ -0,0 +1,69 @@ +.nav-bar { + align-items: center; + flex-direction: row; + height: 44px; + justify-content: center; +} + +.heading { + color: $light-primary; + text-align: center; + font-weight: 600; + font-size: 16px; + position: absolute; + width: 100%; +} + +.heading-dark { + color: $dark-primary; +} + +.action-button { + align-items: center; + flex-direction: row; + height: 100%; + justify-content: center; + min-width: 44px; + padding-left: $grid-unit-20; + padding-right: $grid-unit-20; +} + +.back-button { + align-items: flex-start; + flex: 1; + justify-content: center; + z-index: 2; +} + +.apply-button { + align-items: flex-end; + flex: 1; + justify-content: center; + z-index: 2; +} + +.button-text { + color: $blue-50; + font-size: 16px; +} + +.button-text-dark { + color: $blue-30; +} + +.chevron-left-icon { + color: $blue-50; + margin-left: -11px; +} + +.chevron-left-icon-dark { + color: $blue-30; +} + +.arrow-left-icon { + color: $gray-60; +} + +.arrow-left-icon-dark { + color: $dark-secondary; +} diff --git a/packages/components/src/mobile/bottom-sheet/navigation-header.native.js b/packages/components/src/mobile/bottom-sheet/navigation-header.native.js deleted file mode 100644 index d0a28e2482c40..0000000000000 --- a/packages/components/src/mobile/bottom-sheet/navigation-header.native.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * External dependencies - */ -import { View, TouchableWithoutFeedback, Text, Platform } from 'react-native'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { check, Icon, arrowLeft, close } from '@wordpress/icons'; -import { usePreferredColorSchemeStyle } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import styles from './styles.scss'; -import chevronBack from './chevron-back'; - -function BottomSheetNavigationHeader( { - leftButtonText, - leftButtonOnPress, - screen, - applyButtonOnPress, - isFullscreen, -} ) { - const isIOS = Platform.OS === 'ios'; - - const bottomSheetHeaderTitleStyle = usePreferredColorSchemeStyle( - styles.bottomSheetHeaderTitle, - styles.bottomSheetHeaderTitleDark - ); - const bottomSheetButtonTextStyle = usePreferredColorSchemeStyle( - styles.bottomSheetButtonText, - styles.bottomSheetButtonTextDark - ); - const chevronLeftStyle = usePreferredColorSchemeStyle( - styles.chevronLeftIcon, - styles.chevronLeftIconDark - ); - const arrowLeftStyle = usePreferredColorSchemeStyle( - styles.arrowLeftIcon, - styles.arrowLeftIconDark - ); - const applyButtonStyle = usePreferredColorSchemeStyle( - styles.applyButton, - styles.applyButtonDark - ); - - const renderBackButton = () => { - let backIcon; - let backText; - - if ( isIOS ) { - backIcon = isFullscreen ? undefined : ( - <Icon - icon={ chevronBack } - size={ 21 } - style={ chevronLeftStyle } - /> - ); - if ( leftButtonText ) { - backText = leftButtonText; - } else if ( isFullscreen ) { - backText = __( 'Cancel' ); - } else { - backText = __( 'Back' ); - } - } else { - backIcon = ( - <Icon - icon={ isFullscreen ? close : arrowLeft } - size={ 24 } - style={ arrowLeftStyle } - /> - ); - } - - return ( - <TouchableWithoutFeedback - onPress={ leftButtonOnPress } - accessibilityRole={ 'button' } - accessibilityLabel={ __( 'Go back' ) } - accessibilityHint={ __( - 'Navigates to the previous content sheet' - ) } - > - <View style={ styles.bottomSheetActionButton }> - <> - { backIcon } - { backText && ( - <Text - style={ bottomSheetButtonTextStyle } - maxFontSizeMultiplier={ 2 } - > - { backText } - </Text> - ) } - </> - </View> - </TouchableWithoutFeedback> - ); - }; - - return ( - <View style={ styles.bottomSheetHeader }> - <View style={ styles.bottomSheetHeaderLeft }> - { renderBackButton() } - </View> - <Text - accessibilityRole="header" - style={ bottomSheetHeaderTitleStyle } - maxFontSizeMultiplier={ 3 } - > - { screen } - </Text> - <View style={ styles.bottomSheetHeaderRight }> - { !! applyButtonOnPress && ( - <TouchableWithoutFeedback - onPress={ applyButtonOnPress } - accessibilityRole={ 'button' } - accessibilityLabel={ __( 'Apply' ) } - accessibilityHint={ __( 'Applies the setting' ) } - > - <View style={ styles.bottomSheetActionButton }> - { isIOS ? ( - <Text - style={ bottomSheetButtonTextStyle } - maxFontSizeMultiplier={ 2 } - > - { __( 'Apply' ) } - </Text> - ) : ( - <Icon - icon={ check } - size={ 24 } - style={ applyButtonStyle } - /> - ) } - </View> - </TouchableWithoutFeedback> - ) } - </View> - </View> - ); -} - -export default BottomSheetNavigationHeader; diff --git a/packages/components/src/mobile/bottom-sheet/range-text-input.native.js b/packages/components/src/mobile/bottom-sheet/range-text-input.native.js index 47885260653de..91556f3cb834e 100644 --- a/packages/components/src/mobile/bottom-sheet/range-text-input.native.js +++ b/packages/components/src/mobile/bottom-sheet/range-text-input.native.js @@ -194,6 +194,7 @@ class RangeTextInput extends Component { } ), { width: 50 * fontScale, + borderRightWidth: children ? 1 : 0, }, ]; diff --git a/packages/components/src/mobile/bottom-sheet/styles.native.scss b/packages/components/src/mobile/bottom-sheet/styles.native.scss index 7d60489fa4eac..39ae11c027100 100644 --- a/packages/components/src/mobile/bottom-sheet/styles.native.scss +++ b/packages/components/src/mobile/bottom-sheet/styles.native.scss @@ -304,3 +304,12 @@ .cellHelpLabelIOS { padding-bottom: $grid-unit-10; } + +.cellSubLabelText { + font-size: 12px; + color: $sub-heading; +} + +.cellSubLabelTextDark { + color: $sub-heading-dark; +} diff --git a/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md b/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md index 592039a8a5b14..1e60bb74ac534 100644 --- a/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md +++ b/packages/components/src/mobile/bottom-sheet/sub-sheet/README.md @@ -49,10 +49,10 @@ const ExampleControl = () => { showSheet={ showSubSheet } > <> - <BottomSheet.NavigationHeader - screen={ 'Howdy' } - leftButtonOnPress={ goBack } - /> + <BottomSheet.NavBar> + <BottomSheet.NavBar.BackButton onPress={ goBack } /> + <BottomSheet.NavBar.Heading>{ 'Howdy' }</BottomSheet.NavBar.Heading> + </BottomSheet.NavBar> <View paddingHorizontal={ 16 }> <Text>{ 'World' }</Text> </View> diff --git a/packages/components/src/mobile/color-settings/gradient-picker-screen.native.js b/packages/components/src/mobile/color-settings/gradient-picker-screen.native.js index 74d4e9d30bda5..c94d9e37a17ec 100644 --- a/packages/components/src/mobile/color-settings/gradient-picker-screen.native.js +++ b/packages/components/src/mobile/color-settings/gradient-picker-screen.native.js @@ -13,7 +13,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import CustomGradientPicker from '../../custom-gradient-picker'; -import NavigationHeader from '../bottom-sheet/navigation-header'; +import NavBar from '../bottom-sheet/nav-bar'; const GradientPickerScreen = () => { const navigation = useNavigation(); @@ -21,10 +21,10 @@ const GradientPickerScreen = () => { const { setColor, currentValue, isGradientColor } = route.params; return ( <View> - <NavigationHeader - screen={ __( 'Customize Gradient' ) } - leftButtonOnPress={ navigation.goBack } - /> + <NavBar> + <NavBar.BackButton onPress={ navigation.goBack } /> + <NavBar.Heading>{ __( 'Customize Gradient' ) }</NavBar.Heading> + </NavBar> <CustomGradientPicker setColor={ setColor } currentValue={ currentValue } diff --git a/packages/components/src/mobile/color-settings/palette.screen.native.js b/packages/components/src/mobile/color-settings/palette.screen.native.js index 33456ee03a4fd..f606b407b8832 100644 --- a/packages/components/src/mobile/color-settings/palette.screen.native.js +++ b/packages/components/src/mobile/color-settings/palette.screen.native.js @@ -20,7 +20,7 @@ import { useRoute, useNavigation } from '@react-navigation/native'; */ import ColorPalette from '../../color-palette'; import ColorIndicator from '../../color-indicator'; -import NavigationHeader from '../bottom-sheet/navigation-header'; +import NavBar from '../bottom-sheet/nav-bar'; import SegmentedControls from '../segmented-control'; import { colorsUtils } from './utils'; @@ -164,10 +164,10 @@ const PaletteScreen = () => { } return ( <View> - <NavigationHeader - screen={ label } - leftButtonOnPress={ navigation.goBack } - /> + <NavBar> + <NavBar.BackButton onPress={ navigation.goBack } /> + <NavBar.Heading>{ label } </NavBar.Heading> + </NavBar> <ColorPalette setColor={ setColor } activeColor={ currentValue } diff --git a/packages/components/src/mobile/focal-point-settings-panel/index.native.js b/packages/components/src/mobile/focal-point-settings-panel/index.native.js index 6b294526f1b0d..879f6c2c4e7d6 100644 --- a/packages/components/src/mobile/focal-point-settings-panel/index.native.js +++ b/packages/components/src/mobile/focal-point-settings-panel/index.native.js @@ -14,7 +14,7 @@ import { BottomSheetContext, FocalPointPicker } from '@wordpress/components'; /** * Internal dependencies */ -import NavigationHeader from '../bottom-sheet/navigation-header'; +import NavBar from '../bottom-sheet/nav-bar'; import styles from './styles.scss'; const FocalPointSettingsPanelMemo = memo( @@ -43,12 +43,17 @@ const FocalPointSettingsPanelMemo = memo( return ( <SafeAreaView style={ styles.safearea }> - <NavigationHeader - screen={ __( 'Edit focal point' ) } - leftButtonOnPress={ () => onButtonPress( 'cancel' ) } - applyButtonOnPress={ () => onButtonPress( 'apply' ) } - isFullscreen - /> + <NavBar> + <NavBar.DismissButton + onPress={ () => onButtonPress( 'cancel' ) } + /> + <NavBar.Heading> + { __( 'Edit focal point' ) } + </NavBar.Heading> + <NavBar.ApplyButton + onPress={ () => onButtonPress( 'apply' ) } + /> + </NavBar> <FocalPointPicker focalPoint={ draftFocalPoint } onChange={ setPosition } diff --git a/packages/components/src/mobile/global-styles-context/index.native.js b/packages/components/src/mobile/global-styles-context/index.native.js index e33e86d636aaf..a0fd4ff32691d 100644 --- a/packages/components/src/mobile/global-styles-context/index.native.js +++ b/packages/components/src/mobile/global-styles-context/index.native.js @@ -15,6 +15,7 @@ import { BLOCK_STYLE_ATTRIBUTES, getBlockPaddings, getBlockColors, + getBlockTypography, } from './utils'; const GlobalStylesContext = createContext( { style: {} } ); @@ -27,7 +28,8 @@ export const getMergedGlobalStyles = ( wrapperPropsStyle, blockAttributes, defaultColors, - blockName + blockName, + fontSizes ) => { const baseGlobalColors = { baseColors: baseGlobalStyles || {}, @@ -60,8 +62,19 @@ export const getMergedGlobalStyles = ( blockStyleAttributes, blockColors ); + const blockTypography = getBlockTypography( + blockStyleAttributes, + fontSizes, + blockName, + baseGlobalStyles + ); - return { ...mergedStyle, ...blockPaddings, ...blockColors }; + return { + ...mergedStyle, + ...blockPaddings, + ...blockColors, + ...blockTypography, + }; }; export const useGlobalStyles = () => { diff --git a/packages/components/src/mobile/global-styles-context/test/utils.native.js b/packages/components/src/mobile/global-styles-context/test/utils.native.js index 7b95e8a41a0c5..3aaf7efe7f977 100644 --- a/packages/components/src/mobile/global-styles-context/test/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/test/utils.native.js @@ -131,12 +131,12 @@ describe( 'getGlobalStyles', () => { color: { palette: RAW_FEATURES.color.palette, gradients, + text: true, + background: true, }, typography: { fontSizes: RAW_FEATURES.typography.fontSizes, - custom: { - 'line-height': RAW_FEATURES.custom[ 'line-height' ], - }, + customLineHeight: RAW_FEATURES.custom[ 'line-height' ], }, }, __experimentalGlobalStylesBaseStyles: PARSED_GLOBAL_STYLES, diff --git a/packages/components/src/mobile/global-styles-context/utils.native.js b/packages/components/src/mobile/global-styles-context/utils.native.js index 5838d3aa7d555..af4ed855983e9 100644 --- a/packages/components/src/mobile/global-styles-context/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/utils.native.js @@ -1,13 +1,14 @@ /** * External dependencies */ -import { find, startsWith, get } from 'lodash'; +import { find, startsWith, get, camelCase, has } from 'lodash'; export const BLOCK_STYLE_ATTRIBUTES = [ 'textColor', 'backgroundColor', 'style', 'color', + 'fontSize', ]; // Mapping style properties name to native @@ -121,6 +122,62 @@ export function getBlockColors( return blockStyles; } +export function getBlockTypography( + blockStyleAttributes, + fontSizes, + blockName, + baseGlobalStyles +) { + const typographyStyles = {}; + const customBlockStyles = blockStyleAttributes?.style?.typography || {}; + const blockGlobalStyles = baseGlobalStyles?.blocks?.[ blockName ]; + + // Global styles + if ( blockGlobalStyles?.typography ) { + const fontSize = blockGlobalStyles?.typography?.fontSize; + const lineHeight = blockGlobalStyles?.typography?.lineHeight; + + if ( fontSize ) { + if ( parseInt( fontSize, 10 ) ) { + typographyStyles.fontSize = fontSize; + } else { + const mappedFontSize = find( fontSizes, { + slug: fontSize, + } ); + + if ( mappedFontSize ) { + typographyStyles.fontSize = mappedFontSize?.size; + } + } + } + + if ( lineHeight ) { + typographyStyles.lineHeight = lineHeight; + } + } + + if ( blockStyleAttributes?.fontSize ) { + const mappedFontSize = find( fontSizes, { + slug: blockStyleAttributes?.fontSize, + } ); + + if ( mappedFontSize ) { + typographyStyles.fontSize = mappedFontSize?.size; + } + } + + // Custom styles + if ( customBlockStyles?.fontSize ) { + typographyStyles.fontSize = customBlockStyles?.fontSize; + } + + if ( customBlockStyles?.lineHeight ) { + typographyStyles.lineHeight = customBlockStyles?.lineHeight; + } + + return typographyStyles; +} + export function parseStylesVariables( styles, mappedValues, customValues ) { let stylesBase = styles; const variables = [ 'preset', 'custom' ]; @@ -152,7 +209,15 @@ export function parseStylesVariables( styles, mappedValues, customValues ) { const customValuesData = customValues ?? JSON.parse( stylesBase ); stylesBase = stylesBase.replace( regex, ( _$1, $2 ) => { const path = $2.split( '--' ); - return get( customValuesData, path ); + if ( has( customValuesData, path ) ) { + return get( customValuesData, path ); + } + + // Check for camelcase properties + return get( customValuesData, [ + ...path.slice( 0, path.length - 1 ), + camelCase( path[ path.length - 1 ] ), + ] ); } ); } } ); @@ -161,14 +226,19 @@ export function parseStylesVariables( styles, mappedValues, customValues ) { } export function getMappedValues( features, palette ) { + const typography = features?.typography; const colors = { ...palette?.theme, ...palette?.user }; + const fontSizes = { + ...typography?.fontSizes?.theme, + ...typography?.fontSizes?.user, + }; const mappedValues = { color: { values: colors, slug: 'color', }, 'font-size': { - values: features?.typography?.fontSizes?.theme, + values: fontSizes, slug: 'size', }, }; @@ -203,12 +273,12 @@ export function getGlobalStyles( rawStyles, rawFeatures ) { color: { palette: colors?.palette, gradients, + text: features?.color?.text ?? true, + background: features?.color?.background ?? true, }, typography: { fontSizes: features?.typography?.fontSizes, - custom: { - 'line-height': features?.custom?.[ 'line-height' ], - }, + customLineHeight: features?.custom?.[ 'line-height' ], }, }, __experimentalGlobalStylesBaseStyles: globalStyles, diff --git a/packages/components/src/mobile/link-picker/index.native.js b/packages/components/src/mobile/link-picker/index.native.js index a007795a37450..76797bbd26ef2 100644 --- a/packages/components/src/mobile/link-picker/index.native.js +++ b/packages/components/src/mobile/link-picker/index.native.js @@ -18,7 +18,7 @@ import { usePreferredColorSchemeStyle } from '@wordpress/compose'; * Internal dependencies */ import LinkPickerResults from './link-picker-results'; -import NavigationHeader from '../bottom-sheet/navigation-header'; +import NavBar from '../bottom-sheet/nav-bar'; import styles from './styles.scss'; // this creates a search suggestion for adding a url directly @@ -81,12 +81,11 @@ export const LinkPicker = ( { return ( <SafeAreaView style={ styles.safeArea }> - <NavigationHeader - screen={ __( 'Link to' ) } - leftButtonOnPress={ cancel } - applyButtonOnPress={ onSubmit } - isFullscreen - /> + <NavBar> + <NavBar.DismissButton onPress={ cancel } /> + <NavBar.Heading>{ __( 'Link to' ) }</NavBar.Heading> + <NavBar.ApplyButton onPress={ onSubmit } /> + </NavBar> <View style={ styles.contentContainer }> <BottomSheet.Cell icon={ link } diff --git a/packages/components/src/mobile/segmented-control/index.native.js b/packages/components/src/mobile/segmented-control/index.native.js index b80db26fccd09..2a3071d0963b7 100644 --- a/packages/components/src/mobile/segmented-control/index.native.js +++ b/packages/components/src/mobile/segmented-control/index.native.js @@ -94,6 +94,7 @@ const SegmentedControls = ( { toValue: calculateEndValue( index ), duration: ANIMATION_DURATION, easing: Easing.ease, + useNativeDriver: false, } ).start(); } diff --git a/packages/components/src/modal/frame.js b/packages/components/src/modal/frame.js deleted file mode 100644 index c38d776864e9a..0000000000000 --- a/packages/components/src/modal/frame.js +++ /dev/null @@ -1,113 +0,0 @@ -//@ts-nocheck - -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ - -import { Component } from '@wordpress/element'; -import { ESCAPE } from '@wordpress/keycodes'; -import { - useFocusReturn, - useFocusOnMount, - useConstrainedTabbing, - useMergeRefs, -} from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import withFocusOutside from '../higher-order/with-focus-outside'; - -function ModalFrameContent( { - overlayClassName, - contentLabel, - aria: { describedby, labelledby }, - children, - className, - role, - style, - focusOnMount, - shouldCloseOnEsc, - onRequestClose, -} ) { - function handleEscapeKeyDown( event ) { - if ( - shouldCloseOnEsc && - event.keyCode === ESCAPE && - ! event.defaultPrevented - ) { - event.preventDefault(); - if ( onRequestClose ) { - onRequestClose( event ); - } - } - } - const focusOnMountRef = useFocusOnMount( focusOnMount ); - const constrainedTabbingRef = useConstrainedTabbing(); - const focusReturnRef = useFocusReturn(); - - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions - <div - className={ classnames( - 'components-modal__screen-overlay', - overlayClassName - ) } - onKeyDown={ handleEscapeKeyDown } - > - <div - className={ classnames( 'components-modal__frame', className ) } - style={ style } - ref={ useMergeRefs( [ - constrainedTabbingRef, - focusReturnRef, - focusOnMountRef, - ] ) } - role={ role } - aria-label={ contentLabel } - aria-labelledby={ contentLabel ? null : labelledby } - aria-describedby={ describedby } - tabIndex="-1" - > - { children } - </div> - </div> - ); -} - -class ModalFrame extends Component { - constructor() { - super( ...arguments ); - this.handleFocusOutside = this.handleFocusOutside.bind( this ); - } - - /** - * Callback function called when clicked outside the modal. - * - * @param {Object} event Mouse click event. - */ - handleFocusOutside( event ) { - if ( - this.props.shouldCloseOnClickOutside && - this.props.onRequestClose - ) { - this.props.onRequestClose( event ); - } - } - - /** - * Renders the modal frame element. - * - * @return {WPElement} The modal frame element. - */ - render() { - return <ModalFrameContent { ...this.props } />; - } -} - -export default withFocusOutside( ModalFrame ); diff --git a/packages/components/src/modal/header.js b/packages/components/src/modal/header.js deleted file mode 100644 index ef05861a08ad1..0000000000000 --- a/packages/components/src/modal/header.js +++ /dev/null @@ -1,55 +0,0 @@ -//@ts-nocheck - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { closeSmall } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import Button from '../button'; - -const ModalHeader = ( { - icon, - title, - onClose, - closeLabel, - headingId, - isDismissible, -} ) => { - const label = closeLabel ? closeLabel : __( 'Close dialog' ); - - return ( - <div className="components-modal__header"> - <div className="components-modal__header-heading-container"> - { icon && ( - <span - className="components-modal__icon-container" - aria-hidden - > - { icon } - </span> - ) } - { title && ( - <h1 - id={ headingId } - className="components-modal__header-heading" - > - { title } - </h1> - ) } - </div> - { isDismissible && ( - <Button - onClick={ onClose } - icon={ closeSmall } - label={ label } - /> - ) } - </div> - ); -}; - -export default ModalHeader; diff --git a/packages/components/src/modal/index.js b/packages/components/src/modal/index.js index e689b77171486..90acee41efa5c 100644 --- a/packages/components/src/modal/index.js +++ b/packages/components/src/modal/index.js @@ -1,175 +1,168 @@ //@ts-nocheck +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ -import { Component, createPortal } from '@wordpress/element'; -import { withInstanceId } from '@wordpress/compose'; +import { createPortal, useEffect, useRef } from '@wordpress/element'; +import { + useInstanceId, + useFocusReturn, + useFocusOnMount, + __experimentalUseFocusOutside as useFocusOutside, + useConstrainedTabbing, + useMergeRefs, +} from '@wordpress/compose'; import deprecated from '@wordpress/deprecated'; +import { ESCAPE } from '@wordpress/keycodes'; +import { __ } from '@wordpress/i18n'; +import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies */ -import ModalFrame from './frame'; -import ModalHeader from './header'; import * as ariaHelper from './aria-helper'; +import Button from '../button'; // Used to count the number of open modals. -let parentElement, - openModalCount = 0; - -class Modal extends Component { - constructor( props ) { - super( props ); - this.prepareDOM(); - } - - /** - * Appends the modal's node to the DOM, so the portal can render the - * modal in it. Also calls the openFirstModal when this is the first modal to be - * opened. - */ - componentDidMount() { +let openModalCount = 0; + +export default function Modal( { + bodyOpenClassName = 'modal-open', + role = 'dialog', + title = null, + focusOnMount = true, + shouldCloseOnEsc = true, + shouldCloseOnClickOutside = true, + isDismissable, // Deprecated + isDismissible = isDismissable || true, + /* accessibility */ + aria = { + labelledby: null, + describedby: null, + }, + onRequestClose, + icon, + closeButtonLabel, + children, + style, + overlayClassName, + className, + contentLabel, + onKeyDown, +} ) { + const ref = useRef(); + const instanceId = useInstanceId( Modal ); + const headingId = title + ? `components-modal-header-${ instanceId }` + : aria.labelledby; + const focusOnMountRef = useFocusOnMount( focusOnMount ); + const constrainedTabbingRef = useConstrainedTabbing(); + const focusReturnRef = useFocusReturn(); + const focusOutsideProps = useFocusOutside( onRequestClose ); + + useEffect( () => { openModalCount++; if ( openModalCount === 1 ) { - this.openFirstModal(); + ariaHelper.hideApp( ref.current ); + document.body.classList.add( bodyOpenClassName ); } - } - - /** - * Removes the modal's node from the DOM. Also calls closeLastModal when this is - * the last modal to be closed. - */ - componentWillUnmount() { - openModalCount--; - if ( openModalCount === 0 ) { - this.closeLastModal(); - } - - this.cleanDOM(); + return () => { + openModalCount--; + + if ( openModalCount === 0 ) { + document.body.classList.remove( bodyOpenClassName ); + ariaHelper.showApp(); + } + }; + }, [] ); + + if ( isDismissable ) { + deprecated( 'isDismissable prop of the Modal component', { + since: '5.4', + alternative: 'isDismissible prop (renamed) of the Modal component', + } ); } - /** - * Prepares the DOM for the modals to be rendered. - * - * Every modal is mounted in a separate div appended to a parent div - * that is appended to the document body. - * - * The parent div will be created if it does not yet exist, and the - * separate div for this specific modal will be appended to that. - */ - prepareDOM() { - if ( ! parentElement ) { - parentElement = document.createElement( 'div' ); - document.body.appendChild( parentElement ); + function handleEscapeKeyDown( event ) { + if ( + shouldCloseOnEsc && + event.keyCode === ESCAPE && + ! event.defaultPrevented + ) { + event.preventDefault(); + if ( onRequestClose ) { + onRequestClose( event ); + } } - this.node = document.createElement( 'div' ); - parentElement.appendChild( this.node ); - } - - /** - * Removes the specific mounting point for this modal from the DOM. - */ - cleanDOM() { - parentElement.removeChild( this.node ); - } - - /** - * Prepares the DOM for this modal and any additional modal to be mounted. - * - * It appends an additional div to the body for the modals to be rendered in, - * it hides any other elements from screen-readers and adds an additional class - * to the body to prevent scrolling while the modal is open. - */ - openFirstModal() { - ariaHelper.hideApp( parentElement ); - document.body.classList.add( this.props.bodyOpenClassName ); - } - - /** - * Cleans up the DOM after the last modal is closed and makes the app available - * for screen-readers again. - */ - closeLastModal() { - document.body.classList.remove( this.props.bodyOpenClassName ); - ariaHelper.showApp(); } - /** - * Renders the modal. - * - * @return {WPElement} The modal element. - */ - render() { - const { - onRequestClose, - title, - icon, - closeButtonLabel, - children, - aria, - instanceId, - isDismissible, - isDismissable, //Deprecated - // Many of the documented props for Modal are passed straight through - // to the ModalFrame component and handled there. - ...otherProps - } = this.props; - - const headingId = title - ? `components-modal-header-${ instanceId }` - : aria.labelledby; - - if ( isDismissable ) { - deprecated( 'isDismissable prop of the Modal component', { - since: '5.4', - alternative: - 'isDismissible prop (renamed) of the Modal component', - } ); - } - // Disable reason: this stops mouse events from triggering tooltips and - // other elements underneath the modal overlay. - return createPortal( - <ModalFrame - onRequestClose={ onRequestClose } - aria={ { - labelledby: headingId, - describedby: aria.describedby, - } } - { ...otherProps } + return createPortal( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions + <div + ref={ ref } + className={ classnames( + 'components-modal__screen-overlay', + overlayClassName + ) } + onKeyDown={ handleEscapeKeyDown } + > + <div + className={ classnames( 'components-modal__frame', className ) } + style={ style } + ref={ useMergeRefs( [ + constrainedTabbingRef, + focusReturnRef, + focusOnMountRef, + ] ) } + role={ role } + aria-label={ contentLabel } + aria-labelledby={ contentLabel ? null : headingId } + aria-describedby={ aria.describedby } + tabIndex="-1" + { ...( shouldCloseOnClickOutside ? focusOutsideProps : {} ) } + onKeyDown={ onKeyDown } > <div className={ 'components-modal__content' } role="document"> - <ModalHeader - closeLabel={ closeButtonLabel } - headingId={ title && headingId } - icon={ icon } - isDismissible={ isDismissible || isDismissable } - onClose={ onRequestClose } - title={ title } - /> + <div className="components-modal__header"> + <div className="components-modal__header-heading-container"> + { icon && ( + <span + className="components-modal__icon-container" + aria-hidden + > + { icon } + </span> + ) } + { title && ( + <h1 + id={ headingId } + className="components-modal__header-heading" + > + { title } + </h1> + ) } + </div> + { isDismissible && ( + <Button + onClick={ onRequestClose } + icon={ closeSmall } + label={ + closeButtonLabel || __( 'Close dialog' ) + } + /> + ) } + </div> { children } </div> - </ModalFrame>, - this.node - ); - } + </div> + </div>, + document.body + ); } - -Modal.defaultProps = { - bodyOpenClassName: 'modal-open', - role: 'dialog', - title: null, - focusOnMount: true, - shouldCloseOnEsc: true, - shouldCloseOnClickOutside: true, - isDismissible: true, - /* accessibility */ - aria: { - labelledby: null, - describedby: null, - }, -}; - -export default withInstanceId( Modal ); diff --git a/packages/components/src/navigation/README.md b/packages/components/src/navigation/README.md index 1128874463d57..610b74d77ff1c 100644 --- a/packages/components/src/navigation/README.md +++ b/packages/components/src/navigation/README.md @@ -44,6 +44,15 @@ const MyNavigation = () => ( </Navigation> ); ``` +## CSS Classes leveraged + +The structural CSS for the navigation block targets generic classnames across menu items of multiple types including those automatically generated by the Page List block. Here are some of the notable classnames and what they are used for: + +- `.wp-block-navigation__submenu-container` is applied to submenus to main menu items. +- `.wp-block-navigation-item` is applied to every menu item. +- `.wp-block-navigation-item__content` is applied to the link inside a menu item. +- `.wp-block-navigation-link__label` is applied to the innermost container around the menu item text label. +- `.wp-block-navigation__submenu-icon` is applied to the submenu indicator (chevron). ## Navigation Props diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index a30dfe4fb19fa..88f6be567872f 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -258,6 +258,7 @@ const Popover = ( __unstableBoundaryParent, __unstableForcePosition, __unstableForceXAlignment, + __unstableEditorCanvasWrapper, /* eslint-enable no-unused-vars */ ...contentProps }, @@ -352,7 +353,8 @@ const Popover = ( relativeOffsetTop, boundaryElement, __unstableForcePosition, - __unstableForceXAlignment + __unstableForceXAlignment, + __unstableEditorCanvasWrapper ); if ( diff --git a/packages/components/src/popover/utils.js b/packages/components/src/popover/utils.js index 697a3fceeb6dd..bf389525e4bd8 100644 --- a/packages/components/src/popover/utils.js +++ b/packages/components/src/popover/utils.js @@ -159,18 +159,18 @@ export function computePopoverXAxisPosition( /** * Utility used to compute the popover position over the yAxis * - * @param {Object} anchorRect Anchor Rect. - * @param {Object} contentSize Content Size. - * @param {string} yAxis Desired yAxis. - * @param {string} corner Desired corner. - * @param {boolean} stickyBoundaryElement The boundary element to use when - * switching between sticky and normal - * position. - * @param {Element} anchorRef The anchor element. - * @param {Element} relativeOffsetTop If applicable, top offset of the - * relative positioned parent container. - * @param {boolean} forcePosition Don't adjust position based on anchor. - * + * @param {Object} anchorRect Anchor Rect. + * @param {Object} contentSize Content Size. + * @param {string} yAxis Desired yAxis. + * @param {string} corner Desired corner. + * @param {boolean} stickyBoundaryElement The boundary element to use when switching between sticky + * and normal position. + * @param {Element} anchorRef The anchor element. + * @param {Element} relativeOffsetTop If applicable, top offset of the relative positioned + * parent container. + * @param {boolean} forcePosition Don't adjust position based on anchor. + * @param {Element|null} editorWrapper Element that wraps the editor content. Used to access + * scroll position to determine sticky behavior. * @return {Object} Popover xAxis position and constraints. */ export function computePopoverYAxisPosition( @@ -181,18 +181,47 @@ export function computePopoverYAxisPosition( stickyBoundaryElement, anchorRef, relativeOffsetTop, - forcePosition + forcePosition, + editorWrapper ) { const { height } = contentSize; if ( stickyBoundaryElement ) { const stickyRect = stickyBoundaryElement.getBoundingClientRect(); - const stickyPosition = stickyRect.top + height - relativeOffsetTop; - - if ( anchorRect.top <= stickyPosition ) { + const stickyPositionTop = stickyRect.top + height - relativeOffsetTop; + const stickyPositionBottom = + stickyRect.bottom - height - relativeOffsetTop; + + if ( anchorRect.top <= stickyPositionTop ) { + if ( editorWrapper ) { + // If a popover cannot be positioned above the anchor, even after scrolling, we must + // ensure we use the bottom position instead of the popover slot. This prevents the + // popover from always restricting block content and interaction while selected if the + // block is near the top of the site editor. + + const isRoomAboveInCanvas = + height + HEIGHT_OFFSET < + editorWrapper.scrollTop + anchorRect.top; + if ( ! isRoomAboveInCanvas ) { + return { + yAxis: 'bottom', + // If the bottom of the block is also below the bottom sticky position (ex - + // block is also taller than the editor window), return the bottom sticky + // position instead. We do this instead of the top sticky position both to + // allow a smooth transition and more importantly to ensure every section of + // the block can be free from popover obscuration at some point in the + // scroll position. + popoverTop: Math.min( + anchorRect.bottom, + stickyPositionBottom + ), + }; + } + } + // Default sticky behavior. return { yAxis, - popoverTop: Math.min( anchorRect.bottom, stickyPosition ), + popoverTop: Math.min( anchorRect.bottom, stickyPositionTop ), }; } } @@ -274,22 +303,22 @@ export function computePopoverYAxisPosition( } /** - * Utility used to compute the popover position and the content max width/height - * for a popover given its anchor rect and its content size. - * - * @param {Object} anchorRect Anchor Rect. - * @param {Object} contentSize Content Size. - * @param {string} position Position. - * @param {boolean} stickyBoundaryElement The boundary element to use when - * switching between sticky and normal - * position. - * @param {Element} anchorRef The anchor element. - * @param {number} relativeOffsetTop If applicable, top offset of the - * relative positioned parent container. - * @param {Element} boundaryElement Boundary element. - * @param {boolean} forcePosition Don't adjust position based on anchor. - * @param {boolean} forceXAlignment Don't adjust alignment based on YAxis + * Utility used to compute the popover position and the content max width/height for a popover given + * its anchor rect and its content size. * + * @param {Object} anchorRect Anchor Rect. + * @param {Object} contentSize Content Size. + * @param {string} position Position. + * @param {boolean} stickyBoundaryElement The boundary element to use when switching between + * sticky and normal position. + * @param {Element} anchorRef The anchor element. + * @param {number} relativeOffsetTop If applicable, top offset of the relative positioned + * parent container. + * @param {Element} boundaryElement Boundary element. + * @param {boolean} forcePosition Don't adjust position based on anchor. + * @param {boolean} forceXAlignment Don't adjust alignment based on YAxis + * @param {Element|null} editorWrapper Element that wraps the editor content. Used to access + * scroll position to determine sticky behavior. * @return {Object} Popover position and constraints. */ export function computePopoverPosition( @@ -301,7 +330,8 @@ export function computePopoverPosition( relativeOffsetTop, boundaryElement, forcePosition, - forceXAlignment + forceXAlignment, + editorWrapper ) { const [ yAxis, xAxis = 'center', corner ] = position.split( ' ' ); @@ -313,7 +343,8 @@ export function computePopoverPosition( stickyBoundaryElement, anchorRef, relativeOffsetTop, - forcePosition + forcePosition, + editorWrapper ); const xAxisPosition = computePopoverXAxisPosition( anchorRect, diff --git a/packages/components/src/query-controls/index.native.js b/packages/components/src/query-controls/index.native.js index f7a7c24c12af9..6ba18f646146e 100644 --- a/packages/components/src/query-controls/index.native.js +++ b/packages/components/src/query-controls/index.native.js @@ -61,37 +61,39 @@ const QueryControls = memo( [ order, orderBy, onOrderByChange, onOrderChange ] ); - return [ - onOrderChange && onOrderByChange && ( - <SelectControl - label={ __( 'Order by' ) } - value={ `${ orderBy }/${ order }` } - options={ options } - onChange={ onChange } - hideCancelButton={ true } - /> - ), - onCategoryChange && ( - <CategorySelect - categoriesList={ categoriesList } - label={ __( 'Category' ) } - noOptionLabel={ __( 'All' ) } - selectedCategoryId={ selectedCategoryId } - onChange={ onCategoryChange } - hideCancelButton={ true } - /> - ), - onNumberOfItemsChange && ( - <RangeControl - label={ __( 'Number of items' ) } - value={ numberOfItems } - onChange={ onNumberOfItemsChange } - min={ minItems } - max={ maxItems } - required - /> - ), - ]; + return ( + <> + { onOrderChange && onOrderByChange && ( + <SelectControl + label={ __( 'Order by' ) } + value={ `${ orderBy }/${ order }` } + options={ options } + onChange={ onChange } + hideCancelButton={ true } + /> + ) } + { onCategoryChange && ( + <CategorySelect + categoriesList={ categoriesList } + label={ __( 'Category' ) } + noOptionLabel={ __( 'All' ) } + selectedCategoryId={ selectedCategoryId } + onChange={ onCategoryChange } + hideCancelButton={ true } + /> + ) } + { onNumberOfItemsChange && ( + <RangeControl + label={ __( 'Number of items' ) } + value={ numberOfItems } + onChange={ onNumberOfItemsChange } + min={ minItems } + max={ maxItems } + required + /> + ) } + </> + ); } ); diff --git a/packages/components/src/sandbox/index.native.js b/packages/components/src/sandbox/index.native.js index 3c03e79a84ae7..cde05eeca8fcb 100644 --- a/packages/components/src/sandbox/index.native.js +++ b/packages/components/src/sandbox/index.native.js @@ -92,23 +92,69 @@ const style = ` body > div iframe { width: 100%; } - html.wp-has-aspect-ratio, - body.wp-has-aspect-ratio, - body.wp-has-aspect-ratio > div, - body.wp-has-aspect-ratio > div iframe { - height: auto; - overflow: hidden; /* If it has an aspect ratio, it shouldn't scroll. */ - } body > div > * { margin-top: 0 !important; /* Has to have !important to override inline styles. */ margin-bottom: 0 !important; } + + .wp-block-embed__wrapper { + position: relative; + } + + body.wp-has-aspect-ratio > div iframe { + height: 100%; + overflow: hidden; /* If it has an aspect ratio, it shouldn't scroll. */ + } + + /** + * Add responsiveness to embeds with aspect ratios. + * + * These styles have been copied from the web version (https://git.io/JEFcX) and + * adapted for the native version. + */ + .wp-has-aspect-ratio.wp-block-embed__wrapper::before { + content: ""; + display: block; + padding-top: 50%; // Default to 2:1 aspect ratio. + } + .wp-has-aspect-ratio iframe { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; + width: 100%; + } + .wp-embed-aspect-21-9.wp-block-embed__wrapper::before { + padding-top: 42.85%; // 9 / 21 * 100 + } + .wp-embed-aspect-18-9.wp-block-embed__wrapper::before { + padding-top: 50%; // 9 / 18 * 100 + } + .wp-embed-aspect-16-9.wp-block-embed__wrapper::before { + padding-top: 56.25%; // 9 / 16 * 100 + } + .wp-embed-aspect-4-3.wp-block-embed__wrapper::before { + padding-top: 75%; // 3 / 4 * 100 + } + .wp-embed-aspect-1-1.wp-block-embed__wrapper::before { + padding-top: 100%; // 1 / 1 * 100 + } + .wp-embed-aspect-9-16.wp-block-embed__wrapper::before { + padding-top: 177.77%; // 16 / 9 * 100 + } + .wp-embed-aspect-1-2.wp-block-embed__wrapper::before { + padding-top: 200%; // 2 / 1 * 100 + } `; const EMPTY_ARRAY = []; function Sandbox( { + containerStyle, html = '', + customJS, providerUrl = '', scripts = EMPTY_ARRAY, styles = EMPTY_ARRAY, @@ -117,7 +163,6 @@ function Sandbox( { url, } ) { const ref = useRef(); - const [ width, setWidth ] = useState( 0 ); const [ height, setHeight ] = useState( 0 ); const [ contentHtml, setContentHtml ] = useState( getHtmlDoc() ); @@ -142,7 +187,7 @@ function Sandbox( { // Scripts go into the body rather than the head, to support embedded content such as Instagram // that expect the scripts to be part of the body. const htmlDoc = ( - <html lang={ lang } className={ type }> + <html lang={ lang }> <head> <title>{ title } { scripts.map( ( src ) => ( @@ -207,19 +252,13 @@ function Sandbox( { return; } - setWidth( data.width ); setHeight( data.height ); } function getSizeStyle() { - const contentWidth = Math.ceil( width ); const contentHeight = Math.ceil( height ); - if ( contentWidth && contentHeight ) { - return { width: contentWidth, height: contentHeight }; - } - - return { aspectRatio: 1 }; + return contentHeight ? { height: contentHeight } : { aspectRatio: 1 }; } function onChangeDimensions( dimensions ) { @@ -241,7 +280,6 @@ function Sandbox( { // When device orientation changes we have to recalculate the size, // for this purpose we reset the current size value. if ( wasLandscape.current !== isLandscape ) { - setWidth( 0 ); setHeight( 0 ); } wasLandscape.current = isLandscape; @@ -249,6 +287,10 @@ function Sandbox( { return ( ); } diff --git a/packages/components/src/sandbox/style.native.scss b/packages/components/src/sandbox/style.native.scss index cba25ab95f8ae..d591c6c84a237 100644 --- a/packages/components/src/sandbox/style.native.scss +++ b/packages/components/src/sandbox/style.native.scss @@ -1,3 +1,7 @@ .sandbox-webview__container { + width: 100%; +} + +.sandbox-webview__content { background-color: transparent; } diff --git a/packages/components/src/scrollable/hook.js b/packages/components/src/scrollable/hook.js index bcfab8ef7c927..ce8b01e14724d 100644 --- a/packages/components/src/scrollable/hook.js +++ b/packages/components/src/scrollable/hook.js @@ -12,7 +12,7 @@ import { useCx } from '../utils/hooks/use-cx'; /* eslint-disable jsdoc/valid-types */ /** - * @param {import('../ui/context').PolymorphicComponentProps} props + * @param {import('../ui/context').WordPressComponentProps} props */ /* eslint-enable jsdoc/valid-types */ export function useScrollable( props ) { diff --git a/packages/components/src/search-control/index.native.js b/packages/components/src/search-control/index.native.js index 61241f77fb810..2bf19b8a42b54 100644 --- a/packages/components/src/search-control/index.native.js +++ b/packages/components/src/search-control/index.native.js @@ -8,6 +8,7 @@ import { TouchableOpacity, Platform, useColorScheme, + Keyboard, } from 'react-native'; /** @@ -116,12 +117,20 @@ function SearchControl( { setCurrentStyles( futureStyles ); }, [ isActive, isDark ] ); - useEffect( - () => () => { + useEffect( () => { + const keyboardHideSubscription = Keyboard.addListener( + 'keyboardDidHide', + () => { + if ( ! isIOS ) { + onCancel(); + } + } + ); + return () => { clearTimeout( onCancelTimer.current ); - }, - [] - ); + keyboardHideSubscription.remove(); + }; + }, [] ); const { 'search-control__container': containerStyle, diff --git a/packages/components/src/search-control/platform-style.android.scss b/packages/components/src/search-control/platform-style.android.scss index 1d774af400900..f357c7a4e52fc 100644 --- a/packages/components/src/search-control/platform-style.android.scss +++ b/packages/components/src/search-control/platform-style.android.scss @@ -1,10 +1,7 @@ -.search-control__container { - height: 40px; -} - .search-control__container--active { border-bottom-color: $light-gray-500; border-bottom-width: 1px; + margin-bottom: 0; margin-left: 0; margin-right: 0; border-radius: 0; diff --git a/packages/components/src/search-control/platform-style.ios.scss b/packages/components/src/search-control/platform-style.ios.scss index f083df3e248ff..34884b11b51bc 100644 --- a/packages/components/src/search-control/platform-style.ios.scss +++ b/packages/components/src/search-control/platform-style.ios.scss @@ -1,6 +1,3 @@ -.search-control__container { - height: 40px; -} .search-control__container--active { margin-right: 0; } diff --git a/packages/components/src/search-control/style.native.scss b/packages/components/src/search-control/style.native.scss index 18f2b7490719a..5cac32eddd4a7 100644 --- a/packages/components/src/search-control/style.native.scss +++ b/packages/components/src/search-control/style.native.scss @@ -1,8 +1,6 @@ .search-control__container { - height: 46px; + height: 48px; margin: 0 16px $grid-unit-10; - flex-direction: row; - justify-content: space-between; } .search-control__inner-container { diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx index db0f7917c33d7..e3514f7b67f08 100644 --- a/packages/components/src/select-control/index.tsx +++ b/packages/components/src/select-control/index.tsx @@ -21,7 +21,7 @@ import InputBase from '../input-control/input-base'; import type { InputBaseProps, LabelPosition } from '../input-control/types'; import { Select, DownArrowWrapper } from './styles/select-control-styles'; import type { Size } from './types'; -import type { PolymorphicComponentProps } from '../ui/context'; +import type { WordPressComponentProps } from '../ui/context'; function useUniqueId( idProp?: string ) { const instanceId = useInstanceId( SelectControl ); @@ -71,7 +71,7 @@ function SelectControl( prefix, suffix, ...props - }: PolymorphicComponentProps< SelectControlProps, 'select', false >, + }: WordPressComponentProps< SelectControlProps, 'select', false >, ref: Ref< HTMLSelectElement > ) { const [ isFocused, setIsFocused ] = useState( false ); diff --git a/packages/components/src/spacer/hook.ts b/packages/components/src/spacer/hook.ts index dda1a7d40cf0a..c833ec90d896a 100644 --- a/packages/components/src/spacer/hook.ts +++ b/packages/components/src/spacer/hook.ts @@ -6,8 +6,7 @@ import { css } from '@emotion/react'; /** * Internal dependencies */ -import { useContextSystem } from '../ui/context'; -import type { PolymorphicComponentProps } from '../ui/context'; +import { useContextSystem, WordPressComponentProps } from '../ui/context'; import { space } from '../ui/utils/space'; import { useCx } from '../utils/hooks/use-cx'; import type { Props } from './types'; @@ -15,7 +14,7 @@ import type { Props } from './types'; const isDefined = < T >( o: T ): o is Exclude< T, null | undefined > => typeof o !== 'undefined' && o !== null; -export function useSpacer( props: PolymorphicComponentProps< Props, 'div' > ) { +export function useSpacer( props: WordPressComponentProps< Props, 'div' > ) { const { className, margin, diff --git a/packages/components/src/surface/hook.js b/packages/components/src/surface/hook.js index a25e6d118c6f3..65d5937a47b92 100644 --- a/packages/components/src/surface/hook.js +++ b/packages/components/src/surface/hook.js @@ -11,7 +11,7 @@ import * as styles from './styles'; import { useCx } from '../utils/hooks/use-cx'; /** - * @param {import('../ui/context').PolymorphicComponentProps} props + * @param {import('../ui/context').WordPressComponentProps} props */ export function useSurface( props ) { const { diff --git a/packages/components/src/text/hook.js b/packages/components/src/text/hook.js index 8c3bb3f31521c..70ccee41f629a 100644 --- a/packages/components/src/text/hook.js +++ b/packages/components/src/text/hook.js @@ -23,7 +23,7 @@ import { getLineHeight } from './get-line-height'; import { useCx } from '../utils/hooks/use-cx'; /** - * @param {import('../ui/context').PolymorphicComponentProps} props + * @param {import('../ui/context').WordPressComponentProps} props */ export default function useText( props ) { const { diff --git a/packages/components/src/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/component.tsx index 2ab3df7d10b63..07d635bbba060 100644 --- a/packages/components/src/toggle-group-control/component.tsx +++ b/packages/components/src/toggle-group-control/component.tsx @@ -18,20 +18,20 @@ import { useMergeRefs, useInstanceId } from '@wordpress/compose'; import { contextConnect, useContextSystem, - PolymorphicComponentProps, + WordPressComponentProps, } from '../ui/context'; +import { useUpdateEffect, useCx } from '../utils/hooks'; import { View } from '../view'; import BaseControl from '../base-control'; -import * as styles from './styles'; -import { useUpdateEffect, useCx } from '../utils/hooks'; -import Backdrop from './toggle-group-control-backdrop'; +import ToggleGroupControlBackdrop from './toggle-group-control-backdrop'; import type { ToggleGroupControlProps } from './types'; import ToggleGroupControlContext from './toggle-group-control-context'; +import * as styles from './styles'; const noop = () => {}; function ToggleGroupControl( - props: PolymorphicComponentProps< ToggleGroupControlProps, 'input' >, + props: WordPressComponentProps< ToggleGroupControlProps, 'input' >, forwardedRef: import('react').Ref< any > ) { const { @@ -101,10 +101,11 @@ function ToggleGroupControl( ref={ useMergeRefs( [ containerRef, forwardedRef ] ) } > { resizeListener } - { children } diff --git a/packages/components/src/toggle-group-control/stories/index.js b/packages/components/src/toggle-group-control/stories/index.js index 2944afb9486e2..32145f3c086b8 100644 --- a/packages/components/src/toggle-group-control/stories/index.js +++ b/packages/components/src/toggle-group-control/stories/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { boolean, text } from '@storybook/addon-knobs'; + /** * WordPress dependencies */ @@ -6,7 +11,6 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import { __experimentalSpacer as Spacer } from '../../'; import { ToggleGroupControl, ToggleGroupControlOption } from '../index'; import { View } from '../../view'; @@ -16,52 +20,64 @@ export default { }; const aligns = [ 'Left', 'Center', 'Right', 'Justify' ]; -const alignOptions = aligns.map( ( key ) => ( - -) ); +const KNOBS_GROUPS = { + ToggleGroupControl: 'ToggleGroupControl', + ToggleGroupControlOption: 'ToggleGroupControlOption', +}; export const _default = () => { const [ alignState, setAlignState ] = useState( aligns[ 0 ] ); - const label = 'Toggle Group Control'; + const label = text( + `${ KNOBS_GROUPS.ToggleGroupControl }: label`, + 'Toggle Group Control', + KNOBS_GROUPS.ToggleGroupControl + ); + const hideLabelFromVision = boolean( + `${ KNOBS_GROUPS.ToggleGroupControl }: hideLabelFromVision`, + false, + KNOBS_GROUPS.ToggleGroupControl + ); + const isBlock = boolean( + `${ KNOBS_GROUPS.ToggleGroupControl }: isBlock (render as a css block element)`, + false, + KNOBS_GROUPS.ToggleGroupControl + ); + const help = text( + `${ KNOBS_GROUPS.ToggleGroupControl }: help`, + undefined, + KNOBS_GROUPS.ToggleGroupControl + ); + const isAdaptiveWidth = boolean( + `${ KNOBS_GROUPS.ToggleGroupControl }: isAdaptiveWidth`, + false, + KNOBS_GROUPS.ToggleGroupControl + ); + + const alignOptions = aligns.map( ( key, index ) => ( + + ) ); return ( - - - { alignOptions } - - - - - - - - - - - - - - + + { alignOptions } + ); }; diff --git a/packages/components/src/toggle-group-control/toggle-group-control-backdrop.tsx b/packages/components/src/toggle-group-control/toggle-group-control-backdrop.tsx index bd444d245bc9f..bce2df10eff8f 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-backdrop.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control-backdrop.tsx @@ -12,6 +12,7 @@ import { BackdropView } from './styles'; function ToggleGroupControlBackdrop( { containerRef, containerWidth, + isAdaptiveWidth, state, }: ToggleGroupControlBackdropProps ) { const [ left, setLeft ] = useState( 0 ); @@ -43,7 +44,7 @@ function ToggleGroupControlBackdrop( { setCanAnimate( true ); } ); } - }, [ canAnimate, containerRef, containerWidth, state ] ); + }, [ canAnimate, containerRef, containerWidth, state, isAdaptiveWidth ] ); return ( , + props: WordPressComponentProps< ToggleGroupControlOptionProps, 'input' >, forwardedRef: import('react').Ref< any > ) { const toggleGroupControlContext = useToggleGroupControlContext(); diff --git a/packages/components/src/toggle-group-control/types.ts b/packages/components/src/toggle-group-control/types.ts index ad08436e8da7f..801297ba064bb 100644 --- a/packages/components/src/toggle-group-control/types.ts +++ b/packages/components/src/toggle-group-control/types.ts @@ -29,7 +29,7 @@ export type ToggleGroupControlProps = Omit< * * @default false */ - hideLabelFromVision: boolean; + hideLabelFromVision?: boolean; /** * Determines if segments should be rendered with equal widths. * @@ -88,5 +88,6 @@ export type ToggleGroupControlButtonProps = { export type ToggleGroupControlBackdropProps = { containerRef: MutableRefObject< HTMLElement | undefined >; containerWidth?: number | null; + isAdaptiveWidth?: boolean; state?: any; }; diff --git a/packages/components/src/tools-panel/stories/index.js b/packages/components/src/tools-panel/stories/index.js index 3f40051135857..407b356089ed5 100644 --- a/packages/components/src/tools-panel/stories/index.js +++ b/packages/components/src/tools-panel/stories/index.js @@ -22,6 +22,7 @@ export default { export const _default = () => { const [ height, setHeight ] = useState(); + const [ minHeight, setMinHeight ] = useState(); const [ width, setWidth ] = useState(); const resetAll = () => { @@ -37,6 +38,18 @@ export const _default = () => { label="Display options" resetAll={ resetAll } > + !! width } + label="Width" + onDeselect={ () => setWidth( undefined ) } + > + setWidth( next ) } + /> + !! height } @@ -50,15 +63,14 @@ export const _default = () => { /> !! width } - label="Width" - onDeselect={ () => setWidth( undefined ) } + hasValue={ () => !! minHeight } + label="Minimum height" + onDeselect={ () => setMinHeight( undefined ) } > setWidth( next ) } + label="Minimum height" + value={ minHeight } + onChange={ ( next ) => setMinHeight( next ) } /> diff --git a/packages/components/src/tools-panel/tools-panel-header/component.js b/packages/components/src/tools-panel/tools-panel-header/component.js index 6fa9391e20e0e..b7e2e41634086 100644 --- a/packages/components/src/tools-panel/tools-panel-header/component.js +++ b/packages/components/src/tools-panel/tools-panel-header/component.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { check, moreHorizontal } from '@wordpress/icons'; +import { check, moreVertical } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; /** @@ -32,7 +32,7 @@ const ToolsPanelHeader = ( props, forwardedRef ) => {

{ header } { hasMenuItems && ( - + { ( { onClose } ) => ( <> diff --git a/packages/components/src/truncate/hook.js b/packages/components/src/truncate/hook.js index cd9f313da0e95..64e45c1bed5ea 100644 --- a/packages/components/src/truncate/hook.js +++ b/packages/components/src/truncate/hook.js @@ -17,7 +17,7 @@ import { TRUNCATE_ELLIPSIS, TRUNCATE_TYPE, truncateContent } from './utils'; import { useCx } from '../utils/hooks/use-cx'; /** - * @param {import('../ui/context').PolymorphicComponentProps} props + * @param {import('../ui/context').WordPressComponentProps} props */ export default function useTruncate( props ) { const { diff --git a/packages/components/src/ui/color-picker/component.tsx b/packages/components/src/ui/color-picker/component.tsx index 5fa862a07ef9a..977826c6ccf95 100644 --- a/packages/components/src/ui/color-picker/component.tsx +++ b/packages/components/src/ui/color-picker/component.tsx @@ -19,7 +19,7 @@ import { __ } from '@wordpress/i18n'; import { useContextSystem, contextConnect, - PolymorphicComponentProps, + WordPressComponentProps, } from '../context'; import { HStack } from '../../h-stack'; import Button from '../../button'; @@ -53,7 +53,7 @@ const getSafeColor = ( }; const ColorPicker = ( - props: PolymorphicComponentProps< ColorPickerProps, 'div', false >, + props: WordPressComponentProps< ColorPickerProps, 'div', false >, forwardedRef: Ref< any > ) => { const { diff --git a/packages/components/src/ui/context/context-connect.js b/packages/components/src/ui/context/context-connect.js index df11228b1e17e..5dcbb174dbdd0 100644 --- a/packages/components/src/ui/context/context-connect.js +++ b/packages/components/src/ui/context/context-connect.js @@ -24,12 +24,12 @@ import { getStyledClassNameFromKey } from './get-styled-class-name-from-key'; * The hope is that we can improve render performance by removing functional * component wrappers. * - * @template {import('./polymorphic-component').PolymorphicComponentProps<{}, any, any>} P + * @template {import('./wordpress-component').WordPressComponentProps<{}, any, any>} P * @param {(props: P, ref: import('react').Ref) => JSX.Element | null} Component The component to register into the Context system. * @param {string} namespace The namespace to register the component under. * @param {Object} options * @param {boolean} [options.memo=false] - * @return {import('./polymorphic-component').PolymorphicComponentFromProps

} The connected PolymorphicComponent + * @return {import('./wordpress-component').WordPressComponentFromProps

} The connected WordPressComponent */ export function contextConnect( Component, namespace, options = {} ) { /* eslint-enable jsdoc/valid-types */ @@ -65,7 +65,7 @@ export function contextConnect( Component, namespace, options = {} ) { // @ts-ignore internal property WrappedComponent[ CONNECT_STATIC_NAMESPACE ] = uniq( mergedNamespace ); - // @ts-ignore PolymorphicComponent property + // @ts-ignore WordPressComponent property WrappedComponent.selector = `.${ getStyledClassNameFromKey( namespace ) }`; // @ts-ignore diff --git a/packages/components/src/ui/context/index.js b/packages/components/src/ui/context/index.js index e19795d7b7a13..54694e47304ff 100644 --- a/packages/components/src/ui/context/index.js +++ b/packages/components/src/ui/context/index.js @@ -8,4 +8,4 @@ export { getConnectNamespace, } from './context-connect'; export { useContextSystem } from './use-context-system'; -export * from './polymorphic-component'; +export * from './wordpress-component'; diff --git a/packages/components/src/ui/context/use-context-system.js b/packages/components/src/ui/context/use-context-system.js index 4a842033a2a2f..134ef9046b849 100644 --- a/packages/components/src/ui/context/use-context-system.js +++ b/packages/components/src/ui/context/use-context-system.js @@ -13,7 +13,7 @@ import { useCx } from '../../utils/hooks/use-cx'; /** * @template TProps - * @typedef {TProps & { className: string; }} ConnectedProps + * @typedef {TProps & { className: string }} ConnectedProps */ /** diff --git a/packages/components/src/ui/context/polymorphic-component.ts b/packages/components/src/ui/context/wordpress-component.ts similarity index 79% rename from packages/components/src/ui/context/polymorphic-component.ts rename to packages/components/src/ui/context/wordpress-component.ts index fae6cc41418db..2bf4372ec6b6e 100644 --- a/packages/components/src/ui/context/polymorphic-component.ts +++ b/packages/components/src/ui/context/wordpress-component.ts @@ -11,7 +11,7 @@ import type * as React from 'react'; * by `ComponentPropsWithRef`. The context is that components should require the `children` * prop explicitely when needed (see https://github.com/WordPress/gutenberg/pull/31817). */ -export type PolymorphicComponentProps< +export type WordPressComponentProps< P, T extends React.ElementType, IsPolymorphic extends boolean = true @@ -23,17 +23,17 @@ export type PolymorphicComponentProps< } : { as?: never } ); -export type PolymorphicComponent< +export type WordPressComponent< T extends React.ElementType, O, IsPolymorphic extends boolean > = { < TT extends React.ElementType >( - props: PolymorphicComponentProps< O, TT, IsPolymorphic > & + props: WordPressComponentProps< O, TT, IsPolymorphic > & ( IsPolymorphic extends true ? { as: TT } : { as: never } ) ): JSX.Element | null; ( - props: PolymorphicComponentProps< O, T, IsPolymorphic > + props: WordPressComponentProps< O, T, IsPolymorphic > ): JSX.Element | null; displayName?: string; /** @@ -47,8 +47,8 @@ export type PolymorphicComponent< selector: `.${ string }`; }; -export type PolymorphicComponentFromProps< +export type WordPressComponentFromProps< Props -> = Props extends PolymorphicComponentProps< infer P, infer T, infer I > - ? PolymorphicComponent< T, P, I > +> = Props extends WordPressComponentProps< infer P, infer T, infer I > + ? WordPressComponent< T, P, I > : never; diff --git a/packages/components/src/ui/control-group/component.js b/packages/components/src/ui/control-group/component.js index b1b82c581cdfd..d291d436c11d4 100644 --- a/packages/components/src/ui/control-group/component.js +++ b/packages/components/src/ui/control-group/component.js @@ -11,8 +11,8 @@ import { useControlGroup } from './hook'; import { contextConnect } from '../context'; /** - * @param {import('../context').PolymorphicComponentProps} props - * @param {import('react').Ref} forwardedRef + * @param {import('../context').WordPressComponentProps} props + * @param {import('react').Ref} forwardedRef */ function ControlGroup( props, forwardedRef ) { const { diff --git a/packages/components/src/ui/control-group/hook.js b/packages/components/src/ui/control-group/hook.js index a30b745f94b9a..bc1f5d34c08b3 100644 --- a/packages/components/src/ui/control-group/hook.js +++ b/packages/components/src/ui/control-group/hook.js @@ -8,7 +8,7 @@ import * as styles from './styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../context').PolymorphicComponentProps} props + * @param {import('../context').WordPressComponentProps} props */ export function useControlGroup( props ) { const { diff --git a/packages/components/src/ui/control-label/hook.js b/packages/components/src/ui/control-label/hook.js index e376cc18f726c..c7f10444fab79 100644 --- a/packages/components/src/ui/control-label/hook.js +++ b/packages/components/src/ui/control-label/hook.js @@ -8,7 +8,7 @@ import * as styles from './styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../context').PolymorphicComponentProps} props + * @param {import('../context').WordPressComponentProps} props */ export function useControlLabel( props ) { const { diff --git a/packages/components/src/ui/form-group/form-group-content.js b/packages/components/src/ui/form-group/form-group-content.js index a84f5fa32faab..21b95ff906823 100644 --- a/packages/components/src/ui/form-group/form-group-content.js +++ b/packages/components/src/ui/form-group/form-group-content.js @@ -12,7 +12,7 @@ import FormGroupHelp from './form-group-help'; import FormGroupLabel from './form-group-label'; /** - * @param {import('../context').PolymorphicComponentProps} props + * @param {import('../context').WordPressComponentProps} props */ function FormGroupContent( { alignLabel, diff --git a/packages/components/src/ui/form-group/form-group-label.js b/packages/components/src/ui/form-group/form-group-label.js index 8d6c282018cad..01e9f9be3c51e 100644 --- a/packages/components/src/ui/form-group/form-group-label.js +++ b/packages/components/src/ui/form-group/form-group-label.js @@ -10,7 +10,7 @@ import { ControlLabel } from '../control-label'; import { VisuallyHidden } from '../../visually-hidden'; /** - * @param {import('../context').PolymorphicComponentProps} props + * @param {import('../context').WordPressComponentProps} props * @return {JSX.Element | null} The form group's label. */ function FormGroupLabel( { children, id, labelHidden = false, ...props } ) { diff --git a/packages/components/src/ui/form-group/form-group.js b/packages/components/src/ui/form-group/form-group.js index 9885aa63d2447..fc9eae01325f4 100644 --- a/packages/components/src/ui/form-group/form-group.js +++ b/packages/components/src/ui/form-group/form-group.js @@ -8,8 +8,8 @@ import FormGroupContent from './form-group-content'; import { useFormGroup } from './use-form-group'; /** - * @param {import('../context').PolymorphicComponentProps} props - * @param {import('react').Ref} forwardedRef + * @param {import('../context').WordPressComponentProps} props + * @param {import('react').Ref} forwardedRef */ function FormGroup( props, forwardedRef ) { const { contentProps, horizontal, ...otherProps } = useFormGroup( props ); diff --git a/packages/components/src/ui/form-group/use-form-group.js b/packages/components/src/ui/form-group/use-form-group.js index 3b73fc69f5f4e..eebc5ae350639 100644 --- a/packages/components/src/ui/form-group/use-form-group.js +++ b/packages/components/src/ui/form-group/use-form-group.js @@ -11,7 +11,7 @@ import * as styles from './form-group-styles'; import { useCx } from '../../utils/hooks/use-cx'; /** - * @param {import('../context').PolymorphicComponentProps} props + * @param {import('../context').WordPressComponentProps} props */ export function useFormGroup( props ) { const { diff --git a/packages/components/src/ui/shortcut/component.tsx b/packages/components/src/ui/shortcut/component.tsx index 911aced391c14..f5bbb6a5d791e 100644 --- a/packages/components/src/ui/shortcut/component.tsx +++ b/packages/components/src/ui/shortcut/component.tsx @@ -7,8 +7,11 @@ import type { Ref } from 'react'; /** * Internal dependencies */ -import { useContextSystem, contextConnect } from '../context'; -import type { PolymorphicComponentProps } from '../context'; +import { + useContextSystem, + contextConnect, + WordPressComponentProps, +} from '../context'; import { View } from '../../view'; export interface ShortcutDescription { @@ -22,7 +25,7 @@ export interface Props { } function Shortcut( - props: PolymorphicComponentProps< Props, 'span' >, + props: WordPressComponentProps< Props, 'span' >, forwardedRef: Ref< any > ): JSX.Element | null { const { diff --git a/packages/components/src/ui/spinner/component.js b/packages/components/src/ui/spinner/component.js index 12cb30f0d712b..c5b45fccf0b8a 100644 --- a/packages/components/src/ui/spinner/component.js +++ b/packages/components/src/ui/spinner/component.js @@ -16,8 +16,8 @@ import { COLORS } from '../../utils/colors-values'; /** * - * @param {import('../context').PolymorphicComponentProps} props - * @param {import('react').Ref} forwardedRef + * @param {import('../context').WordPressComponentProps} props + * @param {import('react').Ref} forwardedRef */ function Spinner( props, forwardedRef ) { const { diff --git a/packages/components/src/ui/tooltip/component.js b/packages/components/src/ui/tooltip/component.js index f618f4633d48f..78ab9132407e5 100644 --- a/packages/components/src/ui/tooltip/component.js +++ b/packages/components/src/ui/tooltip/component.js @@ -18,8 +18,8 @@ import TooltipContent from './content'; import { TooltipShortcut } from './styles'; /** - * @param {import('../context').PolymorphicComponentProps} props - * @param {import('react').Ref} forwardedRef + * @param {import('../context').WordPressComponentProps} props + * @param {import('react').Ref} forwardedRef */ function Tooltip( props, forwardedRef ) { const { diff --git a/packages/components/src/ui/tooltip/content.js b/packages/components/src/ui/tooltip/content.js index 08c51288dae23..b2d533f8288c0 100644 --- a/packages/components/src/ui/tooltip/content.js +++ b/packages/components/src/ui/tooltip/content.js @@ -17,8 +17,8 @@ const { TooltipPopoverView } = styles; /** * - * @param {import('../context').PolymorphicComponentProps} props - * @param {import('react').Ref} forwardedRef + * @param {import('../context').WordPressComponentProps} props + * @param {import('react').Ref} forwardedRef */ function TooltipContent( props, forwardedRef ) { const { children, className, ...otherProps } = useContextSystem( diff --git a/packages/components/src/ui/utils/create-component.tsx b/packages/components/src/ui/utils/create-component.tsx index f9bd8e6d7d3ed..8e451cffef11c 100644 --- a/packages/components/src/ui/utils/create-component.tsx +++ b/packages/components/src/ui/utils/create-component.tsx @@ -10,14 +10,14 @@ import type { As } from 'reakit-utils/types'; */ import { contextConnect } from '../context'; import type { - PolymorphicComponentProps, - PolymorphicComponentFromProps, + WordPressComponentProps, + WordPressComponentFromProps, } from '../context'; import { View } from '../../view'; interface Options< A extends As, - P extends PolymorphicComponentProps< {}, A, any > + P extends WordPressComponentProps< {}, A, any > > { as: A; name: string; @@ -37,13 +37,13 @@ interface Options< */ export const createComponent = < A extends As, - P extends PolymorphicComponentProps< {}, A, any > + P extends WordPressComponentProps< {}, A, any > >( { as, name, useHook, memo = false, -}: Options< A, P > ): PolymorphicComponentFromProps< P > => { +}: Options< A, P > ): WordPressComponentFromProps< P > => { function Component( props: P, forwardedRef: Ref< any > ) { const otherProps = useHook( props ); diff --git a/packages/components/src/ui/utils/space.ts b/packages/components/src/ui/utils/space.ts index d6a3748a5a0ec..b3f2c0a8b3e87 100644 --- a/packages/components/src/ui/utils/space.ts +++ b/packages/components/src/ui/utils/space.ts @@ -30,7 +30,8 @@ export function space( value?: SpaceInput ): string | undefined { // test if the input has a unit, was NaN, or was one of the named CSS values (like `auto`), in which case just use that value if ( - CSS.supports?.( 'margin', value.toString() ) || + ( typeof window !== 'undefined' && + window.CSS?.supports?.( 'margin', value.toString() ) ) || Number.isNaN( asInt ) ) { return value.toString(); diff --git a/packages/components/src/ui/utils/test/create-component.js b/packages/components/src/ui/utils/test/create-component.js index 01dbd7b991797..970a774badf29 100644 --- a/packages/components/src/ui/utils/test/create-component.js +++ b/packages/components/src/ui/utils/test/create-component.js @@ -15,7 +15,7 @@ import { createComponent } from '../create-component'; describe( 'createComponent', () => { /** - * @param {import('../context').PolymorphicComponentProps<{}, 'output'>} props + * @param {import('../context').WordPressComponentProps<{}, 'output'>} props */ const useHook = ( props ) => ( { ...props, 'data-hook-test-prop': true } ); const name = 'Output'; diff --git a/packages/components/src/ui/utils/use-responsive-value.ts b/packages/components/src/ui/utils/use-responsive-value.ts index da284a6510a38..d8b324b097a52 100644 --- a/packages/components/src/ui/utils/use-responsive-value.ts +++ b/packages/components/src/ui/utils/use-responsive-value.ts @@ -41,14 +41,11 @@ export const useBreakpointIndex = ( onResize(); if ( typeof window !== 'undefined' ) { - // Disable reason: We don't really care about what document we listen to, we just want to know that we're resizing. - /* eslint-disable @wordpress/no-global-event-listener */ window.addEventListener( 'resize', onResize ); } return () => { if ( typeof window !== 'undefined' ) { window.removeEventListener( 'resize', onResize ); - /* eslint-enable @wordpress/no-global-event-listener */ } }; }, [ value ] ); diff --git a/packages/components/src/unit-control/index.native.js b/packages/components/src/unit-control/index.native.js index 9be10e31845b5..b45f1c84e0679 100644 --- a/packages/components/src/unit-control/index.native.js +++ b/packages/components/src/unit-control/index.native.js @@ -94,6 +94,7 @@ function UnitControl( { accessibilityHint, unitButtonTextStyle, unit, + units, ] ); const getAnchor = useCallback( @@ -115,6 +116,9 @@ function UnitControl( { }; const renderUnitPicker = useCallback( () => { + if ( units === false ) { + return null; + } return ( { renderUnitButton } diff --git a/packages/components/src/v-stack/hook.js b/packages/components/src/v-stack/hook.js index 0dcd4d33c739a..1852da47bad09 100644 --- a/packages/components/src/v-stack/hook.js +++ b/packages/components/src/v-stack/hook.js @@ -6,7 +6,7 @@ import { useHStack } from '../h-stack'; /** * - * @param {import('../ui/context').PolymorphicComponentProps} props + * @param {import('../ui/context').WordPressComponentProps} props */ export function useVStack( props ) { const { expanded = false, ...otherProps } = useContextSystem( diff --git a/packages/components/src/view/component.js b/packages/components/src/view/component.js index 095576a79e886..4d7808807bd1f 100644 --- a/packages/components/src/view/component.js +++ b/packages/components/src/view/component.js @@ -20,7 +20,7 @@ import styled from '@emotion/styled'; * } * ``` * - * @type {import('../ui/context').PolymorphicComponent<'div', { children?: import('react').ReactNode }, true>} + * @type {import('../ui/context').WordPressComponent<'div', { children?: import('react').ReactNode }, true>} */ // @ts-ignore const View = styled.div``; diff --git a/packages/components/src/visually-hidden/component.js b/packages/components/src/visually-hidden/component.js index 01b9e65ddc511..ba0096d3a8bd8 100644 --- a/packages/components/src/visually-hidden/component.js +++ b/packages/components/src/visually-hidden/component.js @@ -6,8 +6,8 @@ import { visuallyHidden } from './styles'; import { View } from '../view'; /** - * @param {import('../ui/context').PolymorphicComponentProps<{ children: import('react').ReactNode }, 'div'>} props - * @param {import('react').Ref} forwardedRef + * @param {import('../ui/context').WordPressComponentProps<{ children: import('react').ReactNode }, 'div'>} props + * @param {import('react').Ref} forwardedRef */ function VisuallyHidden( props, forwardedRef ) { const { style: styleProp, ...contextProps } = useContextSystem( diff --git a/packages/components/src/z-stack/component.tsx b/packages/components/src/z-stack/component.tsx index 32767f2877843..b6ee9fad3f1dc 100644 --- a/packages/components/src/z-stack/component.tsx +++ b/packages/components/src/z-stack/component.tsx @@ -14,7 +14,7 @@ import { isValidElement } from '@wordpress/element'; */ import { getValidChildren } from '../ui/utils/get-valid-children'; import { contextConnect, useContextSystem } from '../ui/context'; -import type { PolymorphicComponentProps } from '../ui/context'; +import type { WordPressComponentProps } from '../ui/context'; import { ZStackView, ZStackChildView } from './styles'; export interface ZStackProps { @@ -43,7 +43,7 @@ export interface ZStackProps { } function ZStack( - props: PolymorphicComponentProps< ZStackProps, 'div' >, + props: WordPressComponentProps< ZStackProps, 'div' >, forwardedRef: Ref< any > ) { const { diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 3c8f146acaff8..2a3b0c09c406d 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -3,7 +3,10 @@ "compilerOptions": { "rootDir": "src", "declarationDir": "build-types", - "types": [ "gutenberg-env" ] + "types": [ "gutenberg-env" ], + // Some errors in Reakit types with TypeScript 4.3 + // Remove the following line when they've been addressed. + "skipLibCheck": true }, "references": [ { "path": "../compose" }, diff --git a/packages/compose/README.md b/packages/compose/README.md index c66787eb8c3d7..8d918eba02570 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -207,7 +207,7 @@ _Parameters_ _Returns_ -- `TFunc & import('lodash').Cancelable`: Debounced function. +- `import('lodash').DebouncedFunc`: Debounced function. ### useFocusOnMount @@ -452,7 +452,7 @@ _Parameters_ _Returns_ -- `TFunc & import('lodash').Cancelable`: Throttled function. +- `import('lodash').DebouncedFunc`: Throttled function. ### useViewportMatch diff --git a/packages/compose/package.json b/packages/compose/package.json index 7f9e47021c454..d1c4efc629477 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "5.0.0", + "version": "5.0.1", "description": "WordPress higher-order components (HOCs).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -30,7 +30,7 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.13.10", - "@types/lodash": "4.14.149", + "@types/lodash": "^4.14.172", "@types/mousetrap": "^1.6.8", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", @@ -38,7 +38,7 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/keycodes": "file:../keycodes", "@wordpress/priority-queue": "file:../priority-queue", - "clipboard": "^2.0.1", + "clipboard": "^2.0.8", "lodash": "^4.17.21", "mousetrap": "^1.6.5", "react-resize-aware": "^3.1.0", diff --git a/packages/compose/src/hooks/use-debounce/index.js b/packages/compose/src/hooks/use-debounce/index.js index 374f20a521a5e..db66e74d590c6 100644 --- a/packages/compose/src/hooks/use-debounce/index.js +++ b/packages/compose/src/hooks/use-debounce/index.js @@ -23,7 +23,7 @@ import { useEffect } from '@wordpress/element'; * @param {TFunc} fn The function to debounce. * @param {number} [wait] The number of milliseconds to delay. * @param {import('lodash').DebounceSettings} [options] The options object. - * @return {TFunc & import('lodash').Cancelable} Debounced function. + * @return {import('lodash').DebouncedFunc} Debounced function. */ export default function useDebounce( fn, wait, options ) { /* eslint-enable jsdoc/valid-types */ diff --git a/packages/compose/src/hooks/use-drop-zone/index.js b/packages/compose/src/hooks/use-drop-zone/index.js index c2a6a4224f55f..ea58eab14b9cb 100644 --- a/packages/compose/src/hooks/use-drop-zone/index.js +++ b/packages/compose/src/hooks/use-drop-zone/index.js @@ -73,18 +73,24 @@ export default function useDropZone( { /** * Checks if an element is in the drop zone. * - * @param {HTMLElement|null} elementToCheck + * @param {EventTarget|null} targetToCheck * * @return {boolean} True if in drop zone, false if not. */ - function isElementInZone( elementToCheck ) { + function isElementInZone( targetToCheck ) { + const { defaultView } = ownerDocument; if ( - ! elementToCheck || - ! element.contains( elementToCheck ) + ! targetToCheck || + ! defaultView || + ! ( targetToCheck instanceof defaultView.HTMLElement ) || + ! element.contains( targetToCheck ) ) { return false; } + /** @type {HTMLElement|null} */ + let elementToCheck = targetToCheck; + do { if ( elementToCheck.dataset.isDropZone ) { return elementToCheck === element; @@ -155,11 +161,7 @@ export default function useDropZone( { // leaving the drop zone, which means the `relatedTarget` // (element that has been entered) should be outside the drop // zone. - if ( - isElementInZone( - /** @type {HTMLElement|null} */ ( event.relatedTarget ) - ) - ) { + if ( isElementInZone( event.relatedTarget ) ) { return; } diff --git a/packages/compose/src/hooks/use-throttle/index.js b/packages/compose/src/hooks/use-throttle/index.js index 8ad9589d112de..45fb2696673ae 100644 --- a/packages/compose/src/hooks/use-throttle/index.js +++ b/packages/compose/src/hooks/use-throttle/index.js @@ -22,7 +22,7 @@ import { useEffect } from '@wordpress/element'; * @param {TFunc} fn The function to throttle. * @param {number} [wait] The number of milliseconds to throttle invocations to. * @param {import('lodash').ThrottleSettings} [options] The options object. See linked documentation for details. - * @return {TFunc & import('lodash').Cancelable} Throttled function. + * @return {import('lodash').DebouncedFunc} Throttled function. */ export default function useThrottle( fn, wait, options ) { const throttled = useMemoOne( () => throttle( fn, wait, options ), [ diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 96f6c1e300404..5d9ee318a0af8 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -69,7 +69,7 @@ _Parameters_ - _recordId_ `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 control descriptor. +- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. ### editEntityRecord @@ -214,6 +214,10 @@ _Returns_ Action triggered to redo the last undoed edit to an entity record, if any. +_Returns_ + +- `undefined`: + ### saveEditedEntityRecord Action triggered to save an entity record's edits. @@ -236,13 +240,17 @@ _Parameters_ - _record_ `Object`: Record to be saved. - _options_ `Object`: Saving options. - _options.isAutosave_ `[boolean]`: Whether this is an autosave. -- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a control descriptor. +- _options.\_\_unstableFetch_ `[Function]`: Internal use only. Function to call instead of `apiFetch()`. Must return a promise. ### undo Action triggered to undo the last edit to an entity record, if any. +_Returns_ + +- `undefined`: + ## Selectors diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 70cabd048f051..56fe06013a322 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "4.0.0", + "version": "4.0.1", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -33,7 +33,6 @@ "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blocks": "file:../blocks", "@wordpress/data": "file:../data", - "@wordpress/data-controls": "file:../data-controls", "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", "@wordpress/html-entities": "file:../html-entities", diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 6c3d392c31210..8dfbaa6571ef3 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -1,14 +1,13 @@ /** * External dependencies */ -import { castArray, get, isEqual, find } from 'lodash'; +import { castArray, isEqual, find } from 'lodash'; import { v4 as uuid } from 'uuid'; /** * WordPress dependencies */ -import { controls } from '@wordpress/data'; -import { apiFetch, __unstableAwaitPromise } from '@wordpress/data-controls'; +import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; /** @@ -16,12 +15,7 @@ import { addQueryArgs } from '@wordpress/url'; */ import { receiveItems, removeItems, receiveQueriedItems } from './queried-data'; import { getKindEntities, DEFAULT_ENTITY_KEY } from './entities'; -import { - __unstableAcquireStoreLock, - __unstableReleaseStoreLock, -} from './locks'; import { createBatch } from './batch'; -import { getDispatch } from './controls'; import { STORE_NAME } from './name'; /** @@ -165,16 +159,16 @@ export function receiveEmbedPreview( url, preview ) { * @param {Object} [options] Delete options. * @param {Function} [options.__unstableFetch] Internal use only. Function to * call instead of `apiFetch()`. - * Must return a control descriptor. + * Must return a promise. */ -export function* deleteEntityRecord( +export const deleteEntityRecord = ( kind, name, recordId, query, - { __unstableFetch = null } = {} -) { - const entities = yield getKindEntities( kind ); + { __unstableFetch = apiFetch } = {} +) => async ( { dispatch } ) => { + const entities = await dispatch( getKindEntities( kind ) ); const entity = find( entities, { kind, name } ); let error; let deletedRecord = false; @@ -182,18 +176,19 @@ export function* deleteEntityRecord( return; } - const lock = yield* __unstableAcquireStoreLock( + const lock = await dispatch.__unstableAcquireStoreLock( STORE_NAME, [ 'entities', 'data', kind, name, recordId ], { exclusive: true } ); + try { - yield { + dispatch( { type: 'DELETE_ENTITY_RECORD_START', kind, name, recordId, - }; + } ); try { let path = `${ entity.baseURL }/${ recordId }`; @@ -202,36 +197,29 @@ export function* deleteEntityRecord( path = addQueryArgs( path, query ); } - const options = { + deletedRecord = await __unstableFetch( { path, method: 'DELETE', - }; - if ( __unstableFetch ) { - deletedRecord = yield __unstableAwaitPromise( - __unstableFetch( options ) - ); - } else { - deletedRecord = yield apiFetch( options ); - } + } ); - yield removeItems( kind, name, recordId, true ); + await dispatch( removeItems( kind, name, recordId, true ) ); } catch ( _error ) { error = _error; } - yield { + dispatch( { type: 'DELETE_ENTITY_RECORD_FINISH', kind, name, recordId, error, - }; + } ); return deletedRecord; } finally { - yield* __unstableReleaseStoreLock( lock ); + dispatch.__unstableReleaseStoreLock( lock ); } -} +}; /** * Returns an action object that triggers an @@ -246,28 +234,22 @@ export function* deleteEntityRecord( * * @return {Object} Action object. */ -export function* editEntityRecord( kind, name, recordId, edits, options = {} ) { - const entity = yield controls.select( STORE_NAME, 'getEntity', kind, name ); +export const editEntityRecord = ( + kind, + name, + recordId, + edits, + options = {} +) => ( { select, dispatch } ) => { + const entity = select.getEntity( kind, name ); if ( ! entity ) { throw new Error( `The entity being edited (${ kind }, ${ name }) does not have a loaded config.` ); } const { transientEdits = {}, mergedEdits = {} } = entity; - const record = yield controls.select( - STORE_NAME, - 'getRawEntityRecord', - kind, - name, - recordId - ); - const editedRecord = yield controls.select( - STORE_NAME, - 'getEditedEntityRecord', - kind, - name, - recordId - ); + const record = select.getRawEntityRecord( kind, name, recordId ); + const editedRecord = select.getEditedEntityRecord( kind, name, recordId ); const edit = { kind, @@ -286,7 +268,7 @@ export function* editEntityRecord( kind, name, recordId, edits, options = {} ) { }, {} ), transientEdits, }; - return { + dispatch( { type: 'EDIT_ENTITY_RECORD', ...edit, meta: { @@ -299,44 +281,44 @@ export function* editEntityRecord( kind, name, recordId, edits, options = {} ) { }, {} ), }, }, - }; -} + } ); +}; /** * Action triggered to undo the last edit to * an entity record, if any. + * + * @return {undefined} */ -export function* undo() { - const undoEdit = yield controls.select( STORE_NAME, 'getUndoEdit' ); +export const undo = () => ( { select, dispatch } ) => { + const undoEdit = select.getUndoEdit(); if ( ! undoEdit ) { return; } - yield { + dispatch( { type: 'EDIT_ENTITY_RECORD', ...undoEdit, - meta: { - isUndo: true, - }, - }; -} + meta: { isUndo: true }, + } ); +}; /** * Action triggered to redo the last undoed * edit to an entity record, if any. + * + * @return {undefined} */ -export function* redo() { - const redoEdit = yield controls.select( STORE_NAME, 'getRedoEdit' ); +export const redo = () => ( { select, dispatch } ) => { + const redoEdit = select.getRedoEdit(); if ( ! redoEdit ) { return; } - yield { + dispatch( { type: 'EDIT_ENTITY_RECORD', ...redoEdit, - meta: { - isRedo: true, - }, - }; -} + meta: { isRedo: true }, + } ); +}; /** * Forces the creation of a new undo level. @@ -357,16 +339,15 @@ export function __unstableCreateUndoLevel() { * @param {boolean} [options.isAutosave=false] Whether this is an autosave. * @param {Function} [options.__unstableFetch] Internal use only. Function to * call instead of `apiFetch()`. - * Must return a control - * descriptor. + * Must return a promise. */ -export function* saveEntityRecord( +export const saveEntityRecord = ( kind, name, record, - { isAutosave = false, __unstableFetch = null } = {} -) { - const entities = yield getKindEntities( kind ); + { isAutosave = false, __unstableFetch = apiFetch } = {} +) => async ( { select, resolveSelect, dispatch } ) => { + const entities = await dispatch( getKindEntities( kind ) ); const entity = find( entities, { kind, name } ); if ( ! entity ) { return; @@ -374,26 +355,21 @@ export function* saveEntityRecord( const entityIdKey = entity.key || DEFAULT_ENTITY_KEY; const recordId = record[ entityIdKey ]; - const lock = yield* __unstableAcquireStoreLock( + const lock = await dispatch.__unstableAcquireStoreLock( STORE_NAME, [ 'entities', 'data', kind, name, recordId || uuid() ], { exclusive: true } ); + try { // Evaluate optimized edits. // (Function edits that should be evaluated on save to avoid expensive computations on every edit.) for ( const [ key, value ] of Object.entries( record ) ) { if ( typeof value === 'function' ) { const evaluatedValue = value( - yield controls.select( - STORE_NAME, - 'getEditedEntityRecord', - kind, - name, - recordId - ) + select.getEditedEntityRecord( kind, name, recordId ) ); - yield editEntityRecord( + dispatch.editEntityRecord( kind, name, recordId, @@ -406,22 +382,20 @@ export function* saveEntityRecord( } } - yield { + dispatch( { type: 'SAVE_ENTITY_RECORD_START', kind, name, recordId, isAutosave, - }; + } ); let updatedRecord; let error; try { const path = `${ entity.baseURL }${ recordId ? '/' + recordId : '' }`; - const persistedRecord = yield controls.select( - STORE_NAME, - 'getRawEntityRecord', + const persistedRecord = select.getRawEntityRecord( kind, name, recordId @@ -432,14 +406,9 @@ export function* saveEntityRecord( // This is fine for now as it is the only supported autosave, // but ideally this should all be handled in the back end, // so the client just sends and receives objects. - const currentUser = yield controls.select( - STORE_NAME, - 'getCurrentUser' - ); + const currentUser = select.getCurrentUser(); const currentUserId = currentUser ? currentUser.id : undefined; - const autosavePost = yield controls.select( - STORE_NAME, - 'getAutosave', + const autosavePost = resolveSelect.getAutosave( persistedRecord.type, persistedRecord.id, currentUserId @@ -454,8 +423,7 @@ export function* saveEntityRecord( if ( [ 'title', 'excerpt', 'content' ].includes( key ) ) { - // Edits should be the "raw" attribute values. - acc[ key ] = get( data[ key ], 'raw', data[ key ] ); + acc[ key ] = data[ key ]; } return acc; }, @@ -466,18 +434,12 @@ export function* saveEntityRecord( : data.status, } ); - const options = { + updatedRecord = await __unstableFetch( { path: `${ path }/autosaves`, method: 'POST', data, - }; - if ( __unstableFetch ) { - updatedRecord = yield __unstableAwaitPromise( - __unstableFetch( options ) - ); - } else { - updatedRecord = yield apiFetch( options ); - } + } ); + // An autosave may be processed by the server as a regular save // when its update is requested by the author and the post had // draft or auto-draft status. @@ -495,12 +457,7 @@ export function* saveEntityRecord( key ) ) { - // Edits should be the "raw" attribute values. - acc[ key ] = get( - newRecord[ key ], - 'raw', - newRecord[ key ] - ); + acc[ key ] = newRecord[ key ]; } else if ( key === 'status' ) { // Status is only persisted in autosaves when going from // "auto-draft" to "draft". @@ -511,17 +468,13 @@ export function* saveEntityRecord( : persistedRecord.status; } else { // These properties are not persisted in autosaves. - acc[ key ] = get( - persistedRecord[ key ], - 'raw', - persistedRecord[ key ] - ); + acc[ key ] = persistedRecord[ key ]; } return acc; }, {} ); - yield receiveEntityRecords( + dispatch.receiveEntityRecords( kind, name, newRecord, @@ -529,7 +482,10 @@ export function* saveEntityRecord( true ); } else { - yield receiveAutosaves( persistedRecord.id, updatedRecord ); + dispatch.receiveAutosaves( + persistedRecord.id, + updatedRecord + ); } } else { let edits = record; @@ -542,19 +498,12 @@ export function* saveEntityRecord( ), }; } - const options = { + updatedRecord = await __unstableFetch( { path, method: recordId ? 'PUT' : 'POST', data: edits, - }; - if ( __unstableFetch ) { - updatedRecord = yield __unstableAwaitPromise( - __unstableFetch( options ) - ); - } else { - updatedRecord = yield apiFetch( options ); - } - yield receiveEntityRecords( + } ); + dispatch.receiveEntityRecords( kind, name, updatedRecord, @@ -566,20 +515,20 @@ export function* saveEntityRecord( } catch ( _error ) { error = _error; } - yield { + dispatch( { type: 'SAVE_ENTITY_RECORD_FINISH', kind, name, recordId, error, isAutosave, - }; + } ); return updatedRecord; } finally { - yield* __unstableReleaseStoreLock( lock ); + dispatch.__unstableReleaseStoreLock( lock ); } -} +}; /** * Runs multiple core-data actions at the same time using one API request. @@ -603,13 +552,12 @@ export function* saveEntityRecord( * @return {Promise} A promise that resolves to an array containing the return * values of each function given in `requests`. */ -export function* __experimentalBatch( requests ) { +export const __experimentalBatch = ( requests ) => async ( { dispatch } ) => { const batch = createBatch(); - const dispatch = yield getDispatch(); const api = { saveEntityRecord( kind, name, record, options ) { return batch.add( ( add ) => - dispatch( STORE_NAME ).saveEntityRecord( kind, name, record, { + dispatch.saveEntityRecord( kind, name, record, { ...options, __unstableFetch: add, } ) @@ -617,38 +565,28 @@ export function* __experimentalBatch( requests ) { }, saveEditedEntityRecord( kind, name, recordId, options ) { return batch.add( ( add ) => - dispatch( STORE_NAME ).saveEditedEntityRecord( - kind, - name, - recordId, - { - ...options, - __unstableFetch: add, - } - ) + dispatch.saveEditedEntityRecord( kind, name, recordId, { + ...options, + __unstableFetch: add, + } ) ); }, deleteEntityRecord( kind, name, recordId, query, options ) { return batch.add( ( add ) => - dispatch( STORE_NAME ).deleteEntityRecord( - kind, - name, - recordId, - query, - { - ...options, - __unstableFetch: add, - } - ) + dispatch.deleteEntityRecord( kind, name, recordId, query, { + ...options, + __unstableFetch: add, + } ) ); }, }; const resultPromises = requests.map( ( request ) => request( api ) ); - const [ , ...results ] = yield __unstableAwaitPromise( - Promise.all( [ batch.run(), ...resultPromises ] ) - ); + const [ , ...results ] = await Promise.all( [ + batch.run(), + ...resultPromises, + ] ); return results; -} +}; /** * Action triggered to save an entity record's edits. @@ -658,28 +596,23 @@ export function* __experimentalBatch( requests ) { * @param {Object} recordId ID of the record. * @param {Object} options Saving options. */ -export function* saveEditedEntityRecord( kind, name, recordId, options ) { - if ( - ! ( yield controls.select( - STORE_NAME, - 'hasEditsForEntityRecord', - kind, - name, - recordId - ) ) - ) { +export const saveEditedEntityRecord = ( + kind, + name, + recordId, + options +) => async ( { select, dispatch } ) => { + if ( ! select.hasEditsForEntityRecord( kind, name, recordId ) ) { return; } - const edits = yield controls.select( - STORE_NAME, - 'getEntityRecordNonTransientEdits', + const edits = select.getEntityRecordNonTransientEdits( kind, name, recordId ); const record = { id: recordId, ...edits }; - return yield* saveEntityRecord( kind, name, record, options ); -} + return await dispatch.saveEntityRecord( kind, name, record, options ); +}; /** * Action triggered to save only specified properties for the entity. @@ -690,27 +623,17 @@ export function* saveEditedEntityRecord( kind, name, recordId, options ) { * @param {Array} itemsToSave List of entity properties to save. * @param {Object} options Saving options. */ -export function* __experimentalSaveSpecifiedEntityEdits( +export const __experimentalSaveSpecifiedEntityEdits = ( kind, name, recordId, itemsToSave, options -) { - if ( - ! ( yield controls.select( - STORE_NAME, - 'hasEditsForEntityRecord', - kind, - name, - recordId - ) ) - ) { +) => async ( { select, dispatch } ) => { + if ( ! select.hasEditsForEntityRecord( kind, name, recordId ) ) { return; } - const edits = yield controls.select( - STORE_NAME, - 'getEntityRecordNonTransientEdits', + const edits = select.getEntityRecordNonTransientEdits( kind, name, recordId @@ -721,8 +644,8 @@ export function* __experimentalSaveSpecifiedEntityEdits( editsToSave[ edit ] = edits[ edit ]; } } - return yield* saveEntityRecord( kind, name, editsToSave, options ); -} + return await dispatch.saveEntityRecord( kind, name, editsToSave, options ); +}; /** * Returns an action object used in signalling that Upload permissions have been received. diff --git a/packages/core-data/src/batch/default-processor.js b/packages/core-data/src/batch/default-processor.js index d459923218e12..34f38ff5c3d0d 100644 --- a/packages/core-data/src/batch/default-processor.js +++ b/packages/core-data/src/batch/default-processor.js @@ -1,10 +1,23 @@ +/** + * External dependencies + */ +import { chunk } from 'lodash'; + /** * WordPress dependencies */ import apiFetch from '@wordpress/api-fetch'; /** - * Default batch processor. Sends its input requests to /v1/batch. + * Maximum number of requests to place in a single batch request. Obtained by + * sending a preflight OPTIONS request to /batch/v1/. + * + * @type {number?} + */ +let maxItems = null; + +/** + * Default batch processor. Sends its input requests to /batch/v1. * * @param {Array} requests List of API requests to perform at once. * @@ -13,33 +26,51 @@ import apiFetch from '@wordpress/api-fetch'; * (if not ). */ export default async function defaultProcessor( requests ) { - const batchResponse = await apiFetch( { - path: '/batch/v1', - method: 'POST', - data: { - validation: 'require-all-validate', - requests: requests.map( ( request ) => ( { - path: request.path, - body: request.data, // Rename 'data' to 'body'. - method: request.method, - headers: request.headers, - } ) ), - }, - } ); - - if ( batchResponse.failed ) { - return batchResponse.responses.map( ( response ) => ( { - error: response?.body, - } ) ); + if ( maxItems === null ) { + const preflightResponse = await apiFetch( { + path: '/batch/v1', + method: 'OPTIONS', + } ); + maxItems = preflightResponse.endpoints[ 0 ].args.requests.maxItems; } - return batchResponse.responses.map( ( response ) => { - const result = {}; - if ( response.status >= 200 && response.status < 300 ) { - result.output = response.body; + const results = []; + + for ( const batchRequests of chunk( requests, maxItems ) ) { + const batchResponse = await apiFetch( { + path: '/batch/v1', + method: 'POST', + data: { + validation: 'require-all-validate', + requests: batchRequests.map( ( request ) => ( { + path: request.path, + body: request.data, // Rename 'data' to 'body'. + method: request.method, + headers: request.headers, + } ) ), + }, + } ); + + let batchResults; + + if ( batchResponse.failed ) { + batchResults = batchResponse.responses.map( ( response ) => ( { + error: response?.body, + } ) ); } else { - result.error = response.body; + batchResults = batchResponse.responses.map( ( response ) => { + const result = {}; + if ( response.status >= 200 && response.status < 300 ) { + result.output = response.body; + } else { + result.error = response.body; + } + return result; + } ); } - return result; - } ); + + results.push( ...batchResults ); + } + + return results; } diff --git a/packages/core-data/src/batch/test/default-processor.js b/packages/core-data/src/batch/test/default-processor.js index c6d2515410b82..c712ecdff21aa 100644 --- a/packages/core-data/src/batch/test/default-processor.js +++ b/packages/core-data/src/batch/test/default-processor.js @@ -11,6 +11,18 @@ import defaultProcessor from '../default-processor'; jest.mock( '@wordpress/api-fetch' ); describe( 'defaultProcessor', () => { + const preflightResponse = { + endpoints: [ + { + args: { + requests: { + maxItems: 25, + }, + }, + }, + ], + }; + const requests = [ { path: '/v1/cricketers', @@ -26,7 +38,12 @@ describe( 'defaultProcessor', () => { }, ]; - const expectedFetchOptions = { + const expectedPreflightOptions = { + path: '/batch/v1', + method: 'OPTIONS', + }; + + const expectedBatchOptions = { path: '/batch/v1', method: 'POST', data: { @@ -49,21 +66,26 @@ describe( 'defaultProcessor', () => { }; it( 'handles a successful request', async () => { - apiFetch.mockImplementation( async () => ( { - failed: false, - responses: [ - { - status: 200, - body: 'Lyon', - }, - { - status: 400, - body: 'Error!', - }, - ], - } ) ); + apiFetch.mockImplementation( async ( { method } ) => + method === 'OPTIONS' + ? preflightResponse + : { + failed: false, + responses: [ + { + status: 200, + body: 'Lyon', + }, + { + status: 400, + body: 'Error!', + }, + ], + } + ); const results = await defaultProcessor( requests ); - expect( apiFetch ).toHaveBeenCalledWith( expectedFetchOptions ); + expect( apiFetch ).toHaveBeenCalledWith( expectedPreflightOptions ); + expect( apiFetch ).toHaveBeenCalledWith( expectedBatchOptions ); expect( results ).toEqual( [ { output: 'Lyon' }, { error: 'Error!' }, @@ -71,18 +93,23 @@ describe( 'defaultProcessor', () => { } ); it( 'handles a failed request', async () => { - apiFetch.mockImplementation( async () => ( { - failed: true, - responses: [ - null, - { - status: 400, - body: 'Error!', - }, - ], - } ) ); + apiFetch.mockImplementation( async ( { method } ) => + method === 'OPTIONS' + ? preflightResponse + : { + failed: true, + responses: [ + null, + { + status: 400, + body: 'Error!', + }, + ], + } + ); const results = await defaultProcessor( requests ); - expect( apiFetch ).toHaveBeenCalledWith( expectedFetchOptions ); + expect( apiFetch ).toHaveBeenCalledWith( expectedPreflightOptions ); + expect( apiFetch ).toHaveBeenCalledWith( expectedBatchOptions ); expect( results ).toEqual( [ { error: undefined }, { error: 'Error!' }, diff --git a/packages/core-data/src/controls.js b/packages/core-data/src/controls.js deleted file mode 100644 index 00f8ca36641c1..0000000000000 --- a/packages/core-data/src/controls.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * WordPress dependencies - */ -import { createRegistryControl } from '@wordpress/data'; - -export function regularFetch( url ) { - return { - type: 'REGULAR_FETCH', - url, - }; -} - -export function getDispatch() { - return { - type: 'GET_DISPATCH', - }; -} - -const controls = { - async REGULAR_FETCH( { url } ) { - const { data } = await window - .fetch( url ) - .then( ( res ) => res.json() ); - - return data; - }, - - GET_DISPATCH: createRegistryControl( ( { dispatch } ) => () => dispatch ), -}; - -export default controls; diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index f22010c58dddc..e00e5f37319d8 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -6,18 +6,18 @@ import { upperFirst, camelCase, map, find, get, startCase } from 'lodash'; /** * WordPress dependencies */ -import { controls } from '@wordpress/data'; -import { apiFetch } from '@wordpress/data-controls'; +import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import { addEntities } from './actions'; -import { STORE_NAME } from './name'; export const DEFAULT_ENTITY_KEY = 'id'; +const POST_RAW_ATTRIBUTES = [ 'title', 'excerpt', 'content' ]; + export const defaultEntities = [ { label: __( 'Base' ), @@ -41,6 +41,7 @@ export const defaultEntities = [ key: 'slug', baseURL: '/wp/v2/types', baseURLParams: { context: 'edit' }, + rawAttributes: POST_RAW_ATTRIBUTES, }, { name: 'media', @@ -167,8 +168,8 @@ export const prePersistPostType = ( persistedRecord, edits ) => { * * @return {Promise} Entities promise */ -function* loadPostTypeEntities() { - const postTypes = yield apiFetch( { path: '/wp/v2/types?context=edit' } ); +async function loadPostTypeEntities() { + const postTypes = await apiFetch( { path: '/wp/v2/types?context=edit' } ); return map( postTypes, ( postType, name ) => { const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( name @@ -184,6 +185,7 @@ function* loadPostTypeEntities() { selection: true, }, mergedEdits: { meta: true }, + rawAttributes: POST_RAW_ATTRIBUTES, getTitle: ( record ) => record?.title?.rendered || record?.title || @@ -199,8 +201,8 @@ function* loadPostTypeEntities() { * * @return {Promise} Entities promise */ -function* loadTaxonomyEntities() { - const taxonomies = yield apiFetch( { +async function loadTaxonomyEntities() { + const taxonomies = await apiFetch( { path: '/wp/v2/taxonomies?context=edit', } ); return map( taxonomies, ( taxonomy, name ) => { @@ -248,12 +250,8 @@ export const getMethodName = ( * * @return {Array} Entities */ -export function* getKindEntities( kind ) { - let entities = yield controls.select( - STORE_NAME, - 'getEntitiesByKind', - kind - ); +export const getKindEntities = ( kind ) => async ( { select, dispatch } ) => { + let entities = select.getEntitiesByKind( kind ); if ( entities && entities.length !== 0 ) { return entities; } @@ -263,8 +261,8 @@ export function* getKindEntities( kind ) { return []; } - entities = yield kindConfig.loadEntities(); - yield addEntities( entities ); + entities = await kindConfig.loadEntities(); + dispatch( addEntities( entities ) ); return entities; -} +}; diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 4830da46ccc98..7821e2ecd4745 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { createReduxStore, register } from '@wordpress/data'; -import { controls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -11,9 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; import * as resolvers from './resolvers'; -import * as locksSelectors from './locks/selectors'; -import * as locksActions from './locks/actions'; -import customControls from './controls'; +import createLocksActions from './locks/actions'; import { defaultEntities, getMethodName } from './entities'; import { STORE_NAME } from './name'; @@ -57,13 +54,13 @@ const entityActions = defaultEntities.reduce( ( result, entity ) => { return result; }, {} ); -const storeConfig = { +const storeConfig = () => ( { reducer, - controls: { ...customControls, ...controls }, - actions: { ...actions, ...entityActions, ...locksActions }, - selectors: { ...selectors, ...entitySelectors, ...locksSelectors }, + actions: { ...actions, ...entityActions, ...createLocksActions() }, + selectors: { ...selectors, ...entitySelectors }, resolvers: { ...resolvers, ...entityResolvers }, -}; + __experimentalUseThunks: true, +} ); /** * Store definition for the code data namespace. @@ -72,7 +69,7 @@ const storeConfig = { * * @type {Object} */ -export const store = createReduxStore( STORE_NAME, storeConfig ); +export const store = createReduxStore( STORE_NAME, storeConfig() ); register( store ); diff --git a/packages/core-data/src/locks/actions.js b/packages/core-data/src/locks/actions.js index aab16dd703bba..b6bcb93a3dd92 100644 --- a/packages/core-data/src/locks/actions.js +++ b/packages/core-data/src/locks/actions.js @@ -1,69 +1,18 @@ -/** - * WordPress dependencies - */ -import { __unstableAwaitPromise } from '@wordpress/data-controls'; -import { controls } from '@wordpress/data'; - /** * Internal dependencies */ -import { STORE_NAME } from '../name'; +import createLocks from './engine'; -export function* __unstableAcquireStoreLock( store, path, { exclusive } ) { - const promise = yield* __unstableEnqueueLockRequest( store, path, { - exclusive, - } ); - yield* __unstableProcessPendingLockRequests(); - return yield __unstableAwaitPromise( promise ); -} +export default function createLocksActions() { + const locks = createLocks(); -export function* __unstableEnqueueLockRequest( store, path, { exclusive } ) { - let notifyAcquired; - const promise = new Promise( ( resolve ) => { - notifyAcquired = resolve; - } ); - yield { - type: 'ENQUEUE_LOCK_REQUEST', - request: { store, path, exclusive, notifyAcquired }, - }; - return promise; -} - -export function* __unstableReleaseStoreLock( lock ) { - yield { - type: 'RELEASE_LOCK', - lock, - }; - yield* __unstableProcessPendingLockRequests(); -} + function __unstableAcquireStoreLock( store, path, { exclusive } ) { + return () => locks.acquire( store, path, exclusive ); + } -export function* __unstableProcessPendingLockRequests() { - yield { - type: 'PROCESS_PENDING_LOCK_REQUESTS', - }; - const lockRequests = yield controls.select( - STORE_NAME, - '__unstableGetPendingLockRequests' - ); - for ( const request of lockRequests ) { - const { store, path, exclusive, notifyAcquired } = request; - const isAvailable = yield controls.select( - STORE_NAME, - '__unstableIsLockAvailable', - store, - path, - { - exclusive, - } - ); - if ( isAvailable ) { - const lock = { store, path, exclusive }; - yield { - type: 'GRANT_LOCK_REQUEST', - lock, - request, - }; - notifyAcquired( lock ); - } + function __unstableReleaseStoreLock( lock ) { + return () => locks.release( lock ); } + + return { __unstableAcquireStoreLock, __unstableReleaseStoreLock }; } diff --git a/packages/core-data/src/locks/engine.js b/packages/core-data/src/locks/engine.js new file mode 100644 index 0000000000000..a3e3ec4e77947 --- /dev/null +++ b/packages/core-data/src/locks/engine.js @@ -0,0 +1,43 @@ +/** + * Internal dependencies + */ +import reducer from './reducer'; +import { isLockAvailable, getPendingLockRequests } from './selectors'; + +export default function createLocks() { + let state = reducer( undefined, { type: '@@INIT' } ); + + function processPendingLockRequests() { + for ( const request of getPendingLockRequests( state ) ) { + const { store, path, exclusive, notifyAcquired } = request; + if ( isLockAvailable( state, store, path, { exclusive } ) ) { + const lock = { store, path, exclusive }; + state = reducer( state, { + type: 'GRANT_LOCK_REQUEST', + lock, + request, + } ); + notifyAcquired( lock ); + } + } + } + + function acquire( store, path, exclusive ) { + return new Promise( ( resolve ) => { + state = reducer( state, { + type: 'ENQUEUE_LOCK_REQUEST', + request: { store, path, exclusive, notifyAcquired: resolve }, + } ); + processPendingLockRequests(); + } ); + } + function release( lock ) { + state = reducer( state, { + type: 'RELEASE_LOCK', + lock, + } ); + processPendingLockRequests(); + } + + return { acquire, release }; +} diff --git a/packages/core-data/src/locks/index.js b/packages/core-data/src/locks/index.js deleted file mode 100644 index 57e124e445d87..0000000000000 --- a/packages/core-data/src/locks/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions'; -export * from './selectors'; -export { default as reducer } from './reducer'; diff --git a/packages/core-data/src/locks/reducer.js b/packages/core-data/src/locks/reducer.js index 878313b67b341..11a2b899e1827 100644 --- a/packages/core-data/src/locks/reducer.js +++ b/packages/core-data/src/locks/reducer.js @@ -19,7 +19,7 @@ const DEFAULT_STATE = { * * @return {Object} Updated state. */ -export function locks( state = DEFAULT_STATE, action ) { +export default function locks( state = DEFAULT_STATE, action ) { switch ( action.type ) { case 'ENQUEUE_LOCK_REQUEST': { const { request } = action; @@ -60,5 +60,3 @@ export function locks( state = DEFAULT_STATE, action ) { return state; } - -export default locks; diff --git a/packages/core-data/src/locks/selectors.js b/packages/core-data/src/locks/selectors.js index 233b36985a5c5..c5f0a42e72237 100644 --- a/packages/core-data/src/locks/selectors.js +++ b/packages/core-data/src/locks/selectors.js @@ -8,13 +8,13 @@ import { getNode, } from './utils'; -export function __unstableGetPendingLockRequests( state ) { - return state.locks.requests; +export function getPendingLockRequests( state ) { + return state.requests; } -export function __unstableIsLockAvailable( state, store, path, { exclusive } ) { +export function isLockAvailable( state, store, path, { exclusive } ) { const storePath = [ store, ...path ]; - const locks = state.locks.tree; + const locks = state.tree; // Validate all parents and the node itself for ( const node of iteratePath( locks, storePath ) ) { diff --git a/packages/core-data/src/locks/test/actions.js b/packages/core-data/src/locks/test/actions.js deleted file mode 100644 index f3d2cbdb1b44a..0000000000000 --- a/packages/core-data/src/locks/test/actions.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Internal dependencies - */ -import { - __unstableAcquireStoreLock, - __unstableEnqueueLockRequest, - __unstableReleaseStoreLock, - __unstableProcessPendingLockRequests, -} from '../actions'; - -const store = 'test'; -const path = [ 'blue', 'bird' ]; - -describe( '__unstableEnqueueLockRequest', () => { - it( 'Enqueues a lock request', async () => { - const fulfillment = __unstableEnqueueLockRequest( store, path, { - exclusive: true, - } ); - - // Start - expect( fulfillment.next().value ).toMatchObject( { - type: 'ENQUEUE_LOCK_REQUEST', - request: { - store, - path, - exclusive: true, - notifyAcquired: expect.any( Function ), - }, - } ); - - // Should return a promise - expect( fulfillment.next() ).toMatchObject( { - done: true, - value: expect.any( Promise ), - } ); - } ); - - it( 'Returns a promise fulfilled only after calling notifyAcquired', async () => { - const fulfillment = __unstableEnqueueLockRequest( store, path, { - exclusive: true, - } ); - const { request } = fulfillment.next().value; - const promise = fulfillment.next().value; - const fulfilled = jest.fn(); - promise.then( fulfilled ); - // Fulfilled should not be called until notifyAcquired is called - await sleep( 1 ); - expect( fulfilled ).not.toBeCalled(); - const lock = {}; - request.notifyAcquired( lock ); - expect( fulfilled ).not.toBeCalled(); - - // Promises are resolved only the next tick, so let's wait a little: - await sleep( 1 ); - expect( fulfilled ).toBeCalledTimes( 1 ); - - // Calling notifyAcquired again shouldn't have any effect: - request.notifyAcquired( lock ); - await sleep( 1 ); - expect( fulfilled ).toBeCalledTimes( 1 ); - } ); -} ); - -describe( '__unstableProcessPendingLockRequests', () => { - const exclusive = true; - const lock = { store, path, exclusive }; - - let notifyAcquired; - let request; - - beforeEach( () => { - notifyAcquired = jest.fn(); - request = { store, path, exclusive, notifyAcquired }; - } ); - - it( 'Grants a lock request that may be granted', async () => { - const fulfillment = __unstableProcessPendingLockRequests(); - - // Start - expect( fulfillment.next().value.type ).toBe( - 'PROCESS_PENDING_LOCK_REQUESTS' - ); - - // Get pending lock requests - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - - // Find one and check if the request may be granted - expect( fulfillment.next( [ request ] ).value.type ).toBe( - '@@data/SELECT' - ); - - // It may, grant it - expect( fulfillment.next( true ).value.type ).toBe( - 'GRANT_LOCK_REQUEST' - ); - - // Ensure the promise isn't fulfilled until after GRANT_LOCK_REQUEST finishes - expect( notifyAcquired ).not.toBeCalled(); - - // All requests processed, return - expect( fulfillment.next() ).toMatchObject( { - done: true, - value: undefined, - } ); - - // Ensure the promise is fulfilled once GRANT_LOCK_REQUEST finishes - expect( notifyAcquired ).toBeCalledWith( lock ); - expect( notifyAcquired ).toBeCalledTimes( 1 ); - - // All requests processed, return - expect( fulfillment.next() ).toMatchObject( { - done: true, - value: undefined, - } ); - } ); - - it( 'Does not grants a lock request that may not be granted', async () => { - const fulfillment = __unstableProcessPendingLockRequests(); - - // Start - expect( fulfillment.next().value.type ).toBe( - 'PROCESS_PENDING_LOCK_REQUESTS' - ); - - // Get pending lock requests - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - - // Find one and check if the request may be granted - expect( fulfillment.next( [ request ] ).value.type ).toBe( - '@@data/SELECT' - ); - - // It may not, let's finish - expect( fulfillment.next( false ) ).toMatchObject( { - done: true, - value: undefined, - } ); - } ); - - it( 'Handles multiple lock requests', async () => { - const fulfillment = __unstableProcessPendingLockRequests(); - - // Start - expect( fulfillment.next().value.type ).toBe( - 'PROCESS_PENDING_LOCK_REQUESTS' - ); - - // Get pending lock requests - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - - // Find one and check if the request may be granted - expect( fulfillment.next( [ request, request ] ).value.type ).toBe( - '@@data/SELECT' - ); - - // It may not, continue - check if the next one may be granted - expect( fulfillment.next( false ).value.type ).toBe( '@@data/SELECT' ); - // It may, grant it - expect( fulfillment.next( true ).value.type ).toBe( - 'GRANT_LOCK_REQUEST' - ); - - // Ensure the promise isn't fulfilled until after GRANT_LOCK_REQUEST finishes - expect( notifyAcquired ).not.toBeCalled(); - - // All requests processed, return - expect( fulfillment.next() ).toMatchObject( { - done: true, - value: undefined, - } ); - - // Ensure the promise is fulfilled once GRANT_LOCK_REQUEST finishes - expect( notifyAcquired ).toBeCalledWith( lock ); - expect( notifyAcquired ).toBeCalledTimes( 1 ); - - // All requests processed, return - expect( fulfillment.next() ).toMatchObject( { - done: true, - value: undefined, - } ); - } ); -} ); - -describe( '__unstableAcquireStoreLock', () => { - const exclusive = true; - const lock = { store, path, exclusive }; - - let notifyAcquired; - let request; - - beforeEach( () => { - notifyAcquired = jest.fn(); - request = { store, path, exclusive, notifyAcquired }; - } ); - - it( 'Enqueues a lock request and attempts to fulfill it', async () => { - const fulfillment = __unstableAcquireStoreLock( store, path, { - exclusive, - } ); - - // Start - expect( fulfillment.next().value.type ).toBe( 'ENQUEUE_LOCK_REQUEST' ); - - // Get pending lock requests - expect( fulfillment.next().value.type ).toBe( - 'PROCESS_PENDING_LOCK_REQUESTS' - ); - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - - // Check if lock may be granted - expect( fulfillment.next( [ request ] ).value.type ).toBe( - '@@data/SELECT' - ); - - // Grant lock request - expect( fulfillment.next( true ).value.type ).toBe( - 'GRANT_LOCK_REQUEST' - ); - - // Ensure the promise isn't fulfilled until after GRANT_LOCK_REQUEST finishes - expect( notifyAcquired ).not.toBeCalled(); - - // Await for lock promise fulfillment - expect( fulfillment.next( 'promise' ).value.type ).toBe( - 'AWAIT_PROMISE' - ); - - // Ensure the promise is fulfilled once GRANT_LOCK_REQUEST finishes - expect( notifyAcquired ).toBeCalledWith( lock ); - - // Return lock - expect( fulfillment.next( lock ) ).toMatchObject( { - done: true, - value: lock, - } ); - } ); - - it( 'Enqueues a lock request and waits until fultillment it when not available', async () => { - const fulfillment = __unstableAcquireStoreLock( store, path, { - exclusive, - } ); - - // Start - expect( fulfillment.next().value.type ).toBe( 'ENQUEUE_LOCK_REQUEST' ); - - // Get pending lock requests - expect( fulfillment.next().value.type ).toBe( - 'PROCESS_PENDING_LOCK_REQUESTS' - ); - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - - // Check if lock may be granted - expect( fulfillment.next( [ request ] ).value.type ).toBe( - '@@data/SELECT' - ); - - // Await until lock request is granted - expect( fulfillment.next( false ).value.type ).toBe( 'AWAIT_PROMISE' ); - - // Ensure the promise isn't fulfilled at this point... - expect( notifyAcquired ).not.toBeCalled(); - - await sleep( 1000 ); - - // ...or even a second lateer - expect( notifyAcquired ).not.toBeCalled(); - - // Let's assume the promise was fulfilled in the end, the action should return - // the lock once that happens. - expect( fulfillment.next( lock ) ).toMatchObject( { - done: true, - value: lock, - } ); - } ); -} ); - -describe( '__unstableReleaseStoreLock', () => { - const lock = { store, path, exclusive: true }; - - it( 'Releases a lock request and attempts to fulfill pending lock requests', async () => { - const fulfillment = __unstableReleaseStoreLock( lock ); - - // Start - expect( fulfillment.next().value ).toMatchObject( { - type: 'RELEASE_LOCK', - lock, - } ); - - // Attempt to grant any pending lock requests, find none, return - expect( fulfillment.next().value.type ).toBe( - 'PROCESS_PENDING_LOCK_REQUESTS' - ); - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - - // Short-circuit with no results and return - expect( fulfillment.next( [] ) ).toMatchObject( { - done: true, - value: undefined, - } ); - } ); -} ); - -const sleep = ( ms ) => { - const promise = new Promise( ( resolve ) => setTimeout( resolve, ms ) ); - jest.advanceTimersByTime( ms + 1 ); - return promise; -}; diff --git a/packages/core-data/src/locks/test/engine.js b/packages/core-data/src/locks/test/engine.js new file mode 100644 index 0000000000000..0c629a3461e13 --- /dev/null +++ b/packages/core-data/src/locks/test/engine.js @@ -0,0 +1,135 @@ +/** + * Internal dependencies + */ +import createLocks from '../engine'; + +jest.useRealTimers(); + +// we correctly await all promises with expect calls, but the rule doesn't detect that +/* eslint-disable jest/valid-expect-in-promise */ + +describe( 'Locks engine', () => { + it( 'does not grant two exclusive locks at once', async () => { + const locks = createLocks(); + + let l1Granted = false; + let l2Granted = false; + + // request two locks + const l1 = locks.acquire( 'store', [ 'root' ], true ); + const l2 = locks.acquire( 'store', [ 'root' ], true ); + + // on each grant, verify that the other lock is not granted at the same time + const check1 = l1.then( () => { + l1Granted = true; + expect( l2Granted ).toBe( false ); + } ); + + const check2 = l2.then( () => { + l2Granted = true; + expect( l1Granted ).toBe( false ); + } ); + + // unlock both + const lock1 = await l1; + locks.release( lock1 ); + l1Granted = false; + + const lock2 = await l2; + locks.release( lock2 ); + l2Granted = false; + + // ensure that both locks were granted and checked + return await Promise.all( [ check1, check2 ] ); + } ); + + it( 'does not grant an exclusive lock if a non-exclusive one already exists', async () => { + const locks = createLocks(); + + let l1Granted = false; + let l2Granted = false; + + // request two locks + const l1 = locks.acquire( 'store', [ 'root' ], false ); + const l2 = locks.acquire( 'store', [ 'root' ], true ); + + // on each grant, verify that the other lock is not granted at the same time + const check1 = l1.then( () => { + l1Granted = true; + expect( l2Granted ).toBe( false ); + } ); + + const check2 = l2.then( () => { + l2Granted = true; + expect( l1Granted ).toBe( false ); + } ); + + // unlock both + const lock1 = await l1; + locks.release( lock1 ); + l1Granted = false; + + const lock2 = await l2; + locks.release( lock2 ); + l2Granted = false; + + // ensure that both locks were granted and checked + return await Promise.all( [ check1, check2 ] ); + } ); + + it( 'does not grant two exclusive locks to parent and child', async () => { + const locks = createLocks(); + + let l1Granted = false; + let l2Granted = false; + + // request two locks + const l1 = locks.acquire( 'store', [ 'root' ], true ); + const l2 = locks.acquire( 'store', [ 'root', 'child' ], true ); + + // on each grant, verify that the other lock is not granted at the same time + const check1 = l1.then( () => { + l1Granted = true; + expect( l2Granted ).toBe( false ); + } ); + + const check2 = l2.then( () => { + l2Granted = true; + expect( l1Granted ).toBe( false ); + } ); + + // unlock both + const lock1 = await l1; + locks.release( lock1 ); + l1Granted = false; + + const lock2 = await l2; + locks.release( lock2 ); + l2Granted = false; + + // ensure that both locks were granted and checked + return await Promise.all( [ check1, check2 ] ); + } ); + + it( 'grants two non-exclusive locks at once', async () => { + const locks = createLocks(); + + const l1 = await locks.acquire( 'store', [ 'root' ], false ); + const l2 = await locks.acquire( 'store', [ 'root' ], false ); + + expect( l1 ).not.toBeUndefined(); + expect( l2 ).not.toBeUndefined(); + } ); + + it( 'grants two exclusive locks to different branches', async () => { + const locks = createLocks(); + + const l1 = await locks.acquire( 'store', [ 'a' ], true ); + const l2 = await locks.acquire( 'store', [ 'b' ], true ); + + expect( l1 ).not.toBeUndefined(); + expect( l2 ).not.toBeUndefined(); + } ); +} ); + +/* eslint-enable jest/valid-expect-in-promise */ diff --git a/packages/core-data/src/locks/test/reducer.js b/packages/core-data/src/locks/test/reducer.js index b422ae9aa5ffa..238531ebe137c 100644 --- a/packages/core-data/src/locks/test/reducer.js +++ b/packages/core-data/src/locks/test/reducer.js @@ -6,7 +6,7 @@ import deepFreeze from 'deep-freeze'; /** * Internal dependencies */ -import { locks } from '../reducer'; +import locks from '../reducer'; const buildNode = ( children = {} ) => ( { locks: [], diff --git a/packages/core-data/src/locks/test/selectors.js b/packages/core-data/src/locks/test/selectors.js index 2f3f1cf77f707..667d93c635c00 100644 --- a/packages/core-data/src/locks/test/selectors.js +++ b/packages/core-data/src/locks/test/selectors.js @@ -6,58 +6,45 @@ import deepFreeze from 'deep-freeze'; /** * Internal dependencies */ -import { - __unstableGetPendingLockRequests, - __unstableIsLockAvailable, -} from '../selectors'; +import { getPendingLockRequests, isLockAvailable } from '../selectors'; import { deepCopyLocksTreePath, getNode } from '../utils'; -describe( '__unstableGetPendingLockRequests', () => { +describe( 'getPendingLockRequests', () => { it( 'returns pending lock requests', () => { const state = deepFreeze( { - locks: { - requests: [ 1, 2, 3 ], - }, + requests: [ 1, 2, 3 ], } ); - expect( __unstableGetPendingLockRequests( state ) ).toEqual( [ - 1, - 2, - 3, - ] ); + expect( getPendingLockRequests( state ) ).toEqual( [ 1, 2, 3 ] ); } ); } ); -describe( '__unstableIsLockAvailable', () => { +describe( 'isLockAvailable', () => { describe( 'smoke tests', () => { it( 'returns true if lock is available', () => { const state = deepFreeze( { - locks: { - tree: { - children: {}, - locks: [], - }, + tree: { + children: {}, + locks: [], }, } ); expect( - __unstableIsLockAvailable( state, 'core', [], { + isLockAvailable( state, 'core', [], { exclusive: true, } ) ).toBe( true ); } ); it( 'returns false if lock is not available', () => { const state = deepFreeze( { - locks: { - tree: { - children: {}, - locks: [ { exclusive: false } ], - }, + tree: { + children: {}, + locks: [ { exclusive: false } ], }, } ); expect( - __unstableIsLockAvailable( state, 'core', [], { + isLockAvailable( state, 'core', [], { exclusive: true, } ) ).toBe( false ); @@ -75,7 +62,7 @@ describe( '__unstableIsLockAvailable', () => { } ); it( `returns true if no parent or descendant has any locks`, () => { expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -89,7 +76,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive: true, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -103,7 +90,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive: true, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -114,41 +101,39 @@ describe( '__unstableIsLockAvailable', () => { it( `returns true if another branch holds a locks (3)`, () => { const subState = { - locks: { - tree: { - locks: [], - children: { - postType: { - locks: [], - children: { - post: { - locks: [], - children: { - 16: { - locks: [ - { - store: 'core', - path: [ - 'entities', - 'data', - 'postType', - 'post', - 16, - ], - exclusive: true, - }, - ], - children: {}, - }, + tree: { + locks: [], + children: { + postType: { + locks: [], + children: { + post: { + locks: [], + children: { + 16: { + locks: [ + { + store: 'core', + path: [ + 'entities', + 'data', + 'postType', + 'post', + 16, + ], + exclusive: true, + }, + ], + children: {}, }, }, - wp_template_part: { - locks: [], - children: { - 17: { - locks: [], - children: {}, - }, + }, + wp_template_part: { + locks: [], + children: { + 17: { + locks: [], + children: {}, }, }, }, @@ -158,7 +143,7 @@ describe( '__unstableIsLockAvailable', () => { }, }; expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( subState ), 'core', [ 'postType', 'wp_template_part', 17 ], @@ -169,41 +154,39 @@ describe( '__unstableIsLockAvailable', () => { it( `returns true if another branch holds a locks (4)`, () => { const subState = { - locks: { - tree: { - locks: [], - children: { - core: { - locks: [], - children: { - entities: { - locks: [], - children: { - data: { - locks: [], - children: { - postType: { - locks: [], - children: { - book: { - locks: [], - children: { - 67: { - locks: [ - { - path: [ - 'core', - 'entities', - 'data', - 'postType', - 'book', - 67, - ], - exclusive: true, - }, - ], - children: {}, - }, + tree: { + locks: [], + children: { + core: { + locks: [], + children: { + entities: { + locks: [], + children: { + data: { + locks: [], + children: { + postType: { + locks: [], + children: { + book: { + locks: [], + children: { + 67: { + locks: [ + { + path: [ + 'core', + 'entities', + 'data', + 'postType', + 'book', + 67, + ], + exclusive: true, + }, + ], + children: {}, }, }, }, @@ -219,7 +202,7 @@ describe( '__unstableIsLockAvailable', () => { }, }; expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( subState ), 'core', [ 'entities', 'data', 'postType', 'book', 67 ], @@ -231,7 +214,7 @@ describe( '__unstableIsLockAvailable', () => { [ true, false ].forEach( ( exclusive ) => { it( `returns true if the path is not accessible and no parent holds a lock`, () => { expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'fake', 'path' ], @@ -245,7 +228,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'fake', 'path' ], @@ -259,7 +242,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -273,7 +256,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -287,7 +270,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -306,7 +289,7 @@ describe( '__unstableIsLockAvailable', () => { } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -325,7 +308,7 @@ describe( '__unstableIsLockAvailable', () => { } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -345,7 +328,7 @@ describe( '__unstableIsLockAvailable', () => { } ); it( `returns true if no parent or descendant has any locks`, () => { expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -361,7 +344,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive: isOtherLockExclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'fake', 'path' ], @@ -375,7 +358,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive: isOtherLockExclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -389,7 +372,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive: isOtherLockExclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -403,7 +386,7 @@ describe( '__unstableIsLockAvailable', () => { exclusive: isOtherLockExclusive, } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -422,7 +405,7 @@ describe( '__unstableIsLockAvailable', () => { } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -441,7 +424,7 @@ describe( '__unstableIsLockAvailable', () => { } ); expect( - __unstableIsLockAvailable( + isLockAvailable( deepFreeze( state ), 'core', [ 'entities', 'root' ], @@ -454,20 +437,18 @@ describe( '__unstableIsLockAvailable', () => { } ); function appendLock( state, store, path, lock ) { - getNode( state.locks.tree, [ store, ...path ] ).locks.push( lock ); + getNode( state.tree, [ store, ...path ] ).locks.push( lock ); } function buildState( paths ) { return { - locks: { - requests: [], - tree: paths.reduce( - ( tree, path ) => deepCopyLocksTreePath( tree, path ), - { - locks: [], - children: {}, - } - ), - }, + requests: [], + tree: paths.reduce( + ( tree, path ) => deepCopyLocksTreePath( tree, path ), + { + locks: [], + children: {}, + } + ), }; } diff --git a/packages/core-data/src/queried-data/get-query-parts.js b/packages/core-data/src/queried-data/get-query-parts.js index 1cd9631495505..9075adb71fabf 100644 --- a/packages/core-data/src/queried-data/get-query-parts.js +++ b/packages/core-data/src/queried-data/get-query-parts.js @@ -60,12 +60,6 @@ export function getQueryParts( query ) { parts.perPage = Number( value ); break; - case 'include': - parts.include = getNormalizedCommaSeparable( value ).map( - Number - ); - break; - case 'context': parts.context = value; break; @@ -82,6 +76,15 @@ export function getQueryParts( query ) { value = parts.fields.join(); } + // Two requests with different include values cannot have same results. + if ( key === 'include' ) { + parts.include = getNormalizedCommaSeparable( value ).map( + Number + ); + // Normalize value for `stableKey`. + value = parts.include.join(); + } + // While it could be any deterministic string, for simplicity's // sake mimic querystring encoding for stable key. // diff --git a/packages/core-data/src/queried-data/test/get-query-parts.js b/packages/core-data/src/queried-data/test/get-query-parts.js index 0de6036d696b3..b3ca692ad09f5 100644 --- a/packages/core-data/src/queried-data/test/get-query-parts.js +++ b/packages/core-data/src/queried-data/test/get-query-parts.js @@ -24,7 +24,7 @@ describe( 'getQueryParts', () => { context: 'default', page: 1, perPage: 10, - stableKey: '', + stableKey: 'include=1', fields: null, include: [ 1 ], } ); diff --git a/packages/core-data/src/queried-data/test/selectors.js b/packages/core-data/src/queried-data/test/selectors.js index 39b46a97a9395..f0a38aab2887e 100644 --- a/packages/core-data/src/queried-data/test/selectors.js +++ b/packages/core-data/src/queried-data/test/selectors.js @@ -82,6 +82,7 @@ describe( 'getQueriedItems', () => { queries: { default: { '': [ 1, 2 ], + 'include=1': [ 1 ], }, }, }; diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 4b975e9a47efd..f78e6fd76eff9 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -15,7 +15,6 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; import { ifMatchingAction, replaceAction } from './utils'; import { reducer as queriedDataReducer } from './queried-data'; import { defaultEntities, DEFAULT_ENTITY_KEY } from './entities'; -import { reducer as locksReducer } from './locks'; /** * Reducer managing terms state. Keyed by taxonomy slug, the value is either @@ -580,5 +579,4 @@ export default combineReducers( { embedPreviews, userPermissions, autosaves, - locks: locksReducer, } ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 0d459c0d0ba65..49f5ae8809ab8 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -7,33 +7,14 @@ import { find, includes, get, hasIn, compact, uniq } from 'lodash'; * WordPress dependencies */ import { addQueryArgs } from '@wordpress/url'; -import { controls } from '@wordpress/data'; -import { apiFetch } from '@wordpress/data-controls'; -/** - * Internal dependencies - */ -import { regularFetch } from './controls'; -import { STORE_NAME } from './name'; +import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import { - receiveUserQuery, - receiveCurrentTheme, - receiveCurrentUser, - receiveEntityRecords, - receiveThemeSupports, - receiveEmbedPreview, - receiveUserPermission, - receiveAutosaves, -} from './actions'; +import { STORE_NAME } from './name'; import { getKindEntities, DEFAULT_ENTITY_KEY } from './entities'; import { ifNotResolved, getNormalizedCommaSeparable } from './utils'; -import { - __unstableAcquireStoreLock, - __unstableReleaseStoreLock, -} from './locks'; /** * Requests authors from the REST API. @@ -41,22 +22,22 @@ import { * @param {Object|undefined} query Optional object of query parameters to * include with request. */ -export function* getAuthors( query ) { +export const getAuthors = ( query ) => async ( { dispatch } ) => { const path = addQueryArgs( '/wp/v2/users/?who=authors&per_page=100', query ); - const users = yield apiFetch( { path } ); - yield receiveUserQuery( path, users ); -} + const users = await apiFetch( { path } ); + dispatch.receiveUserQuery( path, users ); +}; /** * Requests the current user from the REST API. */ -export function* getCurrentUser() { - const currentUser = yield apiFetch( { path: '/wp/v2/users/me' } ); - yield receiveCurrentUser( currentUser ); -} +export const getCurrentUser = () => async ( { dispatch } ) => { + const currentUser = await apiFetch( { path: '/wp/v2/users/me' } ); + dispatch.receiveCurrentUser( currentUser ); +}; /** * Requests an entity's record from the REST API. @@ -67,18 +48,22 @@ export function* getCurrentUser() { * @param {Object|undefined} query Optional object of query parameters to * include with request. */ -export function* getEntityRecord( kind, name, key = '', query ) { - const entities = yield getKindEntities( kind ); +export const getEntityRecord = ( kind, name, key = '', query ) => async ( { + select, + dispatch, +} ) => { + const entities = await dispatch( getKindEntities( kind ) ); const entity = find( entities, { kind, name } ); if ( ! entity ) { return; } - const lock = yield* __unstableAcquireStoreLock( + const lock = await dispatch.__unstableAcquireStoreLock( STORE_NAME, [ 'entities', 'data', kind, name, key ], { exclusive: false } ); + try { if ( query !== undefined && query._fields ) { // If requesting specific fields, items and query association to said @@ -111,27 +96,21 @@ export function* getEntityRecord( kind, name, key = '', query ) { // The resolution cache won't consider query as reusable based on the // fields, so it's tested here, prior to initiating the REST request, // and without causing `getEntityRecords` resolution to occur. - const hasRecords = yield controls.select( - STORE_NAME, - 'hasEntityRecords', - kind, - name, - query - ); + const hasRecords = select.hasEntityRecords( kind, name, query ); if ( hasRecords ) { return; } } - const record = yield apiFetch( { path } ); - yield receiveEntityRecords( kind, name, record, query ); + const record = await apiFetch( { path } ); + dispatch.receiveEntityRecords( kind, name, record, query ); } catch ( error ) { // We need a way to handle and access REST API errors in state // Until then, catching the error ensures the resolver is marked as resolved. } finally { - yield* __unstableReleaseStoreLock( lock ); + dispatch.__unstableReleaseStoreLock( lock ); } -} +}; /** * Requests an entity's record from the REST API. @@ -156,18 +135,21 @@ export const getEditedEntityRecord = ifNotResolved( * @param {string} name Entity name. * @param {Object?} query Query Object. */ -export function* getEntityRecords( kind, name, query = {} ) { - const entities = yield getKindEntities( kind ); +export const getEntityRecords = ( kind, name, query = {} ) => async ( { + dispatch, +} ) => { + const entities = await dispatch( getKindEntities( kind ) ); const entity = find( entities, { kind, name } ); if ( ! entity ) { return; } - const lock = yield* __unstableAcquireStoreLock( + const lock = await dispatch.__unstableAcquireStoreLock( STORE_NAME, [ 'entities', 'data', kind, name ], { exclusive: false } ); + try { if ( query._fields ) { // If requesting specific fields, items and query association to said @@ -187,7 +169,7 @@ export function* getEntityRecords( kind, name, query = {} ) { ...query, } ); - let records = Object.values( yield apiFetch( { path } ) ); + let records = Object.values( await apiFetch( { path } ) ); // If we request fields but the result doesn't contain the fields, // explicitely set these fields as "undefined" // that way we consider the query "fullfilled". @@ -203,7 +185,8 @@ export function* getEntityRecords( kind, name, query = {} ) { } ); } - yield receiveEntityRecords( kind, name, records, query ); + dispatch.receiveEntityRecords( kind, name, records, query ); + // When requesting all fields, the list of results can be used to // resolve the `getEntityRecord` selector in addition to `getEntityRecords`. // See https://github.com/WordPress/gutenberg/pull/26575 @@ -213,21 +196,21 @@ export function* getEntityRecords( kind, name, query = {} ) { .filter( ( record ) => record[ key ] ) .map( ( record ) => [ kind, name, record[ key ] ] ); - yield { + dispatch( { type: 'START_RESOLUTIONS', selectorName: 'getEntityRecord', args: resolutionsArgs, - }; - yield { + } ); + dispatch( { type: 'FINISH_RESOLUTIONS', selectorName: 'getEntityRecord', args: resolutionsArgs, - }; + } ); } } finally { - yield* __unstableReleaseStoreLock( lock ); + dispatch.__unstableReleaseStoreLock( lock ); } -} +}; getEntityRecords.shouldInvalidate = ( action, kind, name ) => { return ( @@ -241,39 +224,39 @@ getEntityRecords.shouldInvalidate = ( action, kind, name ) => { /** * Requests the current theme. */ -export function* getCurrentTheme() { - const activeThemes = yield apiFetch( { +export const getCurrentTheme = () => async ( { dispatch } ) => { + const activeThemes = await apiFetch( { path: '/wp/v2/themes?status=active', } ); - yield receiveCurrentTheme( activeThemes[ 0 ] ); -} + dispatch.receiveCurrentTheme( activeThemes[ 0 ] ); +}; /** * Requests theme supports data from the index. */ -export function* getThemeSupports() { - const activeThemes = yield apiFetch( { +export const getThemeSupports = () => async ( { dispatch } ) => { + const activeThemes = await apiFetch( { path: '/wp/v2/themes?status=active', } ); - yield receiveThemeSupports( activeThemes[ 0 ].theme_supports ); -} + dispatch.receiveThemeSupports( activeThemes[ 0 ].theme_supports ); +}; /** * Requests a preview from the from the Embed API. * * @param {string} url URL to get the preview for. */ -export function* getEmbedPreview( url ) { +export const getEmbedPreview = ( url ) => async ( { dispatch } ) => { try { - const embedProxyResponse = yield apiFetch( { + const embedProxyResponse = await apiFetch( { path: addQueryArgs( '/oembed/1.0/proxy', { url } ), } ); - yield receiveEmbedPreview( url, embedProxyResponse ); + dispatch.receiveEmbedPreview( url, embedProxyResponse ); } catch ( error ) { // Embed API 404s if the URL cannot be embedded, so we have to catch the error from the apiRequest here. - yield receiveEmbedPreview( url, false ); + dispatch.receiveEmbedPreview( url, false ); } -} +}; /** * Checks whether the current user can perform the given action on the given @@ -284,7 +267,7 @@ export function* getEmbedPreview( url ) { * @param {string} resource REST resource to check, e.g. 'media' or 'posts'. * @param {?string} id ID of the rest resource to check. */ -export function* canUser( action, resource, id ) { +export const canUser = ( action, resource, id ) => async ( { dispatch } ) => { const methods = { create: 'POST', read: 'GET', @@ -301,7 +284,7 @@ export function* canUser( action, resource, id ) { let response; try { - response = yield apiFetch( { + response = await apiFetch( { path, // Ideally this would always be an OPTIONS request, but unfortunately there's // a bug in the REST API which causes the Allow header to not be sent on @@ -329,8 +312,8 @@ export function* canUser( action, resource, id ) { const key = compact( [ action, resource, id ] ).join( '/' ); const isAllowed = includes( allowHeader, method ); - yield receiveUserPermission( key, isAllowed ); -} + dispatch.receiveUserPermission( key, isAllowed ); +}; /** * Checks whether the current user can perform the given action on the given @@ -340,16 +323,18 @@ export function* canUser( action, resource, id ) { * @param {string} name Entity name. * @param {string} recordId Record's id. */ -export function* canUserEditEntityRecord( kind, name, recordId ) { - const entities = yield getKindEntities( kind ); +export const canUserEditEntityRecord = ( kind, name, recordId ) => async ( { + dispatch, +} ) => { + const entities = await dispatch( getKindEntities( kind ) ); const entity = find( entities, { kind, name } ); if ( ! entity ) { return; } const resource = entity.__unstable_rest_base; - yield canUser( 'update', resource, recordId ); -} + await dispatch( canUser( 'update', resource, recordId ) ); +}; /** * Request autosave data from the REST API. @@ -357,20 +342,19 @@ export function* canUserEditEntityRecord( kind, name, recordId ) { * @param {string} postType The type of the parent post. * @param {number} postId The id of the parent post. */ -export function* getAutosaves( postType, postId ) { - const { rest_base: restBase } = yield controls.resolveSelect( - STORE_NAME, - 'getPostType', - postType - ); - const autosaves = yield apiFetch( { +export const getAutosaves = ( postType, postId ) => async ( { + dispatch, + resolveSelect, +} ) => { + const { rest_base: restBase } = await resolveSelect.getPostType( postType ); + const autosaves = await apiFetch( { path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit`, } ); if ( autosaves && autosaves.length ) { - yield receiveAutosaves( postId, autosaves ); + dispatch.receiveAutosaves( postId, autosaves ); } -} +}; /** * Request autosave data from the REST API. @@ -381,31 +365,30 @@ export function* getAutosaves( postType, postId ) { * @param {string} postType The type of the parent post. * @param {number} postId The id of the parent post. */ -export function* getAutosave( postType, postId ) { - yield controls.resolveSelect( - STORE_NAME, - 'getAutosaves', - postType, - postId - ); -} +export const getAutosave = ( postType, postId ) => async ( { + resolveSelect, +} ) => { + await resolveSelect.getAutosaves( postType, postId ); +}; /** * Retrieve the frontend template used for a given link. * * @param {string} link Link. */ -export function* __experimentalGetTemplateForLink( link ) { +export const __experimentalGetTemplateForLink = ( link ) => async ( { + dispatch, + resolveSelect, +} ) => { // Ideally this should be using an apiFetch call // We could potentially do so by adding a "filter" to the `wp_template` end point. // Also it seems the returned object is not a regular REST API post type. let template; try { - template = yield regularFetch( - addQueryArgs( link, { - '_wp-find-template': true, - } ) - ); + template = await window + .fetch( addQueryArgs( link, { '_wp-find-template': true } ) ) + .then( ( res ) => res.json() ) + .then( ( { data } ) => data ); } catch ( e ) { // For non-FSE themes, it is possible that this request returns an error. } @@ -414,21 +397,18 @@ export function* __experimentalGetTemplateForLink( link ) { return; } - yield getEntityRecord( 'postType', 'wp_template', template.id ); - const record = yield controls.select( - STORE_NAME, - 'getEntityRecord', + const record = await resolveSelect.getEntityRecord( 'postType', 'wp_template', template.id ); if ( record ) { - yield receiveEntityRecords( 'postType', 'wp_template', [ record ], { + dispatch.receiveEntityRecords( 'postType', 'wp_template', [ record ], { 'find-template': link, } ); } -} +}; __experimentalGetTemplateForLink.shouldInvalidate = ( action ) => { return ( diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index a7bb82da615ab..b3ab91d780960 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -17,7 +17,7 @@ import deprecated from '@wordpress/deprecated'; import { STORE_NAME } from './name'; import { getQueriedItems } from './queried-data'; import { DEFAULT_ENTITY_KEY } from './entities'; -import { getNormalizedCommaSeparable } from './utils'; +import { getNormalizedCommaSeparable, isRawAttribute } from './utils'; /** * Shared reference to an empty array for cases where it is important to avoid @@ -205,14 +205,18 @@ export const getRawEntityRecord = createSelector( return ( record && Object.keys( record ).reduce( ( accumulator, _key ) => { - // Because edits are the "raw" attribute values, - // we return those from record selectors to make rendering, - // comparisons, and joins with edits easier. - accumulator[ _key ] = get( - record[ _key ], - 'raw', - record[ _key ] - ); + if ( isRawAttribute( getEntity( state, kind, name ), _key ) ) { + // Because edits are the "raw" attribute values, + // we return those from record selectors to make rendering, + // comparisons, and joins with edits easier. + accumulator[ _key ] = get( + record[ _key ], + 'raw', + record[ _key ] + ); + } else { + accumulator[ _key ] = record[ _key ]; + } return accumulator; }, {} ) ); diff --git a/packages/core-data/src/test/actions.js b/packages/core-data/src/test/actions.js index 6dfec991e2a03..8aa1ecade3854 100644 --- a/packages/core-data/src/test/actions.js +++ b/packages/core-data/src/test/actions.js @@ -1,7 +1,9 @@ /** * WordPress dependencies */ -import { controls } from '@wordpress/data'; +import apiFetch from '@wordpress/api-fetch'; + +jest.mock( '@wordpress/api-fetch' ); /** * Internal dependencies @@ -10,26 +12,12 @@ import { editEntityRecord, saveEntityRecord, deleteEntityRecord, - receiveEntityRecords, receiveUserPermission, receiveAutosaves, receiveCurrentUser, __experimentalBatch, } from '../actions'; -jest.mock( '../locks/actions', () => ( { - __unstableAcquireStoreLock: jest.fn( () => [ - { - type: 'MOCKED_ACQUIRE_LOCK', - }, - ] ), - __unstableReleaseStoreLock: jest.fn( () => [ - { - type: 'MOCKED_RELEASE_LOCK', - }, - ] ), -} ) ); - jest.mock( '../batch', () => { const { createBatch } = jest.requireActual( '../batch' ); return { @@ -40,120 +28,161 @@ jest.mock( '../batch', () => { } ); describe( 'editEntityRecord', () => { - it( 'throws when the edited entity does not have a loaded config.', () => { + it( 'throws when the edited entity does not have a loaded config.', async () => { const entity = { kind: 'someKind', name: 'someName', id: 'someId' }; - const fulfillment = editEntityRecord( - entity.kind, - entity.name, - entity.id, - {} - ); - expect( fulfillment.next().value ).toEqual( - controls.select( 'core', 'getEntity', entity.kind, entity.name ) - ); - - // Don't pass back an entity config. - expect( fulfillment.next.bind( fulfillment ) ).toThrow( + const select = { + getEntity: jest.fn(), + }; + const fulfillment = () => + editEntityRecord( + entity.kind, + entity.name, + entity.id, + {} + )( { select } ); + expect( fulfillment ).toThrow( `The entity being edited (${ entity.kind }, ${ entity.name }) does not have a loaded config.` ); + expect( select.getEntity ).toHaveBeenCalledTimes( 1 ); } ); } ); describe( 'deleteEntityRecord', () => { + beforeEach( async () => { + apiFetch.mockReset(); + jest.useFakeTimers(); + } ); + it( 'triggers a DELETE request for an existing record', async () => { - const post = 10; + const deletedRecord = { title: 'new post', id: 10 }; const entities = [ { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, ]; - const fulfillment = deleteEntityRecord( 'postType', 'post', post ); - // Trigger generator - fulfillment.next(); + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( entities ); - // Acquire lock - expect( fulfillment.next( entities ).value.type ).toBe( - 'MOCKED_ACQUIRE_LOCK' - ); + // Provide response + apiFetch.mockImplementation( () => deletedRecord ); - // Start - expect( fulfillment.next().value.type ).toEqual( - 'DELETE_ENTITY_RECORD_START' - ); + const result = await deleteEntityRecord( + 'postType', + 'post', + deletedRecord.id + )( { dispatch } ); - // delete api call - const { value: apiFetchAction } = fulfillment.next(); - expect( apiFetchAction.request ).toEqual( { + expect( apiFetch ).toHaveBeenCalledTimes( 1 ); + expect( apiFetch ).toHaveBeenCalledWith( { path: '/wp/v2/posts/10', method: 'DELETE', } ); - expect( fulfillment.next().value.type ).toBe( 'REMOVE_ITEMS' ); - - expect( fulfillment.next().value.type ).toBe( - 'DELETE_ENTITY_RECORD_FINISH' + expect( dispatch ).toHaveBeenCalledTimes( 4 ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'DELETE_ENTITY_RECORD_START', + kind: 'postType', + name: 'post', + recordId: 10, + } ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'DELETE_ENTITY_RECORD_FINISH', + kind: 'postType', + name: 'post', + recordId: 10, + error: undefined, + } ); + expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledTimes( + 1 ); - - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); - expect( fulfillment.next() ).toMatchObject( { - done: true, - value: undefined, - } ); + expect( result ).toBe( deletedRecord ); } ); } ); describe( 'saveEntityRecord', () => { + beforeEach( async () => { + apiFetch.mockReset(); + jest.useFakeTimers(); + } ); + it( 'triggers a POST request for a new record', async () => { const post = { title: 'new post' }; const entities = [ { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, ]; - const fulfillment = saveEntityRecord( 'postType', 'post', post ); - // Trigger generator - fulfillment.next(); + const select = { + getRawEntityRecord: () => post, + }; - // Provide entities and acquire lock - expect( fulfillment.next( entities ).value.type ).toBe( - 'MOCKED_ACQUIRE_LOCK' - ); + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( entities ); - // Trigger apiFetch - expect( fulfillment.next().value.type ).toEqual( - 'SAVE_ENTITY_RECORD_START' - ); + // Provide response + const updatedRecord = { ...post, id: 10 }; + apiFetch.mockImplementation( () => { + return updatedRecord; + } ); - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - const { value: apiFetchAction } = fulfillment.next( {} ); - expect( apiFetchAction.request ).toEqual( { + const result = await saveEntityRecord( + 'postType', + 'post', + post + )( { select, dispatch } ); + + expect( apiFetch ).toHaveBeenCalledTimes( 1 ); + expect( apiFetch ).toHaveBeenCalledWith( { path: '/wp/v2/posts', method: 'POST', data: post, } ); - // Provide response and trigger action - const updatedRecord = { ...post, id: 10 }; - const { value: received } = fulfillment.next( updatedRecord ); - expect( received ).toEqual( - receiveEntityRecords( - 'postType', - 'post', - updatedRecord, - undefined, - true, - { title: 'new post' } - ) + + expect( dispatch ).toHaveBeenCalledTimes( 3 ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SAVE_ENTITY_RECORD_START', + kind: 'postType', + name: 'post', + recordId: undefined, + isAutosave: false, + } ); + expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledTimes( + 1 ); - expect( fulfillment.next().value.type ).toBe( - 'SAVE_ENTITY_RECORD_FINISH' + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SAVE_ENTITY_RECORD_FINISH', + kind: 'postType', + name: 'post', + recordId: undefined, + error: undefined, + isAutosave: false, + } ); + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledTimes( 1 ); + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith( + 'postType', + 'post', + updatedRecord, + undefined, + true, + post ); - expect( fulfillment.next().value ).toBe( updatedRecord ); + expect( result ).toBe( updatedRecord ); } ); it( 'triggers a PUT request for an existing record', async () => { @@ -161,41 +190,71 @@ describe( 'saveEntityRecord', () => { const entities = [ { name: 'post', kind: 'postType', baseURL: '/wp/v2/posts' }, ]; - const fulfillment = saveEntityRecord( 'postType', 'post', post ); - // Trigger generator - fulfillment.next(); + const select = { + getRawEntityRecord: () => post, + }; - // Provide entities and acquire lock - expect( fulfillment.next( entities ).value.type ).toBe( - 'MOCKED_ACQUIRE_LOCK' - ); + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( entities ); - // Trigger apiFetch - expect( fulfillment.next().value.type ).toEqual( - 'SAVE_ENTITY_RECORD_START' - ); - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - const { value: apiFetchAction } = fulfillment.next( {} ); - expect( apiFetchAction.request ).toEqual( { + // Provide response + const updatedRecord = { ...post, id: 10 }; + apiFetch.mockImplementation( () => { + return updatedRecord; + } ); + + const result = await saveEntityRecord( + 'postType', + 'post', + post + )( { select, dispatch } ); + + expect( apiFetch ).toHaveBeenCalledTimes( 1 ); + expect( apiFetch ).toHaveBeenCalledWith( { path: '/wp/v2/posts/10', method: 'PUT', data: post, } ); - // Provide response and trigger action - const { value: received } = fulfillment.next( post ); - expect( received ).toEqual( - receiveEntityRecords( 'postType', 'post', post, undefined, true, { - title: 'new post', - id: 10, - } ) + + expect( dispatch ).toHaveBeenCalledTimes( 3 ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SAVE_ENTITY_RECORD_START', + kind: 'postType', + name: 'post', + recordId: 10, + isAutosave: false, + } ); + expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledTimes( + 1 ); - expect( fulfillment.next().value.type ).toBe( - 'SAVE_ENTITY_RECORD_FINISH' + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SAVE_ENTITY_RECORD_FINISH', + kind: 'postType', + name: 'post', + recordId: 10, + error: undefined, + isAutosave: false, + } ); + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledTimes( 1 ); + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith( + 'postType', + 'post', + updatedRecord, + undefined, + true, + post ); + + expect( result ).toBe( updatedRecord ); } ); it( 'triggers a PUT request for an existing record with a custom key', async () => { @@ -208,45 +267,68 @@ describe( 'saveEntityRecord', () => { key: 'slug', }, ]; - const fulfillment = saveEntityRecord( 'root', 'postType', postType ); - // Trigger generator - fulfillment.next(); + const select = { + getRawEntityRecord: () => ( {} ), + }; - // Provide entities and acquire lock - expect( fulfillment.next( entities ).value.type ).toBe( - 'MOCKED_ACQUIRE_LOCK' - ); + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( entities ); - // Trigger apiFetch - expect( fulfillment.next().value.type ).toEqual( - 'SAVE_ENTITY_RECORD_START' - ); - expect( fulfillment.next().value.type ).toBe( '@@data/SELECT' ); - const { value: apiFetchAction } = fulfillment.next( {} ); - expect( apiFetchAction.request ).toEqual( { + // Provide response + apiFetch.mockImplementation( () => postType ); + + const result = await saveEntityRecord( + 'root', + 'postType', + postType + )( { select, dispatch } ); + + expect( apiFetch ).toHaveBeenCalledTimes( 1 ); + expect( apiFetch ).toHaveBeenCalledWith( { path: '/wp/v2/types/page', method: 'PUT', data: postType, } ); - // Provide response and trigger action - const { value: received } = fulfillment.next( postType ); - expect( received ).toEqual( - receiveEntityRecords( - 'root', - 'postType', - postType, - undefined, - true, - { slug: 'page', title: 'Pages' } - ) + + expect( dispatch ).toHaveBeenCalledTimes( 3 ); + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SAVE_ENTITY_RECORD_START', + kind: 'root', + name: 'postType', + recordId: 'page', + isAutosave: false, + } ); + expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledTimes( + 1 ); - expect( fulfillment.next().value.type ).toBe( - 'SAVE_ENTITY_RECORD_FINISH' + expect( dispatch ).toHaveBeenCalledWith( { + type: 'SAVE_ENTITY_RECORD_FINISH', + kind: 'root', + name: 'postType', + recordId: 'page', + error: undefined, + isAutosave: false, + } ); + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledTimes( 1 ); + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith( + 'root', + 'postType', + postType, + undefined, + true, + { slug: 'page', title: 'Pages' } ); + + expect( result ).toBe( postType ); } ); } ); @@ -305,21 +387,7 @@ describe( 'receiveCurrentUser', () => { describe( '__experimentalBatch', () => { it( 'batches multiple actions together', async () => { - const generator = __experimentalBatch( - [ - ( { saveEntityRecord: _saveEntityRecord } ) => - _saveEntityRecord( 'root', 'widget', {} ), - ( { saveEditedEntityRecord: _saveEditedEntityRecord } ) => - _saveEditedEntityRecord( 'root', 'widget', 123 ), - ( { deleteEntityRecord: _deleteEntityRecord } ) => - _deleteEntityRecord( 'root', 'widget', 123, {} ), - ], - { __unstableProcessor: ( inputs ) => Promise.resolve( inputs ) } - ); - // Run generator up to `yield getDispatch()`. - const { value: getDispatchControl } = generator.next(); - expect( getDispatchControl ).toEqual( { type: 'GET_DISPATCH' } ); - const actions = { + const dispatch = { saveEntityRecord: jest.fn( ( kind, name, record, { __unstableFetch } ) => { __unstableFetch( {} ); @@ -339,36 +407,39 @@ describe( '__experimentalBatch', () => { } ), }; - const dispatch = () => actions; - // Run generator up to `yield __unstableAwaitPromise( ... )`. - const { value: awaitPromiseControl } = generator.next( dispatch ); - expect( actions.saveEntityRecord ).toHaveBeenCalledWith( + + const results = await __experimentalBatch( + [ + ( { saveEntityRecord: _saveEntityRecord } ) => + _saveEntityRecord( 'root', 'widget', {} ), + ( { saveEditedEntityRecord: _saveEditedEntityRecord } ) => + _saveEditedEntityRecord( 'root', 'widget', 123 ), + ( { deleteEntityRecord: _deleteEntityRecord } ) => + _deleteEntityRecord( 'root', 'widget', 123, {} ), + ], + { __unstableProcessor: ( inputs ) => Promise.resolve( inputs ) } + )( { dispatch } ); + + expect( dispatch.saveEntityRecord ).toHaveBeenCalledWith( 'root', 'widget', {}, { __unstableFetch: expect.any( Function ) } ); - expect( actions.saveEditedEntityRecord ).toHaveBeenCalledWith( + expect( dispatch.saveEditedEntityRecord ).toHaveBeenCalledWith( 'root', 'widget', 123, { __unstableFetch: expect.any( Function ) } ); - expect( actions.deleteEntityRecord ).toHaveBeenCalledWith( + expect( dispatch.deleteEntityRecord ).toHaveBeenCalledWith( 'root', 'widget', 123, {}, { __unstableFetch: expect.any( Function ) } ); - expect( awaitPromiseControl ).toEqual( { - type: 'AWAIT_PROMISE', - promise: expect.any( Promise ), - } ); - // Run generator to the end. - const { value: results } = generator.next( - await awaitPromiseControl.promise - ); + expect( results ).toEqual( [ { id: 123, created: true }, { id: 123, updated: true }, diff --git a/packages/core-data/src/test/entities.js b/packages/core-data/src/test/entities.js index ee56890246820..7c654b8d1732f 100644 --- a/packages/core-data/src/test/entities.js +++ b/packages/core-data/src/test/entities.js @@ -1,3 +1,9 @@ +/** + * WordPress dependencies + */ +import triggerFetch from '@wordpress/api-fetch'; +jest.mock( '@wordpress/api-fetch' ); + /** * Internal dependencies */ @@ -7,7 +13,6 @@ import { getKindEntities, prePersistPostType, } from '../entities'; -import { addEntities } from '../actions'; describe( 'getMethodName', () => { it( 'should return the right method name for an entity with the root kind', () => { @@ -45,43 +50,52 @@ describe( 'getMethodName', () => { } ); describe( 'getKindEntities', () => { + beforeEach( async () => { + triggerFetch.mockReset(); + jest.useFakeTimers(); + } ); + it( 'shouldn’t do anything if the entities have already been resolved', async () => { + const dispatch = jest.fn(); + const select = { + getEntitiesByKind: jest.fn( () => entities ), + }; const entities = [ { kind: 'postType' } ]; - const fulfillment = getKindEntities( 'postType' ); - // Start the generator - fulfillment.next(); - // Provide the entities - const end = fulfillment.next( entities ); - expect( end.done ).toBe( true ); + await getKindEntities( 'postType' )( { dispatch, select } ); + expect( dispatch ).not.toHaveBeenCalled(); } ); it( 'shouldn’t do anything if there no defined kind config', async () => { - const fulfillment = getKindEntities( 'unknownKind' ); - // Start the generator - fulfillment.next(); - // Provide no entities to continue - const end = fulfillment.next( [] ); - expect( end.done ).toBe( true ); + const dispatch = jest.fn(); + const select = { + getEntitiesByKind: jest.fn( () => [] ), + }; + await getKindEntities( 'unknownKind' )( { dispatch, select } ); + expect( dispatch ).not.toHaveBeenCalled(); } ); it( 'should fetch and add the entities', async () => { const fetchedEntities = [ { - baseURL: '/wp/v2/posts', - kind: 'postType', - name: 'post', + rest_base: 'posts', + labels: { + singular_name: 'post', + }, }, ]; - const fulfillment = getKindEntities( 'postType' ); - // Start the generator - fulfillment.next(); - // Provide no entities to continue - fulfillment.next( [] ); - // Fetch entities and trigger action - const { value: action } = fulfillment.next( fetchedEntities ); - expect( action ).toEqual( addEntities( fetchedEntities ) ); - const end = fulfillment.next(); - expect( end ).toEqual( { done: true, value: fetchedEntities } ); + const dispatch = jest.fn(); + const select = { + getEntitiesByKind: jest.fn( () => [] ), + }; + triggerFetch.mockImplementation( () => fetchedEntities ); + + await getKindEntities( 'postType' )( { dispatch, select } ); + expect( dispatch ).toHaveBeenCalledTimes( 1 ); + expect( dispatch.mock.calls[ 0 ][ 0 ].type ).toBe( 'ADD_ENTITIES' ); + expect( dispatch.mock.calls[ 0 ][ 0 ].entities.length ).toBe( 1 ); + expect( dispatch.mock.calls[ 0 ][ 0 ].entities[ 0 ].baseURL ).toBe( + '/wp/v2/posts' + ); } ); } ); diff --git a/packages/core-data/src/test/integration.js b/packages/core-data/src/test/integration.js deleted file mode 100644 index 43fbdaeb27bd9..0000000000000 --- a/packages/core-data/src/test/integration.js +++ /dev/null @@ -1,264 +0,0 @@ -/** - * WordPress dependencies - */ -import { createRegistry, controls } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import * as actions from '../actions'; -import * as selectors from '../selectors'; -import * as resolvers from '../resolvers'; -import { store } from '../'; - -// Mock to prevent calling window.fetch in test environment -jest.mock( '@wordpress/data-controls', () => { - const dataControls = jest.requireActual( '@wordpress/data-controls' ); - return { - ...dataControls, - apiFetch: jest.fn(), - }; -} ); -const { apiFetch: actualApiFetch } = jest.requireActual( - '@wordpress/data-controls' -); -import { apiFetch } from '@wordpress/data-controls'; - -jest.mock( '@wordpress/api-fetch', () => { - return { - __esModule: true, - default: jest.fn(), - }; -} ); -import triggerFetch from '@wordpress/api-fetch'; - -const runPromise = async ( promise ) => { - jest.runAllTimers(); - await promise; -}; - -const runPendingPromises = async () => { - jest.runAllTimers(); - const p = new Promise( ( resolve ) => setTimeout( resolve ) ); - jest.runAllTimers(); - await p; -}; - -describe( 'receiveEntityRecord', () => { - function createTestRegistry( getEntityRecord ) { - const registry = createRegistry(); - const initialState = { - entities: { - data: {}, - }, - }; - registry.register( store ); - registry.registerStore( 'test/resolution', { - actions: { - receiveEntityRecords: actions.receiveEntityRecords, - *getEntityRecords( ...args ) { - return yield controls.resolveSelect( - 'test/resolution', - 'getEntityRecords', - ...args - ); - }, - *getEntityRecord( ...args ) { - return yield controls.resolveSelect( - 'test/resolution', - 'getEntityRecord', - ...args - ); - }, - }, - reducer: ( state = initialState ) => { - return state; - }, - selectors: { - getEntityRecord: selectors.getEntityRecord, - getEntityRecords: selectors.getEntityRecords, - }, - resolvers: { - getEntityRecord, - getEntityRecords: resolvers.getEntityRecords, - }, - } ); - return registry; - } - - beforeEach( async () => { - apiFetch.mockReset(); - triggerFetch.mockReset(); - jest.useFakeTimers(); - } ); - - it( 'should not trigger a resolver when the requested record is available via receiveEntityRecords (default entity key).', async () => { - const getEntityRecord = jest.fn(); - const registry = createTestRegistry( getEntityRecord ); - - // Trigger resolution of postType records - apiFetch.mockImplementation( () => ( { - 2: { slug: 'test', id: 2 }, - } ) ); - await runPromise( - registry - .dispatch( 'test/resolution' ) - .getEntityRecords( 'root', 'site' ) - ); - jest.runAllTimers(); - - // Select record with id = 2, it is available and should not trigger the resolver - await runPromise( - registry - .dispatch( 'test/resolution' ) - .getEntityRecord( 'root', 'site', 2 ) - ); - expect( getEntityRecord ).not.toHaveBeenCalled(); - - // Select record with id = 4, it is not available and should trigger the resolver - await runPromise( - registry - .dispatch( 'test/resolution' ) - .getEntityRecord( 'root', 'site', 4 ) - ); - expect( getEntityRecord ).toHaveBeenCalled(); - } ); - - it( 'should not trigger a resolver when the requested record is available via receiveEntityRecords (non-default entity key).', async () => { - const getEntityRecord = jest.fn(); - const registry = createTestRegistry( getEntityRecord ); - - // Trigger resolution of postType records - apiFetch.mockImplementation( () => ( { - 'test-1': { slug: 'test-1', id: 2 }, - } ) ); - await runPromise( - registry - .dispatch( 'test/resolution' ) - .getEntityRecords( 'root', 'taxonomy' ) - ); - jest.runAllTimers(); - - // Select record with id = test-1, it is available and should not trigger the resolver - await runPromise( - registry - .dispatch( 'test/resolution' ) - .getEntityRecord( 'root', 'taxonomy', 'test-1' ) - ); - expect( getEntityRecord ).not.toHaveBeenCalled(); - - // Select record with id = test-2, it is not available and should trigger the resolver - await runPromise( - registry - .dispatch( 'test/resolution' ) - .getEntityRecord( 'root', 'taxonomy', 'test-2' ) - ); - expect( getEntityRecord ).toHaveBeenCalled(); - } ); -} ); - -describe( 'saveEntityRecord', () => { - function createTestRegistry() { - const registry = createRegistry(); - registry.register( store ); - return registry; - } - - beforeEach( async () => { - apiFetch.mockReset(); - triggerFetch.mockReset(); - jest.useFakeTimers( 'modern' ); - } ); - - it( 'should not trigger any GET requests until POST/PUT is finished.', async () => { - const registry = createTestRegistry(); - // Fetch post types from the API {{{ - apiFetch.mockImplementation( () => ( { - 'post-1': { slug: 'post-1' }, - } ) ); - - // Trigger fetch - registry.select( 'core' ).getEntityRecords( 'root', 'postType' ); - jest.runAllTimers(); - await Promise.resolve().then( () => jest.runAllTimers() ); - expect( apiFetch ).toBeCalledTimes( 1 ); - expect( apiFetch ).toBeCalledWith( { - path: '/wp/v2/types?context=edit', - } ); - - // Select fetched results, there should be no subsequent request - apiFetch.mockReset(); - const results = registry - .select( 'core' ) - .getEntityRecords( 'root', 'postType' ); - expect( apiFetch ).toBeCalledTimes( 0 ); - jest.runAllTimers(); - expect( apiFetch ).toBeCalledTimes( 0 ); - expect( results ).toHaveLength( 1 ); - expect( results[ 0 ].slug ).toBe( 'post-1' ); - // }}} Fetch post types from the API - - // Save changes - apiFetch.mockClear(); - apiFetch.mockImplementation( actualApiFetch ); - let resolvePromise; - triggerFetch.mockImplementation( function () { - return new Promise( ( resolve ) => { - resolvePromise = resolve; - } ); - } ); - const savePromise = registry - .dispatch( 'core' ) - .saveEntityRecord( 'root', 'postType', { - slug: 'post-1', - newField: 'a', - } ); - await runPendingPromises(); - - // There should ONLY be a single hanging API call (PUT) by this point. - // If there have been any other requests, it is a race condition of some sorts, - // e.g. a resolution was triggered before the save was finished. - expect( triggerFetch ).toBeCalledTimes( 1 ); - expect( triggerFetch ).toHaveBeenCalledWith( - expect.objectContaining( { - method: 'PUT', - path: '/wp/v2/types/post-1', - data: expect.objectContaining( { - newField: 'a', - slug: 'post-1', - } ), - } ) - ); - triggerFetch.mockClear(); - apiFetch.mockClear(); - - // The PUT is still hanging, let's call a selector now and make sure it won't trigger - // any requests - registry.select( 'core' ).getEntityRecords( 'root', 'postType' ); - jest.runAllTimers(); - expect( triggerFetch ).toBeCalledTimes( 0 ); - - // Now that all timers are exhausted, let's resolve the PUT request and wait until the - // save is complete - resolvePromise( { newField: 'a', slug: 'post-1' } ); - - // Run selector and make sure it doesn't trigger any requests just yet - registry.select( 'core' ).getEntityRecords( 'root', 'postType' ); - jest.runAllTimers(); - expect( triggerFetch ).toBeCalledTimes( 0 ); - - const newRecord = await savePromise; - expect( newRecord ).toEqual( { newField: 'a', slug: 'post-1' } ); - // There should be no other API calls just because saving succeeded - jest.runAllTimers(); - expect( triggerFetch ).toBeCalledTimes( 0 ); - - // Calling the selector after the save is finished should trigger a resolver and a GET request - registry.select( 'core' ).getEntityRecords( 'root', 'postType' ); - await runPendingPromises(); - expect( triggerFetch ).toBeCalledTimes( 1 ); - expect( triggerFetch ).toBeCalledWith( { - path: '/wp/v2/types?context=edit', - } ); - } ); -} ); diff --git a/packages/core-data/src/test/resolvers.js b/packages/core-data/src/test/resolvers.js index 0cb095dc5fb87..f5be29cb1543d 100644 --- a/packages/core-data/src/test/resolvers.js +++ b/packages/core-data/src/test/resolvers.js @@ -1,7 +1,9 @@ /** * WordPress dependencies */ -import { apiFetch } from '@wordpress/data-controls'; +import triggerFetch from '@wordpress/api-fetch'; + +jest.mock( '@wordpress/api-fetch' ); /** * Internal dependencies @@ -14,26 +16,6 @@ import { getAutosaves, getCurrentUser, } from '../resolvers'; -import { - receiveEntityRecords, - receiveEmbedPreview, - receiveUserPermission, - receiveAutosaves, - receiveCurrentUser, -} from '../actions'; - -jest.mock( '../locks/actions', () => ( { - __unstableAcquireStoreLock: jest.fn( () => [ - { - type: 'MOCKED_ACQUIRE_LOCK', - }, - ] ), - __unstableReleaseStoreLock: jest.fn( () => [ - { - type: 'MOCKED_RELEASE_LOCK', - }, - ] ), -} ) ); describe( 'getEntityRecord', () => { const POST_TYPE = { slug: 'post' }; @@ -45,28 +27,44 @@ describe( 'getEntityRecord', () => { baseURLParams: { context: 'edit' }, }, ]; + beforeEach( async () => { + triggerFetch.mockReset(); + jest.useFakeTimers(); + } ); it( 'yields with requested post type', async () => { - const fulfillment = getEntityRecord( 'root', 'postType', 'post' ); - // Trigger generator - fulfillment.next(); - // Provide entities and acquire lock - expect( fulfillment.next( ENTITIES ).value.type ).toEqual( - 'MOCKED_ACQUIRE_LOCK' - ); - // trigger apiFetch - const { value: apiFetchAction } = fulfillment.next(); - expect( apiFetchAction.request ).toEqual( { + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( ENTITIES ); + + // Provide response + triggerFetch.mockImplementation( () => POST_TYPE ); + + await getEntityRecord( 'root', 'postType', 'post' )( { dispatch } ); + + // Fetch request should have been issued + expect( triggerFetch ).toHaveBeenCalledWith( { path: '/wp/v2/types/post?context=edit', } ); - // Provide response and trigger action - const { value: received } = fulfillment.next( POST_TYPE ); - expect( received ).toEqual( - receiveEntityRecords( 'root', 'postType', POST_TYPE ) + + // The record should have been received + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith( + 'root', + 'postType', + POST_TYPE, + undefined ); - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + + // Locks should have been acquired and released + expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledTimes( + 1 + ); + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); } ); @@ -74,42 +72,54 @@ describe( 'getEntityRecord', () => { const query = { context: 'view', _envelope: '1' }; const queryObj = { include: [ 'post' ], ...query }; - const fulfillment = getEntityRecord( + const select = { + hasEntityRecords: jest.fn( () => {} ), + }; + + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( ENTITIES ); + + // Provide response + triggerFetch.mockImplementation( () => POST_TYPE ); + + await getEntityRecord( 'root', 'postType', 'post', query - ); - - // Trigger generator - fulfillment.next(); - - // Provide entities and acquire lock - expect( fulfillment.next( ENTITIES ).value.type ).toEqual( - 'MOCKED_ACQUIRE_LOCK' - ); + )( { dispatch, select } ); // Check resolution cache for an existing entity that fulfills the request with query - const { - value: { args: selectArgs }, - } = fulfillment.next(); - expect( selectArgs ).toEqual( [ 'root', 'postType', queryObj ] ); + expect( select.hasEntityRecords ).toHaveBeenCalledWith( + 'root', + 'postType', + queryObj + ); // Trigger apiFetch, test that the query is present in the url - const { value: apiFetchAction } = fulfillment.next(); - expect( apiFetchAction.request ).toEqual( { + expect( triggerFetch ).toHaveBeenCalledWith( { path: '/wp/v2/types/post?context=view&_envelope=1', } ); - // Receive response - const { value: received } = fulfillment.next( POST_TYPE ); - expect( received ).toEqual( - receiveEntityRecords( 'root', 'postType', POST_TYPE, queryObj ) + // The record should have been received + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith( + 'root', + 'postType', + POST_TYPE, + queryObj ); - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + // Locks should have been acquired and released + expect( dispatch.__unstableAcquireStoreLock ).toHaveBeenCalledTimes( + 1 + ); + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); } ); } ); @@ -134,71 +144,97 @@ describe( 'getEntityRecords', () => { }, ]; - it( 'yields with requested post type', async () => { - const fulfillment = getEntityRecords( 'root', 'postType' ); + beforeEach( async () => { + triggerFetch.mockReset(); + jest.useFakeTimers(); + } ); - // Trigger generator - fulfillment.next(); + it( 'dispatches the requested post type', async () => { + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( ENTITIES ); - // Provide entities and acquire lock - fulfillment.next( ENTITIES ); + // Provide response + triggerFetch.mockImplementation( () => POST_TYPES ); - // trigger apiFetch - const { value: apiFetchAction } = fulfillment.next(); + await getEntityRecords( 'root', 'postType' )( { dispatch } ); - expect( apiFetchAction.request ).toEqual( { + // Fetch request should have been issued + expect( triggerFetch ).toHaveBeenCalledWith( { path: '/wp/v2/types?context=edit', } ); - // Provide response and trigger action - const { value: received } = fulfillment.next( POST_TYPES ); - expect( received ).toEqual( - receiveEntityRecords( - 'root', - 'postType', - Object.values( POST_TYPES ), - {} - ) + + // The record should have been received + expect( dispatch.receiveEntityRecords ).toHaveBeenCalledWith( + 'root', + 'postType', + Object.values( POST_TYPES ), + {} ); } ); it( 'Uses state locks', async () => { - const fulfillment = getEntityRecords( 'root', 'postType' ); + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( ENTITIES ); - // Repeat the steps from `yields with requested post type` test - fulfillment.next(); - // Provide entities and acquire lock - expect( fulfillment.next( ENTITIES ).value.type ).toEqual( - 'MOCKED_ACQUIRE_LOCK' - ); - fulfillment.next(); - fulfillment.next( POST_TYPES ); + // Provide response + triggerFetch.mockImplementation( () => POST_TYPES ); - // Resolve specific entity records - fulfillment.next(); - fulfillment.next(); + await getEntityRecords( 'root', 'postType' )( { dispatch } ); - // Release lock - expect( fulfillment.next().value.type ).toEqual( - 'MOCKED_RELEASE_LOCK' + // Fetch request should have been issued + expect( triggerFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/types?context=edit', + } ); + + // The record should have been received + expect( + dispatch.__unstableAcquireStoreLock + ).toHaveBeenCalledWith( + 'core', + [ 'entities', 'data', 'root', 'postType' ], + { exclusive: false } + ); + expect( dispatch.__unstableReleaseStoreLock ).toHaveBeenCalledTimes( + 1 ); } ); it( 'marks specific entity records as resolved', async () => { - const fulfillment = getEntityRecords( 'root', 'postType' ); + const dispatch = Object.assign( jest.fn(), { + receiveEntityRecords: jest.fn(), + __unstableAcquireStoreLock: jest.fn(), + __unstableReleaseStoreLock: jest.fn(), + } ); + // Provide entities + dispatch.mockReturnValueOnce( ENTITIES ); + + // Provide response + triggerFetch.mockImplementation( () => POST_TYPES ); + + await getEntityRecords( 'root', 'postType' )( { dispatch } ); - // Repeat the steps from `yields with requested post type` test - fulfillment.next(); - fulfillment.next( ENTITIES ); - fulfillment.next(); - fulfillment.next( POST_TYPES ); + // Fetch request should have been issued + expect( triggerFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/types?context=edit', + } ); - // It should mark the entity record that has an ID as resolved - expect( fulfillment.next().value ).toEqual( { + // The record should have been received + expect( dispatch ).toHaveBeenCalledWith( { type: 'START_RESOLUTIONS', selectorName: 'getEntityRecord', args: [ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ] ], } ); - expect( fulfillment.next().value ).toEqual( { + expect( dispatch ).toHaveBeenCalledWith( { type: 'FINISH_RESOLUTIONS', selectorName: 'getEntityRecord', args: [ [ ENTITIES[ 1 ].kind, ENTITIES[ 1 ].name, 2 ] ], @@ -213,130 +249,136 @@ describe( 'getEmbedPreview', () => { const UNEMBEDDABLE_URL = 'http://example.com/'; it( 'yields with fetched embed preview', async () => { - const fulfillment = getEmbedPreview( EMBEDDABLE_URL ); - // Trigger generator - fulfillment.next(); - // Provide apiFetch response and trigger Action - const received = ( await fulfillment.next( SUCCESSFUL_EMBED_RESPONSE ) ) - .value; - expect( received ).toEqual( - receiveEmbedPreview( EMBEDDABLE_URL, SUCCESSFUL_EMBED_RESPONSE ) + const dispatch = Object.assign( jest.fn(), { + receiveEmbedPreview: jest.fn(), + } ); + + // Provide response + triggerFetch.mockResolvedValue( SUCCESSFUL_EMBED_RESPONSE ); + + await getEmbedPreview( EMBEDDABLE_URL )( { dispatch } ); + + expect( dispatch.receiveEmbedPreview ).toHaveBeenCalledWith( + EMBEDDABLE_URL, + SUCCESSFUL_EMBED_RESPONSE ); } ); it( 'yields false if the URL cannot be embedded', async () => { - const fulfillment = getEmbedPreview( UNEMBEDDABLE_URL ); - // Trigger generator - fulfillment.next(); - // Provide invalid response and trigger Action - const received = ( await fulfillment.throw( { status: 404 } ) ).value; - expect( received ).toEqual( - receiveEmbedPreview( UNEMBEDDABLE_URL, UNEMBEDDABLE_RESPONSE ) + const dispatch = Object.assign( jest.fn(), { + receiveEmbedPreview: jest.fn(), + } ); + + // Provide response + triggerFetch.mockRejectedValue( { status: 404 } ); + + await getEmbedPreview( UNEMBEDDABLE_URL )( { dispatch } ); + + expect( dispatch.receiveEmbedPreview ).toHaveBeenCalledWith( + UNEMBEDDABLE_URL, + UNEMBEDDABLE_RESPONSE ); } ); } ); describe( 'canUser', () => { - it( 'does nothing when there is an API error', () => { - const generator = canUser( 'create', 'media' ); - - let received = generator.next(); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - apiFetch( { - path: '/wp/v2/media', - method: 'OPTIONS', - parse: false, - } ) + beforeEach( async () => { + triggerFetch.mockReset(); + } ); + + it( 'does nothing when there is an API error', async () => { + const dispatch = Object.assign( jest.fn(), { + receiveUserPermission: jest.fn(), + } ); + + triggerFetch.mockImplementation( () => + Promise.reject( { status: 404 } ) ); - received = generator.throw( { status: 404 } ); - expect( received.done ).toBe( true ); - expect( received.value ).toBeUndefined(); + await canUser( 'create', 'media' )( { dispatch } ); + + expect( triggerFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/media', + method: 'OPTIONS', + parse: false, + } ); + + expect( dispatch.receiveUserPermission ).not.toHaveBeenCalled(); } ); - it( 'receives false when the user is not allowed to perform an action', () => { - const generator = canUser( 'create', 'media' ); - - let received = generator.next(); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - apiFetch( { - path: '/wp/v2/media', - method: 'OPTIONS', - parse: false, - } ) - ); + it( 'receives false when the user is not allowed to perform an action', async () => { + const dispatch = Object.assign( jest.fn(), { + receiveUserPermission: jest.fn(), + } ); - received = generator.next( { + triggerFetch.mockImplementation( () => ( { headers: { Allow: 'GET', }, + } ) ); + + await canUser( 'create', 'media' )( { dispatch } ); + + expect( triggerFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/media', + method: 'OPTIONS', + parse: false, } ); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - receiveUserPermission( 'create/media', false ) - ); - received = generator.next(); - expect( received.done ).toBe( true ); - expect( received.value ).toBeUndefined(); + expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith( + 'create/media', + false + ); } ); - it( 'receives true when the user is allowed to perform an action', () => { - const generator = canUser( 'create', 'media' ); - - let received = generator.next(); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - apiFetch( { - path: '/wp/v2/media', - method: 'OPTIONS', - parse: false, - } ) - ); + it( 'receives true when the user is allowed to perform an action', async () => { + const dispatch = Object.assign( jest.fn(), { + receiveUserPermission: jest.fn(), + } ); - received = generator.next( { + triggerFetch.mockImplementation( () => ( { headers: { Allow: 'POST, GET, PUT, DELETE', }, + } ) ); + + await canUser( 'create', 'media' )( { dispatch } ); + + expect( triggerFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/media', + method: 'OPTIONS', + parse: false, } ); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - receiveUserPermission( 'create/media', true ) - ); - received = generator.next(); - expect( received.done ).toBe( true ); - expect( received.value ).toBeUndefined(); + expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith( + 'create/media', + true + ); } ); - it( 'receives true when the user is allowed to perform an action on a specific resource', () => { - const generator = canUser( 'update', 'blocks', 123 ); - - let received = generator.next(); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - apiFetch( { - path: '/wp/v2/blocks/123', - method: 'GET', - parse: false, - } ) - ); + it( 'receives true when the user is allowed to perform an action on a specific resource', async () => { + const dispatch = Object.assign( jest.fn(), { + receiveUserPermission: jest.fn(), + } ); - received = generator.next( { + triggerFetch.mockImplementation( () => ( { headers: { Allow: 'POST, GET, PUT, DELETE', }, + } ) ); + + await canUser( 'create', 'blocks', 123 )( { dispatch } ); + + expect( triggerFetch ).toHaveBeenCalledWith( { + path: '/wp/v2/blocks/123', + method: 'GET', + parse: false, } ); - expect( received.done ).toBe( false ); - expect( received.value ).toEqual( - receiveUserPermission( 'update/blocks/123', true ) - ); - received = generator.next(); - expect( received.done ).toBe( true ); - expect( received.value ).toBeUndefined(); + expect( dispatch.receiveUserPermission ).toHaveBeenCalledWith( + 'create/blocks/123', + true + ); } ); } ); @@ -349,28 +391,31 @@ describe( 'getAutosaves', () => { }, ]; + beforeEach( async () => { + triggerFetch.mockReset(); + } ); + it( 'yields with fetched autosaves', async () => { const postType = 'post'; const postId = 1; const restBase = 'posts'; const postEntity = { rest_base: restBase }; - const fulfillment = getAutosaves( postType, postId ); - // Trigger generator - fulfillment.next(); + triggerFetch.mockImplementation( () => SUCCESSFUL_RESPONSE ); + const dispatch = Object.assign( jest.fn(), { + receiveAutosaves: jest.fn(), + } ); + const resolveSelect = Object.assign( jest.fn(), { + getPostType: jest.fn( () => postEntity ), + } ); + await getAutosaves( postType, postId )( { dispatch, resolveSelect } ); - // Trigger generator with the postEntity and assert that correct path is formed - // in the apiFetch request. - const { value: apiFetchAction } = fulfillment.next( postEntity ); - expect( apiFetchAction.request ).toEqual( { + expect( triggerFetch ).toHaveBeenCalledWith( { path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit`, } ); - - // Provide apiFetch response and trigger Action - const received = ( await fulfillment.next( SUCCESSFUL_RESPONSE ) ) - .value; - expect( received ).toEqual( - receiveAutosaves( 1, SUCCESSFUL_RESPONSE ) + expect( dispatch.receiveAutosaves ).toHaveBeenCalledWith( + 1, + SUCCESSFUL_RESPONSE ); } ); @@ -379,21 +424,20 @@ describe( 'getAutosaves', () => { const postId = 1; const restBase = 'posts'; const postEntity = { rest_base: restBase }; - const fulfillment = getAutosaves( postType, postId ); - // Trigger generator - fulfillment.next(); + triggerFetch.mockImplementation( () => [] ); + const dispatch = Object.assign( jest.fn(), { + receiveAutosaves: jest.fn(), + } ); + const resolveSelect = Object.assign( jest.fn(), { + getPostType: jest.fn( () => postEntity ), + } ); + await getAutosaves( postType, postId )( { dispatch, resolveSelect } ); - // Trigger generator with the postEntity and assert that correct path is formed - // in the apiFetch request. - const { value: apiFetchAction } = fulfillment.next( postEntity ); - expect( apiFetchAction.request ).toEqual( { + expect( triggerFetch ).toHaveBeenCalledWith( { path: `/wp/v2/${ restBase }/${ postId }/autosaves?context=edit`, } ); - - // Provide apiFetch response and trigger Action - const received = ( await fulfillment.next( [] ) ).value; - expect( received ).toBeUndefined(); + expect( dispatch.receiveAutosaves ).not.toHaveBeenCalled(); } ); } ); @@ -403,14 +447,17 @@ describe( 'getCurrentUser', () => { }; it( 'yields with fetched user', async () => { - const fulfillment = getCurrentUser(); + const dispatch = Object.assign( jest.fn(), { + receiveCurrentUser: jest.fn(), + } ); - // Trigger generator - fulfillment.next(); + // Provide response + triggerFetch.mockResolvedValue( SUCCESSFUL_RESPONSE ); - // Provide apiFetch response and trigger Action - const received = ( await fulfillment.next( SUCCESSFUL_RESPONSE ) ) - .value; - expect( received ).toEqual( receiveCurrentUser( SUCCESSFUL_RESPONSE ) ); + await getCurrentUser()( { dispatch } ); + + expect( dispatch.receiveCurrentUser ).toHaveBeenCalledWith( + SUCCESSFUL_RESPONSE + ); } ); } ); diff --git a/packages/core-data/src/test/selectors.js b/packages/core-data/src/test/selectors.js index 586d39985e0c5..f76d60a09a4ab 100644 --- a/packages/core-data/src/test/selectors.js +++ b/packages/core-data/src/test/selectors.js @@ -11,6 +11,7 @@ import { __experimentalGetEntityRecordNoResolver, hasEntityRecords, getEntityRecords, + getRawEntityRecord, __experimentalGetDirtyEntityRecords, __experimentalGetEntitiesBeingSaved, getEntityRecordNonTransientEdits, @@ -204,6 +205,76 @@ describe( 'hasEntityRecords', () => { } ); } ); +describe( 'getRawEntityRecord', () => { + const data = { + someKind: { + someName: { + queriedData: { + items: { + default: { + post: { + title: { + raw: { html: '

post

' }, + rendered: + '

rendered post

', + }, + }, + }, + }, + itemIsComplete: { + default: { + post: true, + }, + }, + queries: {}, + }, + }, + }, + }; + it( 'should preserve the structure of `raw` field by default', () => { + const state = deepFreeze( { + entities: { + config: [ + { + kind: 'someKind', + name: 'someName', + }, + ], + data: { ...data }, + }, + } ); + expect( + getRawEntityRecord( state, 'someKind', 'someName', 'post' ) + ).toEqual( { + title: { + raw: { html: '

post

' }, + rendered: '

rendered post

', + }, + } ); + } ); + it( 'should flatten the structure of `raw` field for entities configured with rawAttributes', () => { + const state = deepFreeze( { + entities: { + config: [ + { + kind: 'someKind', + name: 'someName', + rawAttributes: [ 'title' ], + }, + ], + data: { ...data }, + }, + } ); + expect( + getRawEntityRecord( state, 'someKind', 'someName', 'post' ) + ).toEqual( { + title: { + html: '

post

', + }, + } ); + } ); +} ); + describe( 'getEntityRecords', () => { it( 'should return null by default', () => { const state = deepFreeze( { diff --git a/packages/core-data/src/utils/if-not-resolved.js b/packages/core-data/src/utils/if-not-resolved.js index 0baf36f0e5d8d..653827cdfe6c0 100644 --- a/packages/core-data/src/utils/if-not-resolved.js +++ b/packages/core-data/src/utils/if-not-resolved.js @@ -1,13 +1,3 @@ -/** - * WordPress dependencies - */ -import { controls } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from '../name'; - /** * Higher-order function which invokes the given resolver only if it has not * already been resolved with the arguments passed to the enhanced function. @@ -20,21 +10,13 @@ import { STORE_NAME } from '../name'; * * @return {Function} Enhanced resolver. */ -const ifNotResolved = ( resolver, selectorName ) => - /** - * @param {...any} args Original resolver arguments. - */ - function* resolveIfNotResolved( ...args ) { - const hasStartedResolution = yield controls.select( - STORE_NAME, - 'hasStartedResolution', - selectorName, - args - ); - - if ( ! hasStartedResolution ) { - yield* resolver( ...args ); - } - }; +const ifNotResolved = ( resolver, selectorName ) => ( ...args ) => async ( { + select, + dispatch, +} ) => { + if ( ! select.hasStartedResolution( selectorName, args ) ) { + await dispatch( resolver( ...args ) ); + } +}; export default ifNotResolved; diff --git a/packages/core-data/src/utils/index.js b/packages/core-data/src/utils/index.js index 05e8d73bf7630..a4f4bf81373cd 100644 --- a/packages/core-data/src/utils/index.js +++ b/packages/core-data/src/utils/index.js @@ -5,3 +5,4 @@ export { default as ifNotResolved } from './if-not-resolved'; export { default as onSubKey } from './on-sub-key'; export { default as replaceAction } from './replace-action'; export { default as withWeakMapCache } from './with-weak-map-cache'; +export { default as isRawAttribute } from './is-raw-attribute'; diff --git a/packages/core-data/src/utils/is-raw-attribute.js b/packages/core-data/src/utils/is-raw-attribute.js new file mode 100644 index 0000000000000..f8e8d4de359a4 --- /dev/null +++ b/packages/core-data/src/utils/is-raw-attribute.js @@ -0,0 +1,11 @@ +/** + * Checks whether the attribute is a "raw" attribute or not. + * + * @param {Object} entity Entity data. + * @param {string} attribute Attribute name. + * + * @return {boolean} Is the attribute raw + */ +export default function isRawAttribute( entity, attribute ) { + return ( entity.rawAttributes || [] ).includes( attribute ); +} diff --git a/packages/core-data/src/utils/test/if-not-resolved.js b/packages/core-data/src/utils/test/if-not-resolved.js index a773097d70514..291e224b3833c 100644 --- a/packages/core-data/src/utils/test/if-not-resolved.js +++ b/packages/core-data/src/utils/test/if-not-resolved.js @@ -27,49 +27,50 @@ describe( 'ifNotResolved', () => { expect( resolver ).toBeInstanceOf( Function ); } ); - it( 'triggers original resolver if not already resolved', () => { - controls.select.mockImplementation( ( _storeKey, selectorName ) => ( { - _nextValue: - selectorName === 'hasStartedResolution' ? false : undefined, - } ) ); + it( 'triggers original resolver if not already resolved', async () => { + const select = { hasStartedResolution: () => false }; + const dispatch = () => {}; const originalResolver = jest .fn() - .mockImplementation( function* () {} ); + .mockImplementation( async function () {} ); const resolver = ifNotResolved( originalResolver, 'originalResolver' ); - - const runResolver = resolver(); - - let next, nextValue; - do { - next = runResolver.next( nextValue ); - nextValue = next.value?._nextValue; - } while ( ! next.done ); + await resolver()( { select, dispatch } ); expect( originalResolver ).toHaveBeenCalledTimes( 1 ); } ); - it( 'does not trigger original resolver if already resolved', () => { - controls.select.mockImplementation( ( _storeKey, selectorName ) => ( { - _nextValue: - selectorName === 'hasStartedResolution' ? true : undefined, - } ) ); + it( 'does not trigger original resolver if already resolved', async () => { + const select = { hasStartedResolution: () => true }; + const dispatch = () => {}; const originalResolver = jest .fn() - .mockImplementation( function* () {} ); + .mockImplementation( async function () {} ); const resolver = ifNotResolved( originalResolver, 'originalResolver' ); + await resolver()( { select, dispatch } ); + + expect( originalResolver ).toHaveBeenCalledTimes( 0 ); + } ); - const runResolver = resolver(); + it( 'returns a promise when the resolver was not already resolved', async () => { + const select = { hasStartedResolution: () => false }; + let thunkRetval; + const dispatch = jest.fn( ( thunk ) => { + thunkRetval = thunk(); + return thunkRetval; + } ); - let next, nextValue; - do { - next = runResolver.next( nextValue ); - nextValue = next.value?._nextValue; - } while ( ! next.done ); + const originalResolver = jest.fn( () => () => + Promise.resolve( 'success!' ) + ); - expect( originalResolver ).toHaveBeenCalledTimes( 0 ); + const resolver = ifNotResolved( originalResolver, 'originalResolver' ); + const result = resolver()( { select, dispatch } ); + + await expect( result ).resolves.toBe( undefined ); + await expect( thunkRetval ).resolves.toBe( 'success!' ); } ); } ); diff --git a/packages/core-data/src/utils/test/is-raw-attribute.js b/packages/core-data/src/utils/test/is-raw-attribute.js new file mode 100644 index 0000000000000..545fd7c84286f --- /dev/null +++ b/packages/core-data/src/utils/test/is-raw-attribute.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import { isRawAttribute } from '../'; + +describe( 'isRawAttribute', () => { + it( 'should correctly assess that the attribute is not raw', () => { + const entity = { + kind: 'someKind', + name: 'someName', + }; + expect( isRawAttribute( entity, 'title' ) ).toBe( false ); + } ); + it( 'should correctly assess that the attribute is raw', () => { + const entity = { + kind: 'someKind', + name: 'someName', + rawAttributes: [ 'title' ], + }; + expect( isRawAttribute( entity, 'title' ) ).toBe( true ); + } ); +} ); diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 1be81999fee62..ae7260f507fe3 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "2.0.0", + "version": "2.0.1", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/src/controls/inspector-section.js b/packages/customize-widgets/src/controls/inspector-section.js index b6cb32ccf9c8d..48b574a65ea15 100644 --- a/packages/customize-widgets/src/controls/inspector-section.js +++ b/packages/customize-widgets/src/controls/inspector-section.js @@ -16,6 +16,9 @@ export default function getInspectorSection() { 'customize-widgets-layout__inspector' ); } + isContextuallyActive() { + return this.active(); + } onChangeExpanded( expanded, args ) { super.onChangeExpanded( expanded, args ); diff --git a/packages/customize-widgets/src/controls/sidebar-section.js b/packages/customize-widgets/src/controls/sidebar-section.js index a30f4c242b035..e78c47f94f351 100644 --- a/packages/customize-widgets/src/controls/sidebar-section.js +++ b/packages/customize-widgets/src/controls/sidebar-section.js @@ -36,6 +36,9 @@ export default function getSidebarSection() { 'customize-widgets__sidebar-section' ); } + isContextuallyActive() { + return this.active(); + } hasSubSectionOpened() { return this.inspector.expanded(); } diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index 8751caab3a811..ef93ba1264c8b 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -10,6 +10,7 @@ import { import { registerLegacyWidgetBlock, registerLegacyWidgetVariations, + registerWidgetGroupBlock, } from '@wordpress/widgets'; import { setFreeformContentHandlerName } from '@wordpress/blocks'; import { dispatch } from '@wordpress/data'; @@ -56,6 +57,7 @@ export function initialize( editorName, blockEditorSettings ) { } ); } registerLegacyWidgetVariations( blockEditorSettings ); + registerWidgetGroupBlock(); // As we are unregistering `core/freeform` to avoid the Classic block, we must // replace it with something as the default freeform content handler. Failure to diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json index 7407d97bfb2c0..4b132809bea52 100644 --- a/packages/data-controls/package.json +++ b/packages/data-controls/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data-controls", - "version": "2.2.1", + "version": "2.2.2", "description": "A set of common controls for the @wordpress/data api.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/package.json b/packages/data/package.json index ddd920360f30e..7d3fa835579db 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "6.0.0", + "version": "6.0.1", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/src/plugins/persistence/index.js b/packages/data/src/plugins/persistence/index.js index f22e135100f16..24baf115aa1dc 100644 --- a/packages/data/src/plugins/persistence/index.js +++ b/packages/data/src/plugins/persistence/index.js @@ -285,6 +285,7 @@ persistencePlugin.__unstableMigrate = ( pluginOptions ) => { persistence, 'core/customize-widgets' ); + migrateFeaturePreferencesToInterfaceStore( persistence, 'core/edit-post' ); }; export default persistencePlugin; diff --git a/packages/dom/package.json b/packages/dom/package.json index 3adff09388c9e..918415ad3d5fe 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "3.2.1", + "version": "3.2.2", "description": "DOM utilities module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom/src/dom/caret-range-from-point.js b/packages/dom/src/dom/caret-range-from-point.js index fb32c15f0f975..8839e0861f0ab 100644 --- a/packages/dom/src/dom/caret-range-from-point.js +++ b/packages/dom/src/dom/caret-range-from-point.js @@ -4,9 +4,9 @@ * * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint * - * @param {Document} doc The document of the range. - * @param {number} x Horizontal position within the current viewport. - * @param {number} y Vertical position within the current viewport. + * @param {DocumentMaybeWithCaretPositionFromPoint} doc The document of the range. + * @param {number} x Horizontal position within the current viewport. + * @param {number} y Vertical position within the current viewport. * * @return {Range | null} The best range for the given point. */ @@ -34,3 +34,8 @@ export default function caretRangeFromPoint( doc, x, y ) { return range; } + +/** + * @typedef {{caretPositionFromPoint?: (x: number, y: number)=> CaretPosition | null} & Document } DocumentMaybeWithCaretPositionFromPoint + * @typedef {{ readonly offset: number; readonly offsetNode: Node; getClientRect(): DOMRect | null; }} CaretPosition + */ diff --git a/packages/dom/src/phrasing-content.js b/packages/dom/src/phrasing-content.js index d233ef2eec8b4..494b7026f2b3c 100644 --- a/packages/dom/src/phrasing-content.js +++ b/packages/dom/src/phrasing-content.js @@ -32,7 +32,7 @@ const textContentSchema = { s: {}, del: {}, ins: {}, - a: { attributes: [ 'href', 'target', 'rel' ] }, + a: { attributes: [ 'href', 'target', 'rel', 'id' ] }, code: {}, abbr: { attributes: [ 'title' ] }, sub: {}, diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json index 204f11b26901b..e6ac75c7bbb06 100644 --- a/packages/e2e-test-utils/package.json +++ b/packages/e2e-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils", - "version": "5.4.1", + "version": "5.4.2", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md index a322b2e40cc48..5c24e95f8a62d 100644 --- a/packages/e2e-tests/README.md +++ b/packages/e2e-tests/README.md @@ -10,6 +10,68 @@ Install the module npm install @wordpress/e2e-tests --save-dev ``` +## Running tests + +The following commands are available on the Gutenberg repo: + +```json +{ + "test-e2e": "wp-scripts test-e2e --config packages/e2e-tests/jest.config.js", + "test-e2e:debug": "wp-scripts --inspect-brk test-e2e --config packages/e2e-tests/jest.config.js --puppeteer-devtools", + "test-e2e:watch": "npm run test-e2e -- --watch", +} +``` + +### Run all available tests +```bash +npm run test-e2e +``` +### Run all available tests and listen for changes. +```bash +npm run test-e2e:watch +``` + +### Run a specific test file +```bash +npm run test-e2e -- packages/e2e-test/ +# Or, in order to watch for changes: +npm run test-e2e:watch -- packages/e2e-test/ +``` +### Debugging + +Makes e2e tests available to debug in a Chrome Browser. +```bash +npm run test-e2e:debug +``` +After running the command, tests will be available for debugging in Chrome by going to chrome://inspect/#devices and clicking `inspect` under the path to `/test-e2e.js`. + +#### Debugging in `vscode` + +Debugging in a Chrome browser can be replaced with `vscode`'s debugger by adding the following configuration to `.vscode/launch.json`: + +```json +{ + "type": "node", + "request": "launch", + "name": "Debug current e2e test", + "program": "${workspaceRoot}/node_modules/@wordpress/scripts/bin/wp-scripts.js", + "args": [ + "test-e2e", + "--config=${workspaceRoot}/packages/e2e-tests/jest.config.js", + "--verbose=true", + "--runInBand", + "--watch", + "${file}" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "trace": "all" + } +``` + +This will run jest, targetting the spec file currently open in the editor. `vscode`'s debugger can now be used to add breakpoints and inspect tests as you would in Chrome DevTools. + + **Note**: This package requires Node.js 12.0.0 or later. It is not compatible with older versions.

Code is Poetry.

diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index d7e4331cd695b..479f2331230e7 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "2.4.0", + "version": "2.4.1", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/plugins/class-test-widget.php b/packages/e2e-tests/plugins/class-test-widget.php index a3ae60658c0d1..11ce978257fe4 100644 --- a/packages/e2e-tests/plugins/class-test-widget.php +++ b/packages/e2e-tests/plugins/class-test-widget.php @@ -48,7 +48,7 @@ public function form( $instance ) { ?>

- +

{ expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'moves focus from the link editor back to the button when escape is pressed after the URL has been submitted', async () => { + // Regression: https://github.com/WordPress/gutenberg/issues/34307 + await insertBlock( 'Buttons' ); + await pressKeyWithModifier( 'primary', 'k' ); + await page.waitForFunction( + () => !! document.activeElement.closest( '.block-editor-url-input' ) + ); + await page.keyboard.type( 'https://example.com' ); + await page.keyboard.press( 'Enter' ); + await page.waitForFunction( + () => + document.activeElement === + document.querySelector( + '.block-editor-link-control a[href="https://example.com"]' + ) + ); + await page.keyboard.press( 'Escape' ); + + // Focus should move from the link control to the button block's text. + await page.waitForFunction( + () => + document.activeElement === + document.querySelector( '[aria-label="Button text"]' ) + ); + + // The link control should still be visible when a URL is set. + const linkControl = await page.$( '.block-editor-link-control' ); + expect( linkControl ).toBeTruthy(); + } ); + it( 'can jump to the link editor using the keyboard shortcut', async () => { await insertBlock( 'Buttons' ); await page.keyboard.type( 'WordPress' ); diff --git a/packages/e2e-tests/specs/editor/various/a11y.test.js b/packages/e2e-tests/specs/editor/various/a11y.test.js index a25b5d9760c3c..1c7e9fe76b4c9 100644 --- a/packages/e2e-tests/specs/editor/various/a11y.test.js +++ b/packages/e2e-tests/specs/editor/various/a11y.test.js @@ -15,7 +15,7 @@ describe( 'a11y', () => { } ); it( 'tabs header bar', async () => { - await pressKeyWithModifier( 'ctrl', '~' ); + await pressKeyWithModifier( 'ctrl', '`' ); await page.keyboard.press( 'Tab' ); diff --git a/packages/e2e-tests/specs/editor/various/block-deletion.test.js b/packages/e2e-tests/specs/editor/various/block-deletion.test.js index 00e239eae6959..8f79ae9eb9dc0 100644 --- a/packages/e2e-tests/specs/editor/various/block-deletion.test.js +++ b/packages/e2e-tests/specs/editor/various/block-deletion.test.js @@ -156,10 +156,10 @@ describe( 'deleting all blocks', () => { await page.keyboard.type( 'Paragraph' ); await clickOnBlockSettingsMenuRemoveBlockButton(); - // There is a default block: + // There is a default block and post title: expect( await page.$$( '.block-editor-block-list__block' ) - ).toHaveLength( 1 ); + ).toHaveLength( 2 ); // But the effective saved content is still empty: expect( await getEditedPostContent() ).toBe( '' ); diff --git a/packages/e2e-tests/specs/editor/various/block-mover.test.js b/packages/e2e-tests/specs/editor/various/block-mover.test.js index c3a04f6f3a6c4..e0a17fcdceeca 100644 --- a/packages/e2e-tests/specs/editor/various/block-mover.test.js +++ b/packages/e2e-tests/specs/editor/various/block-mover.test.js @@ -16,7 +16,7 @@ describe( 'block mover', () => { await page.keyboard.type( 'Second Paragraph' ); // Select a block so the block mover is rendered. - await page.focus( '.block-editor-block-list__block' ); + await page.focus( '[data-type="core/paragraph"]' ); await showBlockToolbar(); diff --git a/packages/e2e-tests/specs/editor/various/splitting-merging.test.js b/packages/e2e-tests/specs/editor/various/splitting-merging.test.js index 05e08db82afce..fe7a069fe1c8a 100644 --- a/packages/e2e-tests/specs/editor/various/splitting-merging.test.js +++ b/packages/e2e-tests/specs/editor/various/splitting-merging.test.js @@ -193,10 +193,10 @@ describe( 'splitting and merging blocks', () => { await insertBlock( 'Paragraph' ); await page.keyboard.press( 'Backspace' ); - // There is a default block: + // There is a default block and post title: expect( await page.$$( '.block-editor-block-list__block' ) - ).toHaveLength( 1 ); + ).toHaveLength( 2 ); // But the effective saved content is still empty: expect( await getEditedPostContent() ).toBe( '' ); @@ -226,4 +226,50 @@ describe( 'splitting and merging blocks', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + describe( 'test restore selection when merge produces more than one block', () => { + it( 'on forward delete', async () => { + await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'hi' ); + await insertBlock( 'List' ); + await page.keyboard.type( 'item 1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'item 2' ); + await pressKeyTimes( 'ArrowUp', 2 ); + await page.keyboard.press( 'Delete' ); + // Carret should be in the first block and at the proper position. + await page.keyboard.type( '-' ); + expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` + " +

hi-item 1

+ + + +

item 2

+ " + ` ); + } ); + it( 'on backspace', async () => { + await insertBlock( 'Paragraph' ); + await page.keyboard.type( 'hi' ); + await insertBlock( 'List' ); + await page.keyboard.type( 'item 1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'item 2' ); + await page.keyboard.press( 'ArrowUp' ); + await pressKeyTimes( 'ArrowLeft', 6 ); + await page.keyboard.press( 'Backspace' ); + // Carret should be in the first block and at the proper position. + await page.keyboard.type( '-' ); + expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` + " +

hi-item 1

+ + + +

item 2

+ " + ` ); + } ); + } ); } ); diff --git a/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap b/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap index 89f22e7ad5236..82c208896c7dc 100644 --- a/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap +++ b/packages/e2e-tests/specs/experiments/__snapshots__/navigation-editor.test.js.snap @@ -4,7 +4,7 @@ exports[`Navigation editor allows creation of a menu when there are existing men exports[`Navigation editor allows creation of a menu when there are no current menu items 1`] = ` " - + " `; diff --git a/packages/e2e-tests/specs/experiments/blocks/__snapshots__/navigation.test.js.snap b/packages/e2e-tests/specs/experiments/blocks/__snapshots__/navigation.test.js.snap index 40204d478b4af..aa967c86f181d 100644 --- a/packages/e2e-tests/specs/experiments/blocks/__snapshots__/navigation.test.js.snap +++ b/packages/e2e-tests/specs/experiments/blocks/__snapshots__/navigation.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Navigation Creating from existing Menus allows a navigation block to be created from existing menus 1`] = ` -" +" @@ -36,16 +36,16 @@ exports[`Navigation Creating from existing Menus allows a navigation block to be " `; -exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = `""`; +exports[`Navigation Creating from existing Menus creates an empty navigation block when the selected existing menu is also empty 1`] = `""`; exports[`Navigation Creating from existing Pages allows a navigation block to be created using existing pages 1`] = ` -" +" " `; exports[`Navigation allows an empty navigation block to be created and manually populated using a mixture of internal and external links 1`] = ` -" +" @@ -53,13 +53,13 @@ exports[`Navigation allows an empty navigation block to be created and manually `; exports[`Navigation allows pages to be created from the navigation block and their links added to menu 1`] = ` -" +" " `; exports[`Navigation encodes URL when create block if needed 1`] = ` -" +" diff --git a/packages/e2e-tests/specs/experiments/navigation-editor.test.js b/packages/e2e-tests/specs/experiments/navigation-editor.test.js index fa8bdf21d852a..972ba27eb834b 100644 --- a/packages/e2e-tests/specs/experiments/navigation-editor.test.js +++ b/packages/e2e-tests/specs/experiments/navigation-editor.test.js @@ -193,7 +193,18 @@ describe( 'Navigation editor', () => { POST: menuPostResponse, } ), ...getMenuItemMocks( { GET: [] } ), - ...getPagesMocks( { GET: [ {} ] } ), // mock a single page + ...getPagesMocks( { + GET: [ + { + type: 'page', + id: 1, + link: 'https://example.com/1', + title: { + rendered: 'My page', + }, + }, + ], + } ), ] ); await page.keyboard.type( 'Main Menu' ); @@ -354,7 +365,7 @@ describe( 'Navigation editor', () => { ); await navBlock.click(); const startEmptyButton = await page.waitForXPath( - '//button[.="Start empty"]' + '//button[.="Start blank"]' ); await startEmptyButton.click(); diff --git a/packages/e2e-tests/specs/performance/post-editor.test.js b/packages/e2e-tests/specs/performance/post-editor.test.js index 8045291cb331a..0bbfcd1866036 100644 --- a/packages/e2e-tests/specs/performance/post-editor.test.js +++ b/packages/e2e-tests/specs/performance/post-editor.test.js @@ -31,18 +31,18 @@ import { jest.setTimeout( 1000000 ); describe( 'Post Editor Performance', () => { - it( 'Loading, typing and selecting blocks', async () => { - const traceFile = __dirname + '/trace.json'; - let traceResults; - const results = { - load: [], - type: [], - focus: [], - inserterOpen: [], - inserterHover: [], - inserterSearch: [], - }; + const results = { + load: [], + type: [], + focus: [], + inserterOpen: [], + inserterHover: [], + inserterSearch: [], + }; + const traceFile = __dirname + '/trace.json'; + let traceResults; + beforeAll( async () => { const html = readFile( join( __dirname, '../../assets/large-post.html' ) ); @@ -63,7 +63,30 @@ describe( 'Post Editor Performance', () => { dispatch( 'core/block-editor' ).resetBlocks( blocks ); }, html ); await saveDraft(); + } ); + + afterAll( async () => { + const resultsFilename = basename( __filename, '.js' ) + '.results.json'; + writeFileSync( + join( __dirname, resultsFilename ), + JSON.stringify( results, null, 2 ) + ); + deleteFile( traceFile ); + } ); + beforeEach( async () => { + // Disable auto-save to avoid impacting the metrics. + await page.evaluate( () => { + window.wp.data + .dispatch( 'core/edit-post' ) + .__experimentalUpdateLocalAutosaveInterval( 100000000000 ); + window.wp.data + .dispatch( 'core/editor' ) + .updateEditorSettings( { autosaveInterval: 100000000000 } ); + } ); + } ); + + it( 'Loading', async () => { // Measuring loading time let i = 5; while ( i-- ) { @@ -72,7 +95,77 @@ describe( 'Post Editor Performance', () => { await page.waitForSelector( '.wp-block' ); results.load.push( new Date() - startTime ); } + } ); + + it( 'Typing', async () => { + // Measuring typing performance + await insertBlock( 'Paragraph' ); + let i = 20; + await page.tracing.start( { + path: traceFile, + screenshots: false, + categories: [ 'devtools.timeline' ], + } ); + while ( i-- ) { + // Wait for the browser to be idle before starting the monitoring. + // The timeout should be big enough to allow all async tasks tor run. + // And also to allow Rich Text to mark the change as persistent. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( 2000 ); + await page.keyboard.type( 'x' ); + } + await page.tracing.stop(); + traceResults = JSON.parse( readFile( traceFile ) ); + const [ + keyDownEvents, + keyPressEvents, + keyUpEvents, + ] = getTypingEventDurations( traceResults ); + if ( + keyDownEvents.length === keyPressEvents.length && + keyPressEvents.length === keyUpEvents.length + ) { + // The first character typed triggers a longer time (isTyping change) + // It can impact the stability of the metric, so we exclude it. + for ( let j = 1; j < keyDownEvents.length; j++ ) { + results.type.push( + keyDownEvents[ j ] + keyPressEvents[ j ] + keyUpEvents[ j ] + ); + } + } + } ); + + it( 'Selecting blocks', async () => { + // Measuring block selection performance + await createNewPost(); + await page.evaluate( () => { + const { createBlock } = window.wp.blocks; + const { dispatch } = window.wp.data; + const blocks = window.lodash + .times( 1000 ) + .map( () => createBlock( 'core/paragraph' ) ); + dispatch( 'core/block-editor' ).resetBlocks( blocks ); + } ); + const paragraphs = await page.$$( '.wp-block' ); + await page.tracing.start( { + path: traceFile, + screenshots: false, + categories: [ 'devtools.timeline' ], + } ); + await paragraphs[ 0 ].click(); + for ( let j = 1; j <= 10; j++ ) { + // Wait for the browser to be idle before starting the monitoring. + // eslint-disable-next-line no-restricted-syntax + await page.waitForTimeout( 1000 ); + await paragraphs[ j ].click(); + } + await page.tracing.stop(); + traceResults = JSON.parse( readFile( traceFile ) ); + const [ focusEvents ] = getSelectionEventDurations( traceResults ); + results.focus = focusEvents; + } ); + it( 'Opening the inserter', async () => { // Measure time to open inserter await page.waitForSelector( '.edit-post-layout' ); for ( let j = 0; j < 10; j++ ) { @@ -90,7 +183,9 @@ describe( 'Post Editor Performance', () => { } await closeGlobalBlockInserter(); } + } ); + it( 'Searching the inserter', async () => { // Measure time to search the inserter and get results await openGlobalBlockInserter(); for ( let j = 0; j < 10; j++ ) { @@ -123,7 +218,9 @@ describe( 'Post Editor Performance', () => { await page.keyboard.press( 'Backspace' ); } await closeGlobalBlockInserter(); + } ); + it( 'Hovering Inserter Items', async () => { // Measure inserter hover performance const paragraphBlockItem = '.block-editor-inserter__menu .editor-block-list-item-paragraph'; @@ -157,74 +254,5 @@ describe( 'Post Editor Performance', () => { } } await closeGlobalBlockInserter(); - - // Measuring typing performance - await insertBlock( 'Paragraph' ); - i = 20; - await page.tracing.start( { - path: traceFile, - screenshots: false, - categories: [ 'devtools.timeline' ], - } ); - while ( i-- ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 200 ); - await page.keyboard.type( 'x' ); - } - await page.tracing.stop(); - traceResults = JSON.parse( readFile( traceFile ) ); - const [ - keyDownEvents, - keyPressEvents, - keyUpEvents, - ] = getTypingEventDurations( traceResults ); - if ( - keyDownEvents.length === keyPressEvents.length && - keyPressEvents.length === keyUpEvents.length - ) { - for ( let j = 0; j < keyDownEvents.length; j++ ) { - results.type.push( - keyDownEvents[ j ] + keyPressEvents[ j ] + keyUpEvents[ j ] - ); - } - } - - // Measuring block selection performance - await createNewPost(); - await page.evaluate( () => { - const { createBlock } = window.wp.blocks; - const { dispatch } = window.wp.data; - const blocks = window.lodash - .times( 1000 ) - .map( () => createBlock( 'core/paragraph' ) ); - dispatch( 'core/block-editor' ).resetBlocks( blocks ); - } ); - const paragraphs = await page.$$( '.wp-block' ); - await page.tracing.start( { - path: traceFile, - screenshots: false, - categories: [ 'devtools.timeline' ], - } ); - await paragraphs[ 0 ].click(); - for ( let j = 1; j <= 10; j++ ) { - // Wait for the browser to be idle before starting the monitoring. - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 200 ); - await paragraphs[ j ].click(); - } - await page.tracing.stop(); - traceResults = JSON.parse( readFile( traceFile ) ); - const [ focusEvents ] = getSelectionEventDurations( traceResults ); - results.focus = focusEvents; - - const resultsFilename = basename( __filename, '.js' ) + '.results.json'; - writeFileSync( - join( __dirname, resultsFilename ), - JSON.stringify( results, null, 2 ) - ); - deleteFile( traceFile ); - - expect( true ).toBe( true ); } ); } ); diff --git a/packages/e2e-tests/specs/widgets/editing-widgets.test.js b/packages/e2e-tests/specs/widgets/editing-widgets.test.js index 728541cead9e1..a67c1575e75a4 100644 --- a/packages/e2e-tests/specs/widgets/editing-widgets.test.js +++ b/packages/e2e-tests/specs/widgets/editing-widgets.test.js @@ -633,12 +633,14 @@ describe( 'Widgets screen', () => { // Wait for the Legacy Widget block's preview iframe to load. const frame = await new Promise( ( resolve ) => { - const checkFrame = async ( candidateFrame ) => { - const url = await candidateFrame.url(); - if ( url.includes( 'legacy-widget-preview' ) ) { + const checkFrame = async () => { + const frameElement = await page.$( + 'iframe.wp-block-legacy-widget__edit-preview-iframe' + ); + if ( frameElement ) { page.off( 'frameattached', checkFrame ); page.off( 'framenavigated', checkFrame ); - resolve( candidateFrame ); + resolve( frameElement.contentFrame() ); } }; page.on( 'frameattached', checkFrame ); diff --git a/packages/edit-navigation/README.md b/packages/edit-navigation/README.md index 1ee150967f869..1a405fdd4f1e2 100644 --- a/packages/edit-navigation/README.md +++ b/packages/edit-navigation/README.md @@ -41,6 +41,8 @@ Moreover, when the navigation is rendered on the front of the site the system co ### Block-based Mode +**Important**: block-based mode has been temporarily ***disabled*** until it becomes stable. So, if a theme declares support for the `block-nav-menus` feature it will not affect the frontend. + If desired, themes are able to opt into _rendering_ complete block-based menus using the Navigation Editor. This allows for arbitrarily complex navigation block structures to be used in an existing theme whilst still ensuring the navigation data is still _saved_ to the existing (post type powered) Menus system. Themes can opt into this behaviour by declaring: diff --git a/packages/edit-navigation/package.json b/packages/edit-navigation/package.json index ba871b960dfa7..caa7aeb9b9042 100644 --- a/packages/edit-navigation/package.json +++ b/packages/edit-navigation/package.json @@ -45,6 +45,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/interface": "file:../interface", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", + "@wordpress/keycodes": "file:../keycodes", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", "@wordpress/url": "file:../url", diff --git a/packages/edit-navigation/src/components/block-placeholder/index.js b/packages/edit-navigation/src/components/block-placeholder/index.js new file mode 100644 index 0000000000000..f4e47fa6b83dc --- /dev/null +++ b/packages/edit-navigation/src/components/block-placeholder/index.js @@ -0,0 +1,187 @@ +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; +import { + Placeholder, + Button, + DropdownMenu, + MenuGroup, + MenuItem, + Spinner, +} from '@wordpress/components'; +import { + forwardRef, + useCallback, + useState, + useEffect, +} from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { chevronDown } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { useMenuEntityProp, useSelectedMenuId } from '../../hooks'; +import useNavigationEntities from './use-navigation-entities'; +import menuItemsToBlocks from './menu-items-to-blocks'; + +/** + * Convert pages to blocks. + * + * @param {Object[]} pages An array of pages. + * + * @return {WPBlock[]} An array of blocks. + */ +function convertPagesToBlocks( pages ) { + if ( ! pages?.length ) { + return null; + } + + return pages.map( ( { title, type, link: url, id } ) => + createBlock( 'core/navigation-link', { + type, + id, + url, + label: ! title.rendered ? __( '(no title)' ) : title.rendered, + opensInNewTab: false, + } ) + ); +} + +const TOGGLE_PROPS = { variant: 'tertiary' }; +const POPOVER_PROPS = { position: 'bottom center' }; + +function BlockPlaceholder( { onCreate }, ref ) { + const [ selectedMenu, setSelectedMenu ] = useState(); + const [ isCreatingFromMenu, setIsCreatingFromMenu ] = useState( false ); + + const [ selectedMenuId ] = useSelectedMenuId(); + const [ menuName ] = useMenuEntityProp( 'name', selectedMenuId ); + + const { + isResolvingPages, + menus, + isResolvingMenus, + menuItems, + hasResolvedMenuItems, + pages, + hasPages, + hasMenus, + } = useNavigationEntities( selectedMenu ); + + const isLoading = isResolvingPages || isResolvingMenus; + + const createFromMenu = useCallback( () => { + const { innerBlocks: blocks } = menuItemsToBlocks( menuItems ); + const selectNavigationBlock = true; + onCreate( blocks, selectNavigationBlock ); + }, [ menuItems, menuItemsToBlocks, onCreate ] ); + + const onCreateFromMenu = () => { + // If we have menu items, create the block right away. + if ( hasResolvedMenuItems ) { + createFromMenu(); + return; + } + + // Otherwise, create the block when resolution finishes. + setIsCreatingFromMenu( true ); + }; + + const onCreateEmptyMenu = () => { + onCreate( [] ); + }; + + const onCreateAllPages = () => { + const blocks = convertPagesToBlocks( pages ); + const selectNavigationBlock = true; + onCreate( blocks, selectNavigationBlock ); + }; + + useEffect( () => { + // If the user selected a menu but we had to wait for menu items to + // finish resolving, then create the block once resolution finishes. + if ( isCreatingFromMenu && hasResolvedMenuItems ) { + createFromMenu(); + setIsCreatingFromMenu( false ); + } + }, [ isCreatingFromMenu, hasResolvedMenuItems ] ); + + const selectableMenus = menus?.filter( + ( menu ) => menu.id !== selectedMenuId + ); + + const hasSelectableMenus = !! selectableMenus?.length; + + return ( + +
+ { isLoading && ( +
+ +
+ ) } + { ! isLoading && ( +
+ + { hasPages ? ( + + ) : undefined } + { hasSelectableMenus ? ( + + { ( { onClose } ) => ( + + { selectableMenus.map( ( menu ) => { + return ( + { + setSelectedMenu( + menu.id + ); + onCreateFromMenu(); + } } + onClose={ onClose } + key={ menu.id } + > + { menu.name } + + ); + } ) } + + ) } + + ) : undefined } +
+ ) } +
+
+ ); +} + +export default forwardRef( BlockPlaceholder ); diff --git a/packages/edit-navigation/src/components/block-placeholder/menu-items-to-blocks.js b/packages/edit-navigation/src/components/block-placeholder/menu-items-to-blocks.js new file mode 100644 index 0000000000000..db29190438447 --- /dev/null +++ b/packages/edit-navigation/src/components/block-placeholder/menu-items-to-blocks.js @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import { sortBy } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { menuItemToBlockAttributes } from '../../store/utils'; + +/** + * Convert a flat menu item structure to a nested blocks structure. + * + * @param {Object[]} menuItems An array of menu items. + * + * @return {WPBlock[]} An array of blocks. + */ +export default function menuItemsToBlocks( menuItems ) { + if ( ! menuItems ) { + return null; + } + + const menuTree = createDataTree( menuItems ); + return mapMenuItemsToBlocks( menuTree ); +} + +/** @typedef {import('../..store/utils').WPNavMenuItem} WPNavMenuItem */ + +/** + * A recursive function that maps menu item nodes to blocks. + * + * @param {WPNavMenuItem[]} menuItems An array of WPNavMenuItem items. + * @return {Object} Object containing innerBlocks and mapping. + */ +function mapMenuItemsToBlocks( menuItems ) { + let mapping = {}; + + // The menuItem should be in menu_order sort order. + const sortedItems = sortBy( menuItems, 'menu_order' ); + + const innerBlocks = sortedItems.map( ( menuItem ) => { + const attributes = menuItemToBlockAttributes( menuItem ); + + // If there are children recurse to build those nested blocks. + const { + innerBlocks: nestedBlocks = [], // alias to avoid shadowing + mapping: nestedMapping = {}, // alias to avoid shadowing + } = menuItem.children?.length + ? mapMenuItemsToBlocks( menuItem.children ) + : {}; + + // Update parent mapping with nested mapping. + mapping = { + ...mapping, + ...nestedMapping, + }; + + // Create block with nested "innerBlocks". + const block = createBlock( + 'core/navigation-link', + attributes, + nestedBlocks + ); + + // Create mapping for menuItem -> block + mapping[ menuItem.id ] = block.clientId; + + return block; + } ); + + return { + innerBlocks, + mapping, + }; +} + +/** + * Creates a nested, hierarchical tree representation from unstructured data that + * has an inherent relationship defined between individual items. + * + * For example, by default, each element in the dataset should have an `id` and + * `parent` property where the `parent` property indicates a relationship between + * the current item and another item with a matching `id` properties. + * + * This is useful for building linked lists of data from flat data structures. + * + * @param {Array} dataset linked data to be rearranged into a hierarchical tree based on relational fields. + * @param {string} id the property which uniquely identifies each entry within the array. + * @param {*} relation the property which identifies how the current item is related to other items in the data (if at all). + * @return {Array} a nested array of parent/child relationships + */ +function createDataTree( dataset, id = 'id', relation = 'parent' ) { + const hashTable = Object.create( null ); + const dataTree = []; + + for ( const data of dataset ) { + hashTable[ data[ id ] ] = { + ...data, + children: [], + }; + } + for ( const data of dataset ) { + if ( data[ relation ] ) { + hashTable[ data[ relation ] ].children.push( + hashTable[ data[ id ] ] + ); + } else { + dataTree.push( hashTable[ data[ id ] ] ); + } + } + + return dataTree; +} diff --git a/packages/edit-navigation/src/components/block-placeholder/style.scss b/packages/edit-navigation/src/components/block-placeholder/style.scss new file mode 100644 index 0000000000000..bd9b6eab7c9a9 --- /dev/null +++ b/packages/edit-navigation/src/components/block-placeholder/style.scss @@ -0,0 +1,54 @@ +.edit-navigation-block-placeholder { + // The navigation editor already has a border around content. + // Hide the placeholder's border. Requires extra specificity. + &.edit-navigation-block-placeholder { + box-shadow: none; + background: transparent; + + @include break-medium() { + margin: -$grid-unit-20 0; + } + } + + // Show placeholder instructions when it's a medium size. + &.is-medium .components-placeholder__instructions { + display: block; + } + + // Display buttons in a column when placeholder is small. + .edit-navigation-block-placeholder__actions { + display: flex; + flex-direction: column; + align-items: flex-start; + + .components-button { + margin-bottom: $grid-unit-05; + margin-right: 0; + + // Avoid bottom margin on the dropdown since it makes the + // menu anchor itself too far away from the button. + &.components-dropdown-menu__toggle { + margin-bottom: 0; + + svg { + // Make the spacing inside the left of the button match the + // spacing inside the right of the button. + margin-left: -6px; + } + } + } + } + + @include break-medium() { + .edit-navigation-block-placeholder__actions { + flex-direction: row; + } + + // Change the default button margin. Again use extra specificity. + &.edit-navigation-block-placeholder.is-medium .components-button { + margin-bottom: 0; + margin-right: $grid-unit-15; + } + } + +} diff --git a/packages/edit-navigation/src/components/block-placeholder/use-navigation-entities.js b/packages/edit-navigation/src/components/block-placeholder/use-navigation-entities.js new file mode 100644 index 0000000000000..17806deadd9a8 --- /dev/null +++ b/packages/edit-navigation/src/components/block-placeholder/use-navigation-entities.js @@ -0,0 +1,142 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * @typedef {Object} NavigationEntitiesData + * @property {Array|undefined} pages - a collection of WP Post entity objects of post type "Page". + * @property {boolean} isResolvingPages - indicates whether the request to fetch pages is currently resolving. + * @property {boolean} hasResolvedPages - indicates whether the request to fetch pages has finished resolving. + * @property {Array|undefined} menus - a collection of Menu entity objects. + * @property {boolean} isResolvingMenus - indicates whether the request to fetch menus is currently resolving. + * @property {boolean} hasResolvedMenus - indicates whether the request to fetch menus has finished resolving. + * @property {Array|undefined} menusItems - a collection of Menu Item entity objects for the current menuId. + * @property {boolean} hasResolvedMenuItems - indicates whether the request to fetch menuItems has finished resolving. + * @property {boolean} hasPages - indicates whether there is currently any data for pages. + * @property {boolean} hasMenus - indicates whether there is currently any data for menus. + */ + +/** + * Manages fetching and resolution state for all entities required + * for the Navigation block. + * + * @param {number} menuId the menu for which to retrieve menuItem data. + * @return { NavigationEntitiesData } the entity data. + */ +export default function useNavigationEntities( menuId ) { + return { + ...usePageEntities(), + ...useMenuEntities(), + ...useMenuItemEntities( menuId ), + }; +} + +function useMenuEntities() { + const { menus, isResolvingMenus, hasResolvedMenus } = useSelect( + ( select ) => { + const { getMenus, isResolving, hasFinishedResolution } = select( + coreStore + ); + + const menusParameters = [ { per_page: -1 } ]; + + return { + menus: getMenus( ...menusParameters ), + isResolvingMenus: isResolving( 'getMenus', menusParameters ), + hasResolvedMenus: hasFinishedResolution( + 'getMenus', + menusParameters + ), + }; + }, + [] + ); + + return { + menus, + isResolvingMenus, + hasResolvedMenus, + hasMenus: !! ( hasResolvedMenus && menus?.length ), + }; +} + +function useMenuItemEntities( menuId ) { + const { menuItems, hasResolvedMenuItems } = useSelect( + ( select ) => { + const { getMenuItems, hasFinishedResolution } = select( coreStore ); + + const hasSelectedMenu = menuId !== undefined; + const menuItemsParameters = hasSelectedMenu + ? [ + { + menus: menuId, + per_page: -1, + }, + ] + : undefined; + + return { + menuItems: hasSelectedMenu + ? getMenuItems( ...menuItemsParameters ) + : undefined, + hasResolvedMenuItems: hasSelectedMenu + ? hasFinishedResolution( + 'getMenuItems', + menuItemsParameters + ) + : false, + }; + }, + [ menuId ] + ); + + return { + menuItems, + hasResolvedMenuItems, + }; +} + +function usePageEntities() { + const { pages, isResolvingPages, hasResolvedPages } = useSelect( + ( select ) => { + const { + getEntityRecords, + isResolving, + hasFinishedResolution, + } = select( coreStore ); + + const pagesParameters = [ + 'postType', + 'page', + { + parent: 0, + order: 'asc', + orderby: 'id', + per_page: -1, + }, + ]; + + return { + pages: getEntityRecords( ...pagesParameters ) || null, + isResolvingPages: isResolving( + 'getEntityRecords', + pagesParameters + ), + hasResolvedPages: hasFinishedResolution( + 'getEntityRecords', + pagesParameters + ), + }; + }, + [] + ); + + return { + pages, + isResolvingPages, + hasResolvedPages, + hasPages: !! ( hasResolvedPages && pages?.length ), + }; +} diff --git a/packages/edit-navigation/src/components/editor/style.scss b/packages/edit-navigation/src/components/editor/style.scss index bce96deb1515a..5c5d813b02d38 100644 --- a/packages/edit-navigation/src/components/editor/style.scss +++ b/packages/edit-navigation/src/components/editor/style.scss @@ -3,7 +3,12 @@ border: $border-width solid $gray-900; border-radius: $radius-block-ui; max-width: $navigation-editor-width; - margin: auto; + margin: $grid-unit-40 auto 0 auto; + + @include break-medium() { + // Provide space for the floating block toolbar. + margin-top: $grid-unit-50 * 2; + } .editor-styles-wrapper { padding: 0; @@ -25,11 +30,11 @@ font-family: $default-font; // Increase specificity. - .wp-block-navigation-link { + .wp-block-navigation-item { display: block; // Show submenus on click. - > .wp-block-navigation-link__container { + > .wp-block-navigation__submenu-container { // This unsets some styles inherited from the block, meant to only show submenus on click, not hover, when inside the editor. opacity: 1; visibility: visible; @@ -39,8 +44,8 @@ } // Fix focus outlines. - &.is-selected > .wp-block-navigation-link__content, - &.is-selected:hover > .wp-block-navigation-link__content { + &.is-selected > .wp-block-navigation-item__content, + &.is-selected:hover > .wp-block-navigation-item__content { box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } @@ -50,11 +55,11 @@ // Menu items. // This needs high specificity to override inherited values. - &.wp-block-navigation-link.wp-block-navigation-link { + &.wp-block-navigation-item.wp-block-navigation-item { margin-right: 0; } - .wp-block-navigation-link__content.wp-block-navigation-link__content.wp-block-navigation-link__content { + .wp-block-navigation-item__content.wp-block-navigation-item__content.wp-block-navigation-item__content { padding: 0.5em 1em; margin-bottom: 6px; margin-right: 0; @@ -86,7 +91,7 @@ pointer-events: none; margin-right: 0; - .wp-block-pages-list__item { + .wp-block-navigation-item { color: $gray-700; margin-bottom: 6px; border-radius: $radius-block-ui; @@ -96,7 +101,7 @@ } // Submenu icon indicator. - .wp-block-navigation-link__submenu-icon { + .wp-block-navigation__submenu-icon { position: absolute; top: 6px; left: 0; @@ -113,28 +118,28 @@ } // Point downwards when open. - .is-selected.has-child > .wp-block-navigation-link__submenu-icon svg, - .has-child-selected.has-child > .wp-block-navigation-link__submenu-icon svg { + .is-selected.has-child > .wp-block-navigation__submenu-icon svg, + .has-child-selected.has-child > .wp-block-navigation__submenu-icon svg { transform: rotate(0deg); } // Override inherited values to optimize menu items for the screen context. - .wp-block-navigation-link.has-child { + .wp-block-navigation-item.has-child { cursor: default; border-radius: $radius-block-ui; } // Override for deeply nested submenus. .has-child .wp-block-navigation__container .wp-block-navigation__container, - .has-child .wp-block-navigation__container .wp-block-navigation-link__container { + .has-child .wp-block-navigation__container .wp-block-navigation__submenu-container { left: auto; } // When editing a link with children, highlight the parent // and adjust the spacing and submenu icon. - .wp-block-navigation-link.has-child.is-editing { + .wp-block-navigation-item.has-child.is-editing { > .wp-block-navigation__container, - > .wp-block-navigation-link__container { + > .wp-block-navigation__submenu-container { opacity: 1; visibility: visible; position: relative; diff --git a/packages/edit-navigation/src/components/header/index.js b/packages/edit-navigation/src/components/header/index.js index c923caf5b1767..228b53b520ff8 100644 --- a/packages/edit-navigation/src/components/header/index.js +++ b/packages/edit-navigation/src/components/header/index.js @@ -1,14 +1,19 @@ /** * WordPress dependencies */ +import { NavigableToolbar } from '@wordpress/block-editor'; import { DropdownMenu } from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; import { PinnedItems } from '@wordpress/interface'; import { __, sprintf } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies */ import SaveButton from './save-button'; +import UndoButton from './undo-button'; +import RedoButton from './redo-button'; import MenuSwitcher from '../menu-switcher'; import { useMenuEntityProp } from '../../hooks'; @@ -20,6 +25,7 @@ export default function Header( { isPending, navigationPost, } ) { + const isMediumViewport = useViewportMatch( 'medium' ); const [ menuName ] = useMenuEntityProp( 'name', selectedMenuId ); let actionHeaderText; @@ -38,14 +44,23 @@ export default function Header( { return (
-
-

- { __( 'Navigation' ) } -

-

- { isMenuSelected && actionHeaderText } -

-
+ { isMediumViewport && ( +
+

+ { __( 'Navigation' ) } +

+ + + + +
+ ) } +

+ { isMenuSelected && decodeEntities( actionHeaderText ) } +

{ isMenuSelected && (
select( coreStore ).hasRedo() ); + const { redo } = useDispatch( coreStore ); + return ( + + ); +} diff --git a/packages/edit-navigation/src/components/header/style.scss b/packages/edit-navigation/src/components/header/style.scss index 3e4ea409e897b..bc940b93a14bc 100644 --- a/packages/edit-navigation/src/components/header/style.scss +++ b/packages/edit-navigation/src/components/header/style.scss @@ -1,22 +1,51 @@ .edit-navigation-header { display: flex; + justify-content: space-between; align-items: center; padding: $grid-unit-15 $grid-unit-30 $grid-unit-15 20px; } -.edit-navigation-header__title-subtitle { - flex-grow: 1; +.edit-navigation-header__toolbar-wrapper { + display: flex; + align-items: center; + justify-content: center; } .edit-navigation-header__title { - font-size: 23px; - font-weight: 400; - margin: 0; - padding: 7px 0 4px 0; - line-height: 1.3; + font-size: 20px; + padding: 0; + margin: 0 20px 0 0; +} + +.edit-navigation-header__toolbar { + border: none; + + // The Toolbar component adds different styles to buttons, so we reset them + // here to the original button styles + // Specificity bump needed to offset https://github.com/WordPress/gutenberg/blob/8ea29cb04412c80c9adf7c1db0e816d6a0ac1232/packages/components/src/toolbar/style.scss#L76 + > .components-button.has-icon.has-icon.has-icon, + > .components-dropdown > .components-button.has-icon.has-icon { + height: $button-size; + min-width: $button-size; + padding: 6px; + + &.is-pressed { + background: $gray-900; + } + + &:focus:not(:disabled) { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 $border-width $white; + outline: 1px solid transparent; + } + + &::before { + display: none; + } + } } .edit-navigation-header__subtitle { + display: block; margin: 0; font-size: 15px; font-weight: normal; diff --git a/packages/edit-navigation/src/components/header/undo-button.js b/packages/edit-navigation/src/components/header/undo-button.js new file mode 100644 index 0000000000000..82b9caae94556 --- /dev/null +++ b/packages/edit-navigation/src/components/header/undo-button.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { __, isRTL } from '@wordpress/i18n'; +import { ToolbarButton } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; +import { displayShortcut } from '@wordpress/keycodes'; +import { store as coreStore } from '@wordpress/core-data'; + +export default function UndoButton() { + const hasUndo = useSelect( ( select ) => select( coreStore ).hasUndo() ); + const { undo } = useDispatch( coreStore ); + return ( + + ); +} diff --git a/packages/edit-navigation/src/components/layout/index.js b/packages/edit-navigation/src/components/layout/index.js index 65e0ac5343d0c..92dd19040ea0c 100644 --- a/packages/edit-navigation/src/components/layout/index.js +++ b/packages/edit-navigation/src/components/layout/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -13,7 +8,6 @@ import { __unstableUseBlockSelectionClearer as useBlockSelectionClearer, } from '@wordpress/block-editor'; import { Popover, SlotFillProvider, Spinner } from '@wordpress/components'; -import { useViewportMatch } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect, useMemo, useState } from '@wordpress/element'; import { @@ -53,7 +47,6 @@ const interfaceLabels = { export default function Layout( { blockEditorSettings } ) { const contentAreaRef = useBlockSelectionClearer(); - const isLargeViewport = useViewportMatch( 'medium' ); const [ isMenuNameControlFocused, setIsMenuNameControlFocused ] = useState( false ); @@ -94,7 +87,6 @@ export default function Layout( { blockEditorSettings } ) { useMenuNotifications( selectedMenuId ); const hasMenus = !! menus?.length; - const hasPermanentSidebar = isLargeViewport && isMenuSelected; const isBlockEditorReady = !! ( hasMenus && @@ -133,9 +125,7 @@ export default function Layout( { blockEditorSettings } ) { ) } > } sidebar={ - ( hasPermanentSidebar || - hasSidebarEnabled ) && ( + hasSidebarEnabled && ( ) } @@ -190,7 +179,6 @@ export default function Layout( { blockEditorSettings } ) { onSelectMenu={ selectMenu } onDeleteMenu={ deleteMenu } isMenuBeingDeleted={ isMenuBeingDeleted } - hasPermanentSidebar={ hasPermanentSidebar } /> ) } diff --git a/packages/edit-navigation/src/components/layout/style.scss b/packages/edit-navigation/src/components/layout/style.scss index 9689f2ecc1385..0d84561fc19a8 100644 --- a/packages/edit-navigation/src/components/layout/style.scss +++ b/packages/edit-navigation/src/components/layout/style.scss @@ -34,45 +34,14 @@ // Reference element for the block popover position. position: relative; - // The 10px match that of similar settings pages. - padding: $grid-unit-15 10px 10px 10px; - - @include break-medium() { - // Provide space for the floating block toolbar. - padding-top: $navigation-editor-spacing-top; - } - // Ensure the entire layout is full-height, the background // of the editing canvas needs to be full-height for block // deselection to work. flex-grow: 1; } - .interface-interface-skeleton__header { - border-bottom-color: transparent; - } - - // Force the sidebar to the right side of the screen on larger - // breakpoints. - &.has-permanent-sidebar .interface-interface-skeleton__sidebar { - position: fixed !important; - top: $grid-unit-40; - right: 0; - bottom: 0; - left: auto; - - // Hide the toggle as the sidebar should be permanently open. - .edit-navigation-sidebar__panel-tabs > button { - display: none; - } - } - .edit-navigation-header { background: $white; - - @include break-medium() { - background: transparent; - } } } diff --git a/packages/edit-navigation/src/components/menu-switcher/index.js b/packages/edit-navigation/src/components/menu-switcher/index.js index 68162e433a7d6..b6a1843643723 100644 --- a/packages/edit-navigation/src/components/menu-switcher/index.js +++ b/packages/edit-navigation/src/components/menu-switcher/index.js @@ -14,6 +14,7 @@ import { } from '@wordpress/components'; import { useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -37,7 +38,7 @@ export default function MenuSwitcher( { onSelect={ onSelectMenu } choices={ menus.map( ( { id, name } ) => ( { value: id, - label: name, + label: decodeEntities( name ), 'aria-label': sprintf( /* translators: %s: The name of a menu. */ __( "Switch to '%s'" ), diff --git a/packages/edit-navigation/src/components/name-display/index.js b/packages/edit-navigation/src/components/name-display/index.js index 794e686df4b04..6aa124dc0e6ba 100644 --- a/packages/edit-navigation/src/components/name-display/index.js +++ b/packages/edit-navigation/src/components/name-display/index.js @@ -7,6 +7,7 @@ import { BlockControls } from '@wordpress/block-editor'; import { useDispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; import { sprintf, __ } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -27,7 +28,7 @@ export default function NameDisplay() { IsMenuNameControlFocusedContext ); - const menuName = name ?? untitledMenu; + const menuName = decodeEntities( name ?? untitledMenu ); return ( diff --git a/packages/edit-navigation/src/components/name-editor/index.js b/packages/edit-navigation/src/components/name-editor/index.js index 728dcb04b77c4..a1c46345f2375 100644 --- a/packages/edit-navigation/src/components/name-editor/index.js +++ b/packages/edit-navigation/src/components/name-editor/index.js @@ -4,6 +4,7 @@ import { __ } from '@wordpress/i18n'; import { TextControl } from '@wordpress/components'; import { useEffect, useRef, useContext } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -36,7 +37,7 @@ export function NameEditor() { label={ __( 'Name' ) } onBlur={ () => setIsMenuNameEditFocused( false ) } className="edit-navigation-name-editor__text-control" - value={ name || '' } + value={ decodeEntities( name || '' ) } onChange={ setName } /> ); diff --git a/packages/edit-navigation/src/components/notices/style.scss b/packages/edit-navigation/src/components/notices/style.scss index c952988f1a09a..a633a2af3bc5e 100644 --- a/packages/edit-navigation/src/components/notices/style.scss +++ b/packages/edit-navigation/src/components/notices/style.scss @@ -1,7 +1,7 @@ .edit-navigation-notices__snackbar-list { position: fixed; - bottom: 20px; - margin-left: 20px; + bottom: 0; + padding: 20px; } .edit-navigation-notices__notice-list { diff --git a/packages/edit-navigation/src/components/sidebar/index.js b/packages/edit-navigation/src/components/sidebar/index.js index 4123ef911e146..c6301230b1e58 100644 --- a/packages/edit-navigation/src/components/sidebar/index.js +++ b/packages/edit-navigation/src/components/sidebar/index.js @@ -13,6 +13,7 @@ import { ComplementaryArea, store as interfaceStore, } from '@wordpress/interface'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -29,8 +30,8 @@ export default function Sidebar( { isMenuBeingDeleted, onDeleteMenu, onSelectMenu, - hasPermanentSidebar, } ) { + const isLargeViewport = useViewportMatch( 'medium' ); const { sidebar, hasBlockSelection, hasSidebarEnabled } = useSelect( ( select ) => { const _sidebar = select( @@ -79,10 +80,10 @@ export default function Sidebar( { scope={ SIDEBAR_SCOPE } identifier={ sidebarName } icon={ cog } - isActiveByDefault={ hasPermanentSidebar } + isActiveByDefault={ isLargeViewport } header={ } headerClassName="edit-navigation-sidebar__panel-tabs" - isPinnable={ ! hasPermanentSidebar } + isPinnable > { sidebarName === SIDEBAR_MENU && ( <> diff --git a/packages/edit-navigation/src/components/sidebar/manage-locations.js b/packages/edit-navigation/src/components/sidebar/manage-locations.js index b77b0a39ff62b..97b3b3da322cf 100644 --- a/packages/edit-navigation/src/components/sidebar/manage-locations.js +++ b/packages/edit-navigation/src/components/sidebar/manage-locations.js @@ -11,6 +11,7 @@ import { Spinner, SelectControl, } from '@wordpress/components'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -82,7 +83,7 @@ export default function ManageLocations( { sprintf( // translators: menu name. __( 'Currently using %s' ), - menuOnLocation.name + decodeEntities( menuOnLocation.name ) ) } /> @@ -101,13 +102,13 @@ export default function ManageLocations( { className="edit-navigation-manage-locations__select-menu" label={ menuLocation.description } labelPosition="top" - value={ menuLocation.menu } + value={ decodeEntities( menuLocation.menu ) } options={ [ { value: 0, label: __( 'Select a Menu' ), key: 0 }, ...menus.map( ( { id, name } ) => ( { key: id, value: id, - label: name, + label: decodeEntities( name ), } ) ), ] } onChange={ ( menuId ) => { diff --git a/packages/edit-navigation/src/filters/add-navigation-editor-placeholder.js b/packages/edit-navigation/src/filters/add-navigation-editor-placeholder.js new file mode 100644 index 0000000000000..263658d0ffc64 --- /dev/null +++ b/packages/edit-navigation/src/filters/add-navigation-editor-placeholder.js @@ -0,0 +1,28 @@ +/** + * WordPress dependencies + */ +/** + * Internal dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { createHigherOrderComponent } from '@wordpress/compose'; +import BlockPlaceholder from '../components/block-placeholder'; + +const addNavigationEditorPlaceholder = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + if ( props.name !== 'core/navigation' ) { + return ; + } + return ( + + ); + }, + 'withNavigationEditorPlaceholder' +); + +export default () => + addFilter( + 'editor.BlockEdit', + 'core/edit-navigation/with-navigation-editor-placeholder', + addNavigationEditorPlaceholder + ); diff --git a/packages/edit-navigation/src/filters/index.js b/packages/edit-navigation/src/filters/index.js index 96315b46b7eb8..08ab87f0fe4b5 100644 --- a/packages/edit-navigation/src/filters/index.js +++ b/packages/edit-navigation/src/filters/index.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import addNavigationEditorPlaceholder from './add-navigation-editor-placeholder'; import addMenuNameEditor from './add-menu-name-editor'; import disableInsertingNonNavigationBlocks from './disable-inserting-non-navigation-blocks'; import removeEditUnsupportedFeatures from './remove-edit-unsupported-features'; @@ -9,6 +10,7 @@ import removeSettingsUnsupportedFeatures from './remove-settings-unsupported-fea export const addFilters = ( shouldAddDisableInsertingNonNavigationBlocksFilter ) => { + addNavigationEditorPlaceholder(); addMenuNameEditor(); if ( shouldAddDisableInsertingNonNavigationBlocksFilter ) { disableInsertingNonNavigationBlocks(); diff --git a/packages/edit-navigation/src/hooks/use-menu-locations.js b/packages/edit-navigation/src/hooks/use-menu-locations.js index 1b9b50a456efc..b9c5a8872a7c5 100644 --- a/packages/edit-navigation/src/hooks/use-menu-locations.js +++ b/packages/edit-navigation/src/hooks/use-menu-locations.js @@ -29,7 +29,7 @@ export default function useMenuLocations() { const fetchMenuLocationsByName = async () => { const newMenuLocationsByName = await apiFetch( { method: 'GET', - path: '/__experimental/menu-locations/', + path: '/__experimental/menu-locations', } ); if ( isMounted ) { diff --git a/packages/edit-navigation/src/style.scss b/packages/edit-navigation/src/style.scss index d3865a1dfff8e..ef8c1bdea6646 100644 --- a/packages/edit-navigation/src/style.scss +++ b/packages/edit-navigation/src/style.scss @@ -8,6 +8,7 @@ $navigation-editor-spacing-top: $grid-unit-50 * 2; } @import "./components/add-menu/style.scss"; +@import "./components/block-placeholder/style.scss"; @import "../../interface/src/style.scss"; @import "./components/editor/style.scss"; @import "./components/error-boundary/style.scss"; diff --git a/packages/edit-post/README.md b/packages/edit-post/README.md index 9768fe6b6fd21..677e0c8257aa0 100644 --- a/packages/edit-post/README.md +++ b/packages/edit-post/README.md @@ -31,13 +31,10 @@ They can be found in the global variable `wp.editPost` when defining `wp-edit-po Initializes and returns an instance of Editor. -The return value of this function is not necessary if we change where we -call initializeEditor(). This is due to metaBox timing. - _Parameters_ - _id_ `string`: Unique identifier for editor instance. -- _postType_ `Object`: Post type of the post to edit. +- _postType_ `string`: Post type of the post to edit. - _postId_ `Object`: ID of the post to edit. - _settings_ `?Object`: Editor settings object. - _initialEdits_ `Object`: Programmatic edits to apply initially, to be considered as non-user-initiated (bypass for unsaved changes prompt). diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index b5fe7f1c5e301..6919bb5683d1a 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "5.0.0", + "version": "5.0.1", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/src/components/header/feature-toggle/index.js b/packages/edit-post/src/components/header/feature-toggle/index.js deleted file mode 100644 index 13f72ae8d75a4..0000000000000 --- a/packages/edit-post/src/components/header/feature-toggle/index.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * External dependencies - */ -import { flow } from 'lodash'; - -/** - * WordPress dependencies - */ -import { withSelect, withDispatch } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; -import { MenuItem } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { check } from '@wordpress/icons'; -import { speak } from '@wordpress/a11y'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -function FeatureToggle( { - onToggle, - isActive, - label, - info, - messageActivated, - messageDeactivated, - shortcut, -} ) { - const speakMessage = () => { - if ( isActive ) { - speak( messageDeactivated || __( 'Feature deactivated' ) ); - } else { - speak( messageActivated || __( 'Feature activated' ) ); - } - }; - - return ( - - { label } - - ); -} - -export default compose( [ - withSelect( ( select, { feature } ) => ( { - isActive: select( editPostStore ).isFeatureActive( feature ), - } ) ), - withDispatch( ( dispatch, ownProps ) => ( { - onToggle() { - dispatch( editPostStore ).toggleFeature( ownProps.feature ); - }, - } ) ), -] )( FeatureToggle ); diff --git a/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss b/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss index 1cee0b961908f..72f14c823e168 100644 --- a/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss +++ b/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss @@ -11,7 +11,7 @@ align-items: center; align-self: stretch; border: none; - background: #23282e; // WP-admin gray. + background: $gray-900; color: $white; border-radius: 0; height: $header-height + $border-width; @@ -38,7 +38,7 @@ bottom: 9px; left: 9px; border-radius: $radius-block-ui + $border-width + $border-width; - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) #23282e; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $gray-900; } // Hover color. diff --git a/packages/edit-post/src/components/header/more-menu/index.js b/packages/edit-post/src/components/header/more-menu/index.js index 17b763b26f374..ed06ba4725999 100644 --- a/packages/edit-post/src/components/header/more-menu/index.js +++ b/packages/edit-post/src/components/header/more-menu/index.js @@ -2,9 +2,12 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { DropdownMenu, MenuGroup } from '@wordpress/components'; -import { moreVertical } from '@wordpress/icons'; -import { ActionItem, PinnedItems } from '@wordpress/interface'; +import { MenuGroup } from '@wordpress/components'; +import { + ActionItem, + MoreMenuDropdown, + PinnedItems, +} from '@wordpress/interface'; import { useViewportMatch } from '@wordpress/compose'; /** @@ -17,26 +20,18 @@ import WritingMenu from '../writing-menu'; const POPOVER_PROPS = { className: 'edit-post-more-menu__content', - position: 'bottom left', -}; -const TOGGLE_PROPS = { - tooltipPosition: 'bottom', }; const MoreMenu = ( { showIconLabels } ) => { const isLargeViewport = useViewportMatch( 'large' ); return ( - { ( { onClose } ) => ( @@ -61,7 +56,7 @@ const MoreMenu = ( { showIconLabels } ) => { ) } - + ); }; diff --git a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap deleted file mode 100644 index 9ffdb5a699ff0..0000000000000 --- a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,129 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`MoreMenu should match snapshot 1`] = ` - - - - - } - label="Options" - popoverProps={ - Object { - "className": "edit-post-more-menu__content", - "position": "bottom left", - } - } - toggleProps={ - Object { - "showTooltip": true, - "tooltipPosition": "bottom", - } - } - > - -
- - - - } - label="Options" - onClick={[Function]} - onKeyDown={[Function]} - showTooltip={true} - tooltipPosition="bottom" - > - - - - -
-
-
-
-`; diff --git a/packages/edit-post/src/components/header/more-menu/test/index.js b/packages/edit-post/src/components/header/more-menu/test/index.js deleted file mode 100644 index 2da96abe04ea4..0000000000000 --- a/packages/edit-post/src/components/header/more-menu/test/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * External dependencies - */ -import { mount } from 'enzyme'; - -/** - * Internal dependencies - */ -import MoreMenu from '../index'; - -describe( 'MoreMenu', () => { - it( 'should match snapshot', () => { - const wrapper = mount( ); - - expect( wrapper ).toMatchSnapshot(); - } ); -} ); diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index 71d3ddb339d20..bcd4edad123ce 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -5,11 +5,7 @@ import { MenuGroup } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { useViewportMatch } from '@wordpress/compose'; import { displayShortcut } from '@wordpress/keycodes'; - -/** - * Internal dependencies - */ -import FeatureToggle from '../feature-toggle'; +import { MoreMenuFeatureToggle } from '@wordpress/interface'; function WritingMenu() { const isLargeViewport = useViewportMatch( 'medium' ); @@ -19,7 +15,8 @@ function WritingMenu() { return ( - - - - + `; exports[`KeyboardShortcutHelpModal should match snapshot when the modal is not active 1`] = `""`; diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 51311d59ea7b6..202f9648d9517 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -112,12 +112,10 @@ function Layout( { styles } ) { hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), previousShortcut: select( keyboardShortcutsStore - ).getAllShortcutRawKeyCombinations( - 'core/edit-post/previous-region' - ), + ).getAllShortcutKeyCombinations( 'core/edit-post/previous-region' ), nextShortcut: select( keyboardShortcutsStore - ).getAllShortcutRawKeyCombinations( 'core/edit-post/next-region' ), + ).getAllShortcutKeyCombinations( 'core/edit-post/next-region' ), showIconLabels: select( editPostStore ).isFeatureActive( 'showIconLabels' ), diff --git a/packages/edit-post/src/components/meta-boxes/index.js b/packages/edit-post/src/components/meta-boxes/index.js index e5e3e2ac2066d..15851ae524787 100644 --- a/packages/edit-post/src/components/meta-boxes/index.js +++ b/packages/edit-post/src/components/meta-boxes/index.js @@ -6,7 +6,9 @@ import { map } from 'lodash'; /** * WordPress dependencies */ -import { withSelect } from '@wordpress/data'; +import { useSelect, useRegistry } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -15,7 +17,44 @@ import MetaBoxesArea from './meta-boxes-area'; import MetaBoxVisibility from './meta-box-visibility'; import { store as editPostStore } from '../../store'; -function MetaBoxes( { location, isVisible, metaBoxes } ) { +export default function MetaBoxes( { location } ) { + const registry = useRegistry(); + const { + metaBoxes, + isVisible, + areMetaBoxesInitialized, + isEditorReady, + } = useSelect( + ( select ) => { + const { __unstableIsEditorReady } = select( editorStore ); + const { + isMetaBoxLocationVisible, + getMetaBoxesPerLocation, + areMetaBoxesInitialized: _areMetaBoxesInitialized, + } = select( editPostStore ); + return { + metaBoxes: getMetaBoxesPerLocation( location ), + isVisible: isMetaBoxLocationVisible( location ), + areMetaBoxesInitialized: _areMetaBoxesInitialized(), + isEditorReady: __unstableIsEditorReady(), + }; + }, + [ location ] + ); + + // When editor is ready, initialize postboxes (wp core script) and metabox + // saving. This initializes all meta box locations, not just this specific + // one. + useEffect( () => { + if ( isEditorReady && ! areMetaBoxesInitialized ) { + registry.dispatch( editPostStore ).initializeMetaBoxes(); + } + }, [ isEditorReady, areMetaBoxesInitialized ] ); + + if ( ! areMetaBoxesInitialized ) { + return null; + } + return ( <> { map( metaBoxes, ( { id } ) => ( @@ -25,14 +64,3 @@ function MetaBoxes( { location, isVisible, metaBoxes } ) { ); } - -export default withSelect( ( select, { location } ) => { - const { isMetaBoxLocationVisible, getMetaBoxesPerLocation } = select( - editPostStore - ); - - return { - metaBoxes: getMetaBoxesPerLocation( location ), - isVisible: isMetaBoxLocationVisible( location ), - }; -} )( MetaBoxes ); diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index 3a536a74140bd..ad67783a7f10a 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -243,14 +243,12 @@ export default function PreferencesModal() { /> -
- -
+ /> ), }, diff --git a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap index 5e96f24c36fef..97bad699daf32 100644 --- a/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/preferences-modal/test/__snapshots__/index.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`PreferencesModal should match snapshot when the modal is active large viewports 1`] = ` - - + `; exports[`PreferencesModal should match snapshot when the modal is active small viewports 1`] = ` - -
- -
+ /> -
+ `; diff --git a/packages/edit-post/src/components/sidebar/post-link/index.js b/packages/edit-post/src/components/sidebar/post-link/index.js index 5b6ac7f7e9220..a3905a2275e5e 100644 --- a/packages/edit-post/src/components/sidebar/post-link/index.js +++ b/packages/edit-post/src/components/sidebar/post-link/index.js @@ -95,7 +95,11 @@ function PostLink( { />

{ __( 'The last part of the URL.' ) }{ ' ' } - + { __( 'Read about permalinks' ) }

diff --git a/packages/edit-post/src/components/text-editor/style.scss b/packages/edit-post/src/components/text-editor/style.scss index 57d63b894007e..925e88df27180 100644 --- a/packages/edit-post/src/components/text-editor/style.scss +++ b/packages/edit-post/src/components/text-editor/style.scss @@ -5,7 +5,7 @@ flex-grow: 1; // Post title. - .editor-post-title.editor-post-title__block { + .editor-post-title { max-width: none; line-height: $default-line-height; diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index aebef09c4eac3..b93af3ee80c71 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -44,8 +44,9 @@ margin-right: auto; // Margins between the title and the first block, or appender, do not collapse. - // By explicitly setting this to zero, we avoid "double margin". - margin-bottom: 0; + // However in that support block gap, the first items in post content do not have a top margin. + // By leveraging the gap variable, with a fallback of zero, we handle both cases. + margin-bottom: var(--wp--style--block-gap, 0); } } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index e2781d1a6a444..3f34c2a5f32bd 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -156,7 +156,9 @@ function Editor( { ] ); const styles = useMemo( () => { - return hasThemeStyles ? settings.styles : []; + return hasThemeStyles && settings.styles?.length + ? settings.styles + : settings.defaultEditorStyles; }, [ settings, hasThemeStyles ] ); if ( ! post ) { diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index a97ece5c01d41..c20cff1441222 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -6,13 +6,14 @@ import { __experimentalRegisterExperimentalCoreBlocks, } from '@wordpress/block-library'; import { render, unmountComponentAtNode } from '@wordpress/element'; +import { dispatch } from '@wordpress/data'; +import { store as interfaceStore } from '@wordpress/interface'; /** * Internal dependencies */ import './hooks'; import './plugins'; -export { store } from './store'; import Editor from './editor'; /** @@ -61,11 +62,8 @@ export function reinitializeEditor( /** * Initializes and returns an instance of Editor. * - * The return value of this function is not necessary if we change where we - * call initializeEditor(). This is due to metaBox timing. - * * @param {string} id Unique identifier for editor instance. - * @param {Object} postType Post type of the post to edit. + * @param {string} postType Post type of the post to edit. * @param {Object} postId ID of the post to edit. * @param {?Object} settings Editor settings object. * @param {Object} initialEdits Programmatic edits to apply initially, to be @@ -88,6 +86,17 @@ export function initializeEditor( settings, initialEdits ); + + dispatch( interfaceStore ).setFeatureDefaults( 'core/edit-post', { + fixedToolbar: false, + welcomeGuide: true, + fullscreenMode: true, + showIconLabels: false, + themeStyles: true, + showBlockBreadcrumbs: true, + welcomeGuideTemplate: true, + } ); + registerCoreBlocks(); if ( process.env.GUTENBERG_PHASE === 2 ) { __experimentalRegisterExperimentalCoreBlocks( { @@ -157,3 +166,4 @@ export { default as PluginSidebar } from './components/sidebar/plugin-sidebar'; export { default as PluginSidebarMoreMenuItem } from './components/header/plugin-sidebar-more-menu-item'; export { default as __experimentalFullscreenModeClose } from './components/header/fullscreen-mode-close'; export { default as __experimentalMainDashboardButton } from './components/header/main-dashboard-button'; +export { store } from './store'; diff --git a/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js b/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js index 2c0a6b02fc3df..579d68c12a3a0 100644 --- a/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js +++ b/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js @@ -1,8 +1,8 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { MenuItem } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { MoreMenuFeatureToggle } from '@wordpress/interface'; import { __ } from '@wordpress/i18n'; /** @@ -15,17 +15,12 @@ export default function WelcomeGuideMenuItem() { ( select ) => select( editPostStore ).isEditingTemplate(), [] ); - const { toggleFeature } = useDispatch( editPostStore ); return ( - - toggleFeature( - isTemplateMode ? 'welcomeGuideTemplate' : 'welcomeGuide' - ) - } - > - { __( 'Welcome Guide' ) } - + ); } diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 6cd238b06bdfd..23d7c4613de59 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -9,7 +9,7 @@ import { castArray, reduce } from 'lodash'; import { __ } from '@wordpress/i18n'; import { apiFetch } from '@wordpress/data-controls'; import { store as interfaceStore } from '@wordpress/interface'; -import { controls, dispatch, select, subscribe } from '@wordpress/data'; +import { controls, select, subscribe, dispatch } from '@wordpress/data'; import { speak } from '@wordpress/a11y'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; @@ -153,17 +153,17 @@ export function removeEditorPanel( panelName ) { } /** - * Returns an action object used to toggle a feature flag. + * Triggers an action used to toggle a feature flag. * * @param {string} feature Feature name. - * - * @return {Object} Action object. */ -export function toggleFeature( feature ) { - return { - type: 'TOGGLE_FEATURE', - feature, - }; +export function* toggleFeature( feature ) { + yield controls.dispatch( + interfaceStore.name, + 'toggleFeature', + 'core/edit-post', + feature + ); } export function* switchEditorMode( mode ) { @@ -265,8 +265,6 @@ export function showBlockTypes( blockNames ) { }; } -let saveMetaboxUnsubscribe; - /** * Returns an action object used in signaling * what Meta boxes are available in which location. @@ -280,52 +278,6 @@ export function* setAvailableMetaBoxesPerLocation( metaBoxesPerLocation ) { type: 'SET_META_BOXES_PER_LOCATIONS', metaBoxesPerLocation, }; - - const postType = yield controls.select( editorStore, 'getCurrentPostType' ); - if ( window.postboxes.page !== postType ) { - window.postboxes.add_postbox_toggles( postType ); - } - - let wasSavingPost = yield controls.select( editorStore, 'isSavingPost' ); - let wasAutosavingPost = yield controls.select( - editorStore, - 'isAutosavingPost' - ); - - // Meta boxes are initialized once at page load. It is not necessary to - // account for updates on each state change. - // - // See: https://github.com/WordPress/WordPress/blob/5.1.1/wp-admin/includes/post.php#L2307-L2309 - const hasActiveMetaBoxes = yield controls.select( - editPostStore, - 'hasMetaBoxes' - ); - - // First remove any existing subscription in order to prevent multiple saves - if ( !! saveMetaboxUnsubscribe ) { - saveMetaboxUnsubscribe(); - } - - // Save metaboxes when performing a full save on the post. - saveMetaboxUnsubscribe = subscribe( () => { - const isSavingPost = select( editorStore ).isSavingPost(); - const isAutosavingPost = select( editorStore ).isAutosavingPost(); - - // Save metaboxes on save completion, except for autosaves that are not a post preview. - const shouldTriggerMetaboxesSave = - hasActiveMetaBoxes && - wasSavingPost && - ! isSavingPost && - ! wasAutosavingPost; - - // Save current state for next inspection. - wasSavingPost = isSavingPost; - wasAutosavingPost = isAutosavingPost; - - if ( shouldTriggerMetaboxesSave ) { - dispatch( editPostStore ).requestMetaBoxUpdates(); - } - } ); } /** @@ -531,3 +483,69 @@ export function* __unstableCreateTemplate( template ) { } ); } + +let metaBoxesInitialized = false; + +/** + * Initializes WordPress `postboxes` script and the logic for saving meta boxes. + */ +export function* initializeMetaBoxes() { + const isEditorReady = yield controls.select( + editorStore, + '__unstableIsEditorReady' + ); + + if ( ! isEditorReady ) { + return; + } + + const postType = yield controls.select( editorStore, 'getCurrentPostType' ); + + // Only initialize once. + if ( metaBoxesInitialized ) { + return; + } + + if ( window.postboxes.page !== postType ) { + window.postboxes.add_postbox_toggles( postType ); + } + + metaBoxesInitialized = true; + + let wasSavingPost = yield controls.select( editorStore, 'isSavingPost' ); + let wasAutosavingPost = yield controls.select( + editorStore, + 'isAutosavingPost' + ); + const hasMetaBoxes = yield controls.select( editPostStore, 'hasMetaBoxes' ); + + // Save metaboxes when performing a full save on the post. + subscribe( () => { + const isSavingPost = select( editorStore ).isSavingPost(); + const isAutosavingPost = select( editorStore ).isAutosavingPost(); + + // Save metaboxes on save completion, except for autosaves that are not a post preview. + // + // Meta boxes are initialized once at page load. It is not necessary to + // account for updates on each state change. + // + // See: https://github.com/WordPress/WordPress/blob/5.1.1/wp-admin/includes/post.php#L2307-L2309 + const shouldTriggerMetaboxesSave = + hasMetaBoxes && + wasSavingPost && + ! isSavingPost && + ! wasAutosavingPost; + + // Save current state for next inspection. + wasSavingPost = isSavingPost; + wasAutosavingPost = isAutosavingPost; + + if ( shouldTriggerMetaboxesSave ) { + dispatch( editPostStore ).requestMetaBoxUpdates(); + } + } ); + + return { + type: 'META_BOXES_INITIALIZED', + }; +} diff --git a/packages/edit-post/src/store/defaults.js b/packages/edit-post/src/store/defaults.js index 05cb4c8e1957a..9bd5f366a02f9 100644 --- a/packages/edit-post/src/store/defaults.js +++ b/packages/edit-post/src/store/defaults.js @@ -5,15 +5,6 @@ export const PREFERENCES_DEFAULTS = { opened: true, }, }, - features: { - fixedToolbar: false, - welcomeGuide: true, - fullscreenMode: true, - showIconLabels: false, - themeStyles: true, - showBlockBreadcrumbs: true, - welcomeGuideTemplate: true, - }, hiddenBlockTypes: [], preferredStyleVariations: {}, localAutosaveInterval: 15, diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js index 2f9f0ec544bf0..d3eaa38fac7c0 100644 --- a/packages/edit-post/src/store/reducer.js +++ b/packages/edit-post/src/store/reducer.js @@ -78,16 +78,6 @@ export const preferences = flow( [ return state; }, - features( state, action ) { - if ( action.type === 'TOGGLE_FEATURE' ) { - return { - ...state, - [ action.feature ]: ! state[ action.feature ], - }; - } - - return state; - }, editorMode( state, action ) { if ( action.type === 'SWITCH_MODE' ) { return action.mode; @@ -291,9 +281,26 @@ function isEditingTemplate( state = false, action ) { return state; } +/** + * Reducer tracking whether meta boxes are initialized. + * + * @param {boolean} state + * @param {Object} action + * + * @return {boolean} Updated state. + */ +function metaBoxesInitialized( state = false, action ) { + switch ( action.type ) { + case 'META_BOXES_INITIALIZED': + return true; + } + return state; +} + const metaBoxes = combineReducers( { isSaving: isSavingMetaBoxes, locations: metaBoxLocations, + initialized: metaBoxesInitialized, } ); export default combineReducers( { diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index e8a8bddccbc16..e127f6294210a 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -189,9 +189,14 @@ export function isModalActive( state, modalName ) { * * @return {boolean} Is active. */ -export function isFeatureActive( state, feature ) { - return get( state.preferences.features, [ feature ], false ); -} +export const isFeatureActive = createRegistrySelector( + ( select ) => ( state, feature ) => { + return select( interfaceStore ).isFeatureActive( + 'core/edit-post', + feature + ); + } +); /** * Returns true if the plugin item is pinned to the header. @@ -362,6 +367,17 @@ export function isEditingTemplate( state ) { return state.isEditingTemplate; } +/** + * Returns true if meta boxes are initialized. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether meta boxes are initialized. + */ +export function areMetaBoxesInitialized( state ) { + return state.metaBoxes.initialized; +} + /** * Retrieves the template of the currently edited post. * diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 9525fbd1ce880..06739597fd934 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -15,7 +15,6 @@ import { togglePublishSidebar, openModal, closeModal, - toggleFeature, requestMetaBoxUpdates, setIsListViewOpened, } from '../actions'; @@ -90,16 +89,6 @@ describe( 'actions', () => { } ); } ); - describe( 'toggleFeature', () => { - it( 'should return TOGGLE_FEATURE action', () => { - const feature = 'name'; - expect( toggleFeature( feature ) ).toEqual( { - type: 'TOGGLE_FEATURE', - feature, - } ); - } ); - } ); - describe( 'requestMetaBoxUpdates', () => { it( 'should yield the REQUEST_META_BOX_UPDATES action', () => { const fulfillment = requestMetaBoxUpdates(); diff --git a/packages/edit-post/src/store/test/reducer.js b/packages/edit-post/src/store/test/reducer.js index 9ad1574f8de35..67b097e27a8dd 100644 --- a/packages/edit-post/src/store/test/reducer.js +++ b/packages/edit-post/src/store/test/reducer.js @@ -152,18 +152,6 @@ describe( 'state', () => { expect( state.editorMode ).toBe( 'text' ); } ); - it( 'should toggle a feature flag', () => { - const state = preferences( - deepFreeze( { features: { chicken: true } } ), - { - type: 'TOGGLE_FEATURE', - feature: 'chicken', - } - ); - - expect( state.features ).toEqual( { chicken: false } ); - } ); - describe( 'hiddenBlockTypes', () => { it( 'concatenates unique names on disable', () => { const original = deepFreeze( { diff --git a/packages/edit-post/src/store/test/selectors.js b/packages/edit-post/src/store/test/selectors.js index 7ec6bf55546ba..d9828eaa87e5a 100644 --- a/packages/edit-post/src/store/test/selectors.js +++ b/packages/edit-post/src/store/test/selectors.js @@ -11,7 +11,6 @@ import { getPreference, isEditorPanelOpened, isModalActive, - isFeatureActive, hasMetaBoxes, isSavingMetaBoxes, getActiveMetaBoxLocations, @@ -239,51 +238,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'isFeatureActive', () => { - it( 'is tolerant to an undefined features preference', () => { - // See: https://github.com/WordPress/gutenberg/issues/14580 - const state = { - preferences: {}, - }; - - expect( isFeatureActive( state, 'chicken' ) ).toBe( false ); - } ); - - it( 'should return true if feature is active', () => { - const state = { - preferences: { - features: { - chicken: true, - }, - }, - }; - - expect( isFeatureActive( state, 'chicken' ) ).toBe( true ); - } ); - - it( 'should return false if feature is not active', () => { - const state = { - preferences: { - features: { - chicken: false, - }, - }, - }; - - expect( isFeatureActive( state, 'chicken' ) ).toBe( false ); - } ); - - it( 'should return false if feature is not referred', () => { - const state = { - preferences: { - features: {}, - }, - }; - - expect( isFeatureActive( state, 'chicken' ) ).toBe( false ); - } ); - } ); - describe( 'hasMetaBoxes', () => { it( 'should return true if there are active meta boxes', () => { const state = { diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 955d1a94dd569..988c8fce821c8 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "3.0.0", + "version": "3.0.1", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/src/components/editor/global-styles-provider.js b/packages/edit-site/src/components/editor/global-styles-provider.js index 05e672dfa05f1..d85aecdf251bc 100644 --- a/packages/edit-site/src/components/editor/global-styles-provider.js +++ b/packages/edit-site/src/components/editor/global-styles-provider.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { set, get, mergeWith, mapValues, setWith, clone } from 'lodash'; +import { set, get, has, mergeWith, mapValues, setWith, clone } from 'lodash'; /** * WordPress dependencies @@ -75,8 +75,24 @@ export const useGlobalStylesReset = () => { const extractSupportKeys = ( supports ) => { const supportKeys = []; Object.keys( STYLE_PROPERTY ).forEach( ( name ) => { + if ( ! STYLE_PROPERTY[ name ].support ) { + return; + } + + // Opting out means that, for certain support keys like background color, + // blocks have to explicitly set the support value false. If the key is + // unset, we still enable it. + if ( STYLE_PROPERTY[ name ].requiresOptOut ) { + if ( + has( supports, STYLE_PROPERTY[ name ].support[ 0 ] ) && + get( supports, STYLE_PROPERTY[ name ].support ) !== false + ) { + return supportKeys.push( name ); + } + } + if ( get( supports, STYLE_PROPERTY[ name ].support, false ) ) { - supportKeys.push( name ); + return supportKeys.push( name ); } } ); return supportKeys; diff --git a/packages/edit-site/src/components/editor/test/global-styles-provider.js b/packages/edit-site/src/components/editor/test/global-styles-provider.js new file mode 100644 index 0000000000000..0a31516576e57 --- /dev/null +++ b/packages/edit-site/src/components/editor/test/global-styles-provider.js @@ -0,0 +1,131 @@ +/** + * WordPress dependencies + */ +import { dispatch } from '@wordpress/data'; + +/** + * External dependencies + */ +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +/** + * Internal dependencies + */ +import GlobalStylesProvider, { + useGlobalStylesContext, +} from '../global-styles-provider'; + +const settings = { + styles: [ + { + css: 'body {\n\tmargin: 0;\n\tpadding: 0;\n}', + baseURL: 'http://localhost:4759/ponyfill.css', + }, + ], + __experimentalGlobalStylesBaseStyles: {}, +}; + +const generateCoverBlockType = ( colorSupports ) => { + return { + name: 'core/cover', + supports: { + color: colorSupports, + }, + }; +}; + +const FakeCmp = () => { + const globalStylesContext = useGlobalStylesContext(); + const coverBlockSupports = + globalStylesContext.blocks[ 'core/cover' ].supports; + + return
; +}; + +const generateWrapper = () => { + return mount( + + + + ); +}; + +describe( 'global styles provider', () => { + beforeAll( () => { + dispatch( 'core/edit-site' ).updateSettings( settings ); + } ); + + describe( 'when a block enables color support', () => { + describe( 'and disables background color support', () => { + it( 'still enables text color support', () => { + act( () => { + dispatch( 'core/blocks' ).addBlockTypes( + generateCoverBlockType( { + link: true, + background: false, + } ) + ); + } ); + + const wrapper = generateWrapper(); + const actual = wrapper + .findWhere( ( ele ) => Boolean( ele.prop( 'supports' ) ) ) + .prop( 'supports' ); + expect( actual ).not.toContain( 'backgroundColor' ); + expect( actual ).toContain( 'color' ); + + act( () => { + dispatch( 'core/blocks' ).removeBlockTypes( 'core/cover' ); + } ); + } ); + } ); + + describe( 'and both text color and background color support are disabled', () => { + it( 'disables text color and background color support', () => { + act( () => { + dispatch( 'core/blocks' ).addBlockTypes( + generateCoverBlockType( { + text: false, + background: false, + } ) + ); + } ); + + const wrapper = generateWrapper(); + const actual = wrapper + .findWhere( ( ele ) => Boolean( ele.prop( 'supports' ) ) ) + .prop( 'supports' ); + expect( actual ).not.toContain( 'backgroundColor' ); + expect( actual ).not.toContain( 'color' ); + + act( () => { + dispatch( 'core/blocks' ).removeBlockTypes( 'core/cover' ); + } ); + } ); + } ); + + describe( 'and text color and background color supports are omitted', () => { + it( 'still enables both text color and background color supports', () => { + act( () => { + dispatch( 'core/blocks' ).addBlockTypes( + generateCoverBlockType( { link: true } ) + ); + } ); + + const wrapper = generateWrapper(); + const actual = wrapper + .findWhere( ( ele ) => Boolean( ele.prop( 'supports' ) ) ) + .prop( 'supports' ); + expect( actual ).toContain( 'backgroundColor' ); + expect( actual ).toContain( 'color' ); + + act( () => { + dispatch( 'core/blocks' ).removeBlockTypes( 'core/cover' ); + } ); + } ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/components/editor/test/utils.js b/packages/edit-site/src/components/editor/test/utils.js new file mode 100644 index 0000000000000..0f067424687d5 --- /dev/null +++ b/packages/edit-site/src/components/editor/test/utils.js @@ -0,0 +1,146 @@ +/** + * Internal dependencies + */ +import { getPresetVariable, getValueFromVariable } from '../utils'; + +describe( 'editor utils', () => { + const styles = { + version: 1, + settings: { + color: { + palette: { + theme: [ + { + slug: 'primary', + color: '#007cba', + name: 'Primary', + }, + { + slug: 'secondary', + color: '#006ba1', + name: 'Secondary', + }, + ], + user: [ + { + slug: 'primary', + color: '#007cba', + name: 'Primary', + }, + { + slug: 'secondary', + color: '#a65555', + name: 'Secondary', + }, + ], + }, + custom: true, + customDuotone: true, + customGradient: true, + link: true, + }, + custom: { + color: { + primary: 'var(--wp--preset--color--primary)', + secondary: 'var(--wp--preset--color--secondary)', + }, + }, + }, + isGlobalStylesUserThemeJSON: true, + }; + + describe( 'getPresetVariable', () => { + const context = 'root'; + const propertyName = 'color'; + const value = '#007cba'; + + describe( 'when a provided global style (e.g. fontFamily, color,etc.) does not exist', () => { + it( 'returns the originally provided value', () => { + const actual = getPresetVariable( + styles, + context, + 'fakePropertyName', + value + ); + expect( actual ).toBe( value ); + } ); + } ); + + describe( 'when a global style is cleared by the user', () => { + it( 'returns an undefined preset variable', () => { + const actual = getPresetVariable( + styles, + context, + propertyName, + undefined + ); + expect( actual ).toBe( undefined ); + } ); + } ); + + describe( 'when a global style is selected by the user', () => { + describe( 'and it is not a preset value (e.g. custom color)', () => { + it( 'returns the originally provided value', () => { + const customValue = '#6e4545'; + const actual = getPresetVariable( + styles, + context, + propertyName, + customValue + ); + expect( actual ).toBe( customValue ); + } ); + } ); + + describe( 'and it is a preset value', () => { + it( 'returns the preset variable', () => { + const actual = getPresetVariable( + styles, + context, + propertyName, + value + ); + expect( actual ).toBe( 'var:preset|color|primary' ); + } ); + } ); + } ); + } ); + + describe( 'getValueFromVariable', () => { + describe( 'when provided an invalid variable', () => { + it( 'returns the originally provided value', () => { + const actual = getValueFromVariable( + styles, + 'root', + undefined + ); + + expect( actual ).toBe( undefined ); + } ); + } ); + + describe( 'when provided a preset variable', () => { + it( 'retrieves the correct preset value', () => { + const actual = getValueFromVariable( + styles, + 'root', + 'var:preset|color|primary' + ); + + expect( actual ).toBe( '#007cba' ); + } ); + } ); + + describe( 'when provided a custom variable', () => { + it( 'retrieves the correct custom value', () => { + const actual = getValueFromVariable( + styles, + 'root', + 'var(--wp--custom--color--secondary)' + ); + + expect( actual ).toBe( '#a65555' ); + } ); + } ); + } ); +} ); diff --git a/packages/edit-site/src/components/editor/utils.js b/packages/edit-site/src/components/editor/utils.js index ba883f330ea26..81a809dbf4bee 100644 --- a/packages/edit-site/src/components/editor/utils.js +++ b/packages/edit-site/src/components/editor/utils.js @@ -6,6 +6,7 @@ import { get, find, forEach, camelCase, isString } from 'lodash'; * WordPress dependencies */ import { useSelect } from '@wordpress/data'; +import { __EXPERIMENTAL_PATHS_WITH_MERGE as PATHS_WITH_MERGE } from '@wordpress/blocks'; /** * Internal dependencies */ @@ -91,13 +92,6 @@ function getPresetMetadataFromStyleProperty( styleProperty ) { return getPresetMetadataFromStyleProperty.MAP[ styleProperty ]; } -const PATHS_WITH_MERGE = { - 'color.gradients': true, - 'color.palette': true, - 'typography.fontFamilies': true, - 'typography.fontSizes': true, -}; - export function useSetting( path, blockName = '' ) { const settings = useSelect( ( select ) => { return select( editSiteStore ).getSettings(); diff --git a/packages/edit-site/src/components/header/document-actions/index.js b/packages/edit-site/src/components/header/document-actions/index.js index fc9f592215df4..1844f198000b6 100644 --- a/packages/edit-site/src/components/header/document-actions/index.js +++ b/packages/edit-site/src/components/header/document-actions/index.js @@ -29,7 +29,7 @@ function getBlockDisplayText( block ) { } function useSecondaryText() { - const { getBlock } = useSelect( 'core/block-editor' ); + const { getBlock } = useSelect( blockEditorStore ); const activeEntityBlockId = useSelect( ( select ) => select( diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js index e33a4fba22ed5..43f460f26e367 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js @@ -36,7 +36,7 @@ export default function ContentNavigationItem( { item } ) { const template = select( coreStore ).__experimentalGetTemplateForLink( item.link ); - return template?.content?.raw; + return template?.content; }, [ isPreviewVisible ] ); diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss index d3ab0f7ac3d5b..d78d3d4e6f668 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss @@ -17,9 +17,10 @@ background: $gray-900; border-radius: 0; color: $white; - height: $header-height; + height: $header-height + $border-width; width: $header-height; z-index: 1; + margin-bottom: - $border-width; &.has-icon { min-width: $header-height; @@ -44,7 +45,7 @@ bottom: 9px; left: 9px; border-radius: $radius-block-ui + $border-width + $border-width; - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) #23282e; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $gray-900; } // Hover color. diff --git a/packages/edit-site/src/components/sidebar/border-panel.js b/packages/edit-site/src/components/sidebar/border-panel.js index e7d5c801ff935..19cdcff68c6ee 100644 --- a/packages/edit-site/src/components/sidebar/border-panel.js +++ b/packages/edit-site/src/components/sidebar/border-panel.js @@ -32,7 +32,7 @@ export function useHasBorderPanel( { supports, name } ) { useHasBorderWidthControl( { supports, name } ), ]; - return controls.every( Boolean ); + return controls.some( Boolean ); } function useHasBorderColorControl( { supports, name } ) { @@ -123,7 +123,7 @@ export default function BorderPanel( { { hasBorderColor && ( 0 || areCustomSolidsEnabled ); + const hasTextColor = + supports.includes( 'color' ) && + isTextEnabled && + ( solids.length > 0 || areCustomSolidsEnabled ); + const hasBackgroundColor = + supports.includes( 'backgroundColor' ) && + isBackgroundEnabled && + ( solids.length > 0 || areCustomSolidsEnabled ); + const hasGradientColor = + supports.includes( 'background' ) && + ( gradients.length > 0 || areCustomGradientsEnabled ); const settings = []; - if ( supports.includes( 'color' ) ) { + if ( hasTextColor ) { const color = getStyle( name, 'color' ); const userColor = getStyle( name, 'color', 'user' ); settings.push( { @@ -46,7 +68,7 @@ export default function ColorPanel( { } let backgroundSettings = {}; - if ( supports.includes( 'backgroundColor' ) ) { + if ( hasBackgroundColor ) { const backgroundColor = getStyle( name, 'backgroundColor' ); const userBackgroundColor = getStyle( name, 'backgroundColor', 'user' ); backgroundSettings = { @@ -61,7 +83,7 @@ export default function ColorPanel( { } let gradientSettings = {}; - if ( supports.includes( 'background' ) ) { + if ( hasGradientColor ) { const gradient = getStyle( name, 'background' ); const userGradient = getStyle( name, 'background', 'user' ); gradientSettings = { @@ -74,10 +96,7 @@ export default function ColorPanel( { } } - if ( - supports.includes( 'background' ) || - supports.includes( 'backgroundColor' ) - ) { + if ( hasBackgroundColor || hasGradientColor ) { settings.push( { ...backgroundSettings, ...gradientSettings, @@ -85,7 +104,7 @@ export default function ColorPanel( { } ); } - if ( supports.includes( 'linkColor' ) ) { + if ( hasLinkColor ) { const color = getStyle( name, 'linkColor' ); const userColor = getStyle( name, 'linkColor', 'user' ); settings.push( { @@ -95,14 +114,15 @@ export default function ColorPanel( { clearable: color === userColor, } ); } + return ( marginValues && Object.keys( marginValues ).length; + const gapValue = getStyle( name, '--wp--style--block-gap' ); + + const setGapValue = ( newGapValue ) => { + setStyle( name, '--wp--style--block-gap', newGapValue ); + }; + const resetGapValue = () => setGapValue( undefined ); + const hasGapValue = () => !! gapValue; + const resetAll = () => { resetPaddingValue(); resetMarginValue(); + resetGapValue(); }; return ( @@ -163,6 +181,23 @@ export default function DimensionsPanel( { context, getStyle, setStyle } ) { /> ) } + { showGapControl && ( + + + + ) } ); } diff --git a/packages/edit-site/src/components/template-part-converter/convert-to-regular.js b/packages/edit-site/src/components/template-part-converter/convert-to-regular.js index cfc43db26a657..6407f8a22b071 100644 --- a/packages/edit-site/src/components/template-part-converter/convert-to-regular.js +++ b/packages/edit-site/src/components/template-part-converter/convert-to-regular.js @@ -10,13 +10,7 @@ import { MenuItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; export default function ConvertToRegularBlocks( { clientId } ) { - const { innerBlocks } = useSelect( - ( select ) => - select( blockEditorStore ).__unstableGetBlockWithBlockTree( - clientId - ), - [ clientId ] - ); + const { getBlocks } = useSelect( blockEditorStore ); const { replaceBlocks } = useDispatch( blockEditorStore ); return ( @@ -24,7 +18,7 @@ export default function ConvertToRegularBlocks( { clientId } ) { { ( { onClose } ) => ( { - replaceBlocks( clientId, innerBlocks ); + replaceBlocks( clientId, getBlocks( clientId ) ); onClose(); } } > diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index d898501b6b21c..4d753c74255c9 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "3.0.0", + "version": "3.0.1", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js index 0dbc7bb404e02..0969f0dabf9c1 100644 --- a/packages/edit-widgets/src/components/layout/interface.js +++ b/packages/edit-widgets/src/components/layout/interface.js @@ -65,14 +65,12 @@ function Interface( { blockEditorSettings } ) { ).isFeatureActive( 'core/edit-widgets', 'showBlockBreadcrumbs' ), previousShortcut: select( keyboardShortcutsStore - ).getAllShortcutRawKeyCombinations( + ).getAllShortcutKeyCombinations( 'core/edit-widgets/previous-region' ), nextShortcut: select( keyboardShortcutsStore - ).getAllShortcutRawKeyCombinations( - 'core/edit-widgets/next-region' - ), + ).getAllShortcutKeyCombinations( 'core/edit-widgets/next-region' ), } ), [] ); diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 15961a863047e..0c00388e7c0f2 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -16,6 +16,7 @@ import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wor import { registerLegacyWidgetBlock, registerLegacyWidgetVariations, + registerWidgetGroupBlock, } from '@wordpress/widgets'; import { dispatch } from '@wordpress/data'; import { store as interfaceStore } from '@wordpress/interface'; @@ -26,6 +27,7 @@ import { store as interfaceStore } from '@wordpress/interface'; import './store'; import './filters'; import * as widgetArea from './blocks/widget-area'; + import Layout from './components/layout'; import { ALLOW_REUSABLE_BLOCKS, @@ -35,7 +37,7 @@ import { const disabledBlocks = [ 'core/more', 'core/freeform', - ...( ! ALLOW_REUSABLE_BLOCKS && [ 'core/block' ] ), + ...( ALLOW_REUSABLE_BLOCKS ? [] : [ 'core/block' ] ), ]; /** @@ -89,6 +91,8 @@ export function initialize( id, settings ) { } registerLegacyWidgetVariations( settings ); registerBlock( widgetArea ); + registerWidgetGroupBlock(); + settings.__experimentalFetchLinkSuggestions = ( search, searchOptions ) => fetchLinkSuggestions( search, searchOptions, settings ); diff --git a/packages/editor/package.json b/packages/editor/package.json index 4bed95f0740bf..069d27437c8c3 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "11.0.0", + "version": "11.0.1", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/src/components/editor-help/add-blocks.native.js b/packages/editor/src/components/editor-help/add-blocks.native.js index cb3e971503a7f..e9e110a4134ea 100644 --- a/packages/editor/src/components/editor-help/add-blocks.native.js +++ b/packages/editor/src/components/editor-help/add-blocks.native.js @@ -17,7 +17,10 @@ import { HelpDetailBodyText, HelpDetailImage } from './view-sections'; const AddBlocks = () => { return ( <> - + { return ( <> { return ( - + + + + { label } + + - + + + + { title } + + { ( { listProps } ) => { const contentContainerStyle = StyleSheet.flatten( diff --git a/packages/editor/src/components/editor-help/intro-to-blocks.native.js b/packages/editor/src/components/editor-help/intro-to-blocks.native.js index 17b97d9831b6d..79e5711b72d05 100644 --- a/packages/editor/src/components/editor-help/intro-to-blocks.native.js +++ b/packages/editor/src/components/editor-help/intro-to-blocks.native.js @@ -27,7 +27,7 @@ const IntroToBlocks = () => { return ( <> { accessibilityLabel={ __( 'Text formatting controls are located within the toolbar positioned above the keyboard while editing a text block' ) } - source={ require( './images/intro-blocks-2.png' ) } + source={ require( './images/rich-text-light.png' ) } + sourceDarkMode={ require( './images/rich-text-dark.png' ) } /> { ) } /> { ) } /> { return ( <> - + { return ( <> { ); }; -export const HelpDetailImage = ( props ) => { +export const HelpDetailImage = ( { + accessible, + accessibilityLabel, + source, + sourceDarkMode, +} ) => { const imageStyle = usePreferredColorSchemeStyle( styles.helpDetailImage, styles.helpDetailImageDark ); - return ; + const darkModeEnabled = usePreferredColorScheme() === 'dark'; + return ( + + ); }; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 50dfd6885166d..89a7d1ab01035 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -58,6 +58,7 @@ export { default as PostVisibility } from './post-visibility'; export { default as PostVisibilityLabel } from './post-visibility/label'; export { default as PostVisibilityCheck } from './post-visibility/check'; export { default as TableOfContents } from './table-of-contents'; +export { default as ThemeSupportCheck } from './theme-support-check'; export { default as UnsavedChangesWarning } from './unsaved-changes-warning'; export { default as WordCount } from './word-count'; diff --git a/packages/editor/src/components/local-autosave-monitor/index.js b/packages/editor/src/components/local-autosave-monitor/index.js index 4587ec7611ace..b406a2545cf39 100644 --- a/packages/editor/src/components/local-autosave-monitor/index.js +++ b/packages/editor/src/components/local-autosave-monitor/index.js @@ -56,7 +56,7 @@ function useAutosaveNotice() { } ), [] ); - const { getEditedPostAttribute } = useSelect( 'core/editor' ); + const { getEditedPostAttribute } = useSelect( editorStore ); const { createWarningNotice, removeNotice } = useDispatch( noticesStore ); const { editPost, resetEditorBlocks } = useDispatch( editorStore ); diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index ea99b0c815d2b..7cea94d0625a1 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -168,7 +168,7 @@ export default function PostTitle() { // The wp-block className is important for editor styles. // This same block is used in both the visual and the code editor. const className = classnames( - 'wp-block editor-post-title editor-post-title__block editor-post-title__input rich-text', + 'wp-block wp-block-post-title block-editor-block-list__block editor-post-title editor-post-title__input rich-text', { 'is-selected': isSelected, 'is-focus-mode': isFocusMode, diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index 8c9d7f8089bcf..6cdf848cfe69f 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -54,6 +54,7 @@ const postTypeEntities = [ mergedEdits: { meta: true, }, + rawAttributes: [ 'title', 'excerpt', 'content' ], } ) ); import { EditorHelpTopics } from '@wordpress/editor'; @@ -89,10 +90,15 @@ class NativeEditorProvider extends Component { } componentDidMount() { - const { capabilities, updateSettings } = this.props; + const { + capabilities, + updateSettings, + galleryWithImageBlocks, + } = this.props; updateSettings( { ...capabilities, + ...{ __unstableGalleryWithImageBlocks: galleryWithImageBlocks }, ...this.getThemeColors( this.props ), } ); @@ -142,8 +148,13 @@ class NativeEditorProvider extends Component { this.subscriptionParentUpdateEditorSettings = subscribeUpdateEditorSettings( ( editorSettings ) => { - const themeColors = this.getThemeColors( editorSettings ); - updateSettings( themeColors ); + updateSettings( { + ...{ + __unstableGalleryWithImageBlocks: + editorSettings.galleryWithImageBlocks, + }, + ...this.getThemeColors( editorSettings ), + } ); } ); diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 94125a58f1392..03824561549f7 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -14,7 +14,12 @@ export const PREFERENCES_DEFAULTS = { * allowedBlockTypes boolean|Array Allowed block types * richEditingEnabled boolean Whether rich editing is enabled or not * codeEditingEnabled boolean Whether code editing is enabled or not - * enableCustomFields boolean Whether the WordPress custom fields are enabled or not + * enableCustomFields boolean Whether the WordPress custom fields are enabled or not. + * true = the user has opted to show the Custom Fields panel at the bottom of the editor. + * false = the user has opted to hide the Custom Fields panel at the bottom of the editor. + * undefined = the current environment does not support Custom Fields, + * so the option toggle in Preferences -> Panels to + * enable the Custom Fields panel is not displayed. * autosaveInterval number Autosave Interval * availableTemplates array? The available post templates * disablePostFormats boolean Whether or not the post formats are disabled @@ -27,6 +32,6 @@ export const EDITOR_SETTINGS_DEFAULTS = { richEditingEnabled: true, codeEditingEnabled: true, - enableCustomFields: false, + enableCustomFields: undefined, supportsLayout: true, }; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 00d3e6527a71c..d3e185e051e11 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1394,13 +1394,6 @@ export const getBlock = getBlockEditorSelector( 'getBlock' ); */ export const getBlocks = getBlockEditorSelector( 'getBlocks' ); -/** - * @see __unstableGetBlockWithoutInnerBlocks in core/block-editor store. - */ -export const __unstableGetBlockWithoutInnerBlocks = getBlockEditorSelector( - '__unstableGetBlockWithoutInnerBlocks' -); - /** * @see getClientIdsOfDescendants in core/block-editor store. */ diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index ce0b7e044e070..bc8a8aeafa4d1 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -133,7 +133,7 @@ function parseSourceString( sourceString, { workDirectoryPath } ) { } throw new ValidationError( - `Invalid or unrecognized source: "${ sourceString }."` + `Invalid or unrecognized source: "${ sourceString }".` ); } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 587e5a10fd164..f1df1f4c7433e 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,11 +2,20 @@ ## Unreleased +### Enhancement + +- The bundled `eslint-plugin-jsdoc` dependency has been updated from requiring `^34.1.0` to requiring `^36.0.8` ([#34338](https://github.com/WordPress/gutenberg/pull/34338)). + ### Bug Fix -- Include `.jsx` extension when linting import statements in case TypeScript not present ([#33746](https://github.com/WordPress/gutenberg/pull/33746)). - The recommended configuration will now respect `type` imports in TypeScript files ([#34055](https://github.com/WordPress/gutenberg/pull/34055)). +## 9.1.1 (2021-08-23) + +### Bug Fix + +- Include `.jsx` extension when linting import statements in case TypeScript not present ([#33746](https://github.com/WordPress/gutenberg/pull/33746)). + ## 9.1.0 (2021-07-21) ### Enhancement diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 3e2e3daa4b796..de758ce55e20f 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -63,8 +63,9 @@ The granular rulesets will not define any environment globals. As such, if they | [gutenberg-phase](docs/rules/gutenberg-phase.md) | Governs the use of the `process.env.GUTENBERG_PHASE` constant | ✓ | | [no-base-control-with-label-without-id](/packages/eslint-plugin/docs/rules/no-base-control-with-label-without-id.md) | Disallow the usage of BaseControl component with a label prop set but omitting the id property | ✓ | | [no-unguarded-get-range-at](/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md) | Disallow the usage of unguarded `getRangeAt` calls | ✓ | +| [no-unsafe-wp-apis](/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md) | Disallow the usage of unsafe APIs from `@wordpress/*` packages | ✓ | | [no-unused-vars-before-return](/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md) | Disallow assigning variable values if unused before a return | ✓ | -| [react-no-unsafe-timeout](/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md) | Disallow unsafe `setTimeout` in component | +| [react-no-unsafe-timeout](/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md) | Disallow unsafe `setTimeout` in component | | | [valid-sprintf](/packages/eslint-plugin/docs/rules/valid-sprintf.md) | Enforce valid sprintf usage | ✓ | | [i18n-ellipsis](/packages/eslint-plugin/docs/rules/i18n-ellipsis.md) | Disallow using three dots in translatable strings | ✓ | | [i18n-no-collapsible-whitespace](/packages/eslint-plugin/docs/rules/i18n-no-collapsible-whitespace.md) | Disallow collapsible whitespace in translatable strings | ✓ | diff --git a/packages/eslint-plugin/configs/custom.js b/packages/eslint-plugin/configs/custom.js index 8964c5e5b82fc..98417b4122b33 100644 --- a/packages/eslint-plugin/configs/custom.js +++ b/packages/eslint-plugin/configs/custom.js @@ -6,7 +6,6 @@ module.exports = { '@wordpress/no-unguarded-get-range-at': 'error', '@wordpress/no-global-active-element': 'error', '@wordpress/no-global-get-selection': 'error', - '@wordpress/no-global-event-listener': 'warn', '@wordpress/no-unsafe-wp-apis': 'error', }, overrides: [ @@ -25,7 +24,6 @@ module.exports = { rules: { '@wordpress/no-global-active-element': 'off', '@wordpress/no-global-get-selection': 'off', - '@wordpress/no-global-event-listener': 'off', }, }, ], diff --git a/packages/eslint-plugin/configs/jsdoc.js b/packages/eslint-plugin/configs/jsdoc.js index b8f658433aad2..d114241361a07 100644 --- a/packages/eslint-plugin/configs/jsdoc.js +++ b/packages/eslint-plugin/configs/jsdoc.js @@ -105,6 +105,11 @@ module.exports = { 'jsdoc/require-param-description': 'off', 'jsdoc/require-returns': 'off', 'jsdoc/require-yields': 'off', + 'jsdoc/tag-lines': 'off', + 'jsdoc/no-multi-asterisks': [ + 'error', + { preventAtMiddleLines: false }, + ], 'jsdoc/check-access': 'error', 'jsdoc/check-alignment': 'error', 'jsdoc/check-line-alignment': [ diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index a80b690be4276..29a7afbc65f50 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "9.1.0", + "version": "9.1.1", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -39,7 +39,7 @@ "eslint-config-prettier": "^7.1.0", "eslint-plugin-import": "^2.23.4", "eslint-plugin-jest": "^24.1.3", - "eslint-plugin-jsdoc": "^34.1.0", + "eslint-plugin-jsdoc": "^36.0.8", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prettier": "^3.3.0", "eslint-plugin-react": "^7.22.0", diff --git a/packages/eslint-plugin/rules/__tests__/data-no-store-string-literals.js b/packages/eslint-plugin/rules/__tests__/data-no-store-string-literals.js index ac74c7800a50a..f7581ac0b1410 100644 --- a/packages/eslint-plugin/rules/__tests__/data-no-store-string-literals.js +++ b/packages/eslint-plugin/rules/__tests__/data-no-store-string-literals.js @@ -17,24 +17,25 @@ const ruleTester = new RuleTester( { const valid = [ // Callback functions - `import { createRegistrySelector } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; createRegistrySelector(( select ) => { select(store); });`, - `import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useSelect(( select ) => { select(store); });`, - `import { withSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withSelect(( select ) => { select(store); });`, - `import { withDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withDispatch(( select ) => { select(store); });`, - `import { withDispatch as withDispatchAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withDispatchAlias(( select ) => { select(store); });`, + `import { createRegistrySelector } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; createRegistrySelector(( select ) => { select(coreStore); });`, + `import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useSelect(( select ) => { select(coreStore); });`, + `import { withSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withSelect(( select ) => { select(coreStore); });`, + `import { withDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withDispatch(( select ) => { select(coreStore); });`, + `import { withDispatch as withDispatchAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; withDispatchAlias(( select ) => { select(coreStore); });`, // Direct function calls - `import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useDispatch( store );`, - `import { dispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; dispatch( store );`, - `import { select } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; select( store );`, - `import { resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; resolveSelect( store );`, - `import { resolveSelect as resolveSelectAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; resolveSelectAlias( store );`, + `import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useDispatch( coreStore );`, + `import { dispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; dispatch( coreStore );`, + `import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; useSelect( coreStore );`, + `import { select } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; select( coreStore );`, + `import { resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; resolveSelect( coreStore );`, + `import { resolveSelect as resolveSelectAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; resolveSelectAlias( coreStore );`, // Object property function calls - `import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.select( store );`, - `import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.dispatch( store );`, - `import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.resolveSelect( store );`, - `import { controls as controlsAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controlsAlias.resolveSelect( store );`, + `import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.select( coreStore );`, + `import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.dispatch( coreStore );`, + `import { controls } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controls.resolveSelect( coreStore );`, + `import { controls as controlsAlias } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; controlsAlias.resolveSelect( coreStore );`, ]; const createSuggestionTestCase = ( code, output ) => ( { @@ -63,6 +64,7 @@ const invalid = [ // Direct function calls `import { useDispatch } from '@wordpress/data'; useDispatch( 'core' );`, `import { dispatch } from '@wordpress/data'; dispatch( 'core' );`, + `import { useSelect } from '@wordpress/data'; useSelect( 'core' );`, `import { select } from '@wordpress/data'; select( 'core' );`, `import { resolveSelect } from '@wordpress/data'; resolveSelect( 'core' );`, `import { resolveSelect as resolveSelectAlias } from '@wordpress/data'; resolveSelectAlias( 'core' );`, diff --git a/packages/eslint-plugin/rules/__tests__/no-global-event-listener.js b/packages/eslint-plugin/rules/__tests__/no-global-event-listener.js deleted file mode 100644 index b7434cf5b62c9..0000000000000 --- a/packages/eslint-plugin/rules/__tests__/no-global-event-listener.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * External dependencies - */ -import { RuleTester } from 'eslint'; - -/** - * Internal dependencies - */ -import rule from '../no-global-event-listener'; - -const ruleTester = new RuleTester( { - parserOptions: { - ecmaVersion: 6, - }, -} ); - -ruleTester.run( 'no-global-event-listener', rule, { - valid: [ - { - code: 'ownerDocument.addEventListener();', - }, - { - code: 'ownerDocument.removeEventListener();', - }, - { - code: 'defaultView.addEventListener();', - }, - { - code: 'defaultView.removeEventListener();', - }, - ], - invalid: [ - { - code: 'document.addEventListener();', - errors: [ - { - message: - 'Avoid using (add|remove)EventListener with globals. Use `ownerDocument` or `ownerDocument.defaultView` on a node ref instead.', - }, - ], - }, - { - code: 'document.removeEventListener();', - errors: [ - { - message: - 'Avoid using (add|remove)EventListener with globals. Use `ownerDocument` or `ownerDocument.defaultView` on a node ref instead.', - }, - ], - }, - { - code: 'window.addEventListener();', - errors: [ - { - message: - 'Avoid using (add|remove)EventListener with globals. Use `ownerDocument` or `ownerDocument.defaultView` on a node ref instead.', - }, - ], - }, - { - code: 'window.removeEventListener();', - errors: [ - { - message: - 'Avoid using (add|remove)EventListener with globals. Use `ownerDocument` or `ownerDocument.defaultView` on a node ref instead.', - }, - ], - }, - ], -} ); diff --git a/packages/eslint-plugin/rules/data-no-store-string-literals.js b/packages/eslint-plugin/rules/data-no-store-string-literals.js index 72dfc97654f2a..805a9cc6bd610 100644 --- a/packages/eslint-plugin/rules/data-no-store-string-literals.js +++ b/packages/eslint-plugin/rules/data-no-store-string-literals.js @@ -78,9 +78,13 @@ function collectAllNodesFromDirectFunctionCalls( context, node ) { const specifiers = node.specifiers.filter( ( specifier ) => specifier.imported && - [ 'useDispatch', 'dispatch', 'select', 'resolveSelect' ].includes( - specifier.imported.name - ) + [ + 'useDispatch', + 'dispatch', + 'useSelect', + 'select', + 'resolveSelect', + ].includes( specifier.imported.name ) ); const references = getReferences( context, specifiers ); const possibleCallExpressionNodes = references diff --git a/packages/eslint-plugin/rules/no-global-event-listener.js b/packages/eslint-plugin/rules/no-global-event-listener.js deleted file mode 100644 index 6fa73eeff3c2a..0000000000000 --- a/packages/eslint-plugin/rules/no-global-event-listener.js +++ /dev/null @@ -1,35 +0,0 @@ -module.exports = { - meta: { - type: 'problem', - schema: [], - }, - create( context ) { - return { - CallExpression( node ) { - const { callee } = node; - const { object, property } = callee; - - if ( ! object || ! property ) { - return; - } - - if ( object.name !== 'document' && object.name !== 'window' ) { - return; - } - - if ( - property.name !== 'addEventListener' && - property.name !== 'removeEventListener' - ) { - return; - } - - context.report( { - node, - message: - 'Avoid using (add|remove)EventListener with globals. Use `ownerDocument` or `ownerDocument.defaultView` on a node ref instead.', - } ); - }, - }; - }, -}; diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 52fdb9028d451..68a8b2060f597 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "3.0.0", + "version": "3.0.1", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/i18n/src/sprintf.js b/packages/i18n/src/sprintf.js index 98c4bade268a1..550248a81c9e9 100644 --- a/packages/i18n/src/sprintf.js +++ b/packages/i18n/src/sprintf.js @@ -28,8 +28,9 @@ export function sprintf( format, ...args ) { try { return sprintfjs.sprintf( format, ...args ); } catch ( error ) { - logErrorOnce( 'sprintf error: \n\n' + error.toString() ); - + if ( error instanceof Error ) { + logErrorOnce( 'sprintf error: \n\n' + error.toString() ); + } return format; } } diff --git a/packages/icons/package.json b/packages/icons/package.json index 200d3a701139f..463ad625b130e 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/icons", - "version": "5.0.0", + "version": "5.0.1", "description": "WordPress Icons package, based on dashicon.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/package.json b/packages/interface/package.json index ee0e53bae44b6..7880c08042ea6 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "4.0.0", + "version": "4.0.1", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js index 26bf20681f659..1f4c020dd7cf8 100644 --- a/packages/interface/src/components/interface-skeleton/index.js +++ b/packages/interface/src/components/interface-skeleton/index.js @@ -9,7 +9,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { forwardRef, useEffect, useRef } from '@wordpress/element'; +import { forwardRef, useEffect } from '@wordpress/element'; import { __unstableUseNavigateRegions as useNavigateRegions } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useMergeRefs } from '@wordpress/compose'; @@ -44,8 +44,7 @@ function InterfaceSkeleton( }, ref ) { - const fallbackRef = useRef(); - const regionsClassName = useNavigateRegions( fallbackRef, shortcuts ); + const navigateRegionsProps = useNavigateRegions( shortcuts ); useHTMLClass( 'interface-interface-skeleton__html-container' ); @@ -70,11 +69,12 @@ function InterfaceSkeleton( return (
diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index 7342d85b69610..6e18c4dd854ae 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fix + +- Restore the default setting for the `verbose` option. In effect, each test won't get reported during the run ([#34327](https://github.com/WordPress/gutenberg/pull/34327)). + ## 7.0.0 (2021-01-21) ### Breaking Changes diff --git a/packages/jest-preset-default/jest-preset.js b/packages/jest-preset-default/jest-preset.js index 252ac61a9c5df..5dc0277f2d88e 100644 --- a/packages/jest-preset-default/jest-preset.js +++ b/packages/jest-preset-default/jest-preset.js @@ -26,5 +26,4 @@ module.exports = { transform: { '^.+\\.[jt]sx?$': require.resolve( 'babel-jest' ), }, - verbose: true, }; diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index d44c89ddf24f5..007167b069f22 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "3.0.0", + "version": "3.0.1", "description": "Handling keyboard shortcuts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/lazy-import/tsconfig.json b/packages/lazy-import/tsconfig.json index 426ab13d0aa8f..fd6069e3843ae 100644 --- a/packages/lazy-import/tsconfig.json +++ b/packages/lazy-import/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "lib", - "declarationDir": "build-types" + "declarationDir": "build-types", + "useUnknownInCatchVariables": false }, "include": [ "lib/**/*" ] } diff --git a/packages/library-export-default-webpack-plugin/CHANGELOG.md b/packages/library-export-default-webpack-plugin/CHANGELOG.md index f95007336313a..dce16c429d1d8 100644 --- a/packages/library-export-default-webpack-plugin/CHANGELOG.md +++ b/packages/library-export-default-webpack-plugin/CHANGELOG.md @@ -2,9 +2,11 @@ ## Unreleased +## 2.2.0 (2021-08-23) + ### Deprecations -- This plugin is deprecated for webpack 5. Please use [`output.library.export`](https://webpack.js.org/configuration/output/#outputlibraryexport) instead ([#33818](ttps://github.com/WordPress/gutenberg/pull/33818)). +- This plugin is deprecated for webpack 5. Please use [`output.library.export`](https://webpack.js.org/configuration/output/#outputlibraryexport) instead ([#33818](https://github.com/WordPress/gutenberg/pull/33818)). ## 2.0.0 (2021-01-21) diff --git a/packages/library-export-default-webpack-plugin/package.json b/packages/library-export-default-webpack-plugin/package.json index f7585909f5dfe..5f3cedd95c103 100644 --- a/packages/library-export-default-webpack-plugin/package.json +++ b/packages/library-export-default-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/library-export-default-webpack-plugin", - "version": "2.1.0", + "version": "2.2.0", "description": "Webpack plugin for exporting default property for selected libraries.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 9d7699f1b1957..06cb297ceda02 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "3.0.0", + "version": "3.0.1", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/package.json b/packages/notices/package.json index 3abcea38ad2ab..d781ee3dcf84e 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "3.2.1", + "version": "3.2.2", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index 28a704db1cff2..f8297a55a3727 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "5.0.0", + "version": "5.0.1", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index cf95ff899d3ae..3145b2539a19d 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "4.0.0", + "version": "4.0.1", "description": "Plugins module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/prettier-config/README.md b/packages/prettier-config/README.md index 901b6ca61d20f..68730d2d4d543 100644 --- a/packages/prettier-config/README.md +++ b/packages/prettier-config/README.md @@ -23,7 +23,7 @@ Add this to your `package.json` file: Alternatively, add this to `.prettierrc` file: ``` -extends @wordpress/prettier-config +extends: ['@wordpress/prettier-config'] ```

Code is Poetry.

diff --git a/packages/project-management-automation/lib/tasks/add-milestone/index.js b/packages/project-management-automation/lib/tasks/add-milestone/index.js index 3f26cd3ed3f31..f70a6560bea99 100644 --- a/packages/project-management-automation/lib/tasks/add-milestone/index.js +++ b/packages/project-management-automation/lib/tasks/add-milestone/index.js @@ -19,7 +19,7 @@ const DAYS_PER_RELEASE = 14; * Returns true if the given error object represents a duplicate entry error, or * false otherwise. * - * @param {RequestError} requestError Error to test. + * @param {unknown} requestError Error to test. * * @return {boolean} Whether error is a duplicate validation request error. */ @@ -27,7 +27,14 @@ const isDuplicateValidationError = ( requestError ) => { // The included version of RequestError provides no way to access the // full 'errors' array that the github REST API returns. Hopefully they // resolve this soon! - return requestError.message.includes( 'already_exists' ); + const errorMessage = + requestError && + typeof requestError === 'object' && + /** @type {{message?: string}} */ ( requestError ).message; + return ( + typeof errorMessage === 'string' && + errorMessage.includes( 'already_exists' ) + ); }; /** diff --git a/packages/project-management-automation/lib/tasks/first-time-contributor-account-link/index.js b/packages/project-management-automation/lib/tasks/first-time-contributor-account-link/index.js index 9d18ccad467aa..b60f65d5f5dc0 100644 --- a/packages/project-management-automation/lib/tasks/first-time-contributor-account-link/index.js +++ b/packages/project-management-automation/lib/tasks/first-time-contributor-account-link/index.js @@ -86,9 +86,11 @@ async function firstTimeContributorAccountLink( payload, octokit ) { try { hasProfile = await hasWordPressProfile( author ); } catch ( error ) { - debug( - `first-time-contributor-account-link: Error retrieving from profile API:\n\n${ error.toString() }` - ); + if ( error instanceof Object ) { + debug( + `first-time-contributor-account-link: Error retrieving from profile API:\n\n${ error.toString() }` + ); + } return; } diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle index 7eb7ed9b71f62..674620ad78035 100644 --- a/packages/react-native-aztec/android/build.gradle +++ b/packages/react-native-aztec/android/build.gradle @@ -1,7 +1,5 @@ buildscript { ext { - gradlePluginVersion = '4.0.2' - kotlinVersion = '1.5.20' supportLibVersion = '29.0.2' tagSoupVersion = '1.2.1' glideVersion = '3.7.0' @@ -14,30 +12,14 @@ buildscript { aztecVersion = 'v1.3.45' willPublishReactNativeAztecBinary = properties["willPublishReactNativeAztecBinary"]?.toBoolean() ?: false } - - repositories { - maven { - url 'https://a8c-libs.s3.amazonaws.com/android' - content { - includeGroup 'com.automattic.android' - } - } - jcenter() - google() - } - - dependencies { - classpath "com.android.tools.build:gradle:$gradlePluginVersion" - classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath 'com.automattic.android:publish-to-s3:0.6.1' - } } -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'com.github.dcendents.android-maven' -apply plugin: 'com.automattic.android.publish-to-s3' +plugins { + id "com.android.library" + id "org.jetbrains.kotlin.android" + id "maven-publish" + id "com.automattic.android.publish-to-s3" +} // import the `readReactNativeVersion()` function apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle' @@ -105,8 +87,6 @@ dependencies { api "com.github.wordpress-mobile.WordPress-Aztec-Android:glide-loader:$aztecVersion" implementation "org.wordpress:utils:$wordpressUtilsVersion" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" - implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.cardview:cardview:1.0.0' diff --git a/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b38..f3d88b1c2faf2 100644 Binary files a/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar and b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties index 4e1cc9db6b597..af7be50b1015c 100644 --- a/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/react-native-aztec/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/react-native-aztec/android/gradlew b/packages/react-native-aztec/android/gradlew index 8e25e6c19d574..2fe81a7d95e4f 100755 --- a/packages/react-native-aztec/android/gradlew +++ b/packages/react-native-aztec/android/gradlew @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/packages/react-native-aztec/android/settings.gradle b/packages/react-native-aztec/android/settings.gradle index b589da9e83838..afe8958a55e2a 100644 --- a/packages/react-native-aztec/android/settings.gradle +++ b/packages/react-native-aztec/android/settings.gradle @@ -1,2 +1,22 @@ -rootProject.name = '@wordpress_react-native-aztec' +pluginManagement { + gradle.ext.kotlinVersion = '1.5.20' + + plugins { + id "com.android.library" version "4.2.2" + id "org.jetbrains.kotlin.android" version gradle.ext.kotlinVersion + id "com.automattic.android.publish-to-s3" version "0.6.1" + } + repositories { + maven { + url 'https://a8c-libs.s3.amazonaws.com/android' + content { + includeGroup "com.automattic.android" + includeGroup "com.automattic.android.publish-to-s3" + } + } + gradlePluginPortal() + google() + } +} +rootProject.name = '@wordpress_react-native-aztec' diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java index b1652093b8980..62f640f0c9f84 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java @@ -288,6 +288,12 @@ public void setFontSize(ReactAztecText view, float fontSize) { (int) Math.ceil(PixelUtil.toPixelFromSP(fontSize))); } + @ReactProp(name = ViewProps.LINE_HEIGHT) + public void setLineHeight(ReactAztecText view, float lineHeight) { + float textSize = view.getTextSize(); + view.setLineSpacing(textSize * lineHeight, (float) (lineHeight / textSize)); + } + @ReactProp(name = ViewProps.FONT_FAMILY) public void setFontFamily(ReactAztecText view, String fontFamily) { int style = Typeface.NORMAL; diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift index c87316600177e..cdd28ea38d39e 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecView.swift @@ -41,7 +41,7 @@ class RCTAztecView: Aztec.TextView { get { return super.textAlignment } - } + } private var previousContentSize: CGSize = .zero @@ -109,6 +109,10 @@ class RCTAztecView: Aztec.TextView { /// Font weight for all contents. Once this is set, it will always override the font weight for all of its /// contents, regardless of what HTML is provided to Aztec. private var fontWeight: String? = nil + + /// Line height for all contents. Once this is set, it will always override the font size for all of its + /// contents, regardless of what HTML is provided to Aztec. + private var lineHeight: CGFloat? = nil // MARK: - Formats @@ -588,6 +592,7 @@ class RCTAztecView: Aztec.TextView { } fontSize = size refreshFont() + refreshLineHeight() } @objc func setFontWeight(_ weight: String) { @@ -597,6 +602,14 @@ class RCTAztecView: Aztec.TextView { fontWeight = weight refreshFont() } + + @objc func setLineHeight(_ newLineHeight: CGFloat) { + guard lineHeight != newLineHeight else { + return + } + lineHeight = newLineHeight + refreshLineHeight() + } // MARK: - Font Refreshing @@ -650,9 +663,23 @@ class RCTAztecView: Aztec.TextView { /// This method should not be called directly. Call `refreshFont()` instead. /// private func refreshTypingAttributesAndPlaceholderFont() { - let currentFont = font(from: typingAttributes) + let currentFont = font(from: typingAttributes) placeholderLabel.font = currentFont } + + /// This method refreshes the line height. + private func refreshLineHeight() { + if let lineHeight = lineHeight { + let attributeString = NSMutableAttributedString(string: self.text) + let style = NSMutableParagraphStyle() + let currentFontSize = fontSize ?? defaultFont.pointSize + let lineSpacing = ((currentFontSize * lineHeight) / UIScreen.main.scale) - (currentFontSize / lineHeight) / 2 + + style.lineSpacing = lineSpacing + defaultParagraphStyle.regularLineSpacing = lineSpacing + textStorage.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSMakeRange(0, textStorage.length)) + } + } // MARK: - Formatting interface diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.m b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.m index db468a71be0b8..5f88fd15e83e1 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.m +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.m @@ -27,6 +27,7 @@ @interface RCT_EXTERN_MODULE(RCTAztecViewManager, NSObject) RCT_EXPORT_VIEW_PROPERTY(fontFamily, NSString) RCT_EXPORT_VIEW_PROPERTY(fontSize, CGFloat) RCT_EXPORT_VIEW_PROPERTY(fontWeight, NSString) +RCT_EXPORT_VIEW_PROPERTY(lineHeight, CGFloat) RCT_EXPORT_VIEW_PROPERTY(disableEditingMenu, BOOL) RCT_REMAP_VIEW_PROPERTY(textAlign, textAlignment, NSTextAlignment) diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index dbfee6b32a2da..1945562695c36 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.60.0", + "version": "1.61.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index f08bb172dd15f..9c5e09c131366 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -220,10 +220,8 @@ class AztecView extends Component { window.console.warn( "Removing lineHeight style as it's not supported by native AztecView" ); - // IMPORTANT: Current Gutenberg implementation is supporting line-height without unit e.g. 'line-height':1.5 - // and library which we are using to convert css to react-native requires unit to be included with dimension - // https://github.com/kristerkari/css-to-react-native-transform/blob/945866e84a505fdfb1a43b03ebe4bd32784a7f22/src/index.spec.js#L1234 - // which means that we would need to patch the library if we want to support line-height from native AztecView in the future. + // Prevents passing line-heigth within styles to avoid a crash due to values without units + // We now support this but passing line-height as a prop instead } return ( diff --git a/packages/react-native-bridge/android/build.gradle b/packages/react-native-bridge/android/build.gradle index b9a6fc0612e9b..560c66362615f 100644 --- a/packages/react-native-bridge/android/build.gradle +++ b/packages/react-native-bridge/android/build.gradle @@ -2,14 +2,11 @@ buildscript { ext { willPublishReactNativeBridgeBinary = properties["willPublishReactNativeBridgeBinary"]?.toBoolean() ?: false } - repositories { - jcenter() - google() - } +} - dependencies { - classpath 'com.android.tools.build:gradle:4.0.2' - } +plugins { + id "com.android.library" apply false + id "org.jetbrains.kotlin.android" apply false } allprojects { diff --git a/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b38..f3d88b1c2faf2 100644 Binary files a/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.jar and b/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.properties index 4e1cc9db6b597..af7be50b1015c 100644 --- a/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/react-native-bridge/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/react-native-bridge/android/gradlew b/packages/react-native-bridge/android/gradlew index 8e25e6c19d574..2fe81a7d95e4f 100755 --- a/packages/react-native-bridge/android/gradlew +++ b/packages/react-native-bridge/android/gradlew @@ -125,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` @@ -154,19 +154,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/packages/react-native-bridge/android/react-native-bridge/build.gradle b/packages/react-native-bridge/android/react-native-bridge/build.gradle index 70068e4484d0b..efd431242f302 100644 --- a/packages/react-native-bridge/android/react-native-bridge/build.gradle +++ b/packages/react-native-bridge/android/react-native-bridge/build.gradle @@ -1,28 +1,10 @@ -buildscript { - ext.kotlinVersion = '1.5.20' - - repositories { - maven { - url 'https://a8c-libs.s3.amazonaws.com/android' - content { - includeGroup "com.automattic.android" - } - } - jcenter() - google() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath 'com.android.tools.build:gradle:4.0.2' - classpath 'com.automattic.android:publish-to-s3:0.6.1' - } +plugins { + id "com.android.library" + id "org.jetbrains.kotlin.android" + id "maven-publish" + id "com.automattic.android.publish-to-s3" } -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'com.automattic.android.publish-to-s3' - // import the `readReactNativeVersion()` function apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle' @@ -39,8 +21,6 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 29 - versionCode 1 - versionName "1.0" buildConfigField "boolean", "SHOULD_ATTACH_JS_BUNDLE", willPublishReactNativeBridgeBinary.toString() } @@ -66,15 +46,14 @@ android { } repositories { - google() - jcenter() - maven { url "https://jitpack.io" } maven { url "https://a8c-libs.s3.amazonaws.com/android" } maven { url "https://a8c-libs.s3.amazonaws.com/android/hermes-mirror" } + maven { url "https://jitpack.io" } + google() + jcenter() } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" // For animated GIF support implementation 'com.facebook.fresco:animated-gif:2.0.0' implementation 'com.google.android.material:material:1.2.1' @@ -98,7 +77,6 @@ dependencies { implementation "com.github.wordpress-mobile:react-native-prompt-android:${readHashedVersion('../../../react-native-editor/package.json', 'react-native-prompt-android', 'dependencies')}" implementation "com.github.wordpress-mobile:react-native-webview:${readHashedVersion('../../../react-native-editor/package.json', 'react-native-webview', 'dependencies')}" runtimeOnly "org.wordpress-mobile:hermes-release-mirror:0.64.0" - } else { api 'com.facebook.react:react-native:+' api project(':@wordpress_react-native-aztec') diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index a08225d57d422..5b35db5335f8d 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -41,6 +41,10 @@ data class GutenbergProps @JvmOverloads constructor( ?.let { putSerializable(PROP_STYLES, it) } theme.getSerializable(PROP_FEATURES) ?.let { putSerializable(PROP_FEATURES, it) } + theme.getSerializable(PROP_IS_FSE_THEME) + ?.let { putSerializable(PROP_IS_FSE_THEME, it) } + theme.getSerializable(PROP_GALLERY_WITH_IMAGE_BLOCKS) + ?.let { putSerializable(PROP_GALLERY_WITH_IMAGE_BLOCKS, it) } } } @@ -77,6 +81,8 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_GRADIENTS = "gradients" private const val PROP_STYLES = "rawStyles" private const val PROP_FEATURES = "rawFeatures" + private const val PROP_IS_FSE_THEME = "isFSETheme" + private const val PROP_GALLERY_WITH_IMAGE_BLOCKS = "galleryWithImageBlocks" const val PROP_CAPABILITIES = "capabilities" const val PROP_CAPABILITIES_CONTACT_INFO_BLOCK = "contactInfoBlock" diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_check_24px.xml b/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_check_24px.xml index 4c1cdcec4b516..a9d291df5f4a4 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_check_24px.xml +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_check_24px.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_close_24px.xml b/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_close_24px.xml index a922efedc06eb..a20716a20f105 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_close_24px.xml +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/res/drawable/ic_close_24px.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/res/layout/activity_gutenberg_web_view.xml b/packages/react-native-bridge/android/react-native-bridge/src/main/res/layout/activity_gutenberg_web_view.xml index 2dcf133412d64..bd8c8503f89d0 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/res/layout/activity_gutenberg_web_view.xml +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/res/layout/activity_gutenberg_web_view.xml @@ -17,7 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/status_bar_color" - app:titleTextColor="@color/white" + app:titleTextColor="@android:color/white" app:contentInsetEnd="@dimen/toolbar_content_offset_end" app:contentInsetLeft="@dimen/toolbar_content_offset" app:contentInsetRight="@dimen/toolbar_content_offset_end" @@ -36,7 +36,7 @@ android:id="@+id/foreground_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:background="@android:color/white" android:gravity="center" android:paddingStart="@dimen/foreground_view_padding_large" android:paddingEnd="@dimen/foreground_view_padding_large" @@ -49,7 +49,7 @@ android:layout_marginBottom="@dimen/foreground_view_padding_medium" android:layout_marginTop="@dimen/foreground_view_padding_medium" android:src="@drawable/ube_failed" - android:contentDescription="@null" + android:contentDescription="@null" android:visibility="gone" /> [String : Any] { var settingsUpdates = [String : Any]() settingsUpdates["isFSETheme"] = editorSettings?.isFSETheme ?? false + settingsUpdates["galleryWithImageBlocks"] = editorSettings?.galleryWithImageBlocks ?? false if let rawStyles = editorSettings?.rawStyles { settingsUpdates["rawStyles"] = rawStyles diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDataSource.swift b/packages/react-native-bridge/ios/GutenbergBridgeDataSource.swift index 166b257fd3319..d2cb398a36bc9 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDataSource.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDataSource.swift @@ -77,6 +77,7 @@ public extension GutenbergBridgeDataSource { public protocol GutenbergEditorSettings { var isFSETheme: Bool { get } + var galleryWithImageBlocks: Bool { get } var rawStyles: String? { get } var rawFeatures: String? { get } var colors: [[String: String]]? { get } diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 04a797846903c..cff35cf03a22e 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.60.0", + "version": "1.61.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index e8fd2e35beeae..80ff9080ab94a 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,14 @@ For each user feature we should also add a importance categorization label to i ## Unreleased +## 1.61.0 +- [**] Enable embed preview for a list of providers (for now only YouTube and Twitter) [#34446] +- [***] Inserter: Add Inserter Block Search [https://github.com/WordPress/gutenberg/pull/33237] + +## 1.60.1 +- [*] RNmobile: Fix the cancel button on Block Variation Picker / Columns Block. [#34249] +- [*] Column block: Fix Android close button alignment. [#34332] + ## 1.60.0 - [**] Embed block: Add "Resize for smaller devices" setting. [#33654] diff --git a/packages/react-native-editor/android/build.gradle b/packages/react-native-editor/android/build.gradle index 363a59321690c..e05a9dc625f31 100644 --- a/packages/react-native-editor/android/build.gradle +++ b/packages/react-native-editor/android/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - gradlePluginVersion = '4.0.2' + gradlePluginVersion = '4.2.2' kotlinVersion = '1.5.20' buildToolsVersion = "29.0.3" minSdkVersion = 21 diff --git a/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b38..f3d88b1c2faf2 100644 Binary files a/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.jar and b/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.properties b/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.properties index 4e1cc9db6b597..af7be50b1015c 100644 --- a/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/react-native-editor/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/react-native-editor/android/gradlew.bat b/packages/react-native-editor/android/gradlew.bat index 9109989e3cbf6..24467a141f791 100644 --- a/packages/react-native-editor/android/gradlew.bat +++ b/packages/react-native-editor/android/gradlew.bat @@ -29,9 +29,6 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" diff --git a/packages/react-native-editor/ios/Gemfile.lock b/packages/react-native-editor/ios/Gemfile.lock index 4bc064a3c2313..985f9107a3008 100644 --- a/packages/react-native-editor/ios/Gemfile.lock +++ b/packages/react-native-editor/ios/Gemfile.lock @@ -1,6 +1,3 @@ -GEM - specs: - GEM remote: https://rubygems.org/ specs: diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index a33a78ec39036..4b3b86a26b03f 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -12,7 +12,7 @@ PODS: - React-jsi (= 0.64.0) - ReactCommon/turbomodule/core (= 0.64.0) - glog (0.3.5) - - Gutenberg (1.60.0): + - Gutenberg (1.61.0): - React-Core (= 0.64.0) - React-CoreModules (= 0.64.0) - React-RCTImage (= 0.64.0) @@ -303,7 +303,7 @@ PODS: - React-Core - RNSVG (9.13.7-wp): - React-Core - - RNTAztecView (1.60.0): + - RNTAztecView (1.61.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.4) - WordPress-Aztec-iOS (1.19.4) @@ -457,9 +457,9 @@ SPEC CHECKSUMS: BVLinearGradient: 2c791e973a3df0df46028210c530fde52c06b717 DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de FBLazyVector: 49cbe4b43e445b06bf29199b6ad2057649e4c8f5 - FBReactNativeSpec: 80e9cf1155002ee4720084d8813326d913815e2f + FBReactNativeSpec: ca068ae274cbd52c8638d4b44a8b9c6a35ae975e glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62 - Gutenberg: d18ce5f2f99f78cd9c758d89d37c82a91be5cadd + Gutenberg: c0b1c47cf3c63f795570e83a12a5df2cb38d9c5e RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c RCTRequired: 2f8cb5b7533219bf4218a045f92768129cf7050a RCTTypeSafety: 512728b73549e72ad7330b92f3d42936f2a4de5b @@ -496,7 +496,7 @@ SPEC CHECKSUMS: RNReanimated: ca6105fdc2739ea1b3a7a5350b6490d8160143bc RNScreens: eb4e23256e7f2a5a1af87ea24dfeb49aea0ef310 RNSVG: 1b6dcbec5884b6dbe256bf8c38eeeab0acf05926 - RNTAztecView: 9ad0125ab87d59989d5279cdfdc2863e068059cb + RNTAztecView: e632368bd658eb1ead726ce7ac1c010c6bd10a72 WordPress-Aztec-iOS: 870c93297849072aadfc2223e284094e73023e82 Yoga: 8c8436d4171c87504c648ae23b1d81242bdf3bbf diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 80aadd8c241ca..fc64075a376c5 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.60.0", + "version": "1.61.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -31,8 +31,8 @@ "dependencies": { "@babel/runtime": "^7.13.10", "@react-native-community/blur": "3.6.0", - "@react-native-community/masked-view": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#v0.1.11-wp", - "@react-native-community/slider": "git+https://github.com/wordpress-mobile/react-native-slider.git#v3.0.2-wp", + "@react-native-community/masked-view": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#v0.1.11-wp-1", + "@react-native-community/slider": "git+https://github.com/wordpress-mobile/react-native-slider.git#v3.0.2-wp-1", "@react-navigation/core": "5.12.0", "@react-navigation/native": "5.7.0", "@react-navigation/routers": "5.4.9", @@ -55,23 +55,23 @@ "jsdom-jscore-rn": "git+https://github.com/iamcco/jsdom-jscore-rn.git#a562f3d57c27c13e5bfc8cf82d496e69a3ba2800", "node-fetch": "^2.6.0", "react-native": "0.64.0", - "react-native-gesture-handler": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#1.10.1-wp", - "react-native-get-random-values": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#v1.4.0-wp", + "react-native-gesture-handler": "git+https://github.com/wordpress-mobile/react-native-gesture-handler.git#1.10.1-wp-3", + "react-native-get-random-values": "git+https://github.com/wordpress-mobile/react-native-get-random-values.git#v1.4.0-wp-1", "react-native-hr": "git+https://github.com/Riglerr/react-native-hr.git#2d01a5cf77212d100e8b99e0310cce5234f977b3", - "react-native-hsv-color-picker": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker#v1.0.1-wp", + "react-native-hsv-color-picker": "git+https://github.com/wordpress-mobile/react-native-hsv-color-picker#v1.0.1-wp-1", "react-native-keyboard-aware-scroll-view": "git+https://github.com/wordpress-mobile/react-native-keyboard-aware-scroll-view.git#v0.8.8-wp", - "react-native-linear-gradient": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp", + "react-native-linear-gradient": "git+https://github.com/wordpress-mobile/react-native-linear-gradient.git#v2.5.6-wp-1", "react-native-modal": "^11.10.0", - "react-native-prompt-android": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#v1.0.0-wp", - "react-native-reanimated": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1.9.0-wp", + "react-native-prompt-android": "git+https://github.com/wordpress-mobile/react-native-prompt-android.git#v1.0.0-wp-1", + "react-native-reanimated": "git+https://github.com/wordpress-mobile/react-native-reanimated.git#1.9.0-wp-1", "react-native-safe-area": "^0.5.0", - "react-native-safe-area-context": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#v3.2.0-wp", + "react-native-safe-area-context": "git+https://github.com/wordpress-mobile/react-native-safe-area-context.git#v3.2.0-wp-1", "react-native-sass-transformer": "^1.1.1", - "react-native-screens": "git+https://github.com/wordpress-mobile/react-native-screens.git#2.9.0-wp", - "react-native-svg": "git+https://github.com/wordpress-mobile/react-native-svg.git#v9.13.7-wp", + "react-native-screens": "git+https://github.com/wordpress-mobile/react-native-screens.git#2.9.0-wp-1", + "react-native-svg": "git+https://github.com/wordpress-mobile/react-native-svg.git#v9.13.7-wp-1", "react-native-url-polyfill": "^1.1.2", - "react-native-video": "git+https://github.com/wordpress-mobile/react-native-video.git#5.0.2-wp", - "react-native-webview": "git+https://github.com/wordpress-mobile/react-native-webview.git#v11.6.5-wp" + "react-native-video": "git+https://github.com/wordpress-mobile/react-native-video.git#5.0.2-wp-1", + "react-native-webview": "git+https://github.com/wordpress-mobile/react-native-webview.git#v11.6.5-wp-1" }, "publishConfig": { "access": "public" diff --git a/packages/react-native-editor/src/globals.js b/packages/react-native-editor/src/globals.js index 4e66ecb2c6393..cea706b35c137 100644 --- a/packages/react-native-editor/src/globals.js +++ b/packages/react-native-editor/src/globals.js @@ -59,7 +59,7 @@ if ( ! global.window.matchMedia ) { } ); } -global.window.navigator.userAgent = []; +global.window.navigator.userAgent = global.window.navigator.userAgent ?? ''; // Leverages existing console polyfill from react-native global.nativeLoggingHook = nativeLoggingHook; diff --git a/packages/react-native-editor/src/index.js b/packages/react-native-editor/src/index.js index c8b8fd8b152ab..3fd18a1674648 100644 --- a/packages/react-native-editor/src/index.js +++ b/packages/react-native-editor/src/index.js @@ -83,6 +83,7 @@ const setupInitHooks = () => { gradients, rawStyles, rawFeatures, + galleryWithImageBlocks, } = props; if ( initialData === undefined && __DEV__ ) { @@ -110,6 +111,7 @@ const setupInitHooks = () => { gradients, rawStyles, rawFeatures, + galleryWithImageBlocks, }; } ); diff --git a/packages/readable-js-assets-webpack-plugin/package.json b/packages/readable-js-assets-webpack-plugin/package.json index 5689f99182ed6..54cf4f78a2940 100644 --- a/packages/readable-js-assets-webpack-plugin/package.json +++ b/packages/readable-js-assets-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "1.0.1", + "version": "1.0.2", "description": "Generate a readable JS file for each JS asset.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index d259a1b808ba6..f900c6fea1d97 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/reusable-blocks", - "version": "3.0.0", + "version": "3.0.1", "description": "Reusable blocks utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 20a2613b08228..a82b4ed420018 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "5.0.0", + "version": "5.0.1", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index b10b509ef3b53..b5c4f081490e5 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -120,6 +120,7 @@ export function useRichText( { * @param {Object} newRecord The record to sync and apply. */ function handleChange( newRecord ) { + record.current = newRecord; applyRecord( newRecord ); if ( disableFormats ) { @@ -137,8 +138,6 @@ export function useRichText( { } ); } - record.current = newRecord; - const { start, end, formats, text } = newRecord; // Selection must be updated first, so it is recorded in history when @@ -178,11 +177,6 @@ export function useRichText( { hadSelectionUpdate.current = false; }, [ hadSelectionUpdate.current ] ); - function focus() { - ref.current.focus(); - applyRecord( record.current ); - } - const mergedRefs = useMergeRefs( [ ref, useDefaultStyle(), @@ -218,7 +212,6 @@ export function useRichText( { return { value: record.current, onChange: handleChange, - onFocus: focus, ref: mergedRefs, }; } diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 194fadd82cf0d..ec798d3c91546 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -4,7 +4,7 @@ * External dependencies */ import { View, Platform } from 'react-native'; -import { get, pickBy, debounce, isString } from 'lodash'; +import { get, pickBy, debounce } from 'lodash'; import memize from 'memize'; /** @@ -56,6 +56,7 @@ const gutenbergFormatNamesToAztec = { }; const EMPTY_PARAGRAPH_TAGS = '

'; +const DEFAULT_FONT_SIZE = 16; export class RichText extends Component { constructor( { @@ -111,9 +112,6 @@ export class RichText extends Component { ).bind( this ); this.suggestionOptions = this.suggestionOptions.bind( this ); this.insertString = this.insertString.bind( this ); - this.convertFontSizeFromString = this.convertFontSizeFromString.bind( - this - ); this.manipulateEventCounterToForceNativeToRefresh = this.manipulateEventCounterToForceNativeToRefresh.bind( this ); @@ -277,13 +275,6 @@ export class RichText extends Component { .replace( closingTagRegexp, '' ); } - // Fix for crash https://github.com/wordpress-mobile/gutenberg-mobile/issues/2991 - convertFontSizeFromString( fontSize ) { - return fontSize && isString( fontSize ) && fontSize.endsWith( 'px' ) - ? parseFloat( fontSize.substring( 0, fontSize.length - 2 ) ) - : fontSize; - } - /* * Handles any case where the content of the AztecRN instance has changed */ @@ -764,6 +755,14 @@ export class RichText extends Component { this.needsSelectionUpdate = true; this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side } + + if ( + nextProps?.style?.fontSize !== this.props?.style?.fontSize || + nextProps?.style?.lineHeight !== this.props?.style?.lineHeight + ) { + this.needsSelectionUpdate = true; + this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side + } } return true; @@ -853,6 +852,44 @@ export class RichText extends Component { }; } + getFontSize() { + const { baseGlobalStyles } = this.props; + + if ( this.props.fontSize ) { + return parseFloat( this.props.fontSize ); + } + + if ( this.props.style?.fontSize ) { + return parseFloat( this.props.style.fontSize ); + } + + if ( baseGlobalStyles?.typography?.fontSize ) { + return parseFloat( baseGlobalStyles?.typography?.fontSize ); + } + + return DEFAULT_FONT_SIZE; + } + + getLineHeight() { + const { baseGlobalStyles } = this.props; + let lineHeight; + + // eslint-disable-next-line no-undef + if ( ! __DEV__ ) { + return; + } + + if ( baseGlobalStyles?.typography?.lineHeight ) { + lineHeight = parseFloat( baseGlobalStyles?.typography?.lineHeight ); + } + + if ( this.props.style?.lineHeight ) { + lineHeight = parseFloat( this.props.style.lineHeight ); + } + + return lineHeight; + } + render() { const { tagName, @@ -879,6 +916,8 @@ export class RichText extends Component { ); const { color: defaultPlaceholderTextColor } = placeholderStyle; + const fontSize = this.getFontSize(); + const lineHeight = this.getLineHeight(); const { color: defaultColor, @@ -1017,11 +1056,8 @@ export class RichText extends Component { } maxImagesWidth={ 200 } fontFamily={ this.props.fontFamily || defaultFontFamily } - fontSize={ - this.props.fontSize || - ( style && - this.convertFontSizeFromString( style.fontSize ) ) - } + fontSize={ fontSize } + lineHeight={ lineHeight } fontWeight={ this.props.fontWeight } fontStyle={ this.props.fontStyle } disableEditingMenu={ disableEditingMenu } @@ -1079,8 +1115,9 @@ export default compose( [ 'attributes', 'childrenStyles', ] ); - const baseGlobalStyles = getSettings() - ?.__experimentalGlobalStylesBaseStyles; + + const settings = getSettings(); + const baseGlobalStyles = settings?.__experimentalGlobalStylesBaseStyles; return { areMentionsSupported: diff --git a/packages/rich-text/src/component/use-input-and-selection.js b/packages/rich-text/src/component/use-input-and-selection.js index 7f011f6ecb73a..2e0eebea69911 100644 --- a/packages/rich-text/src/component/use-input-and-selection.js +++ b/packages/rich-text/src/component/use-input-and-selection.js @@ -224,7 +224,12 @@ export function useInputAndSelection( props ) { } function onFocus() { - const { record, isSelected, onSelectionChange } = propsRef.current; + const { + record, + isSelected, + onSelectionChange, + applyRecord, + } = propsRef.current; if ( ! isSelected ) { // We know for certain that on focus, the old selection is invalid. @@ -240,6 +245,7 @@ export function useInputAndSelection( props ) { }; onSelectionChange( index, index ); } else { + applyRecord( record.current ); onSelectionChange( record.current.start, record.current.end ); } diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 72acb296712ac..37d3a111e51ab 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,11 +2,21 @@ ## Unreleased +### Enhancements + +- The bundled `jest-dev-server` dependency has been updated to the next major version `^5.0.3` ([#34560](https://github.com/WordPress/gutenberg/pull/34560)). + +### Bug Fixes + +- Bring back support for SVG files in CSS ([#34394](https://github.com/WordPress/gutenberg/pull/34394)). It wasn't correctly migrated when integrating webpack v5. + +## 18.0.0 (2021-08-23) + ### Breaking Changes - Increase the minimum Node.js version to v12.13 matching requirements from bundled dependencies ([#33818](https://github.com/WordPress/gutenberg/pull/33818)). -- The bundled `webpack` dependency has been updated to the next major version `^5.47.1` (see [Breaking Changes](https://webpack.js.org/migrate/5/), [##33818](https://github.com/WordPress/gutenberg/pull/#33818)). -- The bundled `webpack-cli` dependency has been updated to the next major version `^4.7.2` ([##33818](https://github.com/WordPress/gutenberg/pull/#33818)). +- The bundled `webpack` dependency has been updated to the next major version `^5.47.1` (see [Breaking Changes](https://webpack.js.org/migrate/5/), [#33818](https://github.com/WordPress/gutenberg/pull/33818)). +- The bundled `webpack-cli` dependency has been updated to the next major version `^4.7.2` ([#33818](https://github.com/WordPress/gutenberg/pull/33818)). - The bundled `css-loader` dependency has been updated from requiring `^5.1.3` to requiring `^6.2.0` ([#33818](https://github.com/WordPress/gutenberg/pull/33818)). - The bundled `file-loader` dependency has been removed ([#33818](https://github.com/WordPress/gutenberg/pull/33818)). - The bundled `ignore-emit-webpack-plugin` dependency has been removed ([#33818](https://github.com/WordPress/gutenberg/pull/33818)). diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 86ed99807d881..5bbc0a43e18b9 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -184,9 +184,15 @@ const config = { }, { test: /\.svg$/, + issuer: /\.jsx?$/, use: [ '@svgr/webpack', 'url-loader' ], type: 'javascript/auto', }, + { + test: /\.svg$/, + issuer: /\.(sc|sa|c)ss$/, + type: 'asset/inline', + }, { test: /\.(bmp|png|jpe?g|gif)$/i, type: 'asset/resource', diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 561816a693aa9..a42c9a9026903 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "17.1.0", + "version": "18.0.0", "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -59,7 +59,7 @@ "filenamify": "^4.2.0", "jest": "^26.6.3", "jest-circus": "^26.6.3", - "jest-dev-server": "^4.4.0", + "jest-dev-server": "^5.0.3", "jest-environment-node": "^26.6.2", "markdownlint": "^0.23.1", "markdownlint-cli": "^0.27.1", diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 31f439d5eedb5..04e667999c63b 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/server-side-render", - "version": "3.0.0", + "version": "3.0.1", "description": "The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/README.md b/packages/url/README.md index d6b338071b1b9..367ba4c067567 100644 --- a/packages/url/README.md +++ b/packages/url/README.md @@ -132,6 +132,25 @@ _Returns_ - `string|void`: The authority part of the URL. +### getFilename + +Returns the filename part of the URL. + +_Usage_ + +```js +const filename1 = getFilename( 'http://localhost:8080/this/is/a/test.jpg' ); // 'test.jpg' +const filename2 = getFilename( '/this/is/a/test.png' ); // 'test.png' +``` + +_Parameters_ + +- _url_ `string`: The full URL. + +_Returns_ + +- `string|void`: The filename part of the URL. + ### getFragment Returns the fragment part of the URL. diff --git a/packages/url/src/get-filename.js b/packages/url/src/get-filename.js new file mode 100644 index 0000000000000..2941f18fe07b4 --- /dev/null +++ b/packages/url/src/get-filename.js @@ -0,0 +1,25 @@ +/** + * Returns the filename part of the URL. + * + * @param {string} url The full URL. + * + * @example + * ```js + * const filename1 = getFilename( 'http://localhost:8080/this/is/a/test.jpg' ); // 'test.jpg' + * const filename2 = getFilename( '/this/is/a/test.png' ); // 'test.png' + * ``` + * + * @return {string|void} The filename part of the URL. + */ +export function getFilename( url ) { + let filename; + try { + filename = new URL( url, 'http://example.com' ).pathname + .split( '/' ) + .pop(); + } catch ( error ) {} + + if ( filename ) { + return filename; + } +} diff --git a/packages/url/src/index.js b/packages/url/src/index.js index f060ae8152897..eb4ee3237fab8 100644 --- a/packages/url/src/index.js +++ b/packages/url/src/index.js @@ -22,3 +22,4 @@ export { safeDecodeURI } from './safe-decode-uri'; export { safeDecodeURIComponent } from './safe-decode-uri-component'; export { filterURLForDisplay } from './filter-url-for-display'; export { cleanForSlug } from './clean-for-slug'; +export { getFilename } from './get-filename'; diff --git a/packages/url/src/test/index.js b/packages/url/src/test/index.js index f4814b0b0bc83..51bb69419bfa6 100644 --- a/packages/url/src/test/index.js +++ b/packages/url/src/test/index.js @@ -29,6 +29,7 @@ import { filterURLForDisplay, cleanForSlug, getQueryArgs, + getFilename, } from '../'; import wptData from './fixtures/wpt-data'; @@ -240,6 +241,7 @@ describe( 'isValidPath', () => { expect( isValidPath( 'relative/path' ) ).toBe( true ); expect( isValidPath( 'slightly/longer/path/' ) ).toBe( true ); expect( isValidPath( 'path/with/percent%20encoding' ) ).toBe( true ); + expect( isValidPath( '/' ) ).toBe( true ); } ); it( 'returns false if the path is invalid', () => { @@ -252,6 +254,42 @@ describe( 'isValidPath', () => { } ); } ); +describe( 'getFilename', () => { + it( 'returns the filename part of the URL', () => { + expect( getFilename( 'https://wordpress.org/image.jpg' ) ).toBe( + 'image.jpg' + ); + expect( + getFilename( 'https://wordpress.org/image.jpg?query=test' ) + ).toBe( 'image.jpg' ); + expect( getFilename( 'https://wordpress.org/image.jpg#anchor' ) ).toBe( + 'image.jpg' + ); + expect( + getFilename( 'http://localhost:8080/a/path/to/an/image.jpg' ) + ).toBe( 'image.jpg' ); + expect( getFilename( '/path/to/an/image.jpg' ) ).toBe( 'image.jpg' ); + expect( getFilename( 'path/to/an/image.jpg' ) ).toBe( 'image.jpg' ); + expect( getFilename( '/image.jpg' ) ).toBe( 'image.jpg' ); + expect( getFilename( 'image.jpg' ) ).toBe( 'image.jpg' ); + } ); + + it( 'returns undefined when the provided value does not contain a filename', () => { + expect( getFilename( 'http://localhost:8080/' ) ).toBe( undefined ); + expect( getFilename( 'http://localhost:8080/a/path/' ) ).toBe( + undefined + ); + expect( getFilename( 'http://localhost:8080/?query=test' ) ).toBe( + undefined + ); + expect( getFilename( 'http://localhost:8080/#anchor' ) ).toBe( + undefined + ); + expect( getFilename( 'a/path/' ) ).toBe( undefined ); + expect( getFilename( '/' ) ).toBe( undefined ); + } ); +} ); + describe( 'getQueryString', () => { it( 'returns the query string of a URL', () => { expect( diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 8f051774e39c4..7a0271bea68eb 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "4.0.0", + "version": "4.0.1", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/package.json b/packages/widgets/package.json index ae95b3f872ad7..2934f8442b10f 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/widgets", - "version": "2.0.0", + "version": "2.0.1", "description": "Functionality used by the widgets block editor in the Widgets screen and the Customizer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/src/blocks/legacy-widget/edit/preview.js b/packages/widgets/src/blocks/legacy-widget/edit/preview.js index 66109d62558ae..3d731672e8743 100644 --- a/packages/widgets/src/blocks/legacy-widget/edit/preview.js +++ b/packages/widgets/src/blocks/legacy-widget/edit/preview.js @@ -7,57 +7,88 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useRefEffect } from '@wordpress/compose'; -import { addQueryArgs } from '@wordpress/url'; -import { useState } from '@wordpress/element'; -import { Placeholder, Spinner, Disabled } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; +import { Disabled, Placeholder, Spinner } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; export default function Preview( { idBase, instance, isVisible } ) { const [ isLoaded, setIsLoaded ] = useState( false ); + const [ srcDoc, setSrcDoc ] = useState( '' ); + + useEffect( () => { + const abortController = + typeof window.AbortController === 'undefined' + ? undefined + : new window.AbortController(); + + async function fetchPreviewHTML() { + const restRoute = `/wp/v2/widget-types/${ idBase }/render`; + return await apiFetch( { + path: restRoute, + method: 'POST', + signal: abortController?.signal, + data: instance ? { instance } : {}, + } ); + } + + fetchPreviewHTML() + .then( ( response ) => { + setSrcDoc( response.preview ); + } ) + .catch( ( error ) => { + if ( 'AbortError' === error.name ) { + // We don't want to log aborted requests. + return; + } + throw error; + } ); + + return () => abortController?.abort(); + }, [ idBase, instance ] ); // Resize the iframe on either the load event, or when the iframe becomes visible. const ref = useRefEffect( ( iframe ) => { // Only set height if the iframe is loaded, // or it will grow to an unexpected large height in Safari if it's hidden initially. - if ( isLoaded ) { - // If the preview frame has another origin then this won't work. - // One possible solution is to add custom script to call `postMessage` in the preview frame. - // Or, better yet, we migrate away from iframe. - function setHeight() { - // Pick the maximum of these two values to account for margin collapsing. - const height = Math.max( - iframe.contentDocument.documentElement.offsetHeight, - iframe.contentDocument.body.offsetHeight - ); - iframe.style.height = `${ height }px`; - } + if ( ! isLoaded ) { + return; + } + // If the preview frame has another origin then this won't work. + // One possible solution is to add custom script to call `postMessage` in the preview frame. + // Or, better yet, we migrate away from iframe. + function setHeight() { + // Pick the maximum of these two values to account for margin collapsing. + const height = Math.max( + iframe.contentDocument.documentElement.offsetHeight, + iframe.contentDocument.body.offsetHeight + ); + iframe.style.height = `${ height }px`; + } - const { - IntersectionObserver, - } = iframe.ownerDocument.defaultView; + const { IntersectionObserver } = iframe.ownerDocument.defaultView; - // Observe for intersections that might cause a change in the height of - // the iframe, e.g. a Widget Area becoming expanded. - const intersectionObserver = new IntersectionObserver( - ( [ entry ] ) => { - if ( entry.isIntersecting ) { - setHeight(); - } - }, - { - threshold: 1, + // Observe for intersections that might cause a change in the height of + // the iframe, e.g. a Widget Area becoming expanded. + const intersectionObserver = new IntersectionObserver( + ( [ entry ] ) => { + if ( entry.isIntersecting ) { + setHeight(); } - ); - intersectionObserver.observe( iframe ); + }, + { + threshold: 1, + } + ); + intersectionObserver.observe( iframe ); - iframe.addEventListener( 'load', setHeight ); + iframe.addEventListener( 'load', setHeight ); - return () => { - intersectionObserver.disconnect(); - iframe.removeEventListener( 'load', setHeight ); - }; - } + return () => { + intersectionObserver.disconnect(); + iframe.removeEventListener( 'load', setHeight ); + }; }, [ isLoaded ] ); @@ -92,16 +123,9 @@ export default function Preview( { idBase, instance, isVisible } ) {