From c527d4d72949112f02ba3b9c106b538a4c20061e Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 10 Jan 2024 15:54:18 +0000 Subject: [PATCH 01/34] Update Changelog for 17.5.0-rc.1 --- changelog.txt | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/changelog.txt b/changelog.txt index ba43ddc731d01..9268dc7edd1fb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,261 @@ == Changelog == += 17.5.0-rc.1 = + + + +## Changelog + +### Enhancements + +#### Editor Unification +- Editor: Add the show most used blocks preference to the site editor. ([57637](https://github.com/WordPress/gutenberg/pull/57637)) +- Editor: Migrate and unify the panel preferences. ([57529](https://github.com/WordPress/gutenberg/pull/57529)) +- Editor: Unify context text cursor preference. ([57479](https://github.com/WordPress/gutenberg/pull/57479)) +- Editor: Unify list view open preference. ([57504](https://github.com/WordPress/gutenberg/pull/57504)) +- Editor: Unify right click override preference. ([57468](https://github.com/WordPress/gutenberg/pull/57468)) +- Editor: Unify show icon labels preference. ([57480](https://github.com/WordPress/gutenberg/pull/57480)) +- Editor: Unify spotlight mode preference. ([57533](https://github.com/WordPress/gutenberg/pull/57533)) +- Editor: Unify the distraction free preference. ([57590](https://github.com/WordPress/gutenberg/pull/57590)) +- Editor: Unify the show block breadcrumbs preference. ([57506](https://github.com/WordPress/gutenberg/pull/57506)) +- Editor: Unify the top toolbar preference. ([57531](https://github.com/WordPress/gutenberg/pull/57531)) + +#### Components +- Components: Replace `TabPanel` with `Tabs` in inline color picker. ([57292](https://github.com/WordPress/gutenberg/pull/57292) +- Add `compact` size variant to InputControl-based components. ([57398](https://github.com/WordPress/gutenberg/pull/57398)) +- BaseControl: Connect to context system. ([57408](https://github.com/WordPress/gutenberg/pull/57408)) +- Replace `TabPanel` with `Tabs` in the Style Book. ([57287](https://github.com/WordPress/gutenberg/pull/57287)) +- Tooltip: Improve tests. ([57345](https://github.com/WordPress/gutenberg/pull/57345)) +- Update @ariakit/react to v0.3.12 and @ariakit/test to v0.3.7. ([57547](https://github.com/WordPress/gutenberg/pull/57547)) + +#### Font Library +- Font Library: Remove "has_font_mime_type" function. ([57364](https://github.com/WordPress/gutenberg/pull/57364)) +- Font Library: Update font uninstall modal text. ([57368](https://github.com/WordPress/gutenberg/pull/57368)) +- Font Library: Add progress-bar while uploading font assets. ([57463](https://github.com/WordPress/gutenberg/pull/57463)) +- Font Library: Singularize install font families endpoint. ([57569](https://github.com/WordPress/gutenberg/pull/57569)) +- Font Library: Unregister font collection. ([54701](https://github.com/WordPress/gutenberg/pull/54701)) + +#### Site Editor +- Add Template Modal: Update scroll related layout. ([57617](https://github.com/WordPress/gutenberg/pull/57617)) +- Components: Replace `TabPanel` with `Tabs` in the Font Library `Modal`. ([57181](https://github.com/WordPress/gutenberg/pull/57181)) + +#### Interactivity API +- Implement `wp_initial_state()`. ([57556](https://github.com/WordPress/gutenberg/pull/57556)) +- Server directive processing: Stop processing non-interactive blocks. ([56302](https://github.com/WordPress/gutenberg/pull/56302)) +- Interactive template: Use viewModule. ([57712](https://github.com/WordPress/gutenberg/pull/57712)) +- Navigation Block: Use dom.focus for focus control. ([57362](https://github.com/WordPress/gutenberg/pull/57362)) + + +#### Site Editor +- Site editor: Add padding to entity save panel header. ([57471](https://github.com/WordPress/gutenberg/pull/57471)) +- Site editor: Add margin to entity save panel header via a classname. ([57473](https://github.com/WordPress/gutenberg/pull/57473)) + + +#### Block Library +- Post Featured Image: Add a useFirstImageFromPost attribute. ([56573](https://github.com/WordPress/gutenberg/pull/56573)) +- Gallery Block: Add random order setting. ([57477](https://github.com/WordPress/gutenberg/pull/57477)) +- Image Block: Change upload icon label. ([57704](https://github.com/WordPress/gutenberg/pull/57704)) + + +### Bug Fixes + +- Avoid using a memoized selector without dependencies. ([57257](https://github.com/WordPress/gutenberg/pull/57257)) +- Core Data: Pass the 'options' argument to data action shortcuts. ([57383](https://github.com/WordPress/gutenberg/pull/57383)) +- Preferences: Update accessibility scope to "core". ([57563](https://github.com/WordPress/gutenberg/pull/57563)) + +#### Block Editor +- Fix Link UI displaying out of sync results. ([57522](https://github.com/WordPress/gutenberg/pull/57522)) +- Give iframe fallback background color. ([57330](https://github.com/WordPress/gutenberg/pull/57330)) +- Rich text: Add HTML string methods to RichTextData. ([57322](https://github.com/WordPress/gutenberg/pull/57322)) + +#### Block Library +- Footnotes: Fix wrong link when adding more than 9 footnotes. ([57599](https://github.com/WordPress/gutenberg/pull/57599)) +- Table: Remove unnecessary margin override in editor styles. ([57699](https://github.com/WordPress/gutenberg/pull/57699)) +- Template Part block: Fix template part path arg missing from actions. ([56790](https://github.com/WordPress/gutenberg/pull/56790)) + +#### Components +- DuotonePicker: Fix top margin when no duotone options. ([57489](https://github.com/WordPress/gutenberg/pull/57489)) +- NavigatorProvider: Exclude size value from contain CSS rule. ([57498](https://github.com/WordPress/gutenberg/pull/57498)) +- Snackbar: Fix icon positioning. ([57377](https://github.com/WordPress/gutenberg/pull/57377)) + +#### Patterns +- Pattern Overrides: Add `template-lock: All` to pattern inner blocks to prevent deletion/insertion. ([57661](https://github.com/WordPress/gutenberg/pull/57661)) +- Refactor the findOrCreate term method. ([57655](https://github.com/WordPress/gutenberg/pull/57655)) +- Edit source pattern in focus mode in post and site editors. ([57036](https://github.com/WordPress/gutenberg/pull/57036)) + + +#### Site Editor +- Make sure comamnd palette toggle does not disappear while being clicked. ([57420](https://github.com/WordPress/gutenberg/pull/57420)) +- Reinstate iframe CSS for editor canvas container. ([57503](https://github.com/WordPress/gutenberg/pull/57503)) + +#### Global Styles +- Use `is-layout` pattern on layout generated classname. ([57564](https://github.com/WordPress/gutenberg/pull/57564)) +- Global styles revisions: Add individual headings translations, update tests. ([57472](https://github.com/WordPress/gutenberg/pull/57472)) +- Global style revisions: Move change summary code and tests to block editor package. ([57411](https://github.com/WordPress/gutenberg/pull/57411)) +- Reduce specificity of block style variation selectors. ([57659](https://github.com/WordPress/gutenberg/pull/57659)) +- Background image block support: Add tests for size and repeat output. ([57474](https://github.com/WordPress/gutenberg/pull/57474)) + + +#### Post Editor +- Fix Template preview menu item accessibility. ([57456](https://github.com/WordPress/gutenberg/pull/57456)) +- Fullscreen mode description: Use full text instead of abbreviation. ([57518](https://github.com/WordPress/gutenberg/pull/57518)) +- Improve pre-publish checks naming consistency. ([57019](https://github.com/WordPress/gutenberg/pull/57019)) +- Make the Replace featured image button perceivable by assistive technologies. ([57453](https://github.com/WordPress/gutenberg/pull/57453)) + +#### Components +- Label the HeightControl. ([57683](https://github.com/WordPress/gutenberg/pull/57683)) +- NumberControl: Make increment and decrement buttons keyboard accessible. ([57402](https://github.com/WordPress/gutenberg/pull/57402)) + +#### Block Tools +- Update the position of the patterns tab in the inserter menu. ([55688](https://github.com/WordPress/gutenberg/pull/55688)) +- Use full text instead of abbreviation for min height setting. ([57680](https://github.com/WordPress/gutenberg/pull/57680)) +- ResizableEditor: Fix tab order for resize handles. ([57475](https://github.com/WordPress/gutenberg/pull/57475)) +- Keep Lock button it in the toolbar until unmounted. ([57229](https://github.com/WordPress/gutenberg/pull/57229)) +- Custom field connections: Better description on Experiments page. ([57501](https://github.com/WordPress/gutenberg/pull/57501)) + +### Performance + +#### Block Library +- File: Remove 'block-editor' store subscription. ([57511](https://github.com/WordPress/gutenberg/pull/57511)) +- Remove store subscriptions from Audio and Video blocks. ([57449](https://github.com/WordPress/gutenberg/pull/57449)) +- Site Logo: Remove unnecessary 'block-editor' store subscription. ([57513](https://github.com/WordPress/gutenberg/pull/57513)) +- Send numerical post id when uploading image. ([57388](https://github.com/WordPress/gutenberg/pull/57388)) +- PostFeaturedImage: Remove unnecessary 'block-editor' store subscription. ([57554](https://github.com/WordPress/gutenberg/pull/57554)) + +### Experiments + +#### Data Views +- DataViews: Use DropdownMenuRadioItem component when possible. ([57505](https://github.com/WordPress/gutenberg/pull/57505)) +- Align icon size + placement in Patterns data view. ([57548](https://github.com/WordPress/gutenberg/pull/57548)) +- DataViews: Add `duplicate pattern` action in patterns page. ([57592](https://github.com/WordPress/gutenberg/pull/57592)) +- DataViews: Add duplicate template pattern action. ([57638](https://github.com/WordPress/gutenberg/pull/57638)) +- DataViews: Add footer to Pages sidebar. ([57690](https://github.com/WordPress/gutenberg/pull/57690)) +- DataViews: Add new page button in `Pages`. ([57685](https://github.com/WordPress/gutenberg/pull/57685)) +- DataViews: Add sync filter in patterns page. ([57532](https://github.com/WordPress/gutenberg/pull/57532)) +- DataViews: Consolidate CSS selectors naming schema. ([57651](https://github.com/WordPress/gutenberg/pull/57651)) +- DataViews: Fallback to `(no title)` is there's no rendered title. ([57434](https://github.com/WordPress/gutenberg/pull/57434)) +- DataViews: Hide actions menu upon selecting a layout. ([57418](https://github.com/WordPress/gutenberg/pull/57418)) +- DataViews: Make `fields` dependant on `view.type`. ([57450](https://github.com/WordPress/gutenberg/pull/57450)) +- DataViews: Memoize `onSetSelection`. ([57458](https://github.com/WordPress/gutenberg/pull/57458)) +- DataViews: Prevent unnecessary re-renders of Pagination. ([57454](https://github.com/WordPress/gutenberg/pull/57454)) +- DataViews: Prevent unnecessary re-renders. ([57452](https://github.com/WordPress/gutenberg/pull/57452)) +- DataViews: Update names for `DropdownMenuRadioItemCustom`. ([57416](https://github.com/WordPress/gutenberg/pull/57416)) +- DataViews: Use i18n._x to clarify term "Duplicate". ([57686](https://github.com/WordPress/gutenberg/pull/57686)) +- DataViews: Use in patterns page. ([57333](https://github.com/WordPress/gutenberg/pull/57333)) +- Dataview: Change the stacking order of table header. ([57565](https://github.com/WordPress/gutenberg/pull/57565)) +- Dataviews: Add some client side data handling utils. ([57488](https://github.com/WordPress/gutenberg/pull/57488)) +- Make title display in grid views consistent. ([57553](https://github.com/WordPress/gutenberg/pull/57553)) +- Update Table layout design details. ([57644](https://github.com/WordPress/gutenberg/pull/57644)) +- Update pagination spacing in List layout. ([57670](https://github.com/WordPress/gutenberg/pull/57670)) +- Update table header gap. ([57671](https://github.com/WordPress/gutenberg/pull/57671)) +- [Dataviews] Table layout: Ensure focus is not lost on interaction. ([57340](https://github.com/WordPress/gutenberg/pull/57340)) + +#### Patterns +- [Pattern Overrides] Fix duplication of inner blocks. ([57538](https://github.com/WordPress/gutenberg/pull/57538)) +- [Pattern overrides] Allow multiple attributes overrides. ([57573](https://github.com/WordPress/gutenberg/pull/57573)) + + +### Documentation + +- Add links to additional local dev tools in Block Developement Environment readme. ([57682](https://github.com/WordPress/gutenberg/pull/57682)) +- Add new section to the Quick Start Guide about wp-env. ([57559](https://github.com/WordPress/gutenberg/pull/57559)) +- Block JSON schema: Add renaming key to supports definition. ([57373](https://github.com/WordPress/gutenberg/pull/57373)) +- Break out the Curating the Editor Experience doc into its own How-to Guides section. ([57289](https://github.com/WordPress/gutenberg/pull/57289)) +- Change the slug for the theme.json doc to avoid conflicts. ([57410](https://github.com/WordPress/gutenberg/pull/57410)) +- Docs/tutorial: Fix opposite condition for content generation in render.php. ([57445](https://github.com/WordPress/gutenberg/pull/57445)) +- Docs: Fundamentals of Block Development - Static or Dynamic rendering of a block. ([57250](https://github.com/WordPress/gutenberg/pull/57250)) +- Docs: Update sample code to fix React warning error on Tutorial page. ([57412](https://github.com/WordPress/gutenberg/pull/57412)) +- Fix formatting issue due to incorrect link parsing in the Quick Start Guide. ([57693](https://github.com/WordPress/gutenberg/pull/57693)) +- Fix incorrect heading level in Editor curation documentation. ([57409](https://github.com/WordPress/gutenberg/pull/57409)) +- Fix two typos in tutorial.md. ([57627](https://github.com/WordPress/gutenberg/pull/57627)) +- Fix: Create block getting started links. ([57551](https://github.com/WordPress/gutenberg/pull/57551)) +- Improve the static vs dynamic rendering comment in the block tutorial. ([57284](https://github.com/WordPress/gutenberg/pull/57284)) +- Update copyright year to 2024 in `license.md`. ([57481](https://github.com/WordPress/gutenberg/pull/57481)) +- Update the "Build your first block" tutorial based on user feedback. ([57403](https://github.com/WordPress/gutenberg/pull/57403)) +- Update: Material design icons link. ([57550](https://github.com/WordPress/gutenberg/pull/57550)) + + +### Code Quality +- Editor: Unify the DocumentTools component. ([57214](https://github.com/WordPress/gutenberg/pull/57214)) +- Make getLastFocus and setLastFocus private. ([57612](https://github.com/WordPress/gutenberg/pull/57612)) +- Remove deprecated `behaviors` syntax. ([57165](https://github.com/WordPress/gutenberg/pull/57165)) +- Avoid extra `useMarkPersistent` dispatch calls. ([57435](https://github.com/WordPress/gutenberg/pull/57435)) +- Clean up code editor CSS. ([57519](https://github.com/WordPress/gutenberg/pull/57519)) +- Combine selectors in 'useTransformCommands'. ([57424](https://github.com/WordPress/gutenberg/pull/57424)) + +#### Block Library +- Background image: Add has-background classname when background image is applied. ([57495](https://github.com/WordPress/gutenberg/pull/57495)) +- File: Remove unnecessary synchronization effect. ([57585](https://github.com/WordPress/gutenberg/pull/57585)) +- Navigation: Refactor mobile overlay breakpoints to JS. ([57520](https://github.com/WordPress/gutenberg/pull/57520)) +- Search Block: Remove unused `buttonBehavior` attribute. ([53467](https://github.com/WordPress/gutenberg/pull/53467)) + +#### Patterns +- Improve inserter pattern constants. ([57570](https://github.com/WordPress/gutenberg/pull/57570)) +- Remove duplicate setting for `getPostLinkProps` and prefer stable naming. ([57535](https://github.com/WordPress/gutenberg/pull/57535)) +- Rename `patternBlock` to `patternPost`. ([57568](https://github.com/WordPress/gutenberg/pull/57568)) + +#### Post Editor +- Editor: Use hooks instead of HoCs in 'PostVisibilityCheck'. ([57705](https://github.com/WordPress/gutenberg/pull/57705)) +- Quality: Avoid React warning when changing rendering mode. ([57413](https://github.com/WordPress/gutenberg/pull/57413)) + +#### Block Editor +- Editor: Unify the inserter sidebar. ([57466](https://github.com/WordPress/gutenberg/pull/57466)) +- Remove unused parameters from useOnBlockDrop. ([57527](https://github.com/WordPress/gutenberg/pull/57527)) + +#### List View +- Editor: Unify the list view sidebar between the post and site editors. ([57467](https://github.com/WordPress/gutenberg/pull/57467)) +- Add drag cursor to draggable list items. ([57493](https://github.com/WordPress/gutenberg/pull/57493)) + +### Tools + +- Dependency Extraction Webpack Plugin: Use `import` for module externals. ([57577](https://github.com/WordPress/gutenberg/pull/57577)) +- DependencyExtractionWebpackPlugin: Add true shorthand for requestToExternalModule. ([57593](https://github.com/WordPress/gutenberg/pull/57593)) +- DependencyExtractionWebpackPlugin: Use module for @wordpress/interactivity. ([57602](https://github.com/WordPress/gutenberg/pull/57602)) +- Fix webpack not setting environment.module true. ([57714](https://github.com/WordPress/gutenberg/pull/57714)) +- Modules: Load the import map polyfill when needed. ([57256](https://github.com/WordPress/gutenberg/pull/57256)) +- Blocks: Add handling for block.json viewModule. ([57437](https://github.com/WordPress/gutenberg/pull/57437)) + +#### Testing +- Allowed Patterns end-to-end test - move tests that run with a subset of allowed blocks into a group. ([57496](https://github.com/WordPress/gutenberg/pull/57496)) +- Clean up end-to-end tests package. ([57575](https://github.com/WordPress/gutenberg/pull/57575)) +- Fix flaky 'Post publish button' end-to-end test. ([57407](https://github.com/WordPress/gutenberg/pull/57407)) +- Migrate 'allowed patterns' end-to-end tests to Playwright. ([57399](https://github.com/WordPress/gutenberg/pull/57399)) +- Migrate 'block editor keyboard shortcuts' end-to-end tests to Playwright. ([57422](https://github.com/WordPress/gutenberg/pull/57422)) +- Migrate 'core settings' end-to-end tests to Playwright. ([57581](https://github.com/WordPress/gutenberg/pull/57581)) +- Migrate 'datepicker' end-to-end tests to Playwright. ([57545](https://github.com/WordPress/gutenberg/pull/57545)) +- Migrate 'dropdown menu' end-to-end tests to Playwright. ([57663](https://github.com/WordPress/gutenberg/pull/57663)) +- Migrate 'editor modes' end-to-end tests to Playwright. ([57574](https://github.com/WordPress/gutenberg/pull/57574)) +- Migrate 'invalid blocks' end-to-end tests to Playwright. ([57508](https://github.com/WordPress/gutenberg/pull/57508)) +- Migrate 'nux' end-to-end tests to Playwright. ([57542](https://github.com/WordPress/gutenberg/pull/57542)) +- Migrate 'preferences' end-to-end tests to Playwright. ([57446](https://github.com/WordPress/gutenberg/pull/57446)) +- Migrate 'publishing' end-to-end tests to Playwright. ([57521](https://github.com/WordPress/gutenberg/pull/57521)) +- Migrate 'scheduling' end-to-end tests to Playwright. ([57539](https://github.com/WordPress/gutenberg/pull/57539)) +- Migrate 'sidebar' end-to-end tests to Playwright. ([57448](https://github.com/WordPress/gutenberg/pull/57448)) +- Migrate 'taxonomies' end-to-end tests to Playwright. ([57662](https://github.com/WordPress/gutenberg/pull/57662)) +- Migrate `editing-widgets` to Playwright. ([57483](https://github.com/WordPress/gutenberg/pull/57483)) +- Migrate remaining 'publish panel' end-to-end tests to Playwright. ([57432](https://github.com/WordPress/gutenberg/pull/57432)) +- Update 'missing block' end-to-end tests to use the 'setContent' helper. ([57509](https://github.com/WordPress/gutenberg/pull/57509)) + +#### Build Tooling +- Group GitHub Action Dependabot updates. ([57591](https://github.com/WordPress/gutenberg/pull/57591)) +- WP Scripts: Build block.json viewModule. ([57461](https://github.com/WordPress/gutenberg/pull/57461)) +- Dependency Extraction Webpack Plugin: Add Module support. ([57199](https://github.com/WordPress/gutenberg/pull/57199)) + +## First time contributors + +The following PRs were merged by first time contributors: + +- @HrithikDalal: Font Library: Update font uninstall modal text. ([57368](https://github.com/WordPress/gutenberg/pull/57368)) +- @muhme: Fix two typos in tutorial.md. ([57627](https://github.com/WordPress/gutenberg/pull/57627)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @andrewhayward @andrewserong @atachibana @c4rl0sbr4v0 @carolinan @chad1008 @ciampo @DAreRodz @dcalhoun @derekblank @desrosj @ellatrix @fai-sal @fluiddot @geriux @getdave @glendaviesnz @gziolo @hbhalodia @HrithikDalal @jameskoster @jeryj @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @Mamaduka @matiasbenedetto @mcsf @michalczaplinski @mirka @muhme @ndiego @ntsekouras @oandregal @ockham @ramonjd @scruffian @sirreal @Soean @t-hamano @talldan @tellthemachines @youknowriad + + = 17.3.2 = ## Changelog From d8ef7d615049afa5c6903530c8099ca7b69073c7 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 10 Jan 2024 16:05:09 -0300 Subject: [PATCH 02/34] Font Library: add wp_get_font_dir() function. (#57730) * adding wp_font_dir() function * use wp_font_dir() instead of Font Library method * update php unit teests * rename function --- .../font-library/class-wp-font-family.php | 9 ++- .../font-library/class-wp-font-library.php | 67 ------------------- ...class-wp-rest-font-families-controller.php | 6 +- .../fonts/font-library/font-library.php | 49 ++++++++++++++ .../{wpFontLibrary => }/fontsDir.php | 22 +++--- .../fonts/font-library/wpFontFamily/base.php | 2 +- .../wpRestFontFamiliesController/base.php | 2 +- 7 files changed, 69 insertions(+), 88 deletions(-) rename phpunit/tests/fonts/font-library/{wpFontLibrary => }/fontsDir.php (67%) diff --git a/lib/experimental/fonts/font-library/class-wp-font-family.php b/lib/experimental/fonts/font-library/class-wp-font-family.php index e47cf0afdac1d..f64aebc0c8efa 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-family.php +++ b/lib/experimental/fonts/font-library/class-wp-font-family.php @@ -135,7 +135,7 @@ public function uninstall() { */ private static function delete_asset( $src ) { $filename = basename( $src ); - $file_path = path_join( WP_Font_Library::get_fonts_dir(), $filename ); + $file_path = path_join( wp_get_font_dir()['path'], $filename ); wp_delete_file( $file_path ); @@ -163,7 +163,6 @@ private static function delete_font_face_assets( $font_face ) { return true; } - /** * Gets the overrides for the 'wp_handle_upload' function. * @@ -394,7 +393,7 @@ private function download_or_move_font_faces( $files ) { // If the font face requires the use of the filesystem, create the fonts dir if it doesn't exist. if ( ! empty( $font_face['downloadFromUrl'] ) && ! empty( $font_face['uploadedFile'] ) ) { - wp_mkdir_p( WP_Font_Library::get_fonts_dir() ); + wp_mkdir_p( wp_get_font_dir()['path'] ); } // If installing google fonts, download the font face assets. @@ -599,9 +598,9 @@ private function create_or_update_font_post() { */ public function install( $files = null ) { add_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); - add_filter( 'upload_dir', array( 'WP_Font_Library', 'fonts_dir' ) ); + add_filter( 'upload_dir', 'wp_get_font_dir' ); $were_assets_written = $this->download_or_move_font_faces( $files ); - remove_filter( 'upload_dir', array( 'WP_Font_Library', 'fonts_dir' ) ); + remove_filter( 'upload_dir', 'wp_get_font_dir' ); remove_filter( 'upload_mimes', array( 'WP_Font_Library', 'set_allowed_mime_types' ) ); if ( ! $were_assets_written ) { diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php index 99de81e0bd74a..01ee702673e57 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/experimental/fonts/font-library/class-wp-font-library.php @@ -140,74 +140,7 @@ public static function get_font_collection( $id ) { return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); } - /** - * Returns an array containing the current fonts upload directory's path and URL. - * - * @since 6.5.0 - * - * @param array $defaults { - * Array of information about the upload directory. - * - * @type string $path Base directory and subdirectory or full path to the fonts upload directory. - * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. - * @type string $subdir Subdirectory - * @type string $basedir Path without subdir. - * @type string $baseurl URL path without subdir. - * @type string|false $error False or error message. - * } - * - * @return array $defaults { - * Array of information about the upload directory. - * - * @type string $path Base directory and subdirectory or full path to the fonts upload directory. - * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. - * @type string $subdir Subdirectory - * @type string $basedir Path without subdir. - * @type string $baseurl URL path without subdir. - * @type string|false $error False or error message. - * } - */ - public static function fonts_dir( $defaults = array() ) { - $site_path = self::get_multi_site_dir(); - - // Sets the defaults. - $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; - $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; - $defaults['subdir'] = ''; - $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; - $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; - $defaults['error'] = false; - - // Filters the fonts directory data. - return apply_filters( 'fonts_dir', $defaults ); - } - - /** - * Gets the Site dir for fonts, using the blog ID if multi-site, empty otherwise. - * - * @since 6.5.0 - * - * @return string Site dir path. - */ - private static function get_multi_site_dir() { - $font_sub_dir = ''; - if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { - $font_sub_dir = '/sites/' . get_current_blog_id(); - } - return $font_sub_dir; - } - /** - * Gets the upload directory for fonts. - * - * @since 6.5.0 - * - * @return string Path of the upload directory for fonts. - */ - public static function get_fonts_dir() { - $fonts_dir_settings = self::fonts_dir(); - return $fonts_dir_settings['path']; - } /** * Sets the allowed mime types for fonts. diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index 0147d80b7bde9..ede8762c88c6d 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -276,7 +276,7 @@ public function update_font_library_permissions_check() { * @return bool Whether the font directory exists. */ private function has_upload_directory() { - $upload_dir = WP_Font_Library::get_fonts_dir(); + $upload_dir = wp_get_font_dir()['path']; return is_dir( $upload_dir ); } @@ -290,7 +290,7 @@ private function has_upload_directory() { private function has_write_permission() { // The update endpoints requires write access to the temp and the fonts directories. $temp_dir = get_temp_dir(); - $upload_dir = WP_Font_Library::get_fonts_dir(); + $upload_dir = wp_get_font_dir()['path']; if ( ! is_writable( $temp_dir ) || ! wp_is_writable( $upload_dir ) ) { return false; } @@ -353,7 +353,7 @@ public function install_fonts( $request ) { } if ( $this->needs_write_permission( $font_family_settings ) ) { - $upload_dir = WP_Font_Library::get_fonts_dir(); + $upload_dir = wp_get_font_dir()['path']; if ( ! $this->has_upload_directory() ) { if ( ! wp_mkdir_p( $upload_dir ) ) { $errors[] = new WP_Error( diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 711a6bb40c282..4a2f1be69ef71 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -82,3 +82,52 @@ function wp_unregister_font_collection( $collection_id ) { ); wp_register_font_collection( $default_font_collection ); + +// @core-merge: This code should probably go into Core's src/wp-includes/functions.php. +if ( ! function_exists( 'wp_get_font_dir' ) ) { + /** + * Returns an array containing the current fonts upload directory's path and URL. + * + * @since 6.5.0 + * + * @param array $defaults { + * Array of information about the upload directory. + * + * @type string $path Base directory and subdirectory or full path to the fonts upload directory. + * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. + * @type string $subdir Subdirectory + * @type string $basedir Path without subdir. + * @type string $baseurl URL path without subdir. + * @type string|false $error False or error message. + * } + * + * @return array $defaults { + * Array of information about the upload directory. + * + * @type string $path Base directory and subdirectory or full path to the fonts upload directory. + * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. + * @type string $subdir Subdirectory + * @type string $basedir Path without subdir. + * @type string $baseurl URL path without subdir. + * @type string|false $error False or error message. + * } + */ + function wp_get_font_dir( $defaults = array() ) { + // Multi site path + $site_path = ''; + if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { + $site_path = '/sites/' . get_current_blog_id(); + } + + // Sets the defaults. + $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; + $defaults['subdir'] = ''; + $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; + $defaults['error'] = false; + + // Filters the fonts directory data. + return apply_filters( 'font_dir', $defaults ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/fontsDir.php b/phpunit/tests/fonts/font-library/fontsDir.php similarity index 67% rename from phpunit/tests/fonts/font-library/wpFontLibrary/fontsDir.php rename to phpunit/tests/fonts/font-library/fontsDir.php index 9926bb7409088..5c13f1d120f9a 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/fontsDir.php +++ b/phpunit/tests/fonts/font-library/fontsDir.php @@ -1,6 +1,6 @@ assertEquals( $fonts_dir, $this->dir_defaults ); + $font_dir = wp_get_font_dir(); + $this->assertEquals( $font_dir, $this->dir_defaults ); } public function test_fonts_dir_with_filter() { @@ -43,10 +43,10 @@ function set_new_values( $defaults ) { } // Add the filter. - add_filter( 'fonts_dir', 'set_new_values' ); + add_filter( 'font_dir', 'set_new_values' ); // Gets the fonts dir. - $fonts_dir = WP_Font_Library::fonts_dir(); + $font_dir = wp_get_font_dir(); $expected = array( 'path' => '/custom-path/fonts/my-custom-subdir', @@ -57,14 +57,14 @@ function set_new_values( $defaults ) { 'error' => false, ); - $this->assertEquals( $fonts_dir, $expected, 'The fonts_dir() method should return the expected values.' ); + $this->assertEquals( $font_dir, $expected, 'The wp_get_font_dir() method should return the expected values.' ); // Remove the filter. - remove_filter( 'fonts_dir', 'set_new_values' ); + remove_filter( 'font_dir', 'set_new_values' ); // Gets the fonts dir. - $fonts_dir = WP_Font_Library::fonts_dir(); + $font_dir = wp_get_font_dir(); - $this->assertEquals( $fonts_dir, $this->dir_defaults, 'The fonts_dir() method should return the default values.' ); + $this->assertEquals( $font_dir, $this->dir_defaults, 'The wp_get_font_dir() method should return the default values.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpFontFamily/base.php b/phpunit/tests/fonts/font-library/wpFontFamily/base.php index 3650ac7dab997..3f6ff153fa12f 100644 --- a/phpunit/tests/fonts/font-library/wpFontFamily/base.php +++ b/phpunit/tests/fonts/font-library/wpFontFamily/base.php @@ -28,7 +28,7 @@ abstract class WP_Font_Family_UnitTestCase extends WP_UnitTestCase { public static function set_up_before_class() { parent::set_up_before_class(); - static::$fonts_dir = WP_Font_Library::get_fonts_dir(); + static::$fonts_dir = wp_get_font_dir()['path']; wp_mkdir_p( static::$fonts_dir ); } diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php index 5ab71a4379851..e2d190cd76af1 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php @@ -18,7 +18,7 @@ abstract class WP_REST_Font_Families_Controller_UnitTestCase extends WP_UnitTest public function set_up() { parent::set_up(); - static::$fonts_dir = WP_Font_Library::get_fonts_dir(); + static::$fonts_dir = wp_get_font_dir()['path']; // Create a user with administrator role. $admin_id = $this->factory->user->create( From b892bd8be90d2ab26e4a3de9ae1fdbf7a3ec8542 Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 10 Jan 2024 13:50:09 -0600 Subject: [PATCH 03/34] Display a disabled lock button if user cannot control lock state (#57274) --- .../src/components/block-lock/toolbar.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index ccf04c5e5262d..69f0825ed4be3 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -33,16 +33,24 @@ export default function BlockLockToolbar( { clientId } ) { } }, [ isLocked ] ); - if ( ! canLock || ( ! isLocked && ! hasLockButtonShown.current ) ) { + if ( ! isLocked && ! hasLockButtonShown.current ) { return null; } + let label = isLocked ? __( 'Unlock' ) : __( 'Lock' ); + + if ( ! canLock && isLocked ) { + label = __( 'Locked' ); + } + return ( <> Date: Wed, 10 Jan 2024 20:28:26 -0500 Subject: [PATCH 04/34] docs: Note the mobile E2E tests `jq` dependency (#57741) --- packages/react-native-editor/__device-tests__/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-editor/__device-tests__/README.md b/packages/react-native-editor/__device-tests__/README.md index e917a297a491c..719adbbcf9426 100644 --- a/packages/react-native-editor/__device-tests__/README.md +++ b/packages/react-native-editor/__device-tests__/README.md @@ -6,6 +6,7 @@ The Mobile Gutenberg (MG) project maintains a suite of automated end-to-end (E2E 1. Complete the [React Native Getting Started](https://reactnative.dev/docs/environment-setup) guide for both iOS and Android, which covers setting up Xcode, Android Studio, the Android SDK. 1. Open [Xcode settings](https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes#Install-and-manage-Simulator-runtimes-in-settings) to install the iOS 16.2 simulator runtime. +1. Install [`jq`](https://jqlang.github.io/jq/download/) via [Homebrew](https://brew.sh/) or your preferred package manager. 1. `npm run native test:e2e:setup` ## Running Tests From d03285c8f6c75c0d6801186c992bc243ddf874ae Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 10 Jan 2024 21:01:07 -0500 Subject: [PATCH 05/34] chore: Enable offline media upload features (#57731) * chore: Enable offline status indicator * docs: Add change log entry --- packages/edit-post/src/components/layout/index.native.js | 5 +---- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index 50472f7924930..6821aa754eea7 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -152,10 +152,7 @@ class Layout extends Component { onLayout={ this.onRootViewLayout } > - { - // eslint-disable-next-line no-undef - __DEV__ && - } + { isHtmlView ? this.renderHTML() : this.renderVisual() } { ! isHtmlView && Platform.OS === 'android' && ( diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index a62838b066bee..7a1f6997d2a13 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [**] Image block media uploads display a custom error message when there is no internet connection [#56937] - [*] Fix missing custom color indicator for custom gradients [#57605] +- [**] Display a notice when a network connection unavailable [#56934] ## 1.110.0 - [*] [internal] Move InserterButton from components package to block-editor package [#56494] From 19ad158863c86152b7fb26ae575696b031068078 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 11 Jan 2024 11:47:00 +0900 Subject: [PATCH 06/34] Add description to the save panel header when nothing is checked (#57716) * Add description to the save panel header when nothing is checked * Shortening the description --- .../src/components/entities-saved-states/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/entities-saved-states/index.js b/packages/editor/src/components/entities-saved-states/index.js index b5f3ea5b433e1..8e531ce580101 100644 --- a/packages/editor/src/components/entities-saved-states/index.js +++ b/packages/editor/src/components/entities-saved-states/index.js @@ -213,13 +213,13 @@ export function EntitiesSavedStatesExtensible( { { __( 'Are you ready to save?' ) } { additionalPrompt } - { isDirty && ( -

- { __( - 'The following changes have been made to your site, templates, and content.' - ) } -

- ) } +

+ { isDirty + ? __( + 'The following changes have been made to your site, templates, and content.' + ) + : __( 'Select the items you want to save.' ) } +

{ sortedPartitionedSavables.map( ( list ) => { From beb10327bc8942d5e0a7572242846d8d49d67aac Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:18:21 +0900 Subject: [PATCH 07/34] Gallery Block: Remove duplicate return statement (#57746) --- packages/block-library/src/gallery/index.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/block-library/src/gallery/index.php b/packages/block-library/src/gallery/index.php index 97877141ef333..342264de6fce3 100644 --- a/packages/block-library/src/gallery/index.php +++ b/packages/block-library/src/gallery/index.php @@ -42,9 +42,6 @@ function block_core_gallery_random_order( $parsed_block ) { if ( 'core/gallery' === $parsed_block['blockName'] && ! empty( $parsed_block['attrs']['randomOrder'] ) ) { shuffle( $parsed_block['innerBlocks'] ); } - - return $parsed_block; - return $parsed_block; } From d8676808e85f5e97e1c0bbca20145aaa791e7489 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Thu, 11 Jan 2024 12:47:45 +0200 Subject: [PATCH 08/34] Remove obsolete check from dataviews modal actions title (#57753) --- packages/dataviews/src/item-actions.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js index 974c5c5d0d5c3..d3d2c62f504fd 100644 --- a/packages/dataviews/src/item-actions.js +++ b/packages/dataviews/src/item-actions.js @@ -59,10 +59,7 @@ function ActionWithModal( { action, item, ActionTrigger } ) { { isModalOpen && ( { setIsModalOpen( false ); From 940f0fe52cfd1e3599e507a7d3f83146914e978f Mon Sep 17 00:00:00 2001 From: Alex Lende Date: Thu, 11 Jan 2024 05:05:15 -0600 Subject: [PATCH 09/34] Add defaultFontSizes option to theme.json (#56661) * Add defaultFontSizes option for theme.json * Fix defaultFontSizes causing errors when missing * Simplify useMergedSettings * Simplify more * Remove the memo because it's probably fine * Try ignoring how overrides are handled in the dropdown * Minor performance improvement using reverse instead of toReversed * Link to a new issue with a more complete description * Refactor hack into its own funtion --- .../theme-json-reference/theme-json-living.md | 1 + lib/class-wp-theme-json-gutenberg.php | 29 +++++----- lib/theme.json | 1 + .../src/components/global-styles/hooks.js | 2 + .../global-styles/typography-panel.js | 58 ++++++++++++++----- packages/block-editor/src/hooks/utils.js | 20 +++++-- packages/block-editor/src/utils/object.js | 16 +++++ schemas/json/theme.json | 5 ++ 8 files changed, 101 insertions(+), 31 deletions(-) diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 4baa5a6009ded..ee88f779ace1c 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -176,6 +176,7 @@ Settings related to typography. | Property | Type | Default | Props | | --- | --- | --- |--- | +| defaultFontSizes | boolean | true | | | customFontSize | boolean | true | | | fontStyle | boolean | true | | | fontWeight | boolean | true | | diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index aa8de83df9597..a063ab34d9069 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -155,7 +155,7 @@ class WP_Theme_JSON_Gutenberg { ), array( 'path' => array( 'typography', 'fontSizes' ), - 'prevent_override' => false, + 'prevent_override' => array( 'typography', 'defaultFontSizes' ), 'use_default_names' => true, 'value_func' => 'gutenberg_get_typography_font_size_value', 'css_vars' => '--wp--preset--font-size--$slug', @@ -411,19 +411,20 @@ class WP_Theme_JSON_Gutenberg { 'defaultPresets' => null, ), 'typography' => array( - 'fluid' => null, - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textColumns' => null, - 'textDecoration' => null, - 'textTransform' => null, - 'writingMode' => null, + 'fluid' => null, + 'customFontSize' => null, + 'defaultFontSizes' => null, + 'dropCap' => null, + 'fontFamilies' => null, + 'fontSizes' => null, + 'fontStyle' => null, + 'fontWeight' => null, + 'letterSpacing' => null, + 'lineHeight' => null, + 'textColumns' => null, + 'textDecoration' => null, + 'textTransform' => null, + 'writingMode' => null, ), ); diff --git a/lib/theme.json b/lib/theme.json index c2ed7fdca39ed..b7bc3cb89e60f 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -236,6 +236,7 @@ }, "typography": { "customFontSize": true, + "defaultFontSizes": true, "dropCap": true, "fontSizes": [ { diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 1fed98cfd229b..2652732807cfd 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -66,6 +66,7 @@ const VALID_SETTINGS = [ 'spacing.units', 'typography.fluid', 'typography.customFontSize', + 'typography.defaultFontSizes', 'typography.dropCap', 'typography.fontFamilies', 'typography.fontSizes', @@ -238,6 +239,7 @@ export function useSettingsForBlockElement( ...updatedSettings.typography, fontSizes: {}, customFontSize: false, + defaultFontSizes: false, }; } diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 8e6755a6e4c2c..668e8b101be92 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -22,7 +22,7 @@ import TextTransformControl from '../text-transform-control'; import TextDecorationControl from '../text-decoration-control'; import WritingModeControl from '../writing-mode-control'; import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; -import { setImmutably } from '../../utils/object'; +import { setImmutably, uniqByProperty } from '../../utils/object'; const MIN_TEXT_COLUMNS = 1; const MAX_TEXT_COLUMNS = 6; @@ -53,7 +53,10 @@ export function useHasTypographyPanel( settings ) { function useHasFontSizeControl( settings ) { return ( - hasMergedOrigins( settings?.typography?.fontSizes ) || + ( settings?.typography?.defaultFontSizes !== false && + settings?.typography?.fontSizes?.default?.length ) || + settings?.typography?.fontSizes?.theme?.length || + settings?.typography?.fontSizes?.custom?.length || settings?.typography?.customFontSize ); } @@ -100,16 +103,45 @@ function useHasTextColumnsControl( settings ) { return settings?.typography?.textColumns; } -function getUniqueFontSizesBySlug( settings ) { - const fontSizes = settings?.typography?.fontSizes; - const mergedFontSizes = fontSizes ? mergeOrigins( fontSizes ) : []; - const uniqueSizes = []; - for ( const currentSize of mergedFontSizes ) { - if ( ! uniqueSizes.some( ( { slug } ) => slug === currentSize.slug ) ) { - uniqueSizes.push( currentSize ); - } - } - return uniqueSizes; +/** + * TODO: The reversing and filtering of default font sizes is a hack so the + * dropdown UI matches what is generated in the global styles CSS stylesheet. + * + * This is a temporary solution until #57733 is resolved. At which point, + * the mergedFontSizes would just need to be the concatenated array of all + * presets or a custom dropdown with sections for each. + * + * @see {@link https://github.com/WordPress/gutenberg/issues/57733} + * + * @param {Object} settings The global styles settings. + * + * @return {Array} The merged font sizes. + */ +function getMergedFontSizes( settings ) { + // The font size presets are merged in reverse order so that the duplicates + // that may defined later in the array have higher priority to match the CSS. + const mergedFontSizesAll = uniqByProperty( + [ + settings?.typography?.fontSizes?.custom, + settings?.typography?.fontSizes?.theme, + settings?.typography?.fontSizes?.default, + ].flatMap( ( presets ) => presets?.toReversed() ?? [] ), + 'slug' + ).reverse(); + + // Default presets exist in the global styles CSS no matter the setting, so + // filtering them out in the UI has to be done after merging. + const mergedFontSizes = + settings?.typography?.defaultFontSizes === false + ? mergedFontSizesAll.filter( + ( { slug } ) => + ! [ 'small', 'medium', 'large', 'x-large' ].includes( + slug + ) + ) + : mergedFontSizesAll; + + return mergedFontSizes; } function TypographyToolsPanel( { @@ -185,7 +217,7 @@ export default function TypographyPanel( { // Font Size const hasFontSizeEnabled = useHasFontSizeControl( settings ); const disableCustomFontSizes = ! settings?.typography?.customFontSize; - const mergedFontSizes = getUniqueFontSizesBySlug( settings ); + const mergedFontSizes = getMergedFontSizes( settings ); const fontSize = decodeValue( inheritedValue?.typography?.fontSize ); const setFontSize = ( newValue, metadata ) => { diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 1c597836e9ec5..cd660c85826c2 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -177,7 +177,10 @@ export function useBlockSettings( name, parentLayout ) { backgroundImage, backgroundSize, fontFamilies, - fontSizes, + userFontSizes, + themeFontSizes, + defaultFontSizes, + defaultFontSizesEnabled, customFontSize, fontStyle, fontWeight, @@ -222,7 +225,10 @@ export function useBlockSettings( name, parentLayout ) { 'background.backgroundImage', 'background.backgroundSize', 'typography.fontFamilies', - 'typography.fontSizes', + 'typography.fontSizes.custom', + 'typography.fontSizes.theme', + 'typography.fontSizes.default', + 'typography.defaultFontSizes', 'typography.customFontSize', 'typography.fontStyle', 'typography.fontWeight', @@ -304,9 +310,12 @@ export function useBlockSettings( name, parentLayout ) { custom: fontFamilies, }, fontSizes: { - custom: fontSizes, + custom: userFontSizes, + theme: themeFontSizes, + default: defaultFontSizes, }, customFontSize, + defaultFontSizes: defaultFontSizesEnabled, fontStyle, fontWeight, lineHeight, @@ -341,7 +350,10 @@ export function useBlockSettings( name, parentLayout ) { backgroundImage, backgroundSize, fontFamilies, - fontSizes, + userFontSizes, + themeFontSizes, + defaultFontSizes, + defaultFontSizesEnabled, customFontSize, fontStyle, fontWeight, diff --git a/packages/block-editor/src/utils/object.js b/packages/block-editor/src/utils/object.js index 8f6c82a9c3991..c78fe0e656dfe 100644 --- a/packages/block-editor/src/utils/object.js +++ b/packages/block-editor/src/utils/object.js @@ -49,3 +49,19 @@ export const getValueFromObjectPath = ( object, path, defaultValue ) => { } ); return value ?? defaultValue; }; + +/** + * Helper util to filter out objects with duplicate values for a given property. + * + * @param {Object[]} array Array of objects to filter. + * @param {string} property Property to filter unique values by. + * + * @return {Object[]} Array of objects with unique values for the specified property. + */ +export function uniqByProperty( array, property ) { + const seen = new Set(); + return array.filter( ( item ) => { + const value = item[ property ]; + return seen.has( value ) ? false : seen.add( value ); + } ); +} diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 10695f493c40d..9f633fc77ec75 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -484,6 +484,11 @@ "description": "Settings related to typography.", "type": "object", "properties": { + "defaultFontSizes": { + "description": "Allow users to choose font sizes from the default font size presets.", + "type": "boolean", + "default": true + }, "customFontSize": { "description": "Allow users to set custom font sizes.", "type": "boolean", From 2762ec45309469c37d2c90aaf587c25f65b15965 Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Thu, 11 Jan 2024 07:20:05 -0500 Subject: [PATCH 10/34] Font Library: Use data or src file to define font collection data (#57734) * Add ability to use data rather than a file to define font collection * format php * avoid returning data when getting the font collection config * fix rest endpoint get_font_collection --------- Co-authored-by: Matias Benedetto --- .../font-library/class-wp-font-collection.php | 59 +++++++++++--- ...ss-wp-rest-font-collections-controller.php | 15 ++-- .../fonts/font-library/font-library.php | 3 +- .../wpFontCollection/__construct.php | 2 +- .../wpFontCollection/getConfig.php | 76 +++++++++++++++++++ .../{getData.php => getConfigAndData.php} | 28 +++++-- 6 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php rename phpunit/tests/fonts/font-library/wpFontCollection/{getData.php => getConfigAndData.php} (71%) diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php index e8cc7c98fe730..79b0691ede273 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-collection.php +++ b/lib/experimental/fonts/font-library/class-wp-font-collection.php @@ -35,7 +35,7 @@ class WP_Font_Collection { * @since 6.5.0 * * @param array $config Font collection config options. - * See {@see wp_register_font_collection()} for the supported fields. + * See {@see wp_register_font_collection()} for the supported fields. * @throws Exception If the required parameters are missing. */ public function __construct( $config ) { @@ -51,8 +51,8 @@ public function __construct( $config ) { throw new Exception( 'Font Collection config name is required as a non-empty string.' ); } - if ( empty( $config['src'] ) || ! is_string( $config['src'] ) ) { - throw new Exception( 'Font Collection config "src" option is required as a non-empty string.' ); + if ( ( empty( $config['src'] ) || ! is_string( $config['src'] ) ) && ( empty( $config['data'] ) ) ) { + throw new Exception( 'Font Collection config "src" option OR "data" option is required.' ); } $this->config = $config; @@ -63,21 +63,59 @@ public function __construct( $config ) { * * @since 6.5.0 * - * @return array An array containing the font collection config. + * @return array { + * An array of font collection config. + * + * @type string $id The font collection's unique ID. + * @type string $name The font collection's name. + * @type string $description The font collection's description. + * } */ public function get_config() { - return $this->config; + return array( + 'id' => $this->config['id'], + 'name' => $this->config['name'], + 'description' => $this->config['description'] ?? '', + ); } /** - * Gets the font collection data. + * Gets the font collection config and data. + * + * This function returns an array containing the font collection's unique ID, + * name, and its data as a PHP array. * * @since 6.5.0 * - * @return array|WP_Error An array containing the list of font families in theme.json format on success, + * @return array { + * An array of font collection config and data. + * + * @type string $id The font collection's unique ID. + * @type string $name The font collection's name. + * @type string $description The font collection's description. + * @type array $data The font collection's data as a PHP array. + * } + */ + public function get_config_and_data() { + $config_and_data = $this->get_config(); + $config_and_data['data'] = $this->load_data(); + return $config_and_data; + } + + /** + * Loads the font collection data. + * + * @since 6.5.0 + * + * @return array|WP_Error An array containing the list of font families in font-collection.json format on success, * else an instance of WP_Error on failure. */ - public function get_data() { + public function load_data() { + + if ( ! empty( $this->config['data'] ) ) { + return $this->config['data']; + } + // If the src is a URL, fetch the data from the URL. if ( str_contains( $this->config['src'], 'http' ) && str_contains( $this->config['src'], '://' ) ) { if ( ! wp_http_validate_url( $this->config['src'] ) ) { @@ -104,9 +142,6 @@ public function get_data() { } } - $collection_data = $this->get_config(); - $collection_data['data'] = $data; - unset( $collection_data['src'] ); - return $collection_data; + return $data; } } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php index 2367cba0b870a..0efd56f61d43e 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php @@ -77,13 +77,16 @@ public function get_font_collection( $request ) { $collection->add_data( array( 'status' => 404 ) ); return $collection; } - $collection_with_data = $collection->get_data(); + $config_and_data = $collection->get_config_and_data(); + $collection_data = $config_and_data['data']; + // If there was an error getting the collection data, return the error. - if ( is_wp_error( $collection_with_data ) ) { - $collection_with_data->add_data( array( 'status' => 500 ) ); - return $collection_with_data; + if ( is_wp_error( $collection_data ) ) { + $collection_data->add_data( array( 'status' => 500 ) ); + return $collection_data; } - return new WP_REST_Response( $collection_with_data ); + + return new WP_REST_Response( $config_and_data ); } /** @@ -96,7 +99,7 @@ public function get_font_collection( $request ) { public function get_font_collections() { $collections = array(); foreach ( WP_Font_Library::get_font_collections() as $collection ) { - $collections[] = $collection->get_config(); + $collections[] = $collection->get_config_and_data(); } return new WP_REST_Response( $collections, 200 ); diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 4a2f1be69ef71..c2545171dace0 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -50,7 +50,8 @@ function gutenberg_init_font_library_routes() { * Font collection associative array of configuration options. * * @type string $id The font collection's unique ID. - * @type string $src The font collection's data JSON file. + * @type string $src The font collection's data as a JSON file path. + * @type array $data The font collection's data as a PHP array. * } * @return WP_Font_Collection|WP_Error A font collection is it was registered * successfully, else WP_Error. diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php index 5c2b7b5c02793..21f529d71a297 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -84,7 +84,7 @@ public function data_should_throw_exception() { 'name' => 'My Collection', 'description' => 'My collection description', ), - 'Font Collection config "src" option is required as a non-empty string.', + 'Font Collection config "src" option OR "data" option is required.', ), ); diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php b/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php new file mode 100644 index 0000000000000..03f59b6a1a6eb --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php @@ -0,0 +1,76 @@ +assertSame( $expected_data, $collection->get_config() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_get_config() { + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, '{"this is mock data":true}' ); + + return array( + 'with a file' => array( + 'config' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'src' => $mock_file, + ), + 'expected_data' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + ), + ), + 'with a url' => array( + 'config' => array( + 'id' => 'my-collection-with-url', + 'name' => 'My Collection with URL', + 'description' => 'My collection description', + 'src' => 'https://localhost/fonts/mock-font-collection.json', + ), + 'expected_data' => array( + 'id' => 'my-collection-with-url', + 'name' => 'My Collection with URL', + 'description' => 'My collection description', + ), + ), + 'with data' => array( + 'config' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'data' => array( 'this is mock data' => true ), + ), + 'expected_data' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + ), + ), + ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php similarity index 71% rename from phpunit/tests/fonts/font-library/wpFontCollection/getData.php rename to phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php index 4d0b2eb92b595..a71928193c9aa 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/getData.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php @@ -1,6 +1,6 @@ assertSame( $expected_data, $collection->get_data() ); + $this->assertSame( $expected_data, $collection->get_config_and_data() ); } /** @@ -62,7 +62,7 @@ public function test_should_get_data( $config, $expected_data ) { * * @return array[] */ - public function data_should_get_data() { + public function data_should_get_config_and_data() { $mock_file = wp_tempnam( 'my-collection-data-' ); file_put_contents( $mock_file, '{"this is mock data":true}' ); @@ -98,6 +98,20 @@ public function data_should_get_data() { ), ), ), + 'with data' => array( + 'config' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'data' => array( 'this is mock data' => true ), + ), + 'expected_data' => array( + 'id' => 'my-collection', + 'name' => 'My Collection', + 'description' => 'My collection description', + 'data' => array( 'this is mock data' => true ), + ), + ), ); } } From 8599e15cd2d80da4f213b0c7e56884ab8171d6ad Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Thu, 11 Jan 2024 08:04:17 -0500 Subject: [PATCH 11/34] Use `slug` instead of `id` for Font Collection (#57735) * Add ability to use data rather than a file to define font collection * Use slug instead of id for Font Collections * format php * merge file changes * format php --------- Co-authored-by: Matias Benedetto --- .../font-library/class-wp-font-collection.php | 10 +++--- .../font-library/class-wp-font-library.php | 36 +++++++++---------- ...ss-wp-rest-font-collections-controller.php | 6 ++-- .../fonts/font-library/font-library.php | 2 +- .../wpFontCollection/__construct.php | 6 ++-- .../wpFontCollection/getConfig.php | 12 +++---- .../wpFontCollection/getConfigAndData.php | 12 +++---- .../wpFontLibrary/getFontCollection.php | 4 +-- .../wpFontLibrary/getFontCollections.php | 2 +- .../wpFontLibrary/registerFontCollection.php | 16 ++++----- .../unregisterFontCollection.php | 4 +-- .../getFontCollection.php | 8 ++--- .../getFontCollections.php | 4 +-- .../registerRoutes.php | 4 +-- 14 files changed, 63 insertions(+), 63 deletions(-) diff --git a/lib/experimental/fonts/font-library/class-wp-font-collection.php b/lib/experimental/fonts/font-library/class-wp-font-collection.php index 79b0691ede273..6189da5fa984b 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-collection.php +++ b/lib/experimental/fonts/font-library/class-wp-font-collection.php @@ -43,8 +43,8 @@ public function __construct( $config ) { throw new Exception( 'Font Collection config options is required as a non-empty array.' ); } - if ( empty( $config['id'] ) || ! is_string( $config['id'] ) ) { - throw new Exception( 'Font Collection config ID is required as a non-empty string.' ); + if ( empty( $config['slug'] ) || ! is_string( $config['slug'] ) ) { + throw new Exception( 'Font Collection config slug is required as a non-empty string.' ); } if ( empty( $config['name'] ) || ! is_string( $config['name'] ) ) { @@ -66,14 +66,14 @@ public function __construct( $config ) { * @return array { * An array of font collection config. * - * @type string $id The font collection's unique ID. + * @type string $slug The font collection's unique slug. * @type string $name The font collection's name. * @type string $description The font collection's description. * } */ public function get_config() { return array( - 'id' => $this->config['id'], + 'slug' => $this->config['slug'], 'name' => $this->config['name'], 'description' => $this->config['description'] ?? '', ); @@ -90,7 +90,7 @@ public function get_config() { * @return array { * An array of font collection config and data. * - * @type string $id The font collection's unique ID. + * @type string $slug The font collection's unique ID. * @type string $name The font collection's name. * @type string $description The font collection's description. * @type array $data The font collection's data as a PHP array. diff --git a/lib/experimental/fonts/font-library/class-wp-font-library.php b/lib/experimental/fonts/font-library/class-wp-font-library.php index 01ee702673e57..fd36f6ba073c4 100644 --- a/lib/experimental/fonts/font-library/class-wp-font-library.php +++ b/lib/experimental/fonts/font-library/class-wp-font-library.php @@ -63,11 +63,11 @@ public static function get_expected_font_mime_types_per_php_version( $php_versio */ public static function register_font_collection( $config ) { $new_collection = new WP_Font_Collection( $config ); - if ( self::is_collection_registered( $config['id'] ) ) { + if ( self::is_collection_registered( $config['slug'] ) ) { $error_message = sprintf( - /* translators: %s: Font collection id. */ - __( 'Font collection with id: "%s" is already registered.', 'default' ), - $config['id'] + /* translators: %s: Font collection slug. */ + __( 'Font collection with slug: "%s" is already registered.', 'default' ), + $config['slug'] ); _doing_it_wrong( __METHOD__, @@ -76,7 +76,7 @@ public static function register_font_collection( $config ) { ); return new WP_Error( 'font_collection_registration_error', $error_message ); } - self::$collections[ $config['id'] ] = $new_collection; + self::$collections[ $config['slug'] ] = $new_collection; return $new_collection; } @@ -85,20 +85,20 @@ public static function register_font_collection( $config ) { * * @since 6.5.0 * - * @param string $collection_id Font collection ID. + * @param string $collection_slug Font collection slug. * @return bool True if the font collection was unregistered successfully and false otherwise. */ - public static function unregister_font_collection( $collection_id ) { - if ( ! self::is_collection_registered( $collection_id ) ) { + public static function unregister_font_collection( $slug ) { + if ( ! self::is_collection_registered( $slug ) ) { _doing_it_wrong( __METHOD__, - /* translators: %s: Font collection id. */ - sprintf( __( 'Font collection "%s" not found.', 'default' ), $collection_id ), + /* translators: %s: Font collection slug. */ + sprintf( __( 'Font collection "%s" not found.', 'default' ), $slug ), '6.5.0' ); return false; } - unset( self::$collections[ $collection_id ] ); + unset( self::$collections[ $slug ] ); return true; } @@ -107,11 +107,11 @@ public static function unregister_font_collection( $collection_id ) { * * @since 6.5.0 * - * @param string $collection_id Font collection ID. + * @param string $slug Font collection slug. * @return bool True if the font collection is registered and false otherwise. */ - private static function is_collection_registered( $collection_id ) { - return array_key_exists( $collection_id, self::$collections ); + private static function is_collection_registered( $slug ) { + return array_key_exists( $slug, self::$collections ); } /** @@ -130,12 +130,12 @@ public static function get_font_collections() { * * @since 6.5.0 * - * @param string $id Font collection id. + * @param string $slug Font collection slug. * @return array List of font collections. */ - public static function get_font_collection( $id ) { - if ( array_key_exists( $id, self::$collections ) ) { - return self::$collections[ $id ]; + public static function get_font_collection( $slug ) { + if ( array_key_exists( $slug, self::$collections ) ) { + return self::$collections[ $slug ]; } return new WP_Error( 'font_collection_not_found', 'Font collection not found.' ); } diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php index 0efd56f61d43e..c7595a56413b9 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php @@ -50,7 +50,7 @@ public function register_routes() { register_rest_route( $this->namespace, - '/' . $this->rest_base . '/(?P[\/\w-]+)', + '/' . $this->rest_base . '/(?P[\/\w-]+)', array( array( 'methods' => WP_REST_Server::READABLE, @@ -70,8 +70,8 @@ public function register_routes() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_font_collection( $request ) { - $id = $request->get_param( 'id' ); - $collection = WP_Font_Library::get_font_collection( $id ); + $slug = $request->get_param( 'slug' ); + $collection = WP_Font_Library::get_font_collection( $slug ); // If the collection doesn't exist returns a 404. if ( is_wp_error( $collection ) ) { $collection->add_data( array( 'status' => 404 ) ); diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index c2545171dace0..d1ad8e1447ad9 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -76,7 +76,7 @@ function wp_unregister_font_collection( $collection_id ) { } $default_font_collection = array( - 'id' => 'default-font-collection', + 'slug' => 'default-font-collection', 'name' => 'Google Fonts', 'description' => __( 'Add from Google Fonts. Fonts are copied to and served from your site.', 'gutenberg' ), 'src' => 'https://s.w.org/images/fonts/16.7/collections/google-fonts-with-preview.json', diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php index 21f529d71a297..380226ee8af8a 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -17,7 +17,7 @@ public function test_should_initialize_data() { $property->setAccessible( true ); $config = array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'src' => 'my-collection-data.json', @@ -55,7 +55,7 @@ public function data_should_throw_exception() { 'description' => 'My collection description', 'src' => 'my-collection-data.json', ), - 'Font Collection config ID is required as a non-empty string.', + 'Font Collection config slug is required as a non-empty string.', ), 'no config' => array( @@ -80,7 +80,7 @@ public function data_should_throw_exception() { 'missing src' => array( array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', ), diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php b/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php index 03f59b6a1a6eb..5f1f082297d41 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getConfig.php @@ -34,39 +34,39 @@ public function data_should_get_config() { return array( 'with a file' => array( 'config' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'src' => $mock_file, ), 'expected_data' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', ), ), 'with a url' => array( 'config' => array( - 'id' => 'my-collection-with-url', + 'slug' => 'my-collection-with-url', 'name' => 'My Collection with URL', 'description' => 'My collection description', 'src' => 'https://localhost/fonts/mock-font-collection.json', ), 'expected_data' => array( - 'id' => 'my-collection-with-url', + 'slug' => 'my-collection-with-url', 'name' => 'My Collection with URL', 'description' => 'My collection description', ), ), 'with data' => array( 'config' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'data' => array( 'this is mock data' => true ), ), 'expected_data' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', ), diff --git a/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php b/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php index a71928193c9aa..885b0a0b9036c 100644 --- a/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php +++ b/phpunit/tests/fonts/font-library/wpFontCollection/getConfigAndData.php @@ -69,13 +69,13 @@ public function data_should_get_config_and_data() { return array( 'with a file' => array( 'config' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'src' => $mock_file, ), 'expected_data' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'data' => array( 'this is mock data' => true ), @@ -83,13 +83,13 @@ public function data_should_get_config_and_data() { ), 'with a url' => array( 'config' => array( - 'id' => 'my-collection-with-url', + 'slug' => 'my-collection-with-url', 'name' => 'My Collection with URL', 'description' => 'My collection description', 'src' => 'https://localhost/fonts/mock-font-collection.json', ), 'expected_data' => array( - 'id' => 'my-collection-with-url', + 'slug' => 'my-collection-with-url', 'name' => 'My Collection with URL', 'description' => 'My collection description', 'data' => array( @@ -100,13 +100,13 @@ public function data_should_get_config_and_data() { ), 'with data' => array( 'config' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'data' => array( 'this is mock data' => true ), ), 'expected_data' => array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My collection description', 'data' => array( 'this is mock data' => true ), diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php index 00d5ca2dcb2e7..082ca89211465 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -14,7 +14,7 @@ class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTe public function test_should_get_font_collection() { $my_font_collection_config = array( - 'id' => 'my-font-collection', + 'slug' => 'my-font-collection', 'name' => 'My Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), @@ -24,7 +24,7 @@ public function test_should_get_font_collection() { $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); } - public function test_should_get_no_font_collection_if_the_id_is_not_registered() { + public function test_should_get_no_font_collection_if_the_slug_is_not_registered() { $font_collection = WP_Font_Library::get_font_collection( 'not-registered-font-collection' ); $this->assertWPError( $font_collection ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php index 40eacba8e18c5..a405584efccc2 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -18,7 +18,7 @@ public function test_should_get_an_empty_list() { public function test_should_get_mock_font_collection() { $my_font_collection_config = array( - 'id' => 'my-font-collection', + 'slug' => 'my-font-collection', 'name' => 'My Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => path_join( __DIR__, 'my-font-collection-data.json' ), diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php index 2569830f6bf2a..a7ea2870957e9 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -14,7 +14,7 @@ class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_U public function test_should_register_font_collection() { $config = array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'name' => 'My Collection', 'description' => 'My Collection Description', 'src' => 'my-collection-data.json', @@ -23,20 +23,20 @@ public function test_should_register_font_collection() { $this->assertInstanceOf( 'WP_Font_Collection', $collection ); } - public function test_should_return_error_if_id_is_missing() { + public function test_should_return_error_if_slug_is_missing() { $config = array( 'name' => 'My Collection', 'description' => 'My Collection Description', 'src' => 'my-collection-data.json', ); $this->expectException( 'Exception' ); - $this->expectExceptionMessage( 'Font Collection config ID is required as a non-empty string.' ); + $this->expectExceptionMessage( 'Font Collection config slug is required as a non-empty string.' ); WP_Font_Library::register_font_collection( $config ); } public function test_should_return_error_if_name_is_missing() { $config = array( - 'id' => 'my-collection', + 'slug' => 'my-collection', 'description' => 'My Collection Description', 'src' => 'my-collection-data.json', ); @@ -52,15 +52,15 @@ public function test_should_return_error_if_config_is_empty() { WP_Font_Library::register_font_collection( $config ); } - public function test_should_return_error_if_id_is_repeated() { + public function test_should_return_error_if_slug_is_repeated() { $config1 = array( - 'id' => 'my-collection-1', + 'slug' => 'my-collection-1', 'name' => 'My Collection 1', 'description' => 'My Collection 1 Description', 'src' => 'my-collection-1-data.json', ); $config2 = array( - 'id' => 'my-collection-1', + 'slug' => 'my-collection-1', 'name' => 'My Collection 2', 'description' => 'My Collection 2 Description', 'src' => 'my-collection-2-data.json', @@ -72,7 +72,7 @@ public function test_should_return_error_if_id_is_repeated() { // Expects a _doing_it_wrong notice. $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' ); - // Try to register a second collection with same id. + // Try to register a second collection with same slug. $collection2 = WP_Font_Library::register_font_collection( $config2 ); $this->assertWPError( $collection2, 'A WP_Error should be returned.' ); } diff --git a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php index e6e16956814fb..3c19a1d2089e7 100644 --- a/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -15,7 +15,7 @@ class Tests_Fonts_WpFontLibrary_UnregisterFontCollection extends WP_Font_Library public function test_should_unregister_font_collection() { // Registers two mock font collections. $config = array( - 'id' => 'mock-font-collection-1', + 'slug' => 'mock-font-collection-1', 'name' => 'Mock Collection to be unregistered', 'description' => 'A mock font collection to be unregistered.', 'src' => 'my-collection-data.json', @@ -23,7 +23,7 @@ public function test_should_unregister_font_collection() { WP_Font_Library::register_font_collection( $config ); $config = array( - 'id' => 'mock-font-collection-2', + 'slug' => 'mock-font-collection-2', 'name' => 'Mock Collection', 'description' => 'A mock font collection.', 'src' => 'my-mock-data.json', diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php index 94e7daaa16634..c9d003389997b 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php @@ -25,7 +25,7 @@ public function set_up() { add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 ); $config_with_file = array( - 'id' => 'one-collection', + 'slug' => 'one-collection', 'name' => 'One Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => $mock_file, @@ -33,7 +33,7 @@ public function set_up() { wp_register_font_collection( $config_with_file ); $config_with_url = array( - 'id' => 'collection-with-url', + 'slug' => 'collection-with-url', 'name' => 'Another Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => 'https://wordpress.org/fonts/mock-font-collection.json', @@ -42,7 +42,7 @@ public function set_up() { wp_register_font_collection( $config_with_url ); $config_with_non_existing_file = array( - 'id' => 'collection-with-non-existing-file', + 'slug' => 'collection-with-non-existing-file', 'name' => 'Another Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => '/home/non-existing-file.json', @@ -51,7 +51,7 @@ public function set_up() { wp_register_font_collection( $config_with_non_existing_file ); $config_with_non_existing_url = array( - 'id' => 'collection-with-non-existing-url', + 'slug' => 'collection-with-non-existing-url', 'name' => 'Another Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => 'https://non-existing-url-1234x.com.ar/fake-path/missing-file.json', diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php index 224dab07cf0b7..0a8d24e8f392b 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php @@ -27,7 +27,7 @@ public function test_get_font_collections() { // Add a font collection. $config = array( - 'id' => 'my-font-collection', + 'slug' => 'my-font-collection', 'name' => 'My Font Collection', 'description' => 'Demo about how to a font collection to your WordPress Font Library.', 'src' => $mock_file, @@ -39,7 +39,7 @@ public function test_get_font_collections() { $data = $response->get_data(); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); $this->assertCount( 1, $data, 'The response data is not an array with one element.' ); - $this->assertArrayHasKey( 'id', $data[0], 'The response data does not have the key with the collection ID.' ); + $this->assertArrayHasKey( 'slug', $data[0], 'The response data does not have the key with the collection slug.' ); $this->assertArrayHasKey( 'name', $data[0], 'The response data does not have the key with the collection name.' ); } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php index c2c019fa70a02..fb100a400fb4c 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php @@ -16,9 +16,9 @@ class Tests_Fonts_WPRESTFontCollectionsController_RegisterRoutes extends WP_Unit public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'Rest server has not the collections path initialized.' ); - $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); } } From 47026abfabe59f9baa89e18931d457a85c72ff65 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Thu, 11 Jan 2024 16:15:10 +0200 Subject: [PATCH 12/34] Group templates in sidebar list (#57711) * Group templates in sidebar list * address feedback * Section heading styling --------- Co-authored-by: James Koster --- .../index.js | 113 +++++++++++++----- .../style.scss | 9 ++ packages/edit-site/src/style.scss | 1 + 3 files changed, 95 insertions(+), 28 deletions(-) create mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js index 527cb37ceddaf..3ff934ac100a8 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/index.js @@ -4,6 +4,7 @@ import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, + __experimentalVStack as VStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useEntityRecords } from '@wordpress/core-data'; @@ -30,20 +31,11 @@ const TemplateItem = ( { postType, postId, ...props } ) => { export default function SidebarNavigationScreenTemplates() { const isMobileViewport = useViewportMatch( 'medium', '<' ); - const { records: templates, isResolving: isLoading } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, - { - per_page: -1, - } - ); - - const sortedTemplates = templates ? [ ...templates ] : []; - sortedTemplates.sort( ( a, b ) => - a.title.rendered.localeCompare( b.title.rendered ) + { per_page: -1 } ); - const browseAllLink = useLink( { path: '/wp_template/all' } ); const canCreate = ! isMobileViewport; return ( @@ -66,24 +58,7 @@ export default function SidebarNavigationScreenTemplates() { <> { isLoading && __( 'Loading templates…' ) } { ! isLoading && ( - - { ! templates?.length && ( - { __( 'No templates found' ) } - ) } - { sortedTemplates.map( ( template ) => ( - - { decodeEntities( - template.title?.rendered || - template.slug - ) } - - ) ) } - + ) } } @@ -97,3 +72,85 @@ export default function SidebarNavigationScreenTemplates() { /> ); } + +function TemplatesGroup( { title, templates } ) { + return ( + + { !! title && ( + + { title } + + ) } + { templates.map( ( template ) => ( + + { decodeEntities( + template.title?.rendered || template.slug + ) } + + ) ) } + + ); +} +function SidebarTemplatesList( { templates } ) { + if ( ! templates?.length ) { + return ( + + { __( 'No templates found' ) } + + ); + } + const sortedTemplates = templates ? [ ...templates ] : []; + sortedTemplates.sort( ( a, b ) => + a.title.rendered.localeCompare( b.title.rendered ) + ); + const { hierarchyTemplates, customTemplates, ...plugins } = + sortedTemplates.reduce( + ( accumulator, template ) => { + const { + original_source: originalSource, + author_text: authorText, + } = template; + if ( originalSource === 'plugin' ) { + if ( ! accumulator[ authorText ] ) { + accumulator[ authorText ] = []; + } + accumulator[ authorText ].push( template ); + } else if ( template.is_custom ) { + accumulator.customTemplates.push( template ); + } else { + accumulator.hierarchyTemplates.push( template ); + } + return accumulator; + }, + { hierarchyTemplates: [], customTemplates: [] } + ); + return ( + + { !! hierarchyTemplates.length && ( + + ) } + { !! customTemplates.length && ( + + ) } + { Object.entries( plugins ).map( + ( [ plugin, pluginTemplates ] ) => { + return ( + + ); + } + ) } + + ); +} diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss new file mode 100644 index 0000000000000..ec2b7744d4e23 --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-templates/style.scss @@ -0,0 +1,9 @@ +.edit-site-sidebar-navigation-screen-templates__templates-group-title.components-item { + text-transform: uppercase; + color: $gray-200; + // 6px right padding to align with + button + padding: $grid-unit-30 6px $grid-unit-20 $grid-unit-20; + border-top: 1px solid $gray-800; + font-size: 11px; + font-weight: 500; +} diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index c7d0609b4e771..164a8523b1962 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -34,6 +34,7 @@ @import "./components/sidebar-navigation-screen-details-footer/style.scss"; @import "./components/sidebar-navigation-screen-navigation-menu/style.scss"; @import "./components/sidebar-navigation-screen-page/style.scss"; +@import "./components/sidebar-navigation-screen-templates/style.scss"; @import "components/sidebar-navigation-screen-details-panel/style.scss"; @import "./components/sidebar-navigation-screen-pattern/style.scss"; @import "./components/sidebar-navigation-screen-patterns/style.scss"; From b64eec7b536e83b72b24c776a8181889f96a9abd Mon Sep 17 00:00:00 2001 From: Andrea Fercia Date: Thu, 11 Jan 2024 15:17:15 +0100 Subject: [PATCH 13/34] Fix Widgets page Undo and Redo accessibility and keyboard interaction (#57677) * Fix styling and focus style. * Fix Widgets toolbar Undo and Redo buttons keyboard interaction. --- .../src/components/header/document-tools/index.js | 5 +++-- .../edit-widgets/src/components/header/style.scss | 3 +-- .../src/components/header/undo-redo/redo.js | 11 ++++++++--- .../src/components/header/undo-redo/undo.js | 11 ++++++++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/edit-widgets/src/components/header/document-tools/index.js b/packages/edit-widgets/src/components/header/document-tools/index.js index a9799ac993f9a..4391ece0b89e2 100644 --- a/packages/edit-widgets/src/components/header/document-tools/index.js +++ b/packages/edit-widgets/src/components/header/document-tools/index.js @@ -83,6 +83,7 @@ function DocumentTools() { className="edit-widgets-header-toolbar" aria-label={ __( 'Document tools' ) } shouldUseKeyboardFocusShortcut={ ! blockToolbarCanBeFocused } + variant="unstyled" > { isMediumViewport && ( <> - - + + ); } + +export default forwardRef( RedoButton ); diff --git a/packages/edit-widgets/src/components/header/undo-redo/undo.js b/packages/edit-widgets/src/components/header/undo-redo/undo.js index 827ed1a415d74..271c73a452d9e 100644 --- a/packages/edit-widgets/src/components/header/undo-redo/undo.js +++ b/packages/edit-widgets/src/components/header/undo-redo/undo.js @@ -2,20 +2,23 @@ * WordPress dependencies */ import { __, isRTL } from '@wordpress/i18n'; -import { ToolbarButton } from '@wordpress/components'; +import { Button } 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'; +import { forwardRef } from '@wordpress/element'; -export default function UndoButton() { +function UndoButton( props, ref ) { const hasUndo = useSelect( ( select ) => select( coreStore ).hasUndo(), [] ); const { undo } = useDispatch( coreStore ); return ( - ); } + +export default forwardRef( UndoButton ); From cd548afcc23253e4be4b5ca382d3f4b19b9267f4 Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:17:40 +0100 Subject: [PATCH 14/34] Block Bindings API: Add block bindings PHP registration mechanisms and "Post meta" source under the experimental flag (#57249) * Change block bindings experiment name * Remove old custom fields UI * Add block bindings PHP logic * Add pattern source * Add post meta source * Adapt partially synced patterns experiment * Add domain to the translations in sources * Remove unused post_meta attrs * Change variable name * Remove a couple of comments * Add image alt to permitted attributes * Rename variables and clean up code * Fix bug wherein original pattern values were not being displayed * fix typo * Format a comment * fix comment indentation * remove the .vscode/settings.json file that sneaked in * Sync with added support for multiple attributes in patterns * Remove block supports requirement for partial syncing * Remove __experimentalBlockBindings supports from paragraph * Simplify source callback and update comments * Remove erroneous comment * Revert "Remove block supports requirement for partial syncing" This reverts commit 7fb646cae16f1703cfdee25a7f8629e6b020fec3. * Revert "Remove __experimentalBlockBindings supports from paragraph" This reverts commit 34e3f29078e9f9196befda3ded01a200509266d5. * Remove block supports dependency * Update signature and docblock of register function * Empty-Commit * Update the PHPDoc string --------- Co-authored-by: Ricardo Artemio Morales Co-authored-by: Michal Czaplinski --- lib/block-supports/pattern.php | 2 +- .../block-bindings/html-processing.php | 110 ++++++++++++ lib/experimental/block-bindings/index.php | 20 +++ .../block-bindings/sources/index.php | 32 ++++ .../block-bindings/sources/pattern.php | 21 +++ .../block-bindings/sources/post-meta.php | 25 +++ lib/experimental/blocks.php | 170 ++++++++---------- lib/experimental/connection-sources/index.php | 23 --- lib/experimental/editor-settings.php | 4 +- lib/experiments-page.php | 6 +- .../block-editor/src/hooks/custom-fields.js | 115 ------------ packages/block-editor/src/hooks/index.js | 2 - packages/block-library/src/block/edit.js | 14 +- .../block-library/src/paragraph/block.json | 1 - .../src/hooks/pattern-partial-syncing.js | 7 - .../components/partial-syncing-controls.js | 44 +++-- 16 files changed, 317 insertions(+), 279 deletions(-) create mode 100644 lib/experimental/block-bindings/html-processing.php create mode 100644 lib/experimental/block-bindings/index.php create mode 100644 lib/experimental/block-bindings/sources/index.php create mode 100644 lib/experimental/block-bindings/sources/pattern.php create mode 100644 lib/experimental/block-bindings/sources/post-meta.php delete mode 100644 lib/experimental/connection-sources/index.php delete mode 100644 packages/block-editor/src/hooks/custom-fields.js diff --git a/lib/block-supports/pattern.php b/lib/block-supports/pattern.php index a783135c793e3..2f7717091ff50 100644 --- a/lib/block-supports/pattern.php +++ b/lib/block-supports/pattern.php @@ -13,7 +13,7 @@ * @param WP_Block_Type $block_type Block Type. */ function gutenberg_register_pattern_support( $block_type ) { - $pattern_support = property_exists( $block_type, 'supports' ) ? _wp_array_get( $block_type->supports, array( '__experimentalConnections' ), false ) : false; + $pattern_support = 'core/paragraph' === $block_type->name ? true : false; if ( $pattern_support ) { if ( ! $block_type->uses_context ) { diff --git a/lib/experimental/block-bindings/html-processing.php b/lib/experimental/block-bindings/html-processing.php new file mode 100644 index 0000000000000..515749d0a8e75 --- /dev/null +++ b/lib/experimental/block-bindings/html-processing.php @@ -0,0 +1,110 @@ +get_registered( $block_name ); + if ( null === $block_type ) { + return; + } + + // Depending on the attribute source, the processing will be different. + switch ( $block_type->attributes[ $block_attr ]['source'] ) { + case 'html': + case 'rich-text': + $block_reader = new WP_HTML_Tag_Processor( $block_content ); + + // TODO: Support for CSS selectors whenever they are ready in the HTML API. + // In the meantime, support comma-separated selectors by exploding them into an array. + $selectors = explode( ',', $block_type->attributes[ $block_attr ]['selector'] ); + // Add a bookmark to the first tag to be able to iterate over the selectors. + $block_reader->next_tag(); + $block_reader->set_bookmark( 'iterate-selectors' ); + + // TODO: This shouldn't be needed when the `set_inner_html` function is ready. + // Store the parent tag and its attributes to be able to restore them later in the button. + // The button block has a wrapper while the paragraph and heading blocks don't. + if ( 'core/button' === $block_name ) { + $button_wrapper = $block_reader->get_tag(); + $button_wrapper_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $button_wrapper_attrs = array(); + foreach ( $button_wrapper_attribute_names as $name ) { + $button_wrapper_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + } + + foreach ( $selectors as $selector ) { + // If the parent tag, or any of its children, matches the selector, replace the HTML. + if ( strcasecmp( $block_reader->get_tag( $selector ), $selector ) === 0 || $block_reader->next_tag( + array( + 'tag_name' => $selector, + ) + ) ) { + $block_reader->release_bookmark( 'iterate-selectors' ); + + // TODO: Use `set_inner_html` method whenever it's ready in the HTML API. + // Until then, it is hardcoded for the paragraph, heading, and button blocks. + // Store the tag and its attributes to be able to restore them later. + $selector_attribute_names = $block_reader->get_attribute_names_with_prefix( '' ); + $selector_attrs = array(); + foreach ( $selector_attribute_names as $name ) { + $selector_attrs[ $name ] = $block_reader->get_attribute( $name ); + } + $selector_markup = "<$selector>" . esc_html( $source_value ) . ""; + $amended_content = new WP_HTML_Tag_Processor( $selector_markup ); + $amended_content->next_tag(); + foreach ( $selector_attrs as $attribute_key => $attribute_value ) { + $amended_content->set_attribute( $attribute_key, $attribute_value ); + } + if ( 'core/paragraph' === $block_name || 'core/heading' === $block_name ) { + return $amended_content->get_updated_html(); + } + if ( 'core/button' === $block_name ) { + $button_markup = "<$button_wrapper>{$amended_content->get_updated_html()}"; + $amended_button = new WP_HTML_Tag_Processor( $button_markup ); + $amended_button->next_tag(); + foreach ( $button_wrapper_attrs as $attribute_key => $attribute_value ) { + $amended_button->set_attribute( $attribute_key, $attribute_value ); + } + return $amended_button->get_updated_html(); + } + } else { + $block_reader->seek( 'iterate-selectors' ); + } + } + $block_reader->release_bookmark( 'iterate-selectors' ); + return $block_content; + + case 'attribute': + $amended_content = new WP_HTML_Tag_Processor( $block_content ); + if ( ! $amended_content->next_tag( + array( + // TODO: build the query from CSS selector. + 'tag_name' => $block_type->attributes[ $block_attr ]['selector'], + ) + ) ) { + return $block_content; + } + $amended_content->set_attribute( $block_type->attributes[ $block_attr ]['attribute'], esc_attr( $source_value ) ); + return $amended_content->get_updated_html(); + break; + + default: + return $block_content; + break; + } + return; + } +} diff --git a/lib/experimental/block-bindings/index.php b/lib/experimental/block-bindings/index.php new file mode 100644 index 0000000000000..cca857e93702f --- /dev/null +++ b/lib/experimental/block-bindings/index.php @@ -0,0 +1,20 @@ + $label, + 'apply' => $apply, + ); + } +} diff --git a/lib/experimental/block-bindings/sources/pattern.php b/lib/experimental/block-bindings/sources/pattern.php new file mode 100644 index 0000000000000..e3456aa468d3e --- /dev/null +++ b/lib/experimental/block-bindings/sources/pattern.php @@ -0,0 +1,21 @@ +attributes, array( 'metadata', 'id' ), false ) ) { + return null; + } + $block_id = $block_instance->attributes['metadata']['id']; + return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, $attribute_name ), null ); + }; + register_block_bindings_source( + 'pattern_attributes', + __( 'Pattern Attributes', 'gutenberg' ), + $pattern_source_callback + ); +} diff --git a/lib/experimental/block-bindings/sources/post-meta.php b/lib/experimental/block-bindings/sources/post-meta.php new file mode 100644 index 0000000000000..99b6afc03c0d4 --- /dev/null +++ b/lib/experimental/block-bindings/sources/post-meta.php @@ -0,0 +1,25 @@ +context['postId'] but it wasn't available in the image block. + $post_id = get_the_ID(); + } + + return get_post_meta( $post_id, $source_attrs['value'], true ); + }; + register_block_bindings_source( + 'post_meta', + __( 'Post Meta', 'gutenberg' ), + $post_meta_source_callback + ); +} diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index d4bb6c9b4586e..42663e127870c 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -83,118 +83,90 @@ function wp_enqueue_block_view_script( $block_name, $args ) { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && ( - array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) || + array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) || array_key_exists( 'gutenberg-pattern-partial-syncing', $gutenberg_experiments ) ) ) { - /** - * Renders the block meta attributes. - * - * @param string $block_content Block Content. - * @param array $block Block attributes. - * @param WP_Block $block_instance The block instance. - */ - function gutenberg_render_block_connections( $block_content, $block, $block_instance ) { - $connection_sources = require __DIR__ . '/connection-sources/index.php'; - $block_type = $block_instance->block_type; - - // Allowlist of blocks that support block connections. - // Currently, we only allow the following blocks and attributes: - // - Paragraph: content. - // - Image: url. - $blocks_attributes_allowlist = array( - 'core/paragraph' => array( 'content' ), - 'core/image' => array( 'url' ), - ); - - // Whitelist of the block types that support block connections. - // Currently, we only allow the Paragraph and Image blocks to use block connections. - if ( ! in_array( $block['blockName'], array_keys( $blocks_attributes_allowlist ), true ) ) { - return $block_content; - } - - // If for some reason, the block type is not found, skip it. - if ( null === $block_type ) { - return $block_content; - } - - // If the block does not have support for block connections, skip it. - if ( ! block_has_support( $block_type, array( '__experimentalConnections' ), false ) ) { - return $block_content; - } - - // Get all the attributes that have a connection. - $connected_attributes = $block['attrs']['connections']['attributes'] ?? false; - if ( ! $connected_attributes ) { - return $block_content; - } - - foreach ( $connected_attributes as $attribute_name => $attribute_value ) { - - // If the attribute is not in the allowlist, skip it. - if ( ! in_array( $attribute_name, $blocks_attributes_allowlist[ $block['blockName'] ], true ) ) { - continue; - } - - // Skip if the source value is not "meta_fields" or "pattern_attributes". - if ( 'meta_fields' !== $attribute_value['source'] && 'pattern_attributes' !== $attribute_value['source'] ) { - continue; - } - // If the attribute does not have a source, skip it. - if ( ! isset( $block_type->attributes[ $attribute_name ]['source'] ) ) { - continue; + require_once __DIR__ . '/block-bindings/index.php'; + // Allowed blocks that support block bindings. + // TODO: Look for a mechanism to opt-in for this. Maybe adding a property to block attributes? + global $block_bindings_allowed_blocks; + $block_bindings_allowed_blocks = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text' ), + ); + if ( ! function_exists( 'process_block_bindings' ) ) { + /** + * Process the block bindings attribute. + * + * @param string $block_content Block Content. + * @param array $block Block attributes. + * @param WP_Block $block_instance The block instance. + */ + function process_block_bindings( $block_content, $block, $block_instance ) { + // If the block doesn't have the bindings property, return. + if ( ! isset( $block['attrs']['metadata']['bindings'] ) ) { + return $block_content; } - if ( 'pattern_attributes' === $attribute_value['source'] ) { - if ( ! _wp_array_get( $block_instance->attributes, array( 'metadata', 'id' ), false ) ) { + // Assuming the following format for the bindings property of the "metadata" attribute: + // + // "bindings": { + // "title": { + // "source": { + // "name": "post_meta", + // "attributes": { "value": "text_custom_field" } + // } + // }, + // "url": { + // "source": { + // "name": "post_meta", + // "attributes": { "value": "text_custom_field" } + // } + // } + // } + // + global $block_bindings_allowed_blocks; + global $block_bindings_sources; + $modified_block_content = $block_content; + foreach ( $block['attrs']['metadata']['bindings'] as $binding_attribute => $binding_source ) { + // If the block is not in the list, stop processing. + if ( ! isset( $block_bindings_allowed_blocks[ $block['blockName'] ] ) ) { + return $block_content; + } + // If the attribute is not in the list, process next attribute. + if ( ! in_array( $binding_attribute, $block_bindings_allowed_blocks[ $block['blockName'] ], true ) ) { continue; } - - $custom_value = $connection_sources[ $attribute_value['source'] ]( $block_instance, $attribute_name ); - } else { - // If the attribute does not specify the name of the custom field, skip it. - if ( ! isset( $attribute_value['value'] ) ) { + // If no source is provided, or that source is not registered, process next attribute. + if ( ! isset( $binding_source['source'] ) || ! isset( $binding_source['source']['name'] ) || ! isset( $block_bindings_sources[ $binding_source['source']['name'] ] ) ) { continue; } - // Get the content from the connection source. - $custom_value = $connection_sources[ $attribute_value['source'] ]( - $block_instance, - $attribute_value['value'] - ); - } - - if ( false === $custom_value ) { - continue; - } + $source_callback = $block_bindings_sources[ $binding_source['source']['name'] ]['apply']; + // Get the value based on the source. + if ( ! isset( $binding_source['source']['attributes'] ) ) { + $source_args = array(); + } else { + $source_args = $binding_source['source']['attributes']; + } + $source_value = $source_callback( $source_args, $block_instance, $binding_attribute ); + // If the value is null, process next attribute. + if ( is_null( $source_value ) ) { + continue; + } - $tags = new WP_HTML_Tag_Processor( $block_content ); - $found = $tags->next_tag( - array( - // TODO: In the future, when blocks other than Paragraph and Image are - // supported, we should build the full query from CSS selector. - 'tag_name' => $block_type->attributes[ $attribute_name ]['selector'], - ) - ); - if ( ! $found ) { - return $block_content; + // Process the HTML based on the block and the attribute. + $modified_block_content = block_bindings_replace_html( $modified_block_content, $block['blockName'], $binding_attribute, $source_value ); } - $tag_name = $tags->get_tag(); - $markup = "<$tag_name>$custom_value"; - $updated_tags = new WP_HTML_Tag_Processor( $markup ); - $updated_tags->next_tag(); - - // Get all the attributes from the original block and add them to the new markup. - $names = $tags->get_attribute_names_with_prefix( '' ); - foreach ( $names as $name ) { - $updated_tags->set_attribute( $name, $tags->get_attribute( $name ) ); - } - - return $updated_tags->get_updated_html(); + return $modified_block_content; } - return $block_content; + // Add filter only to the blocks in the list. + foreach ( $block_bindings_allowed_blocks as $block_name => $attributes ) { + add_filter( 'render_block_' . $block_name, 'process_block_bindings', 20, 3 ); + } } - - add_filter( 'render_block', 'gutenberg_render_block_connections', 10, 3 ); } diff --git a/lib/experimental/connection-sources/index.php b/lib/experimental/connection-sources/index.php deleted file mode 100644 index 4f9e06cb13b94..0000000000000 --- a/lib/experimental/connection-sources/index.php +++ /dev/null @@ -1,23 +0,0 @@ - 'meta', - 'meta_fields' => function ( $block_instance, $meta_field ) { - // We should probably also check if the meta field exists but for now it's okay because - // if it doesn't, `get_post_meta()` will just return an empty string. - return get_post_meta( $block_instance->context['postId'], $meta_field, true ); - }, - 'pattern_attributes' => function ( $block_instance, $attribute_name ) { - $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( - $block_instance->context, - array( 'pattern/overrides', $block_id, $attribute_name ), - false - ); - }, -); diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 5f61684e8b134..729376cf030dd 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -26,8 +26,8 @@ function gutenberg_enable_experiments() { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGroupGridVariation = true', 'before' ); } - if ( $gutenberg_experiments && array_key_exists( 'gutenberg-connections', $gutenberg_experiments ) ) { - wp_add_inline_script( 'wp-block-editor', 'window.__experimentalConnections = true', 'before' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-bindings', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalBlockBindings = true', 'before' ); } if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 2d2e76273d2d5..8af1eb82c6bed 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -128,13 +128,13 @@ function gutenberg_initialize_experiments_settings() { add_settings_field( 'gutenberg-custom-fields', - __( 'Connections', 'gutenberg' ), + __( 'Block Bindings & Custom Fields', 'gutenberg' ), 'gutenberg_display_experiment_field', 'gutenberg-experiments', 'gutenberg_experiments_section', array( - 'label' => __( 'Test connecting block attribute values to a custom field value', 'gutenberg' ), - 'id' => 'gutenberg-connections', + 'label' => __( 'Test connecting block attributes to different sources like custom fields', 'gutenberg' ), + 'id' => 'gutenberg-block-bindings', ) ); diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js deleted file mode 100644 index 9b677933adc13..0000000000000 --- a/packages/block-editor/src/hooks/custom-fields.js +++ /dev/null @@ -1,115 +0,0 @@ -/** - * WordPress dependencies - */ -import { addFilter } from '@wordpress/hooks'; -import { PanelBody, TextControl } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { hasBlockSupport } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { InspectorControls } from '../components'; -import { useBlockEditingMode } from '../components/block-editing-mode'; - -/** - * Filters registered block settings, extending attributes to include `connections`. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addAttribute( settings ) { - if ( hasBlockSupport( settings, '__experimentalConnections', true ) ) { - // Gracefully handle if settings.attributes.connections is undefined. - settings.attributes = { - ...settings.attributes, - connections: { - type: 'object', - }, - }; - } - - return settings; -} - -function CustomFieldsControlPure( { name, connections, setAttributes } ) { - const blockEditingMode = useBlockEditingMode(); - if ( blockEditingMode !== 'default' ) { - return null; - } - - // If the block is a paragraph or image block, we need to know which - // attribute to use for the connection. Only the `content` attribute - // of the paragraph block and the `url` attribute of the image block are supported. - let attributeName; - if ( name === 'core/paragraph' ) attributeName = 'content'; - if ( name === 'core/image' ) attributeName = 'url'; - - return ( - - - { - if ( nextValue === '' ) { - setAttributes( { - connections: undefined, - [ attributeName ]: undefined, - placeholder: undefined, - } ); - } else { - setAttributes( { - connections: { - attributes: { - // The attributeName will be either `content` or `url`. - [ attributeName ]: { - // Source will be variable, could be post_meta, user_meta, term_meta, etc. - // Could even be a custom source like a social media attribute. - source: 'meta_fields', - value: nextValue, - }, - }, - }, - [ attributeName ]: undefined, - placeholder: sprintf( - 'This content will be replaced on the frontend by the value of "%s" custom field.', - nextValue - ), - } ); - } - } } - /> - - - ); -} - -export default { - edit: CustomFieldsControlPure, - attributeKeys: [ 'connections' ], - hasSupport( name ) { - return ( - hasBlockSupport( name, '__experimentalConnections', false ) && - // Check if the current block is a paragraph or image block. - // Currently, only these two blocks are supported. - [ 'core/paragraph', 'core/image' ].includes( name ) - ); - }, -}; - -if ( - window.__experimentalConnections || - window.__experimentalPatternPartialSyncing -) { - addFilter( - 'blocks.registerBlockType', - 'core/editor/connections/attribute', - addAttribute - ); -} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 385b9fe6b1511..f17c0a22166e4 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -25,7 +25,6 @@ import layout from './layout'; import childLayout from './layout-child'; import contentLockUI from './content-lock-ui'; import './metadata'; -import customFields from './custom-fields'; import blockHooks from './block-hooks'; import blockRenaming from './block-renaming'; @@ -39,7 +38,6 @@ createBlockEditFilter( position, layout, contentLockUI, - window.__experimentalConnections ? customFields : null, blockHooks, blockRenaming, ].filter( Boolean ) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 57db2d166f9f9..6331d33c27a7b 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -27,7 +27,7 @@ import { store as blockEditorStore, BlockControls, } from '@wordpress/block-editor'; -import { getBlockSupport, parse, cloneBlock } from '@wordpress/blocks'; +import { parse, cloneBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -38,17 +38,17 @@ const { useLayoutClasses } = unlock( blockEditorPrivateApis ); function isPartiallySynced( block ) { return ( - !! getBlockSupport( block.name, '__experimentalConnections', false ) && - !! block.attributes.connections?.attributes && - Object.values( block.attributes.connections.attributes ).some( - ( connection ) => connection.source === 'pattern_attributes' + 'core/paragraph' === block.name && + !! block.attributes.metadata?.bindings && + Object.values( block.attributes.metadata.bindings ).some( + ( binding ) => binding.source.name === 'pattern_attributes' ) ); } function getPartiallySyncedAttributes( block ) { - return Object.entries( block.attributes.connections.attributes ) + return Object.entries( block.attributes.metadata.bindings ) .filter( - ( [ , connection ] ) => connection.source === 'pattern_attributes' + ( [ , binding ] ) => binding.source.name === 'pattern_attributes' ) .map( ( [ attributeKey ] ) => attributeKey ); } diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 3fe4fbb34e102..a81d754d8ca1b 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -41,7 +41,6 @@ "text": true } }, - "__experimentalConnections": true, "spacing": { "margin": true, "padding": true, diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-partial-syncing.js index 40bd1e16dfc00..a940890dfa693 100644 --- a/packages/editor/src/hooks/pattern-partial-syncing.js +++ b/packages/editor/src/hooks/pattern-partial-syncing.js @@ -5,7 +5,6 @@ import { addFilter } from '@wordpress/hooks'; import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useBlockEditingMode } from '@wordpress/block-editor'; -import { hasBlockSupport } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; /** @@ -32,11 +31,6 @@ const { const withPartialSyncingControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const blockEditingMode = useBlockEditingMode(); - const hasCustomFieldsSupport = hasBlockSupport( - props.name, - '__experimentalConnections', - false - ); const isEditingPattern = useSelect( ( select ) => select( editorStore ).getCurrentPostType() === @@ -45,7 +39,6 @@ const withPartialSyncingControls = createHigherOrderComponent( ); const shouldShowPartialSyncingControls = - hasCustomFieldsSupport && props.isSelected && isEditingPattern && blockEditingMode === 'default' && diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js index d20bd1d347012..f5ac19bc05f3d 100644 --- a/packages/patterns/src/components/partial-syncing-controls.js +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -19,7 +19,7 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; const attributeSources = Object.keys( syncedAttributes ).map( ( attributeName ) => - attributes.connections?.attributes?.[ attributeName ]?.source + attributes.metadata?.bindings?.[ attributeName ]?.source?.name ); const isConnectedToOtherSources = attributeSources.every( ( source ) => source && source !== 'pattern_attributes' @@ -30,52 +30,58 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { return null; } - function updateConnections( isChecked ) { - let updatedConnections = { - ...attributes.connections, - attributes: { ...attributes.connections?.attributes }, + function updateBindings( isChecked ) { + let updatedBindings = { + ...attributes?.metadata?.bindings, }; if ( ! isChecked ) { for ( const attributeName of Object.keys( syncedAttributes ) ) { if ( - updatedConnections.attributes[ attributeName ]?.source === + updatedBindings[ attributeName ]?.source?.name === 'pattern_attributes' ) { - delete updatedConnections.attributes[ attributeName ]; + delete updatedBindings[ attributeName ]; } } - if ( ! Object.keys( updatedConnections.attributes ).length ) { - delete updatedConnections.attributes; - } - if ( ! Object.keys( updatedConnections ).length ) { - updatedConnections = undefined; + if ( ! Object.keys( updatedBindings ).length ) { + updatedBindings = undefined; } setAttributes( { - connections: updatedConnections, + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, } ); return; } for ( const attributeName of Object.keys( syncedAttributes ) ) { - if ( ! updatedConnections.attributes[ attributeName ] ) { - updatedConnections.attributes[ attributeName ] = { - source: 'pattern_attributes', + if ( ! updatedBindings[ attributeName ] ) { + updatedBindings[ attributeName ] = { + source: { + name: 'pattern_attributes', + }, }; } } if ( typeof attributes.metadata?.id === 'string' ) { - setAttributes( { connections: updatedConnections } ); + setAttributes( { + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, + } ); return; } const id = nanoid( 6 ); setAttributes( { - connections: updatedConnections, metadata: { ...attributes.metadata, id, + bindings: updatedBindings, }, } ); } @@ -93,7 +99,7 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { ( source ) => source === 'pattern_attributes' ) } onChange={ ( isChecked ) => { - updateConnections( isChecked ); + updateBindings( isChecked ); } } /> From ea90b753197c6cc39a400b31bd68e5b4d648ccd4 Mon Sep 17 00:00:00 2001 From: Takashi Kitajima Date: Fri, 12 Jan 2024 01:12:27 +0900 Subject: [PATCH 15/34] Display guidance text when there is no font (#56825) * #56734 When there is no font, the border should not appear. Display further guidance text. * update text * Fix formatting in font-families.js * Improve readability --------- Co-authored-by: Matias Benedetto Co-authored-by: Sarah Norris --- .../components/global-styles/font-families.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-families.js b/packages/edit-site/src/components/global-styles/font-families.js index 06bf595328332..7a7597878eacd 100644 --- a/packages/edit-site/src/components/global-styles/font-families.js +++ b/packages/edit-site/src/components/global-styles/font-families.js @@ -26,6 +26,8 @@ function FontFamilies() { const { modalTabOpen, toggleModal, themeFonts, customFonts } = useContext( FontLibraryContext ); + const hasFonts = 0 < customFonts.length || 0 < themeFonts.length; + return ( <> { !! modalTabOpen && ( @@ -51,14 +53,18 @@ function FontFamilies() { - - { customFonts.map( ( font ) => ( - - ) ) } - { themeFonts.map( ( font ) => ( - - ) ) } - + { hasFonts ? ( + + { customFonts.map( ( font ) => ( + + ) ) } + { themeFonts.map( ( font ) => ( + + ) ) } + + ) : ( + <>{ __( 'No fonts installed.' ) } + ) } ); From 4f97a3fcebffb0eb5ce18ae3bc0182a66f0e03fa Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Thu, 11 Jan 2024 17:48:41 +0100 Subject: [PATCH 16/34] ColorPicker: store internal HSLA state for better slider UX (#57555) * ColorPicker: store internal HSLA state for better slider UX * CHANGELOG * Comments * Memoize `colorPropHSL`, rename to `colorPropHSLA` * Add unit test * Set internal SLA only when not calling onChange to reduce visual glitches * More test --- packages/components/CHANGELOG.md | 1 + .../components/src/color-picker/hsl-input.tsx | 86 +++++--- .../src/color-picker/test/index.tsx | 206 ++++++++++++++++-- 3 files changed, 247 insertions(+), 46 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index a8dd57900cfae..5856259e47730 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -23,6 +23,7 @@ ### Enhancements +- `ColorPicker`: improve the UX around HSL sliders ([#57555](https://github.com/WordPress/gutenberg/pull/57555)). - Update `ariakit` to version `0.3.10` ([#57325](https://github.com/WordPress/gutenberg/pull/57325)). - Update `@ariakit/react` to version `0.3.12` and @ariakit/test to version `0.3.7` ([#57547](https://github.com/WordPress/gutenberg/pull/57547)). - `DropdownMenuV2`: do not collapse suffix width ([#57238](https://github.com/WordPress/gutenberg/pull/57238)). diff --git a/packages/components/src/color-picker/hsl-input.tsx b/packages/components/src/color-picker/hsl-input.tsx index 3331a97b3d4de..8d2b0c7c44489 100644 --- a/packages/components/src/color-picker/hsl-input.tsx +++ b/packages/components/src/color-picker/hsl-input.tsx @@ -3,6 +3,11 @@ */ import { colord } from 'colord'; +/** + * WordPress dependencies + */ +import { useState, useEffect, useMemo } from '@wordpress/element'; + /** * Internal dependencies */ @@ -10,7 +15,49 @@ import { InputWithSlider } from './input-with-slider'; import type { HslInputProps } from './types'; export const HslInput = ( { color, onChange, enableAlpha }: HslInputProps ) => { - const { h, s, l, a } = color.toHsl(); + const colorPropHSLA = useMemo( () => color.toHsl(), [ color ] ); + + const [ internalHSLA, setInternalHSLA ] = useState( { ...colorPropHSLA } ); + + const isInternalColorSameAsReceivedColor = color.isEqual( + colord( internalHSLA ) + ); + + useEffect( () => { + if ( ! isInternalColorSameAsReceivedColor ) { + // Keep internal HSLA color up to date with the received color prop + setInternalHSLA( colorPropHSLA ); + } + }, [ colorPropHSLA, isInternalColorSameAsReceivedColor ] ); + + // If the internal color is equal to the received color prop, we can use the + // HSLA values from the local state which, compared to the received color prop, + // retain more details about the actual H and S values that the user selected, + // and thus allow for better UX when interacting with the H and S sliders. + const colorValue = isInternalColorSameAsReceivedColor + ? internalHSLA + : colorPropHSLA; + + const updateHSLAValue = ( + partialNewValue: Partial< typeof colorPropHSLA > + ) => { + const nextOnChangeValue = colord( { + ...colorValue, + ...partialNewValue, + } ); + + // Fire `onChange` only if the resulting color is different from the + // current one. + // Otherwise, update the internal HSLA color to cause a re-render. + if ( ! color.isEqual( nextOnChangeValue ) ) { + onChange( nextOnChangeValue ); + } else { + setInternalHSLA( ( prevHSLA ) => ( { + ...prevHSLA, + ...partialNewValue, + } ) ); + } + }; return ( <> @@ -19,9 +66,9 @@ export const HslInput = ( { color, onChange, enableAlpha }: HslInputProps ) => { max={ 359 } label="Hue" abbreviation="H" - value={ h } + value={ colorValue.h } onChange={ ( nextH: number ) => { - onChange( colord( { h: nextH, s, l, a } ) ); + updateHSLAValue( { h: nextH } ); } } /> { max={ 100 } label="Saturation" abbreviation="S" - value={ s } + value={ colorValue.s } onChange={ ( nextS: number ) => { - onChange( - colord( { - h, - s: nextS, - l, - a, - } ) - ); + updateHSLAValue( { s: nextS } ); } } /> { max={ 100 } label="Lightness" abbreviation="L" - value={ l } + value={ colorValue.l } onChange={ ( nextL: number ) => { - onChange( - colord( { - h, - s, - l: nextL, - a, - } ) - ); + updateHSLAValue( { l: nextL } ); } } /> { enableAlpha && ( @@ -64,16 +97,9 @@ export const HslInput = ( { color, onChange, enableAlpha }: HslInputProps ) => { max={ 100 } label="Alpha" abbreviation="A" - value={ Math.trunc( 100 * a ) } + value={ Math.trunc( 100 * colorValue.a ) } onChange={ ( nextA: number ) => { - onChange( - colord( { - h, - s, - l, - a: nextA / 100, - } ) - ); + updateHSLAValue( { a: nextA / 100 } ); } } /> ) } diff --git a/packages/components/src/color-picker/test/index.tsx b/packages/components/src/color-picker/test/index.tsx index 8d584d626487a..98e059d5994de 100644 --- a/packages/components/src/color-picker/test/index.tsx +++ b/packages/components/src/color-picker/test/index.tsx @@ -1,13 +1,19 @@ /** * External dependencies */ -import { screen, render } from '@testing-library/react'; +import { fireEvent, screen, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + /** * Internal dependencies */ import { ColorPicker } from '..'; +import { click } from '@ariakit/test'; const hslaMatcher = expect.objectContaining( { h: expect.any( Number ), @@ -133,20 +139,39 @@ describe( 'ColorPicker', () => { } ); } ); - describe.each( [ - [ 'hue', 'Hue', '#aad52a' ], - [ 'saturation', 'Saturation', '#20dfdf' ], - [ 'lightness', 'Lightness', '#95eaea' ], - ] )( 'HSL inputs', ( colorInput, inputLabel, expected ) => { - it( `should fire onChange with the correct value when the ${ colorInput } value is updated`, async () => { + describe( 'HSL inputs', () => { + it( 'sliders should use accurate H and S values based on user interaction when possible', async () => { const user = userEvent.setup(); const onChange = jest.fn(); - const color = '#2ad5d5'; + + const ControlledColorPicker = ( { + onChange: onChangeProp, + ...restProps + }: React.ComponentProps< typeof ColorPicker > ) => { + const [ colorState, setColorState ] = useState( '#000000' ); + + const internalOnChange: typeof onChangeProp = ( newColor ) => { + onChangeProp?.( newColor ); + setColorState( newColor ); + }; + + return ( + <> + + + + ); + }; render( - ); @@ -156,16 +181,165 @@ describe( 'ColorPicker', () => { await user.selectOptions( formatSelector, 'hsl' ); - const inputElement = screen.getByRole( 'spinbutton', { - name: inputLabel, + const hueSliders = screen.getAllByRole( 'slider', { + name: 'Hue', } ); - expect( inputElement ).toBeVisible(); + expect( hueSliders ).toHaveLength( 2 ); - await user.clear( inputElement ); - await user.type( inputElement, '75' ); + // Reason for the `!` post-fix expression operator: if the check above + // doesn't fail, we're guaranteed that `hueSlider` is not undefined. + const hueSlider = hueSliders.at( -1 )!; + const saturationSlider = screen.getByRole( 'slider', { + name: 'Saturation', + } ); + const lightnessSlider = screen.getByRole( 'slider', { + name: 'Lightness', + } ); + const hueNumberInput = screen.getByRole( 'spinbutton', { + name: 'Hue', + } ); + const saturationNumberInput = screen.getByRole( 'spinbutton', { + name: 'Saturation', + } ); + const lightnessNumberInput = screen.getByRole( 'spinbutton', { + name: 'Lightness', + } ); + + // All initial inputs should have a value of `0` since the color is black. + expect( hueSlider ).toHaveValue( '0' ); + expect( saturationSlider ).toHaveValue( '0' ); + expect( lightnessSlider ).toHaveValue( '0' ); + expect( hueNumberInput ).toHaveValue( 0 ); + expect( saturationNumberInput ).toHaveValue( 0 ); + expect( lightnessNumberInput ).toHaveValue( 0 ); + + // Interact with the Hue slider, it should change its value (and the + // value of the associated number input), but it shouldn't cause the + // `onChange` callback to fire, since the resulting color is still black. + fireEvent.change( hueSlider, { target: { value: 80 } } ); + + expect( hueSlider ).toHaveValue( '80' ); + expect( hueNumberInput ).toHaveValue( 80 ); + expect( onChange ).not.toHaveBeenCalled(); + + // Interact with the Saturation slider, it should change its value (and the + // value of the associated number input), but it shouldn't cause the + // `onChange` callback to fire, since the resulting color is still black. + fireEvent.change( saturationSlider, { target: { value: 50 } } ); + + expect( saturationSlider ).toHaveValue( '50' ); + expect( saturationNumberInput ).toHaveValue( 50 ); + expect( onChange ).not.toHaveBeenCalled(); + + // Interact with the Lightness slider, it should change its value (and the + // value of the associated number input). It should also cause the + // `onChange` callback to fire, since changing the lightness actually + // causes the color to change. + fireEvent.change( lightnessSlider, { target: { value: 30 } } ); + await waitFor( () => + expect( lightnessSlider ).toHaveValue( '30' ) + ); + expect( lightnessNumberInput ).toHaveValue( 30 ); + expect( onChange ).toHaveBeenCalledTimes( 1 ); + expect( onChange ).toHaveBeenLastCalledWith( '#597326' ); + + // Interact with the Lightness slider, setting to 100 (ie. white). + // It should also cause the `onChange` callback to fire, and reset the + // hue and saturation inputs to `0`. + fireEvent.change( lightnessSlider, { target: { value: 100 } } ); + + await waitFor( () => + expect( lightnessSlider ).toHaveValue( '100' ) + ); + expect( lightnessNumberInput ).toHaveValue( 100 ); + expect( hueSlider ).toHaveValue( '0' ); + expect( saturationSlider ).toHaveValue( '0' ); + expect( hueNumberInput ).toHaveValue( 0 ); + expect( saturationNumberInput ).toHaveValue( 0 ); + expect( onChange ).toHaveBeenCalledTimes( 2 ); + expect( onChange ).toHaveBeenLastCalledWith( '#ffffff' ); + + // Interact with the Hue slider, it should change its value (and the + // value of the associated number input), but it shouldn't cause the + // `onChange` callback to fire, since the resulting color is still white. + fireEvent.change( hueSlider, { target: { value: 147 } } ); + + expect( hueSlider ).toHaveValue( '147' ); + expect( hueNumberInput ).toHaveValue( 147 ); + expect( onChange ).toHaveBeenCalledTimes( 2 ); + + // Interact with the Saturation slider, it should change its value (and the + // value of the associated number input), but it shouldn't cause the + // `onChange` callback to fire, since the resulting color is still white. + fireEvent.change( saturationSlider, { target: { value: 82 } } ); + + expect( saturationSlider ).toHaveValue( '82' ); + expect( saturationNumberInput ).toHaveValue( 82 ); + expect( onChange ).toHaveBeenCalledTimes( 2 ); + + // Interact with the Lightness slider, it should change its value (and the + // value of the associated number input). It should also cause the + // `onChange` callback to fire, since changing the lightness actually + // causes the color to change. + fireEvent.change( lightnessSlider, { target: { value: 14 } } ); + + await waitFor( () => + expect( lightnessSlider ).toHaveValue( '14' ) + ); + expect( lightnessNumberInput ).toHaveValue( 14 ); expect( onChange ).toHaveBeenCalledTimes( 3 ); - expect( onChange ).toHaveBeenLastCalledWith( expected ); + expect( onChange ).toHaveBeenLastCalledWith( '#064121' ); + + // Set the color externally. All inputs should update to match the H/S/L + // value of the new color. + const setColorButton = screen.getByRole( 'button', { + name: /set color/i, + } ); + await click( setColorButton ); + + expect( hueSlider ).toHaveValue( '208' ); + expect( hueNumberInput ).toHaveValue( 208 ); + expect( saturationSlider ).toHaveValue( '44' ); + expect( saturationNumberInput ).toHaveValue( 44 ); + expect( lightnessSlider ).toHaveValue( '52' ); + expect( lightnessNumberInput ).toHaveValue( 52 ); + } ); + + describe.each( [ + [ 'hue', 'Hue', '#aad52a' ], + [ 'saturation', 'Saturation', '#20dfdf' ], + [ 'lightness', 'Lightness', '#95eaea' ], + ] )( 'HSL inputs', ( colorInput, inputLabel, expected ) => { + it( `should fire onChange with the correct value when the ${ colorInput } value is updated`, async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + const color = '#2ad5d5'; + + render( + + ); + + const formatSelector = screen.getByRole( 'combobox' ); + expect( formatSelector ).toBeVisible(); + + await user.selectOptions( formatSelector, 'hsl' ); + + const inputElement = screen.getByRole( 'spinbutton', { + name: inputLabel, + } ); + expect( inputElement ).toBeVisible(); + + await user.clear( inputElement ); + await user.type( inputElement, '75' ); + + expect( onChange ).toHaveBeenCalledTimes( 3 ); + expect( onChange ).toHaveBeenLastCalledWith( expected ); + } ); } ); } ); } ); From 50ac86a8d530c8673e9ce5f3eeadbca437c40d98 Mon Sep 17 00:00:00 2001 From: Jason Crist Date: Thu, 11 Jan 2024 12:06:31 -0500 Subject: [PATCH 17/34] Download then upload font face assets when installing from a collection (#57694) * Download then upload font face assets when installing from a collection * Add error handling to font download-then-upload * Check for downloadFromUrl property before downloading to upload * Simplify downloadFontFaceAsset and move to utils. * Add error handling to downloadFontFaceAsset * Use Promise.all to handle multiple asynchronous font face downloads. > > Co-authored-by: Sarah Norris --------- Co-authored-by: Jeff Ong Co-authored-by: Sarah Norris --- .../font-library-modal/font-collection.js | 30 +++++++++++++++- .../font-library-modal/utils/index.js | 35 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js index fc39e2e009653..f7f33032f1e3f 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/font-collection.js @@ -31,6 +31,7 @@ import { toggleFont } from './utils/toggleFont'; import { getFontsOutline } from './utils/fonts-outline'; import GoogleFontsConfirmDialog from './google-fonts-confirm-dialog'; import { getNoticeFromInstallResponse } from './utils/get-notice-from-response'; +import { downloadFontFaceAsset } from './utils'; const DEFAULT_CATEGORY = { id: 'all', @@ -154,7 +155,34 @@ function FontCollection( { id } ) { }; const handleInstall = async () => { - const response = await installFont( fontsToInstall[ 0 ] ); + const fontFamily = fontsToInstall[ 0 ]; + + try { + if ( fontFamily?.fontFace ) { + await Promise.all( + fontFamily.fontFace.map( async ( fontFace ) => { + if ( fontFace.downloadFromUrl ) { + fontFace.file = await downloadFontFaceAsset( + fontFace.downloadFromUrl + ); + delete fontFace.downloadFromUrl; + } + } ) + ); + } + } catch ( error ) { + // If any of the fonts fail to download, + // show an error notice and stop the request from being sent. + setNotice( { + type: 'error', + message: __( + 'Error installing the fonts, could not be downloaded.' + ), + } ); + return; + } + + const response = await installFont( fontFamily ); const installNotice = getNoticeFromInstallResponse( response ); setNotice( installNotice ); resetFontsToInstall(); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index 2874dd446efb4..0aa0f7edb4aec 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -9,6 +9,11 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { FONT_WEIGHTS, FONT_STYLES } from './constants'; import { unlock } from '../../../../lock-unlock'; +/** + * Browser dependencies + */ +const { File } = window; + export function setUIValuesNeeded( font, extraValues = {} ) { if ( ! font.name && ( font.fontFamily || font.slug ) ) { font.name = font.fontFamily || font.slug; @@ -164,3 +169,33 @@ export function makeFormDataFromFontFamily( fontFamily ) { formData.append( 'font_family_settings', JSON.stringify( newFontFamily ) ); return formData; } + +/* + * Downloads a font face asset from a URL to the client and returns a File object. + */ +export async function downloadFontFaceAsset( url ) { + return fetch( new Request( url ) ) + .then( ( response ) => { + if ( ! response.ok ) { + throw new Error( + `Error downloading font face asset from ${ url }. Server responded with status: ${ response.status }` + ); + } + return response.blob(); + } ) + .then( ( blob ) => { + const filename = url.split( '/' ).pop(); + const file = new File( [ blob ], filename, { + type: blob.type, + } ); + return file; + } ) + .catch( ( error ) => { + // eslint-disable-next-line no-console + console.error( + `Error downloading font face asset from ${ url }:`, + error + ); + throw error; + } ); +} From 7dd920589ab5abe5ec7a53e4b5c98a17add9cc4a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 11 Jan 2024 17:13:07 +0000 Subject: [PATCH 18/34] Fix: Typos on __unstableSetTemporarilyEditingAsBlocks documentation. (#57768) --- packages/block-editor/src/store/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index da9beb0ba73a9..4b396045a73c2 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -1679,13 +1679,13 @@ export function setBlockVisibility( updates ) { } /** - * Action that sets whether a block is being temporaritly edited as blocks. + * Action that sets whether a block is being temporarily edited as blocks. * * DO-NOT-USE in production. * This action is created for internal/experimental only usage and may be * removed anytime without any warning, causing breakage on any plugin or theme invoking it. * - * @param {?string} temporarilyEditingAsBlocks The block's clientId being temporaritly edited as blocks. + * @param {?string} temporarilyEditingAsBlocks The block's clientId being temporarily edited as blocks. */ export function __unstableSetTemporarilyEditingAsBlocks( temporarilyEditingAsBlocks From 167c439e2f899337ee510d35b05b23b97b8aa091 Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:19:15 +0000 Subject: [PATCH 19/34] Scripts: Remove unused variable in bin/list-experimental-api-matches.sh (#57771) --- bin/list-experimental-api-matches.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/list-experimental-api-matches.sh b/bin/list-experimental-api-matches.sh index 156464c4e7375..d9399e63e5cf6 100755 --- a/bin/list-experimental-api-matches.sh +++ b/bin/list-experimental-api-matches.sh @@ -31,7 +31,7 @@ namespace() { awk -F: ' { print module($1), $2 } function module(path) { - n = split(path, parts, "/") + split(path, parts, "/") if (parts[1] == "lib") return "lib" return parts[1] "/" parts[2] }' From f154dc7f2cdec6d6253132e388cf60a64692ee9a Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Thu, 11 Jan 2024 17:31:52 +0000 Subject: [PATCH 20/34] Add: Bulk actions to dataviews with the new design. (#57255) * Add: Bulk actions to dataviews with the new design. * post rebase fixes. * Add missing secondary variant * Move bulk actions button position * Don't render bulk actions functionality when there are no bulk actions avaliable. * Fix button padding * Fix focus loss * fix flex shring * Fix focus issues * remove data items meanwhile removed from the selection * remove line accidently added * Fix vertical alignment post rebase. * Fix unrequired decodeEntities usage. * Apply feedback to the promises * lint fix * Feedback application * Labels object instead of function * Introduce removeTemplates action * fix css rebase issue * lint fixes. * remove labels * simplify logic condition * fix some classnames * multiple small changes of feedback * remove getItemTitle * Inspect Promise.allSettled result * typo * case fixing * update patterns actions * Added a selection mark * lint fixes * Style adjustments --------- Co-authored-by: James Koster --- packages/dataviews/src/bulk-actions.js | 187 ++++++++++++++++++ packages/dataviews/src/dataviews.js | 31 ++- packages/dataviews/src/item-actions.js | 6 +- packages/dataviews/src/style.scss | 56 +++++- packages/dataviews/src/view-table.js | 146 +++++++++++++- .../edit-site/src/components/actions/index.js | 21 +- .../dataviews-pattern-actions.js | 17 +- .../src/components/page-templates/index.js | 6 +- .../page-templates/template-actions.js | 114 +++++++---- packages/edit-site/src/store/actions.js | 56 +----- .../edit-site/src/store/private-actions.js | 103 ++++++++++ 11 files changed, 628 insertions(+), 115 deletions(-) create mode 100644 packages/dataviews/src/bulk-actions.js diff --git a/packages/dataviews/src/bulk-actions.js b/packages/dataviews/src/bulk-actions.js new file mode 100644 index 0000000000000..9fd9f628286e0 --- /dev/null +++ b/packages/dataviews/src/bulk-actions.js @@ -0,0 +1,187 @@ +/** + * WordPress dependencies + */ +import { + privateApis as componentsPrivateApis, + Button, + Modal, +} from '@wordpress/components'; +import { __, sprintf, _n } from '@wordpress/i18n'; +import { useMemo, useState, useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { unlock } from './lock-unlock'; + +const { + DropdownMenuV2: DropdownMenu, + DropdownMenuGroupV2: DropdownMenuGroup, + DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuSeparatorV2: DropdownMenuSeparator, +} = unlock( componentsPrivateApis ); + +function ActionWithModal( { + action, + selectedItems, + setActionWithModal, + onMenuOpenChange, +} ) { + const eligibleItems = useMemo( () => { + return selectedItems.filter( ( item ) => action.isEligible( item ) ); + }, [ action, selectedItems ] ); + const { RenderModal, hideModalHeader } = action; + const onCloseModal = useCallback( () => { + setActionWithModal( undefined ); + }, [ setActionWithModal ] ); + return ( + + onMenuOpenChange( false ) } + /> + + ); +} + +function BulkActionItem( { action, selectedItems, setActionWithModal } ) { + const eligibleItems = useMemo( () => { + return selectedItems.filter( ( item ) => action.isEligible( item ) ); + }, [ action, selectedItems ] ); + + const shouldShowModal = !! action.RenderModal; + + return ( + { + if ( shouldShowModal ) { + setActionWithModal( action ); + } else { + await action.callback( eligibleItems ); + } + } } + suffix={ + eligibleItems.length > 0 ? eligibleItems.length : undefined + } + > + { action.label } + + ); +} + +function ActionsMenuGroup( { actions, selectedItems, setActionWithModal } ) { + return ( + <> + + { actions.map( ( action ) => ( + + ) ) } + + + + ); +} + +export default function BulkActions( { + data, + actions, + selection, + onSelectionChange, + getItemId, +} ) { + const bulkActions = useMemo( + () => actions.filter( ( action ) => action.supportsBulk ), + [ actions ] + ); + const areAllSelected = selection && selection.length === data.length; + const [ isMenuOpen, onMenuOpenChange ] = useState( false ); + const [ actionWithModal, setActionWithModal ] = useState(); + const selectedItems = useMemo( () => { + return data.filter( ( item ) => + selection.includes( getItemId( item ) ) + ); + }, [ selection, data, getItemId ] ); + + if ( bulkActions.length === 0 ) { + return null; + } + return ( + <> + + { selection.length + ? sprintf( + /* translators: %d: Number of items. */ + _n( + 'Edit %d item', + 'Edit %d items', + selection.length + ), + selection.length + ) + : __( 'Bulk edit' ) } + + } + > + + + { + onSelectionChange( data ); + } } + suffix={ data.length } + > + { __( 'Select all' ) } + + { + onSelectionChange( [] ); + } } + > + { __( 'Deselect' ) } + + + + { actionWithModal && ( + + ) } + + ); +} diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 61837e4f8fc96..64a70d46c7d12 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -5,7 +5,7 @@ import { __experimentalVStack as VStack, __experimentalHStack as HStack, } from '@wordpress/components'; -import { useMemo, useState, useCallback } from '@wordpress/element'; +import { useMemo, useState, useCallback, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -14,7 +14,8 @@ import Pagination from './pagination'; import ViewActions from './view-actions'; import Filters from './filters'; import Search from './search'; -import { VIEW_LAYOUTS } from './constants'; +import { VIEW_LAYOUTS, LAYOUT_TABLE } from './constants'; +import BulkActions from './bulk-actions'; const defaultGetItemId = ( item ) => item.id; const defaultOnSelectionChange = () => {}; @@ -37,6 +38,23 @@ export default function DataViews( { } ) { const [ selection, setSelection ] = useState( [] ); + useEffect( () => { + if ( + selection.length > 0 && + selection.some( + ( id ) => ! data.some( ( item ) => item.id === id ) + ) + ) { + const newSelection = selection.filter( ( id ) => + data.some( ( item ) => item.id === id ) + ); + setSelection( newSelection ); + onSelectionChange( + data.filter( ( item ) => newSelection.includes( item.id ) ) + ); + } + }, [ selection, data, onSelectionChange ] ); + const onSetSelection = useCallback( ( items ) => { setSelection( items.map( ( item ) => item.id ) ); @@ -75,6 +93,15 @@ export default function DataViews( { onChangeView={ onChangeView } /> + { view.type === LAYOUT_TABLE && ( + + ) } setIsModalOpen( false ) } /> @@ -96,7 +96,7 @@ function ActionsDropdownMenuGroup( { actions, item } ) { action.callback( item ) } + onClick={ () => action.callback( [ item ] ) } /> ); } ) } @@ -157,7 +157,7 @@ export default function ItemActions( { item, actions, isCompact } ) { action.callback( item ) } + onClick={ () => action.callback( [ item ] ) } /> ); } ) } diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 80630050b68ef..d934ea0df62d0 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -18,6 +18,10 @@ } } +.dataviews-filters__view-actions.components-h-stack { + align-items: center; +} + .dataviews-filters-button { position: relative; } @@ -81,6 +85,14 @@ &[data-field-id="actions"] { text-align: right; } + + &.dataviews-view-table__checkbox-column { + padding-right: 0; + } + + .components-checkbox-control__input-container { + margin: $grid-unit-05; + } } tr { border-bottom: 1px solid $gray-100; @@ -109,8 +121,32 @@ } &:hover { - td { - background-color: #f8f8f8; + background-color: #f8f8f8; + } + + .components-checkbox-control__input { + opacity: 0; + + &:checked, + &:indeterminate, + &:focus { + opacity: 1; + } + } + + &:focus-within, + &:hover { + .components-checkbox-control__input { + opacity: 1; + } + } + + &.is-selected { + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.04); + color: $gray-700; + + &:hover { + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); } } } @@ -373,7 +409,23 @@ padding: 0 $grid-unit-40; } +.dataviews-view-table-selection-checkbox label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + .dataviews-filters__custom-menu-radio-item-prefix { display: block; width: 24px; } + +.dataviews-bulk-edit-button.components-button { + flex-shrink: 0; +} diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index dc76572e30494..e59c4e001919c 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -1,18 +1,19 @@ /** * External dependencies */ -import classNames from 'classnames'; +import classnames from 'classnames'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { useAsyncList } from '@wordpress/compose'; import { unseen, funnel } from '@wordpress/icons'; import { Button, Icon, privateApis as componentsPrivateApis, + CheckboxControl, } from '@wordpress/components'; import { Children, @@ -306,6 +307,80 @@ function WithSeparators( { children } ) { ) ); } +function BulkSelectionCheckbox( { selection, onSelectionChange, data } ) { + const areAllSelected = selection.length === data.length; + return ( + { + if ( areAllSelected ) { + onSelectionChange( [] ); + } else { + onSelectionChange( data ); + } + } } + label={ areAllSelected ? __( 'Deselect all' ) : __( 'Select all' ) } + /> + ); +} + +function SingleSelectionCheckbox( { + selection, + onSelectionChange, + item, + data, + getItemId, + primaryField, +} ) { + const id = getItemId( item ); + const isSelected = selection.includes( id ); + let selectionLabel; + if ( primaryField?.getValue && item ) { + // eslint-disable-next-line @wordpress/valid-sprintf + selectionLabel = sprintf( + /* translators: %s: item title. */ + isSelected ? __( 'Deselect item: %s' ) : __( 'Select item: %s' ), + primaryField.getValue( { item } ) + ); + } else { + selectionLabel = isSelected + ? __( 'Select a new item' ) + : __( 'Deselect item' ); + } + return ( + { + if ( ! isSelected ) { + onSelectionChange( + data.filter( ( _item ) => { + const itemId = getItemId?.( _item ); + return ( + itemId === id || selection.includes( itemId ) + ); + } ) + ); + } else { + onSelectionChange( + data.filter( ( _item ) => { + const itemId = getItemId?.( _item ); + return ( + itemId !== id && selection.includes( itemId ) + ); + } ) + ); + } + } } + /> + ); +} + function ViewTable( { view, onChangeView, @@ -315,7 +390,10 @@ function ViewTable( { getItemId, isLoading = false, deferredRendering, + selection, + onSelectionChange, } ) { + const hasBulkActions = actions?.some( ( action ) => action.supportsBulk ); const headerMenuRefs = useRef( new Map() ); const headerMenuToFocusRef = useRef(); const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = useState(); @@ -348,14 +426,16 @@ function ViewTable( { const visibleFields = fields.filter( ( field ) => ! view.hiddenFields.includes( field.id ) && - ! [ view.layout.mediaField, view.layout.primaryField ].includes( - field.id - ) + ! [ view.layout.mediaField ].includes( field.id ) ); const usedData = deferredRendering ? asyncData : data; const hasData = !! usedData?.length; const sortValues = { asc: 'ascending', desc: 'descending' }; + const primaryField = fields.find( + ( field ) => field.id === view.layout.primaryField + ); + return (
- + + { hasBulkActions && ( + + ) } { visibleFields.map( ( field, index ) => ( { hasData && - usedData.map( ( item ) => ( - + usedData.map( ( item, index ) => ( + + { hasBulkActions && ( + + ) } { visibleFields.map( ( field ) => (
+ +
+ +
{ + RenderModal: ( { items: posts, closeModal } ) => { + // Todo - handle multiple posts + const post = posts[ 0 ]; const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); const { deleteEntityRecord } = useDispatch( coreStore ); @@ -109,7 +111,9 @@ export function usePermanentlyDeletePostAction() { isEligible( { status } ) { return status === 'trash'; }, - async callback( post ) { + async callback( posts ) { + // Todo - handle multiple posts + const post = posts[ 0 ]; try { await deleteEntityRecord( 'postType', @@ -160,7 +164,9 @@ export function useRestorePostAction() { isEligible( { status } ) { return status === 'trash'; }, - async callback( post ) { + async callback( posts ) { + // Todo - handle multiple posts + const post = posts[ 0 ]; await editEntityRecord( 'postType', post.type, post.id, { status: 'draft', } ); @@ -211,7 +217,8 @@ export const viewPostAction = { isEligible( post ) { return post.status !== 'trash'; }, - callback( post ) { + callback( posts ) { + const post = posts[ 0 ]; document.location.href = post.link; }, }; @@ -225,7 +232,8 @@ export function useEditPostAction() { isEligible( { status } ) { return status !== 'trash'; }, - callback( post ) { + callback( posts ) { + const post = posts[ 0 ]; history.push( { postId: post.id, postType: post.type, @@ -250,7 +258,8 @@ export const postRevisionsAction = { post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; return lastRevisionId && revisionsCount > 1; }, - callback( post ) { + callback( posts ) { + const post = posts[ 0 ]; const href = addQueryArgs( 'revision.php', { revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id, } ); diff --git a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js index bf5210beb49fb..0c44c996ed373 100644 --- a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js +++ b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js @@ -45,7 +45,7 @@ export const exportJSONaction = { id: 'export-pattern', label: __( 'Export as JSON' ), isEligible: ( item ) => item.type === PATTERN_TYPES.user, - callback: ( item ) => { + callback: ( [ item ] ) => { const json = { __file: item.type, title: item.title || item.name, @@ -71,7 +71,8 @@ export const renameAction = { const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file; return isCustomPattern && ! hasThemeFile; }, - RenderModal: ( { item, closeModal } ) => { + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; const [ title, setTitle ] = useState( () => item.title ); const { editEntityRecord, saveEditedEntityRecord } = useDispatch( coreStore ); @@ -160,7 +161,8 @@ export const deleteAction = { return canDeleteOrReset( item ) && ! hasThemeFile; }, hideModalHeader: true, - RenderModal: ( { item, closeModal } ) => { + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; const { __experimentalDeleteReusableBlock } = useDispatch( reusableBlocksStore ); const { createErrorNotice, createSuccessNotice } = @@ -224,7 +226,8 @@ export const resetAction = { return canDeleteOrReset( item ) && hasThemeFile; }, hideModalHeader: true, - RenderModal: ( { item, closeModal } ) => { + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; const { removeTemplate } = useDispatch( editSiteStore ); return ( @@ -254,7 +257,8 @@ export const duplicatePatternAction = { label: _x( 'Duplicate', 'action label' ), isEligible: ( item ) => item.type !== TEMPLATE_PART_POST_TYPE, modalHeader: _x( 'Duplicate pattern', 'action label' ), - RenderModal: ( { item, closeModal } ) => { + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs( window.location.href ); @@ -288,7 +292,8 @@ export const duplicateTemplatePartAction = { label: _x( 'Duplicate', 'action label' ), isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE, modalHeader: _x( 'Duplicate template part', 'action label' ), - RenderModal: ( { item, closeModal } ) => { + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; const { createSuccessNotice } = useDispatch( noticesStore ); const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs( window.location.href diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index c0e0289311db6..ddc48542ee1b7 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -65,7 +65,9 @@ const { useHistory } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: {}, + [ LAYOUT_TABLE ]: { + primaryField: 'title', + }, [ LAYOUT_GRID ]: { mediaField: 'preview', primaryField: 'title', @@ -84,7 +86,7 @@ const DEFAULT_VIEW = { // All fields are visible by default, so it's // better to keep track of the hidden ones. hiddenFields: [ 'preview' ], - layout: {}, + layout: defaultConfigPerViewType[ LAYOUT_TABLE ], filters: [], }; diff --git a/packages/edit-site/src/components/page-templates/template-actions.js b/packages/edit-site/src/components/page-templates/template-actions.js index 9f5897e31fb93..7029d464ca867 100644 --- a/packages/edit-site/src/components/page-templates/template-actions.js +++ b/packages/edit-site/src/components/page-templates/template-actions.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { backup, trash } from '@wordpress/icons'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, sprintf, _n } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; import { useMemo, useState } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; @@ -19,6 +19,7 @@ import { /** * Internal dependencies */ +import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; import isTemplateRevertable from '../../utils/is-template-revertable'; import isTemplateRemovable from '../../utils/is-template-removable'; @@ -32,39 +33,64 @@ export function useResetTemplateAction() { return useMemo( () => ( { id: 'reset-template', - label: __( 'Reset template' ), + label: __( 'Reset' ), isPrimary: true, icon: backup, isEligible: isTemplateRevertable, - async callback( template ) { + supportsBulk: true, + async callback( templates ) { try { - await revertTemplate( template, { allowUndo: false } ); - await saveEditedEntityRecord( - 'postType', - template.type, - template.id - ); + for ( const template of templates ) { + await revertTemplate( template, { + allowUndo: false, + } ); + await saveEditedEntityRecord( + 'postType', + template.type, + template.id + ); + } createSuccessNotice( - sprintf( - /* translators: The template/part's name. */ - __( '"%s" reverted.' ), - decodeEntities( template.title.rendered ) - ), + templates.length > 1 + ? sprintf( + /* translators: The number of items. */ + __( '%s items reverted.' ), + templates.length + ) + : sprintf( + /* translators: The template/part's name. */ + __( '"%s" reverted.' ), + decodeEntities( + templates[ 0 ].title.rendered + ) + ), { type: 'snackbar', id: 'edit-site-template-reverted', } ); } catch ( error ) { - const fallbackErrorMessage = - template.type === TEMPLATE_POST_TYPE - ? __( - 'An error occurred while reverting the template.' - ) - : __( - 'An error occurred while reverting the template part.' - ); + let fallbackErrorMessage; + if ( templates[ 0 ].type === TEMPLATE_POST_TYPE ) { + fallbackErrorMessage = + templates.length === 1 + ? __( + 'An error occurred while reverting the template.' + ) + : __( + 'An error occurred while reverting the templates.' + ); + } else { + fallbackErrorMessage = + templates.length === 1 + ? __( + 'An error occurred while reverting the template part.' + ) + : __( + 'An error occurred while reverting the template parts.' + ); + } const errorMessage = error.message && error.code !== 'unknown_error' ? error.message @@ -85,21 +111,34 @@ export function useResetTemplateAction() { export const deleteTemplateAction = { id: 'delete-template', - label: __( 'Delete template' ), + label: __( 'Delete' ), isPrimary: true, icon: trash, isEligible: isTemplateRemovable, + supportsBulk: true, hideModalHeader: true, - RenderModal: ( { item: template, closeModal } ) => { - const { removeTemplate } = useDispatch( editSiteStore ); + RenderModal: ( { items: templates, closeModal, onPerform } ) => { + const { removeTemplates } = unlock( useDispatch( editSiteStore ) ); return ( - { sprintf( - // translators: %s: The template or template part's title. - __( 'Are you sure you want to delete "%s"?' ), - decodeEntities( template.title.rendered ) - ) } + { templates.length > 1 + ? sprintf( + // translators: %d: number of items to delete. + _n( + 'Delete %d item?', + 'Delete %d items?', + templates.length + ), + templates.length + ) + : sprintf( + // translators: %s: The template or template part's titles + __( 'Delete "%s"?' ), + decodeEntities( + templates?.[ 0 ]?.title?.rendered + ) + ) } @@ -126,7 +169,8 @@ export const renameTemplateAction = { label: __( 'Rename' ), isEligible: ( template ) => isTemplateRemovable( template ) && template.is_custom, - RenderModal: ( { item: template, closeModal } ) => { + RenderModal: ( { items: templates, closeModal } ) => { + const template = templates[ 0 ]; const title = decodeEntities( template.title.rendered ); const [ editedTitle, setEditedTitle ] = useState( title ); const { diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 5a8adad8e198b..e7f2671784e1d 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -5,7 +5,7 @@ import apiFetch from '@wordpress/api-fetch'; import { parse, __unstableSerializeAndClean } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; import { addQueryArgs } from '@wordpress/url'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; @@ -13,7 +13,6 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as editorStore } from '@wordpress/editor'; import { speak } from '@wordpress/a11y'; import { store as preferencesStore } from '@wordpress/preferences'; -import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -25,6 +24,8 @@ import { TEMPLATE_PART_POST_TYPE, NAVIGATION_POST_TYPE, } from '../utils/constants'; +import { removeTemplates } from './private-actions'; + /** * Dispatches an action that toggles a feature flag. * @@ -133,54 +134,9 @@ export const addTemplate = * * @param {Object} template The template object. */ -export const removeTemplate = - ( template ) => - async ( { registry } ) => { - try { - await registry - .dispatch( coreStore ) - .deleteEntityRecord( 'postType', template.type, template.id, { - force: true, - } ); - - const lastError = registry - .select( coreStore ) - .getLastEntityDeleteError( - 'postType', - template.type, - template.id - ); - - if ( lastError ) { - throw lastError; - } - - // Depending on how the entity was retrieved it's title might be - // an object or simple string. - const templateTitle = - typeof template.title === 'string' - ? template.title - : template.title?.rendered; - - registry.dispatch( noticesStore ).createSuccessNotice( - sprintf( - /* translators: The template/part's name. */ - __( '"%s" deleted.' ), - decodeEntities( templateTitle ) - ), - { type: 'snackbar', id: 'site-editor-template-deleted-success' } - ); - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( 'An error occurred while deleting the template.' ); - - registry - .dispatch( noticesStore ) - .createErrorNotice( errorMessage, { type: 'snackbar' } ); - } - }; +export const removeTemplate = ( template ) => { + return removeTemplates( [ template ] ); +}; /** * Action that sets a template part. diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index 7354f7b9b8843..71f35dc66399e 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -4,6 +4,10 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as preferencesStore } from '@wordpress/preferences'; import { store as editorStore } from '@wordpress/editor'; +import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; +import { __, sprintf } from '@wordpress/i18n'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Action that switches the canvas mode. @@ -49,3 +53,102 @@ export const setEditorCanvasContainerView = view, } ); }; + +/** + * Action that removes an array of templates. + * + * @param {Array} templates An array of template objects to remove. + */ +export const removeTemplates = + ( templates ) => + async ( { registry } ) => { + const promiseResult = await Promise.allSettled( + templates.map( ( template ) => { + return registry + .dispatch( coreStore ) + .deleteEntityRecord( + 'postType', + template.type, + template.id, + { force: true }, + { throwOnError: true } + ); + } ) + ); + + // If all the promises were fulfilled with sucess. + if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) { + let successMessage; + + if ( templates.length === 1 ) { + // Depending on how the entity was retrieved its title might be + // an object or simple string. + const templateTitle = + typeof templates[ 0 ].title === 'string' + ? templates[ 0 ].title + : templates[ 0 ].title?.rendered; + successMessage = sprintf( + /* translators: The template/part's name. */ + __( '"%s" deleted.' ), + decodeEntities( templateTitle ) + ); + } else { + successMessage = __( 'Templates deleted.' ); + } + + registry + .dispatch( noticesStore ) + .createSuccessNotice( successMessage, { + type: 'snackbar', + id: 'site-editor-template-deleted-success', + } ); + } else { + // If there was at lease one failure. + let errorMessage; + // If we were trying to delete a single template. + if ( promiseResult.length === 1 ) { + if ( promiseResult[ 0 ].reason?.message ) { + errorMessage = promiseResult[ 0 ].reason.message; + } else { + errorMessage = __( + 'An error occurred while deleting the template.' + ); + } + // If we were trying to delete a multiple templates + } else { + const errorMessages = new Set(); + const failedPromises = promiseResult.filter( + ( { status } ) => status === 'rejected' + ); + for ( const failedPromise of failedPromises ) { + if ( failedPromise.reason?.message ) { + errorMessages.add( failedPromise.reason.message ); + } + } + if ( errorMessages.size === 0 ) { + errorMessage = __( + 'An error occurred while deleting the templates.' + ); + } else if ( errorMessages.size === 1 ) { + errorMessage = sprintf( + /* translators: %s: an error message */ + __( + 'An error occurred while deleting the templates: %s' + ), + [ ...errorMessages ][ 0 ] + ); + } else { + errorMessage = sprintf( + /* translators: %s: a list of comma separated error messages */ + __( + 'Some errors occurred while deleting the templates: %s' + ), + [ ...errorMessages ].join( ',' ) + ); + } + } + registry + .dispatch( noticesStore ) + .createErrorNotice( errorMessage, { type: 'snackbar' } ); + } + }; From 512027e45181778e508aea99abfafc2254700962 Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Fri, 12 Jan 2024 09:01:03 +1300 Subject: [PATCH 21/34] Patterns: Add black border back when editing synced pattern in the post editor (#57631) --- .../block-editor/src/components/block-canvas/style.scss | 9 ++++++++- packages/editor/src/components/editor-canvas/style.scss | 5 ----- packages/editor/src/style.scss | 1 - 3 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 packages/editor/src/components/editor-canvas/style.scss diff --git a/packages/block-editor/src/components/block-canvas/style.scss b/packages/block-editor/src/components/block-canvas/style.scss index 54ccd407d74a2..631024b7c3aec 100644 --- a/packages/block-editor/src/components/block-canvas/style.scss +++ b/packages/block-editor/src/components/block-canvas/style.scss @@ -1,6 +1,13 @@ iframe[name="editor-canvas"] { width: 100%; height: 100%; - background-color: $white; display: block; } + +iframe[name="editor-canvas"]:not(.has-history) { + background-color: $white; +} + +iframe[name="editor-canvas"].has-history { + padding: $grid-unit-60 $grid-unit-60 0; +} diff --git a/packages/editor/src/components/editor-canvas/style.scss b/packages/editor/src/components/editor-canvas/style.scss deleted file mode 100644 index d5baf48012452..0000000000000 --- a/packages/editor/src/components/editor-canvas/style.scss +++ /dev/null @@ -1,5 +0,0 @@ -.editor-canvas__iframe { - &.has-history { - padding: $grid-unit-60 $grid-unit-60 0; - } -} diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index ff5a55a3881f9..09e50d1abed79 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -29,4 +29,3 @@ @import "./components/preview-dropdown/style.scss"; @import "./components/table-of-contents/style.scss"; @import "./components/template-validation-notice/style.scss"; -@import "./components/editor-canvas/style.scss"; From 8ef45064a7ee07c2bea0ce32cc0f54eae430355f Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Thu, 11 Jan 2024 15:25:25 -0500 Subject: [PATCH 22/34] Components: replace `TabPanel` with `Tabs` in the Editor Preferences Modal (#57293) * add support for private apis * implement Tabs * update styles * update code comment * classnames rather than role selectors * remove style nesting --- package-lock.json | 2 + packages/interface/lock-unlock.js | 10 +++ packages/interface/package.json | 1 + .../preferences-modal-tabs/index.js | 60 +++++++++++----- .../preferences-modal-tabs/style.scss | 69 +++++++++---------- packages/private-apis/src/implementation.js | 1 + 6 files changed, 89 insertions(+), 54 deletions(-) create mode 100644 packages/interface/lock-unlock.js diff --git a/package-lock.json b/package-lock.json index 96a14ec8eeb50..dc84ae2fba494 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55297,6 +55297,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/plugins": "file:../plugins", "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/viewport": "file:../viewport", "classnames": "^2.3.1" }, @@ -70104,6 +70105,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/plugins": "file:../plugins", "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/viewport": "file:../viewport", "classnames": "^2.3.1" } diff --git a/packages/interface/lock-unlock.js b/packages/interface/lock-unlock.js new file mode 100644 index 0000000000000..b6e29bb71c7c0 --- /dev/null +++ b/packages/interface/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', + '@wordpress/interface' + ); diff --git a/packages/interface/package.json b/packages/interface/package.json index df3d53990e0f5..7356a7b52d1d4 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -42,6 +42,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/plugins": "file:../plugins", "@wordpress/preferences": "file:../preferences", + "@wordpress/private-apis": "file:../private-apis", "@wordpress/viewport": "file:../viewport", "classnames": "^2.3.1" }, diff --git a/packages/interface/src/components/preferences-modal-tabs/index.js b/packages/interface/src/components/preferences-modal-tabs/index.js index bc8f7358b834d..985d963227257 100644 --- a/packages/interface/src/components/preferences-modal-tabs/index.js +++ b/packages/interface/src/components/preferences-modal-tabs/index.js @@ -13,15 +13,22 @@ import { __experimentalText as Text, __experimentalTruncate as Truncate, FlexItem, - TabPanel, Card, CardHeader, CardBody, + privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { useMemo, useCallback, useState } from '@wordpress/element'; +import { useMemo, useState } from '@wordpress/element'; import { chevronLeft, chevronRight, Icon } from '@wordpress/icons'; import { isRTL, __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { unlock } from '../../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); + const PREFERENCES_MENU = 'preferences-menu'; export default function PreferencesModalTabs( { sections } ) { @@ -32,7 +39,7 @@ export default function PreferencesModalTabs( { sections } ) { const [ activeMenu, setActiveMenu ] = useState( PREFERENCES_MENU ); /** * Create helper objects from `sections` for easier data handling. - * `tabs` is used for creating the `TabPanel` and `sectionsContentMap` + * `tabs` is used for creating the `Tabs` and `sectionsContentMap` * is used for easier access to active tab's content. */ const { tabs, sectionsContentMap } = useMemo( () => { @@ -53,26 +60,41 @@ export default function PreferencesModalTabs( { sections } ) { return mappedTabs; }, [ sections ] ); - const getCurrentTab = useCallback( - ( tab ) => sectionsContentMap[ tab.name ] || null, - [ sectionsContentMap ] - ); - let modalContent; // We render different components based on the viewport size. if ( isLargeViewport ) { modalContent = ( - - { getCurrentTab } - +
+ + + { tabs.map( ( tab ) => ( + + { tab.title } + + ) ) } + + { tabs.map( ( tab ) => ( + + { sectionsContentMap[ tab.name ] || null } + + ) ) } + +
); } else { modalContent = ( diff --git a/packages/interface/src/components/preferences-modal-tabs/style.scss b/packages/interface/src/components/preferences-modal-tabs/style.scss index 04b71f0a773a2..f598545d69c25 100644 --- a/packages/interface/src/components/preferences-modal-tabs/style.scss +++ b/packages/interface/src/components/preferences-modal-tabs/style.scss @@ -1,45 +1,44 @@ $vertical-tabs-width: 160px; -.interface-preferences__tabs { - .components-tab-panel__tabs { - position: absolute; - top: $header-height + $grid-unit-30; - // Aligns button text instead of button box. - left: $grid-unit-20; - width: $vertical-tabs-width; - - .components-tab-panel__tabs-item { - border-radius: $radius-block-ui; - font-weight: 400; - - &.is-active { - background: $gray-100; - box-shadow: none; - font-weight: 500; - } - - &.is-active::after { - content: none; - } - - &:focus:not(:disabled) { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - // Windows high contrast mode. - outline: 2px solid transparent; - } - - &:focus-visible::before { - content: none; - } - } +.interface-preferences__tabs-tablist { + position: absolute; + top: $header-height + $grid-unit-30; + // Aligns button text instead of button box. + left: $grid-unit-20; + width: $vertical-tabs-width; + +} + +.interface-preferences__tabs-tab { + border-radius: $radius-block-ui; + font-weight: 400; + + &[aria-selected="true"] { + background: $gray-100; + box-shadow: none; + font-weight: 500; + } + + &[aria-selected="true"]::after { + content: none; + } + + &[role="tab"]:focus:not(:disabled) { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + // Windows high contrast mode. + outline: 2px solid transparent; } - .components-tab-panel__tab-content { - padding-left: $grid-unit-30; - margin-left: $vertical-tabs-width; + &:focus-visible::before { + content: none; } } +.interface-preferences__tabs-tabpanel { + padding-left: $grid-unit-30; + margin-left: $vertical-tabs-width; +} + @media (max-width: #{ ($break-medium - 1) }) { // Keep the navigator component from overflowing the modal content area // to ensure that sticky position elements stick where intended. diff --git a/packages/private-apis/src/implementation.js b/packages/private-apis/src/implementation.js index 619478cf76386..4195991381d02 100644 --- a/packages/private-apis/src/implementation.js +++ b/packages/private-apis/src/implementation.js @@ -25,6 +25,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/edit-widgets', '@wordpress/editor', '@wordpress/format-library', + '@wordpress/interface', '@wordpress/patterns', '@wordpress/reusable-blocks', '@wordpress/router', From 2d5baf49f43d1cc3adae0a414aad991f9457c39f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Fri, 12 Jan 2024 00:19:38 +0100 Subject: [PATCH 23/34] Scripts: Fix misplaced ReactRefreshWebpackPlugin (#57777) Restore ReactRefreshWebpackPlugin to @wordpress/scripts webpack config. #57461 accidentally moved the plugin to the modules config. It should have remained in the scripts webpack config and not been included in the modules config. --- packages/scripts/config/webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 57bd258d32539..cc76568f6fb1b 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -380,6 +380,8 @@ const scriptConfig = { process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. new MiniCSSExtractPlugin( { filename: '[name].css' } ), + // React Fast Refresh. + hasReactFastRefresh && new ReactRefreshWebpackPlugin(), // WP_NO_EXTERNALS global variable controls whether scripts' assets get // generated, and the default externals set. ! process.env.WP_NO_EXTERNALS && @@ -423,8 +425,6 @@ if ( hasExperimentalModulesFlag ) { process.env.WP_BUNDLE_ANALYZER && new BundleAnalyzerPlugin(), // MiniCSSExtractPlugin to extract the CSS thats gets imported into JavaScript. new MiniCSSExtractPlugin( { filename: '[name].css' } ), - // React Fast Refresh. - hasReactFastRefresh && new ReactRefreshWebpackPlugin(), // WP_NO_EXTERNALS global variable controls whether scripts' assets get // generated, and the default externals set. ! process.env.WP_NO_EXTERNALS && From d439f8ba42855b5d11c8c652a63405a95c3af484 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 12 Jan 2024 10:55:26 +1100 Subject: [PATCH 24/34] Only prioritise Quote transform where relevant (#57749) * Only prioritise Quote transform where relevant * Add explanatory comment. --- .../block-transformations-menu.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js index 033201d7facad..271b7fabd5173 100644 --- a/packages/block-editor/src/components/block-switcher/block-transformations-menu.js +++ b/packages/block-editor/src/components/block-switcher/block-transformations-menu.js @@ -37,7 +37,7 @@ function useGroupedTransforms( possibleBlockTransformations ) { const priorityTextTranformsNames = Object.keys( priorityContentTranformationBlocks ); - return possibleBlockTransformations.reduce( + const groupedPossibleTransforms = possibleBlockTransformations.reduce( ( accumulator, item ) => { const { name } = item; if ( priorityTextTranformsNames.includes( name ) ) { @@ -49,6 +49,23 @@ function useGroupedTransforms( possibleBlockTransformations ) { }, { priorityTextTransformations: [], restTransformations: [] } ); + /** + * If there is only one priority text transformation and it's a Quote, + * is should move to the rest transformations. This is because Quote can + * be a container for any block type, so in multi-block selection it will + * always be suggested, even for non-text blocks. + */ + if ( + groupedPossibleTransforms.priorityTextTransformations.length === + 1 && + groupedPossibleTransforms.priorityTextTransformations[ 0 ].name === + 'core/quote' + ) { + const singleQuote = + groupedPossibleTransforms.priorityTextTransformations.pop(); + groupedPossibleTransforms.restTransformations.push( singleQuote ); + } + return groupedPossibleTransforms; }, [ possibleBlockTransformations ] ); // Order the priority text transformations. From b41c08fb3fec22743b4dcdd23a37b9e4df55b054 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Thu, 11 Jan 2024 21:04:40 -0300 Subject: [PATCH 25/34] Font Library: Add font collection JSON schema (#57736) * move font family to their on definition * adding font-collection.json schema * adding settings * updating README * update property name * resolve json schema references * throw error on definition not found * add description * disallow additional properties * fix typo in function name Co-authored-by: Jeff Ong * fix typo in function name Co-authored-by: Jeff Ong * fix wording Co-authored-by: Jeff Ong * remove unwanted comment Co-authored-by: Jeff Ong * format js --------- Co-authored-by: Jeff Ong --- bin/api-docs/gen-theme-reference.js | 40 ++++- schemas/README.md | 20 ++- schemas/json/font-collection.json | 59 +++++++ schemas/json/theme.json | 236 ++++++++++++++-------------- 4 files changed, 236 insertions(+), 119 deletions(-) create mode 100644 schemas/json/font-collection.json diff --git a/bin/api-docs/gen-theme-reference.js b/bin/api-docs/gen-theme-reference.js index f638bb708890a..0ea9e282e5463 100644 --- a/bin/api-docs/gen-theme-reference.js +++ b/bin/api-docs/gen-theme-reference.js @@ -74,6 +74,42 @@ const keys = ( maybeObject ) => { return Object.keys( maybeObject ); }; +/** + * Get definition from ref. + * + * @param {string} ref + * @return {Object} definition + * @throws {Error} If the referenced definition is not found in 'themejson.definitions'. + * + * @example + * getDefinition( '#/definitions/typographyProperties/properties/fontFamily' ) + * // returns themejson.definitions.typographyProperties.properties.fontFamily + */ +const resolveDefinitionRef = ( ref ) => { + const refParts = ref.split( '/' ); + const definition = refParts[ refParts.length - 1 ]; + if ( ! themejson.definitions[ definition ] ) { + throw new Error( `Can't resolve '${ ref }'. Definition not found` ); + } + return themejson.definitions[ definition ]; +}; + +/** + * Get properties from an array. + * + * @param {Object} items + * @return {Object} properties + */ +const getPropertiesFromArray = ( items ) => { + // if its a $ref resolve it + if ( items.$ref ) { + return resolveDefinitionRef( items.$ref ).properties; + } + + // otherwise just return the properties + return items.properties; +}; + /** * Convert settings properties to markup. * @@ -96,7 +132,9 @@ const getSettingsPropertiesMarkup = ( struct ) => { const def = 'default' in props[ key ] ? props[ key ].default : ''; const ps = props[ key ].type === 'array' - ? keys( props[ key ].items.properties ).sort().join( ', ' ) + ? keys( getPropertiesFromArray( props[ key ].items ) ) + .sort() + .join( ', ' ) : ''; markup += `| ${ key } | ${ props[ key ].type } | ${ def } | ${ ps } |\n`; } ); diff --git a/schemas/README.md b/schemas/README.md index 1b2d8992cccb9..fa695115f33ab 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -1,6 +1,6 @@ # Schemas -The collection of schemas used in WordPress, including the `theme.json` and `block.json` schemas. +The collection of schemas used in WordPress, including the `theme.json`, `block.json` and `font-collection.json` schemas. JSON schemas are used by code editors to offer tooltips, autocomplete, and validation. @@ -24,6 +24,14 @@ Or in your `theme.json`: } ``` +Or in your `font-collection.json`: + +```json +{ + "$schema": "https://schemas.wp.org/trunk/font-collection.json" +} +``` + For a specific version of the schema, replace `trunk` with `wp/X.X`: ```json @@ -56,8 +64,16 @@ To allow this you will need to: } ``` +- update your font collections's `font-collection.json` to include: + +```json +{ + "$schema": "file://{{FULL_FILE_PATH}}/schemas/json/font-collection.json" +} +``` + Be sure to replace `{{FULL_FILE_PATH}}` with the full local path to your Gutenberg repo. -With this in place you should now be able to edit either `schemas/json/theme .json` or `schemas/json/block.json` in order to see changes reflected in `theme.json` or `block.json` in your IDE. +With this in place you should now be able to edit either `schemas/json/theme .json`, `schemas/json/block.json` or `schemas/json/font-collection.json` in order to see changes reflected in `theme.json`, `block.json` or `font-collection.json` in your IDE.

Code is Poetry.

diff --git a/schemas/json/font-collection.json b/schemas/json/font-collection.json new file mode 100644 index 0000000000000..a6ca2b1412e6d --- /dev/null +++ b/schemas/json/font-collection.json @@ -0,0 +1,59 @@ +{ + "title": "JSON schema for WordPress Font Collections", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "$schema": { + "description": "JSON schema URI for font-collection.json.", + "type": "string" + }, + "version": { + "description": "Version of font-collection.json schema to use.", + "type": "integer", + "enum": [ 1 ] + }, + "font_families": { + "type": "array", + "description": "Array of font families ready to be installed", + "items": { + "type": "object", + "properties": { + "font_family_settings": { + "description": "Font family settings as in theme.json", + "allOf": [ + { "$ref": "./theme.json#/definitions/fontFamily" } + ] + }, + "categories": { + "type": "array", + "description": "Array of category slugs", + "items": { + "type": "string" + } + } + }, + "required": [ "font_family_settings" ], + "additionalProperties": false + } + }, + "categories": { + "type": "array", + "description": "Array of category objects", + "items": { + "type": "object", + "properties": { + "slug": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ "slug", "name" ], + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "required": [ "$schema", "version", "font_families" ] +} diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 9f633fc77ec75..6ae8d15df63d2 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -614,132 +614,136 @@ "description": "Font family presets for the font family selector.\nGenerates a single custom property (`--wp--preset--font-family--{slug}`) per preset value.", "type": "array", "items": { - "type": "object", - "properties": { - "name": { - "description": "Name of the font family preset, translatable.", - "type": "string" - }, - "slug": { - "description": "Kebab-case unique identifier for the font family preset.", - "type": "string" - }, - "fontFamily": { - "description": "CSS font-family value.", + "$ref": "#/definitions/fontFamily" + } + } + }, + "additionalProperties": false + } + } + }, + "fontFamily": { + "type": "object", + "description": "Font family preset", + "properties": { + "name": { + "description": "Name of the font family preset, translatable.", + "type": "string" + }, + "slug": { + "description": "Kebab-case unique identifier for the font family preset.", + "type": "string" + }, + "fontFamily": { + "description": "CSS font-family value.", + "type": "string" + }, + "preview": { + "description": "URL to a preview image of the font family.", + "type": "string" + }, + "fontFace": { + "description": "Array of font-face declarations.", + "type": "array", + "items": { + "type": "object", + "properties": { + "fontFamily": { + "description": "CSS font-family value.", + "type": "string", + "default": "" + }, + "fontStyle": { + "description": "CSS font-style value.", + "type": "string", + "default": "normal" + }, + "fontWeight": { + "description": "List of available font weights, separated by a space.", + "default": "400", + "oneOf": [ + { "type": "string" }, - "preview": { - "description": "URL to a preview image of the font family.", + { + "type": "integer" + } + ] + }, + "fontDisplay": { + "description": "CSS font-display value.", + "type": "string", + "default": "fallback", + "enum": [ + "auto", + "block", + "fallback", + "swap", + "optional" + ] + }, + "src": { + "description": "Paths or URLs to the font files.", + "oneOf": [ + { "type": "string" }, - "fontFace": { - "description": "Array of font-face declarations.", + { "type": "array", "items": { - "type": "object", - "properties": { - "fontFamily": { - "description": "CSS font-family value.", - "type": "string", - "default": "" - }, - "fontStyle": { - "description": "CSS font-style value.", - "type": "string", - "default": "normal" - }, - "fontWeight": { - "description": "List of available font weights, separated by a space.", - "default": "400", - "oneOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "fontDisplay": { - "description": "CSS font-display value.", - "type": "string", - "default": "fallback", - "enum": [ - "auto", - "block", - "fallback", - "swap", - "optional" - ] - }, - "src": { - "description": "Paths or URLs to the font files.", - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ], - "default": [] - }, - "fontStretch": { - "description": "CSS font-stretch value.", - "type": "string" - }, - "ascentOverride": { - "description": "CSS ascent-override value.", - "type": "string" - }, - "descentOverride": { - "description": "CSS descent-override value.", - "type": "string" - }, - "fontVariant": { - "description": "CSS font-variant value.", - "type": "string" - }, - "fontFeatureSettings": { - "description": "CSS font-feature-settings value.", - "type": "string" - }, - "fontVariationSettings": { - "description": "CSS font-variation-settings value.", - "type": "string" - }, - "lineGapOverride": { - "description": "CSS line-gap-override value.", - "type": "string" - }, - "sizeAdjust": { - "description": "CSS size-adjust value.", - "type": "string" - }, - "unicodeRange": { - "description": "CSS unicode-range value.", - "type": "string" - }, - "preview": { - "description": "URL to a preview image of the font face.", - "type": "string" - } - }, - "required": [ "fontFamily", "src" ], - "additionalProperties": false + "type": "string" } } - }, - "additionalProperties": false + ], + "default": [] + }, + "fontStretch": { + "description": "CSS font-stretch value.", + "type": "string" + }, + "ascentOverride": { + "description": "CSS ascent-override value.", + "type": "string" + }, + "descentOverride": { + "description": "CSS descent-override value.", + "type": "string" + }, + "fontVariant": { + "description": "CSS font-variant value.", + "type": "string" + }, + "fontFeatureSettings": { + "description": "CSS font-feature-settings value.", + "type": "string" + }, + "fontVariationSettings": { + "description": "CSS font-variation-settings value.", + "type": "string" + }, + "lineGapOverride": { + "description": "CSS line-gap-override value.", + "type": "string" + }, + "sizeAdjust": { + "description": "CSS size-adjust value.", + "type": "string" + }, + "unicodeRange": { + "description": "CSS unicode-range value.", + "type": "string" + }, + "preview": { + "description": "URL to a preview image of the font face.", + "type": "string" } - } - }, - "additionalProperties": false + }, + "required": [ "fontFamily", "src" ], + "additionalProperties": false + } } - } + }, + "additionalProperties": false }, "settingsPropertiesCustom": { "type": "object", From 3dba5c9f37cf4700252cda58f5b818215b356625 Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 12 Jan 2024 12:46:59 +1100 Subject: [PATCH 26/34] Remove the unnecessary padding from preview iframe body tags (#57748) --- packages/edit-site/src/components/revisions/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/revisions/index.js b/packages/edit-site/src/components/revisions/index.js index 3d69788089579..bb2cc8feabb93 100644 --- a/packages/edit-site/src/components/revisions/index.js +++ b/packages/edit-site/src/components/revisions/index.js @@ -78,7 +78,7 @@ function Revisions( { userConfig, blocks } ) { { // Forming a "block formatting context" to prevent margin collapsing. // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context - `.is-root-container { display: flow-root; } body { position: relative; padding: 32px; }` + `.is-root-container { display: flow-root; }` } From 9dcd4ccdee77893b29212bec42e309ea95a1152d Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:04:17 -0800 Subject: [PATCH 27/34] `BoxControl`: update design (#56665) --- packages/components/CHANGELOG.md | 1 + .../src/box-control/all-input-control.tsx | 88 +++++--- .../src/box-control/axial-input-controls.tsx | 145 ++++++------- packages/components/src/box-control/index.tsx | 99 ++++----- .../src/box-control/input-controls.tsx | 194 ++++++++++-------- .../box-control/styles/box-control-styles.ts | 82 ++------ .../styles/box-control-visualizer-styles.ts | 75 ------- .../components/src/box-control/test/index.tsx | 144 +++++++++++-- packages/components/src/box-control/types.ts | 25 +-- .../src/box-control/unit-control.tsx | 74 ------- packages/components/src/box-control/utils.ts | 51 ++++- 11 files changed, 481 insertions(+), 497 deletions(-) delete mode 100644 packages/components/src/box-control/styles/box-control-visualizer-styles.ts delete mode 100644 packages/components/src/box-control/unit-control.tsx diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 5856259e47730..646fc6eaa9dd0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -41,6 +41,7 @@ - `Tabs`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). - `Tabs`: improve hover and text alignment styles ([#57275](https://github.com/WordPress/gutenberg/pull/57275)). - `Tabs`: make sure `Tab`s are associated to the right `TabPanel`s, regardless of the order they're rendered in ([#57033](https://github.com/WordPress/gutenberg/pull/57033)). +- `BoxControl`: Update design ([#56665](https://github.com/WordPress/gutenberg/pull/56665)). ## 25.14.0 (2023-12-13) diff --git a/packages/components/src/box-control/all-input-control.tsx b/packages/components/src/box-control/all-input-control.tsx index b66e10fdb4ce3..9c18694bbd0b6 100644 --- a/packages/components/src/box-control/all-input-control.tsx +++ b/packages/components/src/box-control/all-input-control.tsx @@ -1,15 +1,25 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ import type { UnitControlProps } from '../unit-control/types'; +import { + FlexedRangeControl, + StyledUnitControl, +} from './styles/box-control-styles'; +import { HStack } from '../h-stack'; import type { BoxControlInputControlProps } from './types'; -import UnitControl from './unit-control'; +import { parseQuantityAndUnitFromRawValue } from '../unit-control'; import { LABELS, applyValueToSides, getAllValue, isValuesMixed, isValuesDefined, + CUSTOM_VALUE_SETTINGS, } from './utils'; const noop = () => {}; @@ -17,26 +27,29 @@ const noop = () => {}; export default function AllInputControl( { onChange = noop, onFocus = noop, - onHoverOn = noop, - onHoverOff = noop, values, sides, selectedUnits, setSelectedUnits, ...props }: BoxControlInputControlProps ) { + const inputId = useInstanceId( AllInputControl, 'box-control-input-all' ); + const allValue = getAllValue( values, selectedUnits, sides ); const hasValues = isValuesDefined( values ); const isMixed = hasValues && isValuesMixed( values, selectedUnits, sides ); const allPlaceholder = isMixed ? LABELS.mixed : undefined; + const [ parsedQuantity, parsedUnit ] = + parseQuantityAndUnitFromRawValue( allValue ); + const handleOnFocus: React.FocusEventHandler< HTMLInputElement > = ( event ) => { onFocus( event, { side: 'all' } ); }; - const handleOnChange: UnitControlProps[ 'onChange' ] = ( next ) => { + const onValueChange = ( next?: string ) => { const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); const nextValue = isNumeric ? next : undefined; const nextValues = applyValueToSides( values, nextValue, sides ); @@ -44,6 +57,12 @@ export default function AllInputControl( { onChange( nextValues ); }; + const sliderOnChange = ( next?: number ) => { + onValueChange( + next !== undefined ? [ next, parsedUnit ].join( '' ) : undefined + ); + }; + // Set selected unit so it can be used as fallback by unlinked controls // when individual sides do not have a value containing a unit. const handleOnUnitChange: UnitControlProps[ 'onUnitChange' ] = ( unit ) => { @@ -51,36 +70,37 @@ export default function AllInputControl( { setSelectedUnits( newUnits ); }; - const handleOnHoverOn = () => { - onHoverOn( { - top: true, - bottom: true, - left: true, - right: true, - } ); - }; - - const handleOnHoverOff = () => { - onHoverOff( { - top: false, - bottom: false, - left: false, - right: false, - } ); - }; - return ( - + + + + + ); } diff --git a/packages/components/src/box-control/axial-input-controls.tsx b/packages/components/src/box-control/axial-input-controls.tsx index bc8a4bd420bbd..173605f68a872 100644 --- a/packages/components/src/box-control/axial-input-controls.tsx +++ b/packages/components/src/box-control/axial-input-controls.tsx @@ -1,10 +1,19 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; -import UnitControl from './unit-control'; -import { LABELS } from './utils'; -import { Layout } from './styles/box-control-styles'; +import Tooltip from '../tooltip'; +import { CUSTOM_VALUE_SETTINGS, LABELS } from './utils'; +import { + FlexedBoxControlIcon, + FlexedRangeControl, + InputWrapper, + StyledUnitControl, +} from './styles/box-control-styles'; import type { BoxControlInputControlProps } from './types'; const groupedSides = [ 'vertical', 'horizontal' ] as const; @@ -13,14 +22,17 @@ type GroupedSide = ( typeof groupedSides )[ number ]; export default function AxialInputControls( { onChange, onFocus, - onHoverOn, - onHoverOff, values, selectedUnits, setSelectedUnits, sides, ...props }: BoxControlInputControlProps ) { + const generatedId = useInstanceId( + AxialInputControls, + `box-control-input` + ); + const createHandleOnFocus = ( side: GroupedSide ) => ( event: React.FocusEvent< HTMLInputElement > ) => { @@ -30,43 +42,7 @@ export default function AxialInputControls( { onFocus( event, { side } ); }; - const createHandleOnHoverOn = ( side: GroupedSide ) => () => { - if ( ! onHoverOn ) { - return; - } - if ( side === 'vertical' ) { - onHoverOn( { - top: true, - bottom: true, - } ); - } - if ( side === 'horizontal' ) { - onHoverOn( { - left: true, - right: true, - } ); - } - }; - - const createHandleOnHoverOff = ( side: GroupedSide ) => () => { - if ( ! onHoverOff ) { - return; - } - if ( side === 'vertical' ) { - onHoverOff( { - top: false, - bottom: false, - } ); - } - if ( side === 'horizontal' ) { - onHoverOff( { - left: false, - right: false, - } ); - } - }; - - const createHandleOnChange = ( side: GroupedSide ) => ( next?: string ) => { + const handleOnValueChange = ( side: GroupedSide, next?: string ) => { if ( ! onChange ) { return; } @@ -109,16 +85,8 @@ export default function AxialInputControls( { ? groupedSides.filter( ( side ) => sides.includes( side ) ) : groupedSides; - const first = filteredSides[ 0 ]; - const last = filteredSides[ filteredSides.length - 1 ]; - const only = first === last && first; - return ( - + <> { filteredSides.map( ( side ) => { const [ parsedQuantity, parsedUnit ] = parseQuantityAndUnitFromRawValue( @@ -128,26 +96,65 @@ export default function AxialInputControls( { side === 'vertical' ? selectedUnits.top : selectedUnits.left; + + const inputId = [ generatedId, side ].join( '-' ); + return ( - + + + + + handleOnValueChange( side, newValue ) + } + onUnitChange={ createHandleOnUnitChange( + side + ) } + onFocus={ createHandleOnFocus( side ) } + label={ LABELS[ side ] } + hideLabelFromVision + key={ side } + /> + + + handleOnValueChange( + side, + newValue !== undefined + ? [ + newValue, + selectedUnit ?? parsedUnit, + ].join( '' ) + : undefined + ) + } + min={ 0 } + max={ + CUSTOM_VALUE_SETTINGS[ selectedUnit ?? 'px' ] + ?.max ?? 10 + } + step={ + CUSTOM_VALUE_SETTINGS[ selectedUnit ?? 'px' ] + ?.step ?? 0.1 + } + value={ parsedQuantity ?? 0 } + withInputField={ false } + /> + ); } ) } - + ); } diff --git a/packages/components/src/box-control/index.tsx b/packages/components/src/box-control/index.tsx index c7fcf066c545c..dcc890e8e3c51 100644 --- a/packages/components/src/box-control/index.tsx +++ b/packages/components/src/box-control/index.tsx @@ -9,17 +9,16 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { BaseControl } from '../base-control'; -import Button from '../button'; -import { FlexItem, FlexBlock } from '../flex'; import AllInputControl from './all-input-control'; import InputControls from './input-controls'; import AxialInputControls from './axial-input-controls'; -import BoxControlIcon from './icon'; import LinkedButton from './linked-button'; +import { Grid } from '../grid'; import { - Root, - Header, - HeaderControlWrapper, + FlexedBoxControlIcon, + InputWrapper, + ResetButton, + LinkedButtonWrapper, } from './styles/box-control-styles'; import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; import { @@ -155,57 +154,49 @@ function BoxControl( { }; return ( - -
- - - { label } - - - { allowReset && ( - - - - ) } -
- - - - - { isLinked && ( - - - - ) } - { ! isLinked && splitOnAxis && ( - - - - ) } - { ! hasOneSide && ( - - - - ) } - + + + { label } + + { isLinked && ( + + + + + ) } + { ! hasOneSide && ( + + + + ) } + + { ! isLinked && splitOnAxis && ( + + ) } { ! isLinked && ! splitOnAxis && ( ) } -
+ { allowReset && ( + + { __( 'Reset' ) } + + ) } + ); } diff --git a/packages/components/src/box-control/input-controls.tsx b/packages/components/src/box-control/input-controls.tsx index f72179f0d18c1..c8aaeae222749 100644 --- a/packages/components/src/box-control/input-controls.tsx +++ b/packages/components/src/box-control/input-controls.tsx @@ -1,82 +1,81 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; /** * Internal dependencies */ -import UnitControl from './unit-control'; +import Tooltip from '../tooltip'; import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; -import { ALL_SIDES, LABELS } from './utils'; -import { LayoutContainer, Layout } from './styles/box-control-styles'; +import { ALL_SIDES, CUSTOM_VALUE_SETTINGS, LABELS } from './utils'; +import { + FlexedBoxControlIcon, + FlexedRangeControl, + InputWrapper, + StyledUnitControl, +} from './styles/box-control-styles'; import type { BoxControlInputControlProps, BoxControlValue } from './types'; -import type { UnitControlProps } from '../unit-control/types'; const noop = () => {}; export default function BoxInputControls( { onChange = noop, onFocus = noop, - onHoverOn = noop, - onHoverOff = noop, values, selectedUnits, setSelectedUnits, sides, ...props }: BoxControlInputControlProps ) { + const generatedId = useInstanceId( BoxInputControls, 'box-control-input' ); + const createHandleOnFocus = ( side: keyof BoxControlValue ) => ( event: React.FocusEvent< HTMLInputElement > ) => { onFocus( event, { side } ); }; - const createHandleOnHoverOn = ( side: keyof BoxControlValue ) => () => { - onHoverOn( { [ side ]: true } ); - }; - - const createHandleOnHoverOff = ( side: keyof BoxControlValue ) => () => { - onHoverOff( { [ side ]: false } ); - }; - const handleOnChange = ( nextValues: BoxControlValue ) => { onChange( nextValues ); }; - const createHandleOnChange: ( - side: keyof BoxControlValue - ) => UnitControlProps[ 'onChange' ] = - ( side ) => - ( next, { event } ) => { - const nextValues = { ...values }; - const isNumeric = - next !== undefined && ! isNaN( parseFloat( next ) ); - const nextValue = isNumeric ? next : undefined; + const handleOnValueChange = ( + side: keyof BoxControlValue, + next?: string, + extra?: { event: React.SyntheticEvent< Element, Event > } + ) => { + const nextValues = { ...values }; + const isNumeric = next !== undefined && ! isNaN( parseFloat( next ) ); + const nextValue = isNumeric ? next : undefined; - nextValues[ side ] = nextValue; + nextValues[ side ] = nextValue; - /** - * Supports changing pair sides. For example, holding the ALT key - * when changing the TOP will also update BOTTOM. - */ - // @ts-expect-error - TODO: event.altKey is only present when the change event was - // triggered by a keyboard event. Should this feature be implemented differently so - // it also works with drag events? - if ( event.altKey ) { - switch ( side ) { - case 'top': - nextValues.bottom = nextValue; - break; - case 'bottom': - nextValues.top = nextValue; - break; - case 'left': - nextValues.right = nextValue; - break; - case 'right': - nextValues.left = nextValue; - break; - } + /** + * Supports changing pair sides. For example, holding the ALT key + * when changing the TOP will also update BOTTOM. + */ + // @ts-expect-error - TODO: event.altKey is only present when the change event was + // triggered by a keyboard event. Should this feature be implemented differently so + // it also works with drag events? + if ( extra?.event.altKey ) { + switch ( side ) { + case 'top': + nextValues.bottom = nextValue; + break; + case 'bottom': + nextValues.top = nextValue; + break; + case 'left': + nextValues.right = nextValue; + break; + case 'right': + nextValues.left = nextValue; + break; } + } - handleOnChange( nextValues ); - }; + handleOnChange( nextValues ); + }; const createHandleOnUnitChange = ( side: keyof BoxControlValue ) => ( next?: string ) => { @@ -90,45 +89,74 @@ export default function BoxInputControls( { ? ALL_SIDES.filter( ( side ) => sides.includes( side ) ) : ALL_SIDES; - const first = filteredSides[ 0 ]; - const last = filteredSides[ filteredSides.length - 1 ]; - const only = first === last && first; - return ( - - - { filteredSides.map( ( side ) => { - const [ parsedQuantity, parsedUnit ] = - parseQuantityAndUnitFromRawValue( values[ side ] ); + <> + { filteredSides.map( ( side ) => { + const [ parsedQuantity, parsedUnit ] = + parseQuantityAndUnitFromRawValue( values[ side ] ); + + const computedUnit = values[ side ] + ? parsedUnit + : selectedUnits[ side ]; + + const inputId = [ generatedId, side ].join( '-' ); - const computedUnit = values[ side ] - ? parsedUnit - : selectedUnits[ side ]; + return ( + + + + + handleOnValueChange( + side, + nextValue, + extra + ) + } + onUnitChange={ createHandleOnUnitChange( + side + ) } + onFocus={ createHandleOnFocus( side ) } + label={ LABELS[ side ] } + hideLabelFromVision + /> + - return ( - { + handleOnValueChange( + side, + newValue !== undefined + ? [ newValue, computedUnit ].join( '' ) + : undefined + ); + } } + min={ 0 } + max={ + CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ] + ?.max ?? 10 + } + step={ + CUSTOM_VALUE_SETTINGS[ computedUnit ?? 'px' ] + ?.step ?? 0.1 + } + value={ parsedQuantity ?? 0 } + withInputField={ false } /> - ); - } ) } - - + + ); + } ) } + ); } diff --git a/packages/components/src/box-control/styles/box-control-styles.ts b/packages/components/src/box-control/styles/box-control-styles.ts index d961d4322ba5a..ce2c8aa227e58 100644 --- a/packages/components/src/box-control/styles/box-control-styles.ts +++ b/packages/components/src/box-control/styles/box-control-styles.ts @@ -1,80 +1,40 @@ /** * External dependencies */ -import { css } from '@emotion/react'; import styled from '@emotion/styled'; /** * Internal dependencies */ -import { Flex } from '../../flex'; -import BaseUnitControl from '../../unit-control'; -import { rtl } from '../../utils'; -import type { BoxUnitControlProps } from '../types'; - -export const Root = styled.div` - box-sizing: border-box; - max-width: 235px; - padding-bottom: 12px; - width: 100%; +import BoxControlIcon from '../icon'; +import Button from '../../button'; +import { HStack } from '../../h-stack'; +import RangeControl from '../../range-control'; +import UnitControl from '../../unit-control'; +import { space } from '../../utils/space'; + +export const StyledUnitControl = styled( UnitControl )` + max-width: 90px; `; -export const Header = styled( Flex )` - margin-bottom: 8px; +export const InputWrapper = styled( HStack )` + grid-column: 1 / span 3; `; -export const HeaderControlWrapper = styled( Flex )` - min-height: 30px; - gap: 0; +export const ResetButton = styled( Button )` + grid-area: 1 / 2; + justify-self: end; `; -export const UnitControlWrapper = styled.div` - box-sizing: border-box; - max-width: 80px; +export const LinkedButtonWrapper = styled.div` + grid-area: 1 / 3; + justify-self: end; `; -export const LayoutContainer = styled( Flex )` - justify-content: center; - padding-top: 8px; +export const FlexedBoxControlIcon = styled( BoxControlIcon )` + flex: 0 0 auto; `; -export const Layout = styled( Flex )` - position: relative; - height: 100%; +export const FlexedRangeControl = styled( RangeControl )` width: 100%; - justify-content: flex-start; -`; - -const unitControlBorderRadiusStyles = ( { - isFirst, - isLast, - isOnly, -}: Pick< BoxUnitControlProps, 'isFirst' | 'isLast' | 'isOnly' > ) => { - if ( isFirst ) { - return rtl( { borderTopRightRadius: 0, borderBottomRightRadius: 0 } )(); - } - if ( isLast ) { - return rtl( { borderTopLeftRadius: 0, borderBottomLeftRadius: 0 } )(); - } - if ( isOnly ) { - return css( { borderRadius: 2 } ); - } - - return css( { - borderRadius: 0, - } ); -}; - -const unitControlMarginStyles = ( { - isFirst, - isOnly, -}: Pick< BoxUnitControlProps, 'isFirst' | 'isOnly' > ) => { - const marginLeft = isFirst || isOnly ? 0 : -1; - - return rtl( { marginLeft } )(); -}; - -export const UnitControl = styled( BaseUnitControl )` - max-width: 60px; - ${ unitControlBorderRadiusStyles }; - ${ unitControlMarginStyles }; + margin-inline-end: ${ space( 2 ) }; `; diff --git a/packages/components/src/box-control/styles/box-control-visualizer-styles.ts b/packages/components/src/box-control/styles/box-control-visualizer-styles.ts deleted file mode 100644 index bbfe66c9a71e9..0000000000000 --- a/packages/components/src/box-control/styles/box-control-visualizer-styles.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * External dependencies - */ -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; - -/** - * Internal dependencies - */ -import { COLORS, rtl } from '../../utils'; - -const containerPositionStyles = ( { - isPositionAbsolute, -}: { - isPositionAbsolute: boolean; -} ) => { - if ( ! isPositionAbsolute ) return ''; - - return css` - bottom: 0; - left: 0; - pointer-events: none; - position: absolute; - right: 0; - top: 0; - z-index: 1; - `; -}; - -export const Container = styled.div` - box-sizing: border-box; - position: relative; - ${ containerPositionStyles }; -`; - -export const Side = styled.div` - box-sizing: border-box; - background: ${ COLORS.theme.accent }; - filter: brightness( 1 ); - opacity: 0; - position: absolute; - pointer-events: none; - transition: opacity 120ms linear; - z-index: 1; - - ${ ( { isActive }: { isActive: boolean } ) => - isActive && - ` - opacity: 0.3; - ` } -`; - -export const TopView = styled( Side )` - top: 0; - left: 0; - right: 0; -`; - -export const RightView = styled( Side )` - top: 0; - bottom: 0; - ${ rtl( { right: 0 } ) }; -`; - -export const BottomView = styled( Side )` - bottom: 0; - left: 0; - right: 0; -`; - -export const LeftView = styled( Side )` - top: 0; - bottom: 0; - ${ rtl( { left: 0 } ) }; -`; diff --git a/packages/components/src/box-control/test/index.tsx b/packages/components/src/box-control/test/index.tsx index 8a861eff37e1b..1ea3c84aae922 100644 --- a/packages/components/src/box-control/test/index.tsx +++ b/packages/components/src/box-control/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, screen } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -33,7 +33,10 @@ describe( 'BoxControl', () => { render( {} } /> ); expect( - screen.getByRole( 'textbox', { name: 'Box Control' } ) + screen.getByRole( 'group', { name: 'Box Control' } ) + ).toBeVisible(); + expect( + screen.getByRole( 'textbox', { name: 'All sides' } ) ).toBeVisible(); } ); @@ -42,15 +45,41 @@ describe( 'BoxControl', () => { render( {} } /> ); - const input = screen.getByRole( 'textbox', { - name: 'Box Control', - } ); + const input = screen.getByRole( 'textbox', { name: 'All sides' } ); await user.type( input, '100' ); await user.keyboard( '{Enter}' ); expect( input ).toHaveValue( '100' ); } ); + + it( 'should update input values when interacting with slider', () => { + render( {} } /> ); + + const slider = screen.getByRole( 'slider' ); + + fireEvent.change( slider, { target: { value: 50 } } ); + + expect( slider ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'All sides' } ) + ).toHaveValue( '50' ); + } ); + + it( 'should update slider values when interacting with input', async () => { + const user = userEvent.setup(); + render( {} } /> ); + + const input = screen.getByRole( 'textbox', { + name: 'All sides', + } ); + + await user.type( input, '50' ); + await user.keyboard( '{Enter}' ); + + expect( input ).toHaveValue( '50' ); + expect( screen.getByRole( 'slider' ) ).toHaveValue( '50' ); + } ); } ); describe( 'Reset', () => { @@ -60,7 +89,7 @@ describe( 'BoxControl', () => { render( {} } /> ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -79,7 +108,7 @@ describe( 'BoxControl', () => { render( ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -98,7 +127,7 @@ describe( 'BoxControl', () => { render( ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -118,7 +147,7 @@ describe( 'BoxControl', () => { render( spyChange( v ) } /> ); const input = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); await user.type( input, '100' ); @@ -152,21 +181,49 @@ describe( 'BoxControl', () => { ); await user.type( - screen.getByRole( 'textbox', { name: 'Top' } ), + screen.getByRole( 'textbox', { name: 'Top side' } ), '100' ); expect( - screen.getByRole( 'textbox', { name: 'Top' } ) + screen.getByRole( 'textbox', { name: 'Top side' } ) ).toHaveValue( '100' ); expect( - screen.getByRole( 'textbox', { name: 'Right' } ) + screen.getByRole( 'textbox', { name: 'Right side' } ) ).not.toHaveValue(); expect( - screen.getByRole( 'textbox', { name: 'Bottom' } ) + screen.getByRole( 'textbox', { name: 'Bottom side' } ) ).not.toHaveValue(); expect( - screen.getByRole( 'textbox', { name: 'Left' } ) + screen.getByRole( 'textbox', { name: 'Left side' } ) + ).not.toHaveValue(); + } ); + + it( 'should update a single side value when using slider unlinked', async () => { + const user = userEvent.setup(); + + render( ); + + await user.click( + screen.getByRole( 'button', { name: 'Unlink sides' } ) + ); + + const slider = screen.getByRole( 'slider', { name: 'Right side' } ); + + fireEvent.change( slider, { target: { value: 50 } } ); + + expect( slider ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'Top side' } ) + ).not.toHaveValue(); + expect( + screen.getByRole( 'textbox', { name: 'Right side' } ) + ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'Bottom side' } ) + ).not.toHaveValue(); + expect( + screen.getByRole( 'textbox', { name: 'Left side' } ) ).not.toHaveValue(); } ); @@ -181,17 +238,68 @@ describe( 'BoxControl', () => { await user.type( screen.getByRole( 'textbox', { - name: 'Vertical', + name: 'Top and bottom sides', } ), '100' ); expect( - screen.getByRole( 'textbox', { name: 'Vertical' } ) + screen.getByRole( 'textbox', { name: 'Top and bottom sides' } ) ).toHaveValue( '100' ); expect( - screen.getByRole( 'textbox', { name: 'Horizontal' } ) + screen.getByRole( 'textbox', { name: 'Left and right sides' } ) + ).not.toHaveValue(); + } ); + + it( 'should update a whole axis using a slider when value is changed when unlinked', async () => { + const user = userEvent.setup(); + + render( ); + + await user.click( + screen.getByRole( 'button', { name: 'Unlink sides' } ) + ); + + const slider = screen.getByRole( 'slider', { + name: 'Left and right sides', + } ); + + fireEvent.change( slider, { target: { value: 50 } } ); + + expect( slider ).toHaveValue( '50' ); + expect( + screen.getByRole( 'textbox', { name: 'Top and bottom sides' } ) ).not.toHaveValue(); + expect( + screen.getByRole( 'textbox', { name: 'Left and right sides' } ) + ).toHaveValue( '50' ); + } ); + + it( 'should show "Mixed" label when sides have different values but are linked', async () => { + const user = userEvent.setup(); + + render( ); + + const unlinkButton = screen.getByRole( 'button', { + name: 'Unlink sides', + } ); + + await user.click( unlinkButton ); + + await user.type( + screen.getByRole( 'textbox', { + name: 'Right side', + } ), + '13' + ); + + expect( + screen.getByRole( 'textbox', { name: 'Right side' } ) + ).toHaveValue( '13' ); + + await user.click( unlinkButton ); + + expect( screen.getByPlaceholderText( 'Mixed' ) ).toHaveValue( '' ); } ); } ); @@ -287,7 +395,7 @@ describe( 'BoxControl', () => { render( ); const valueInput = screen.getByRole( 'textbox', { - name: 'Box Control', + name: 'All sides', } ); const unitSelect = screen.getByRole( 'combobox', { name: 'Select unit', diff --git a/packages/components/src/box-control/types.ts b/packages/components/src/box-control/types.ts index 0eba0e58fd33c..12524559564ab 100644 --- a/packages/components/src/box-control/types.ts +++ b/packages/components/src/box-control/types.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import type { useHover } from '@use-gesture/react'; - /** * Internal dependencies */ @@ -16,6 +11,10 @@ export type BoxControlValue = { left?: string; }; +export type CustomValueUnits = { + [ key: string ]: { max: number; step: number }; +}; + type UnitControlPassthroughProps = Omit< UnitControlProps, 'label' | 'onChange' | 'onFocus' | 'onMouseOver' | 'onMouseOut' | 'units' @@ -92,22 +91,6 @@ export type BoxControlInputControlProps = UnitControlPassthroughProps & { values: BoxControlValue; }; -export type BoxUnitControlProps = UnitControlPassthroughProps & - Pick< UnitControlProps, 'onChange' | 'onFocus' > & { - isFirst?: boolean; - isLast?: boolean; - isOnly?: boolean; - label?: string; - onHoverOff?: ( - event: ReturnType< typeof useHover >[ 'event' ], - state: Omit< ReturnType< typeof useHover >, 'event' > - ) => void; - onHoverOn?: ( - event: ReturnType< typeof useHover >[ 'event' ], - state: Omit< ReturnType< typeof useHover >, 'event' > - ) => void; - }; - export type BoxControlIconProps = { /** * @default 24 diff --git a/packages/components/src/box-control/unit-control.tsx b/packages/components/src/box-control/unit-control.tsx deleted file mode 100644 index 24d71cf0d6cd3..0000000000000 --- a/packages/components/src/box-control/unit-control.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/** - * External dependencies - */ -import { useHover } from '@use-gesture/react'; - -/** - * Internal dependencies - */ -import BaseTooltip from '../tooltip'; -import { UnitControlWrapper, UnitControl } from './styles/box-control-styles'; -import type { BoxUnitControlProps } from './types'; - -const noop = () => {}; - -export default function BoxUnitControl( { - isFirst, - isLast, - isOnly, - onHoverOn = noop, - onHoverOff = noop, - label, - value, - ...props -}: BoxUnitControlProps ) { - const bindHoverGesture = useHover( ( { event, ...state } ) => { - if ( state.hovering ) { - onHoverOn( event, state ); - } else { - onHoverOff( event, state ); - } - } ); - - return ( - - - - - - ); -} - -function Tooltip( { - children, - text, -}: { - children: JSX.Element; - text?: string; -} ) { - if ( ! text ) return children; - - /** - * Wrapping the children in a `
` as Tooltip as it attempts - * to render the . Using a plain `
` appears to - * resolve this issue. - * - * Originally discovered and referenced here: - * https://github.com/WordPress/gutenberg/pull/24966#issuecomment-685875026 - */ - return ( - -
{ children }
-
- ); -} diff --git a/packages/components/src/box-control/utils.ts b/packages/components/src/box-control/utils.ts index 6614342d3da6d..e480c9a9f4674 100644 --- a/packages/components/src/box-control/utils.ts +++ b/packages/components/src/box-control/utils.ts @@ -7,17 +7,52 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { parseQuantityAndUnitFromRawValue } from '../unit-control/utils'; -import type { BoxControlProps, BoxControlValue } from './types'; +import type { + BoxControlProps, + BoxControlValue, + CustomValueUnits, +} from './types'; + +export const CUSTOM_VALUE_SETTINGS: CustomValueUnits = { + px: { max: 300, step: 1 }, + '%': { max: 100, step: 1 }, + vw: { max: 100, step: 1 }, + vh: { max: 100, step: 1 }, + em: { max: 10, step: 0.1 }, + rm: { max: 10, step: 0.1 }, + svw: { max: 100, step: 1 }, + lvw: { max: 100, step: 1 }, + dvw: { max: 100, step: 1 }, + svh: { max: 100, step: 1 }, + lvh: { max: 100, step: 1 }, + dvh: { max: 100, step: 1 }, + vi: { max: 100, step: 1 }, + svi: { max: 100, step: 1 }, + lvi: { max: 100, step: 1 }, + dvi: { max: 100, step: 1 }, + vb: { max: 100, step: 1 }, + svb: { max: 100, step: 1 }, + lvb: { max: 100, step: 1 }, + dvb: { max: 100, step: 1 }, + vmin: { max: 100, step: 1 }, + svmin: { max: 100, step: 1 }, + lvmin: { max: 100, step: 1 }, + dvmin: { max: 100, step: 1 }, + vmax: { max: 100, step: 1 }, + svmax: { max: 100, step: 1 }, + lvmax: { max: 100, step: 1 }, + dvmax: { max: 100, step: 1 }, +}; export const LABELS = { - all: __( 'All' ), - top: __( 'Top' ), - bottom: __( 'Bottom' ), - left: __( 'Left' ), - right: __( 'Right' ), + all: __( 'All sides' ), + top: __( 'Top side' ), + bottom: __( 'Bottom side' ), + left: __( 'Left side' ), + right: __( 'Right side' ), mixed: __( 'Mixed' ), - vertical: __( 'Vertical' ), - horizontal: __( 'Horizontal' ), + vertical: __( 'Top and bottom sides' ), + horizontal: __( 'Left and right sides' ), }; export const DEFAULT_VALUES = { From 876bf2bd41957ecf02abf45b68e865cfe29d0215 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:09:20 +0900 Subject: [PATCH 28/34] Remove right negative margin from pinned items (#57666) --- packages/interface/src/components/pinned-items/style.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/interface/src/components/pinned-items/style.scss b/packages/interface/src/components/pinned-items/style.scss index 66062b7fa3dbb..420b94e2994b1 100644 --- a/packages/interface/src/components/pinned-items/style.scss +++ b/packages/interface/src/components/pinned-items/style.scss @@ -27,7 +27,4 @@ // Gap between pinned items. gap: $grid-unit-10; - - // Account for larger grid from parent container gap. - margin-right: -$grid-unit-05; } From d0287db174947e81fa4cb7350daf8303fa01b609 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:18:59 +0900 Subject: [PATCH 29/34] PaletteEdit: improve unit tests (#57645) * PaletteEdit: improve unit tests * Update changelog * Use toBeVisible instead of toBeInTheDocument Co-authored-by: Lena Morita * Update test name Co-authored-by: Lena Morita * Update test name Co-authored-by: Lena Morita * Update test name Co-authored-by: Lena Morita * Update test name Co-authored-by: Lena Morita * Update test name Co-authored-by: Lena Morita * Remove Unnecessary uerr click event Co-authored-by: Lena Morita * Use toBeVisible instead of toBeInTheDocument * use a fresh onChange mock for each test --------- Co-authored-by: Lena Morita --- packages/components/CHANGELOG.md | 1 + .../src/palette-edit/test/index.tsx | 329 +++++++++++++++++- 2 files changed, 320 insertions(+), 10 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 646fc6eaa9dd0..022248e3706cb 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -34,6 +34,7 @@ - `ToggleGroupControl`: Update button size in large variant to be 32px ([#57338](https://github.com/WordPress/gutenberg/pull/57338)). - `Tooltip`: improve unit tests ([#57345](https://github.com/WordPress/gutenberg/pull/57345)). - `Tooltip`: no-op when nested inside other `Tooltip` components ([#57202](https://github.com/WordPress/gutenberg/pull/57202)). +- `PaletteEdit`: improve unit tests ([#57645](https://github.com/WordPress/gutenberg/pull/57645)). ### Experimental diff --git a/packages/components/src/palette-edit/test/index.tsx b/packages/components/src/palette-edit/test/index.tsx index 1bf2802709de7..31b712225a8dc 100644 --- a/packages/components/src/palette-edit/test/index.tsx +++ b/packages/components/src/palette-edit/test/index.tsx @@ -1,7 +1,8 @@ /** * External dependencies */ -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; /** * Internal dependencies @@ -9,6 +10,8 @@ import { render, fireEvent, screen } from '@testing-library/react'; import PaletteEdit, { getNameForPosition } from '..'; import type { PaletteElement } from '../types'; +const noop = () => {}; + describe( 'getNameForPosition', () => { test( 'should return 1 by default', () => { const slugPrefix = 'test-'; @@ -82,18 +85,324 @@ describe( 'getNameForPosition', () => { describe( 'PaletteEdit', () => { const defaultProps = { - colors: [ { color: '#ffffff', name: 'Base', slug: 'base' } ], - onChange: jest.fn(), paletteLabel: 'Test label', - emptyMessage: 'Test empty message', - canOnlyChangeValues: true, - canReset: true, slugPrefix: '', + onChange: noop, }; - it( 'opens color selector for color palettes', () => { - render( ); - fireEvent.click( screen.getByLabelText( 'Color: Base' ) ); - expect( screen.getByLabelText( 'Hex color' ) ).toBeInTheDocument(); + const colors = [ + { color: '#1a4548', name: 'Primary', slug: 'primary' }, + { color: '#0000ff', name: 'Secondary', slug: 'secondary' }, + ]; + const gradients = [ + { + gradient: + 'linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%)', + name: 'Pale ocean', + slug: 'pale-ocean', + }, + { + gradient: + 'linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%)', + name: 'Midnight', + slug: 'midnight', + }, + ]; + + it( 'shows heading label', () => { + render( ); + + const paletteLabel = screen.getByRole( 'heading', { + level: 2, + name: 'Test label', + } ); + + expect( paletteLabel ).toBeVisible(); + } ); + + it( 'shows heading label with custom heading level', () => { + render( + + ); + + expect( + screen.getByRole( 'heading', { + level: 5, + name: 'Test label', + } ) + ).toBeVisible(); + } ); + + it( 'shows empty message', () => { + render( + + ); + + expect( screen.getByText( 'Test empty message' ) ).toBeVisible(); + } ); + + it( 'shows an option to remove all colors', async () => { + const user = userEvent.setup(); + render( ); + + await user.click( + screen.getByRole( 'button', { + name: 'Color options', + } ) + ); + + expect( + screen.getByRole( 'button', { + name: 'Remove all colors', + } ) + ).toBeVisible(); + } ); + + it( 'shows a reset option when the `canReset` prop is enabled', async () => { + const user = userEvent.setup(); + render( + + ); + + await user.click( + screen.getByRole( 'button', { + name: 'Color options', + } ) + ); + expect( + screen.getByRole( 'button', { + name: 'Reset colors', + } ) + ).toBeVisible(); + } ); + + it( 'does not show a reset colors option when `canReset` is disabled', async () => { + const user = userEvent.setup(); + render( ); + + await user.click( + screen.getByRole( 'button', { + name: 'Color options', + } ) + ); + expect( + screen.queryByRole( 'button', { + name: 'Reset colors', + } ) + ).not.toBeInTheDocument(); + } ); + + it( 'calls the `onChange` with the new color appended', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + render( + + ); + + await user.click( + screen.getByRole( 'button', { + name: 'Add color', + } ) + ); + + await waitFor( () => { + expect( onChange ).toHaveBeenCalledWith( [ + ...colors, + { + color: '#000', + name: 'Color 1', + slug: 'color-1', + }, + ] ); + } ); + } ); + + it( 'calls the `onChange` with the new gradient appended', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + render( + + ); + + await user.click( + screen.getByRole( 'button', { + name: 'Add gradient', + } ) + ); + + await waitFor( () => { + expect( onChange ).toHaveBeenCalledWith( [ + ...gradients, + { + gradient: + 'linear-gradient(135deg, rgba(6, 147, 227, 1) 0%, rgb(155, 81, 224) 100%)', + name: 'Color 1', + slug: 'color-1', + }, + ] ); + } ); + } ); + + it( 'can not add new colors when `canOnlyChangeValues` is enabled', () => { + render( ); + + expect( + screen.queryByRole( 'button', { + name: 'Add color', + } ) + ).not.toBeInTheDocument(); + } ); + + it( 'can remove a color', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + render( + + ); + + await user.click( + screen.getByRole( 'button', { + name: 'Color options', + } ) + ); + await user.click( + screen.getByRole( 'button', { + name: 'Show details', + } ) + ); + await user.click( screen.getByText( 'Primary' ) ); + await user.click( + screen.getByRole( 'button', { + name: 'Remove color', + } ) + ); + + await waitFor( () => { + expect( onChange ).toHaveBeenCalledWith( [ colors[ 1 ] ] ); + } ); + } ); + + it( 'can update palette name', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + render( + + ); + + await user.click( + screen.getByRole( 'button', { + name: 'Color options', + } ) + ); + await user.click( + screen.getByRole( 'button', { + name: 'Show details', + } ) + ); + await user.click( screen.getByText( 'Primary' ) ); + const nameInput = screen.getByRole( 'textbox', { + name: 'Color name', + } ); + await user.clear( nameInput ); + await user.type( nameInput, 'Primary Updated' ); + + await waitFor( () => { + expect( onChange ).toHaveBeenCalledWith( [ + { + ...colors[ 0 ], + name: 'Primary Updated', + slug: 'primary-updated', + }, + colors[ 1 ], + ] ); + } ); + } ); + + it( 'can update color palette value', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + render( + + ); + + await user.click( screen.getByLabelText( 'Color: Primary' ) ); + const hexInput = screen.getByRole( 'textbox', { + name: 'Hex color', + } ); + await user.clear( hexInput ); + await user.type( hexInput, '000000' ); + + await waitFor( () => { + expect( onChange ).toHaveBeenCalledWith( [ + { + ...colors[ 0 ], + color: '#000000', + }, + colors[ 1 ], + ] ); + } ); + } ); + + it( 'can update gradient palette value', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + + render( + + ); + + await user.click( screen.getByLabelText( 'Gradient: Pale ocean' ) ); + + const typeSelectElement = screen.getByRole( 'combobox', { + name: 'Type', + } ); + await user.selectOptions( typeSelectElement, 'radial-gradient' ); + + await waitFor( () => { + expect( onChange ).toHaveBeenCalledWith( [ + { + ...gradients[ 0 ], + gradient: + 'radial-gradient(rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%)', + }, + gradients[ 1 ], + ] ); + } ); } ); } ); From eec8951360a8e4a5205f592a972cb32628cd2002 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Fri, 12 Jan 2024 11:32:38 +0400 Subject: [PATCH 30/34] Migrate 'typewriter' e2e tests to Playwright (#57673) * Migrate 'typewriter' e2e tests to Playwright * Remove old test file --- .../specs/editor/various/typewriter.spec.js | 169 +++++++++++------- 1 file changed, 106 insertions(+), 63 deletions(-) rename packages/e2e-tests/specs/editor/various/typewriter.test.js => test/e2e/specs/editor/various/typewriter.spec.js (58%) diff --git a/packages/e2e-tests/specs/editor/various/typewriter.test.js b/test/e2e/specs/editor/various/typewriter.spec.js similarity index 58% rename from packages/e2e-tests/specs/editor/various/typewriter.test.js rename to test/e2e/specs/editor/various/typewriter.spec.js index d935197b14f87..abf24cbfc298e 100644 --- a/packages/e2e-tests/specs/editor/various/typewriter.test.js +++ b/test/e2e/specs/editor/various/typewriter.spec.js @@ -1,40 +1,40 @@ /** * WordPress dependencies */ -import { createNewPost } from '@wordpress/e2e-test-utils'; +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); -describe( 'TypeWriter', () => { - beforeEach( async () => { - await createNewPost(); - } ); +/** @typedef {import('@playwright/test').Page} Page */ - const getCaretPosition = async () => - await page.evaluate( - () => - wp.dom.computeCaretRect( - document.activeElement?.contentWindow ?? window - ).y - ); +// Allow the scroll position to be 1px off. +const BUFFER = 1; - // Allow the scroll position to be 1px off. - const BUFFER = 1; +test.use( { + typewriterUtils: async ( { page }, use ) => { + await use( new TypewriterUtils( { page } ) ); + }, +} ); - const getDiff = async ( caretPosition ) => - Math.abs( ( await getCaretPosition() ) - caretPosition ); +test.describe( 'Typewriter', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); - it( 'should maintain caret position', async () => { - // Create first block. + test( 'should maintain caret position', async ( { + page, + typewriterUtils, + } ) => { + // Create test blocks. await page.keyboard.press( 'Enter' ); - - // Create second block. await page.keyboard.press( 'Enter' ); - const initialPosition = await getCaretPosition(); + const initialPosition = await typewriterUtils.getCaretPosition(); // The page shouldn't be scrolled when it's being filled. await page.keyboard.press( 'Enter' ); - expect( await getCaretPosition() ).toBeGreaterThan( initialPosition ); + expect( await typewriterUtils.getCaretPosition() ).toBeGreaterThan( + initialPosition + ); // Create blocks until the typewriter effect kicks in. while ( @@ -42,19 +42,22 @@ describe( 'TypeWriter', () => { const { activeElement } = document.activeElement?.contentDocument ?? document; return ( - wp.dom.getScrollContainer( activeElement ).scrollTop === 0 + window.wp.dom.getScrollContainer( activeElement ) + .scrollTop === 0 ); } ) ) { await page.keyboard.press( 'Enter' ); } - const newPosition = await getCaretPosition(); + const newPosition = await typewriterUtils.getCaretPosition(); // Now the scroll position should be maintained. await page.keyboard.press( 'Enter' ); - expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); + expect( + await typewriterUtils.getDiff( newPosition ) + ).toBeLessThanOrEqual( BUFFER ); // Type until the text wraps. while ( @@ -63,37 +66,47 @@ describe( 'TypeWriter', () => { document.activeElement?.contentDocument ?? document; return ( activeElement.clientHeight <= - parseInt( getComputedStyle( activeElement ).lineHeight, 10 ) + parseInt( + window.getComputedStyle( activeElement ).lineHeight, + 10 + ) ); } ) ) { await page.keyboard.type( 'a' ); } - expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); + expect( + await typewriterUtils.getDiff( newPosition ) + ).toBeLessThanOrEqual( BUFFER ); // Pressing backspace will reposition the caret to the previous line. // Scroll position should be adjusted again. await page.keyboard.press( 'Backspace' ); - expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); + expect( + await typewriterUtils.getDiff( newPosition ) + ).toBeLessThanOrEqual( BUFFER ); // Should reset scroll position to maintain. await page.keyboard.press( 'ArrowUp' ); - const positionAfterArrowUp = await getCaretPosition(); + const positionAfterArrowUp = await typewriterUtils.getCaretPosition(); expect( positionAfterArrowUp ).toBeLessThanOrEqual( newPosition ); // Should be scrolled to new position. await page.keyboard.press( 'Enter' ); - expect( await getDiff( positionAfterArrowUp ) ).toBeLessThanOrEqual( - BUFFER - ); + expect( + await typewriterUtils.getDiff( positionAfterArrowUp ) + ).toBeLessThanOrEqual( BUFFER ); } ); - it( 'should maintain caret position after scroll', async () => { + test( 'should maintain caret position after scroll', async ( { + page, + typewriterUtils, + } ) => { // Create first block. await page.keyboard.press( 'Enter' ); @@ -104,7 +117,7 @@ describe( 'TypeWriter', () => { const { activeElement } = document.activeElement?.contentDocument ?? document; const scrollContainer = - wp.dom.getScrollContainer( activeElement ); + window.wp.dom.getScrollContainer( activeElement ); return ( scrollContainer.scrollHeight === scrollContainer.clientHeight @@ -117,8 +130,9 @@ describe( 'TypeWriter', () => { const scrollPosition = await page.evaluate( () => { const { activeElement } = document.activeElement?.contentDocument ?? document; - return wp.dom.getScrollContainer( activeElement ).scrollTop; + return window.wp.dom.getScrollContainer( activeElement ).scrollTop; } ); + // Expect scrollbar to be at the top. expect( scrollPosition ).toBe( 0 ); @@ -127,7 +141,7 @@ describe( 'TypeWriter', () => { await page.evaluate( () => { const { activeElement } = document.activeElement?.contentDocument ?? document; - wp.dom.getScrollContainer( activeElement ).scrollTop += 2; + window.wp.dom.getScrollContainer( activeElement ).scrollTop += 2; } ); // Wait for the caret rectangle to be recalculated. await page.evaluate( @@ -136,39 +150,44 @@ describe( 'TypeWriter', () => { // After hitting Enter to create a new block, the caret screen // coordinates should be the same. - const initialPosition = await getCaretPosition(); + const initialPosition = await typewriterUtils.getCaretPosition(); await page.keyboard.press( 'Enter' ); await page.waitForFunction( () => { const { activeElement } = document.activeElement?.contentDocument ?? document; // Wait for the Typewriter to scroll down past the initial position. - return wp.dom.getScrollContainer( activeElement ).scrollTop > 2; + return ( + window.wp.dom.getScrollContainer( activeElement ).scrollTop > 2 + ); } ); - expect( await getDiff( initialPosition ) ).toBe( 0 ); + + expect( await typewriterUtils.getDiff( initialPosition ) ).toBe( 0 ); } ); - it( 'should maintain caret position after leaving last editable', async () => { - // Create first block. + test( 'should maintain caret position after leaving last editable', async ( { + page, + typewriterUtils, + } ) => { + // Create test blocks. await page.keyboard.press( 'Enter' ); - // Create second block. await page.keyboard.press( 'Enter' ); - // Create third block. await page.keyboard.press( 'Enter' ); // Move to first block. await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); - const initialPosition = await getCaretPosition(); + const initialPosition = await typewriterUtils.getCaretPosition(); // Should maintain scroll position. - await page.keyboard.press( 'Enter' ); - - expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( - BUFFER - ); + expect( + await typewriterUtils.getDiff( initialPosition ) + ).toBeLessThanOrEqual( BUFFER ); } ); - it( 'should scroll caret into view from the top', async () => { + test( 'should scroll caret into view from the top', async ( { + page, + typewriterUtils, + } ) => { // Create first block. await page.keyboard.press( 'Enter' ); @@ -177,7 +196,7 @@ describe( 'TypeWriter', () => { await page.evaluate( () => { const { activeElement } = document.activeElement?.contentDocument ?? document; - return ! wp.dom.getScrollContainer( activeElement ); + return ! window.wp.dom.getScrollContainer( activeElement ); } ) ) { await page.keyboard.press( 'Enter' ); @@ -186,13 +205,14 @@ describe( 'TypeWriter', () => { let count = 0; // Create blocks until the typewriter effect kicks in, create at - // least 10 blocks to properly test the . + // least 10 blocks to properly test it. while ( ( await page.evaluate( () => { const { activeElement } = document.activeElement?.contentDocument ?? document; return ( - wp.dom.getScrollContainer( activeElement ).scrollTop === 0 + window.wp.dom.getScrollContainer( activeElement ) + .scrollTop === 0 ); } ) ) || count < 10 @@ -207,25 +227,25 @@ describe( 'TypeWriter', () => { const { activeElement } = document.activeElement?.contentDocument ?? document; activeElement.scrollIntoView( false ); - wp.dom.getScrollContainer( activeElement ).scrollTop -= + window.wp.dom.getScrollContainer( activeElement ).scrollTop -= activeElement.offsetHeight + 10; } ); - const bottomPostition = await getCaretPosition(); + const bottomPostition = await typewriterUtils.getCaretPosition(); // Should scroll the caret back into view (preserve browser behaviour). await page.keyboard.type( 'a' ); - const newBottomPosition = await getCaretPosition(); + const newBottomPosition = await typewriterUtils.getCaretPosition(); expect( newBottomPosition ).toBeLessThanOrEqual( bottomPostition ); // Should maintain new caret position. await page.keyboard.press( 'Enter' ); - expect( await getDiff( newBottomPosition ) ).toBeLessThanOrEqual( - BUFFER - ); + expect( + await typewriterUtils.getDiff( newBottomPosition ) + ).toBeLessThanOrEqual( BUFFER ); await page.keyboard.press( 'Backspace' ); @@ -239,22 +259,45 @@ describe( 'TypeWriter', () => { const { activeElement } = document.activeElement?.contentDocument ?? document; activeElement.scrollIntoView(); - wp.dom.getScrollContainer( activeElement ).scrollTop += + window.wp.dom.getScrollContainer( activeElement ).scrollTop += activeElement.offsetHeight + 10; } ); - const topPostition = await getCaretPosition(); + const topPostition = await typewriterUtils.getCaretPosition(); // Should scroll the caret back into view (preserve browser behaviour). await page.keyboard.type( 'a' ); - const newTopPosition = await getCaretPosition(); + const newTopPosition = await typewriterUtils.getCaretPosition(); expect( newTopPosition ).toBeGreaterThan( topPostition ); // Should maintain new caret position. await page.keyboard.press( 'Enter' ); - expect( await getDiff( newTopPosition ) ).toBeLessThanOrEqual( BUFFER ); + expect( + await typewriterUtils.getDiff( newTopPosition ) + ).toBeLessThanOrEqual( BUFFER ); } ); } ); + +class TypewriterUtils { + /** @type {Page} */ + #page; + + constructor( { page } ) { + this.#page = page; + } + + async getCaretPosition() { + return await this.#page.evaluate( () => { + return window.wp.dom.computeCaretRect( + document.activeElement?.contentWindow ?? window + ).y; + } ); + } + + async getDiff( caretPosition ) { + return Math.abs( ( await this.getCaretPosition() ) - caretPosition ); + } +} From d6aaa3f1e320238ac6f5ca0b02a097911461f151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:07:26 +0100 Subject: [PATCH 31/34] DataViews: show loading / no result message for the list layout (#57764) --- packages/dataviews/src/view-list.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index 70755c4aa35c5..df4297334d178 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -20,6 +20,7 @@ export default function ViewList( { view, fields, data, + isLoading, getItemId, onSelectionChange, onDetailsChange, @@ -49,6 +50,22 @@ export default function ViewList( { } }; + const hasData = usedData?.length; + if ( ! hasData ) { + return ( +
+ { ! hasData && ( +

{ isLoading ? __( 'Loading…' ) : __( 'No results' ) }

+ ) } +
+ ); + } + return (
    { usedData.map( ( item ) => { From 44ba13d4c26cb6e9dfea65d92530414345dad00a Mon Sep 17 00:00:00 2001 From: arthur791004 Date: Fri, 12 Jan 2024 16:16:46 +0800 Subject: [PATCH 32/34] Live Preview: Show the current theme name on the theme activation modal (#57588) * Live Preview: Show the current theme name on the theme activation modal * Handle error * Update comments * Add missing period --- .../src/middlewares/theme-preview.js | 37 +++++++++++-------- .../src/components/save-panel/index.js | 25 +++++++------ .../src/utils/use-actual-current-theme.js | 27 ++++++++++++++ 3 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 packages/edit-site/src/utils/use-actual-current-theme.js diff --git a/packages/api-fetch/src/middlewares/theme-preview.js b/packages/api-fetch/src/middlewares/theme-preview.js index 8ab21fc328476..56098c88cb351 100644 --- a/packages/api-fetch/src/middlewares/theme-preview.js +++ b/packages/api-fetch/src/middlewares/theme-preview.js @@ -1,32 +1,39 @@ /** * WordPress dependencies */ -import { addQueryArgs, hasQueryArg } from '@wordpress/url'; +import { addQueryArgs, getQueryArg, removeQueryArgs } from '@wordpress/url'; /** * This appends a `wp_theme_preview` parameter to the REST API request URL if * the admin URL contains a `theme` GET parameter. * + * If the REST API request URL has contained the `wp_theme_preview` parameter as `''`, + * then bypass this middleware. + * * @param {Record} themePath * @return {import('../types').APIFetchMiddleware} Preloading middleware. */ const createThemePreviewMiddleware = ( themePath ) => ( options, next ) => { - if ( - typeof options.url === 'string' && - ! hasQueryArg( options.url, 'wp_theme_preview' ) - ) { - options.url = addQueryArgs( options.url, { - wp_theme_preview: themePath, - } ); + if ( typeof options.url === 'string' ) { + const wpThemePreview = getQueryArg( options.url, 'wp_theme_preview' ); + if ( wpThemePreview === undefined ) { + options.url = addQueryArgs( options.url, { + wp_theme_preview: themePath, + } ); + } else if ( wpThemePreview === '' ) { + options.url = removeQueryArgs( options.url, 'wp_theme_preview' ); + } } - if ( - typeof options.path === 'string' && - ! hasQueryArg( options.path, 'wp_theme_preview' ) - ) { - options.path = addQueryArgs( options.path, { - wp_theme_preview: themePath, - } ); + if ( typeof options.path === 'string' ) { + const wpThemePreview = getQueryArg( options.path, 'wp_theme_preview' ); + if ( wpThemePreview === undefined ) { + options.path = addQueryArgs( options.path, { + wp_theme_preview: themePath, + } ); + } else if ( wpThemePreview === '' ) { + options.path = removeQueryArgs( options.path, 'wp_theme_preview' ); + } } return next( options ); diff --git a/packages/edit-site/src/components/save-panel/index.js b/packages/edit-site/src/components/save-panel/index.js index 5c1bcc1df281e..2840191b4849f 100644 --- a/packages/edit-site/src/components/save-panel/index.js +++ b/packages/edit-site/src/components/save-panel/index.js @@ -23,10 +23,8 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editSiteStore } from '../../store'; import { unlock } from '../../lock-unlock'; import { useActivateTheme } from '../../utils/use-activate-theme'; -import { - currentlyPreviewingTheme, - isPreviewingTheme, -} from '../../utils/is-previewing-theme'; +import { useActualCurrentTheme } from '../../utils/use-actual-current-theme'; +import { isPreviewingTheme } from '../../utils/is-previewing-theme'; const { EntitiesSavedStatesExtensible } = unlock( privateApis ); @@ -39,19 +37,22 @@ const EntitiesSavedStatesForPreview = ( { onClose } ) => { activateSaveLabel = __( 'Activate' ); } - const themeName = useSelect( ( select ) => { - const theme = select( coreStore ).getTheme( - currentlyPreviewingTheme() - ); + const currentTheme = useActualCurrentTheme(); - return theme?.name?.rendered; - }, [] ); + const previewingTheme = useSelect( + ( select ) => select( coreStore ).getCurrentTheme(), + [] + ); const additionalPrompt = (

    { sprintf( - 'Saving your changes will change your active theme to %s.', - themeName + /* translators: %1$s: The name of active theme, %2$s: The name of theme to be activated. */ + __( + 'Saving your changes will change your active theme from %1$s to %2$s.' + ), + currentTheme?.name?.rendered ?? '...', + previewingTheme?.name?.rendered ?? '...' ) }

    ); diff --git a/packages/edit-site/src/utils/use-actual-current-theme.js b/packages/edit-site/src/utils/use-actual-current-theme.js new file mode 100644 index 0000000000000..6f8310c2f7de1 --- /dev/null +++ b/packages/edit-site/src/utils/use-actual-current-theme.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { useState, useEffect } from '@wordpress/element'; +import { addQueryArgs } from '@wordpress/url'; + +const ACTIVE_THEMES_URL = '/wp/v2/themes?status=active'; + +export function useActualCurrentTheme() { + const [ currentTheme, setCurrentTheme ] = useState(); + + useEffect( () => { + // Set the `wp_theme_preview` to empty string to bypass the createThemePreviewMiddleware. + const path = addQueryArgs( ACTIVE_THEMES_URL, { + context: 'edit', + wp_theme_preview: '', + } ); + + apiFetch( { path } ) + .then( ( activeThemes ) => setCurrentTheme( activeThemes[ 0 ] ) ) + // Do nothing + .catch( () => {} ); + }, [] ); + + return currentTheme; +} From 75acb7e7035040283a309566070b4f707d111854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:18:29 +0100 Subject: [PATCH 33/34] DataViews: add front page to sidebar (#57759) --- .../index.js | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js index 171d59c108e9b..1ef4cbddee884 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-pages-dataviews/index.js @@ -7,9 +7,10 @@ import { } from '@wordpress/components'; import { layout } from '@wordpress/icons'; import { useMemo } from '@wordpress/element'; -import { useEntityRecords } from '@wordpress/core-data'; +import { useEntityRecords, store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -41,13 +42,36 @@ export default function SidebarNavigationScreenPagesDataViews() { per_page: -1, } ); - const templates = useMemo( - () => - templateRecords?.filter( ( { slug } ) => + + const { frontPage, postsPage } = useSelect( ( select ) => { + const { getEntityRecord } = select( coreStore ); + const siteSettings = getEntityRecord( 'root', 'site' ); + return { + frontPage: siteSettings?.page_on_front, + postsPage: siteSettings?.page_for_posts, + }; + }, [] ); + + const templates = useMemo( () => { + if ( ! templateRecords ) { + return []; + } + + const isHomePageBlog = frontPage === postsPage; + const homeTemplate = + templateRecords?.find( + ( template ) => template.slug === 'front-page' + ) || + templateRecords?.find( ( template ) => template.slug === 'home' ) || + templateRecords?.find( ( template ) => template.slug === 'index' ); + + return [ + isHomePageBlog ? homeTemplate : null, + ...templateRecords?.filter( ( { slug } ) => [ '404', 'search' ].includes( slug ) ), - [ templateRecords ] - ); + ].filter( Boolean ); + }, [ templateRecords, frontPage, postsPage ] ); return ( Date: Fri, 12 Jan 2024 09:30:59 +0100 Subject: [PATCH 34/34] Restore visual separator between mover buttons when show button label is on (#57640) * Restore visual separator between mover buttons when show button labels is on. * Style block movers separator for Top toolbar. * Reuse existing CSS selector. * Improve horizontal separator positioning. --- .../src/components/block-toolbar/style.scss | 31 +++++++++++-------- .../src/components/header/style.scss | 17 +++++++--- .../components/header-edit-mode/style.scss | 20 +++++++++--- .../src/components/header/style.scss | 2 +- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 85020cea2aa23..fc18dba7cc2cf 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -203,6 +203,24 @@ .block-editor-block-mover .block-editor-block-mover__move-button-container { width: auto; + + @include break-small() { + position: relative; + + &::before { + content: ""; + height: $border-width; + width: 100%; + background: $gray-900; + position: absolute; + top: 50%; + left: 50%; + // With Top toolbar enabled, this separator has a smaller width. Translating the + // X axis allows to make the separator always centered regardless of its width. + transform: translate(-50%, 0); + margin-top: -$border-width * 0.5; + } + } } .block-editor-block-mover.is-horizontal { @@ -231,19 +249,6 @@ padding-right: $grid-unit-15; } - @include break-small() { - // Specificity override for https://github.com/WordPress/gutenberg/blob/try/block-toolbar-labels/packages/block-editor/src/components/block-mover/style.scss#L69 - .is-up-button.is-up-button.is-up-button { - margin-right: 0; - border-radius: 0; - order: 1; - } - - .is-down-button.is-down-button.is-down-button { - order: 2; - } - } - .block-editor-block-contextual-toolbar .block-editor-block-mover.is-horizontal .block-editor-block-mover-button.block-editor-block-mover-button { width: auto; } diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index a3cf623a2d1aa..84e8328393d7a 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -69,7 +69,7 @@ margin-left: $grid-unit; } - // Modified group borders + // Modified group borders. .components-toolbar-group, .components-toolbar { border-right: none; @@ -194,18 +194,27 @@ } .show-icon-labels { - .edit-post-header__toolbar .block-editor-block-mover { + // Modified group borders. border-left: none; &::before { content: ""; width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; + margin-top: $grid-unit-15; + margin-bottom: $grid-unit-15; background-color: $gray-300; margin-left: $grid-unit; } + + // Modified block movers horizontal separator. + .block-editor-block-mover__move-button-container { + &::before { + width: calc(100% - #{$grid-unit-30}); + background: $gray-300; + left: calc(50% + 1px); + } + } } } diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index 44b762a667bb7..710c20b4e15f1 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -143,16 +143,26 @@ $header-toolbar-min-width: 335px; } .block-editor-block-mover { + // Modified group borders. border-left: none; &::before { content: ""; width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; + margin-top: $grid-unit-15; + margin-bottom: $grid-unit-15; background-color: $gray-300; margin-left: $grid-unit; } + + // Modified block movers horizontal separator. + .block-editor-block-mover__move-button-container { + &::before { + width: calc(100% - #{$grid-unit-30}); + background: $gray-300; + left: calc(50% + 1px); + } + } } } @@ -164,7 +174,7 @@ $header-toolbar-min-width: 335px; border-bottom: 0; } - // Modified group borders + // Modified group borders. .components-toolbar-group, .components-toolbar { border-right: none; @@ -172,8 +182,8 @@ $header-toolbar-min-width: 335px; &::after { content: ""; width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; + margin-top: $grid-unit-15; + margin-bottom: $grid-unit-15; background-color: $gray-300; margin-left: $grid-unit; } diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index c5bdc2561d5d5..2a3e3509cb412 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -17,7 +17,7 @@ border-bottom: 0; } - // Modified group borders + // Modified group borders. .components-toolbar-group, .components-toolbar { border-right: none;