diff --git a/bin/build-plugin-zip.sh b/bin/build-plugin-zip.sh index a8e91093205a2f..5d9de9b55b79ef 100755 --- a/bin/build-plugin-zip.sh +++ b/bin/build-plugin-zip.sh @@ -111,8 +111,11 @@ mv gutenberg.tmp.php gutenberg.php build_files=$( ls build/*/*.{js,css,asset.php} \ - build/block-library/blocks/*.php build/block-library/blocks/*/block.json \ - build/edit-widgets/blocks/*.php build/edit-widgets/blocks/*/block.json \ + build/block-library/blocks/*.php \ + build/block-library/blocks/*/block.json \ + build/block-library/blocks/*/*.css \ + build/edit-widgets/blocks/*.php \ + build/edit-widgets/blocks/*/block.json \ ) diff --git a/changelog.txt b/changelog.txt index a3130d3329dfca..a39ec97ddda9a4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,195 @@ == Changelog == -= 9.6.0-rc.1 = += 9.6.1 = + +### Bugfixes + +- Include block's CSS in the release for FSE themes ([27884](https://github.com/WordPress/gutenberg/pull/27884)) + + += 9.6.0 = + + +### Features + +- Allow dragging blocks from the inserter into the canvas. ([27669](https://github.com/WordPress/gutenberg/pull/27669)) +- Buttons: Add variations for vertical layout. ([27297](https://github.com/WordPress/gutenberg/pull/27297)) + +### Enhancements + +- Buttons block: Change position of the link popover. ([27408](https://github.com/WordPress/gutenberg/pull/27408)) + +### New APIs + +- Add a useFocusOnMount hook to the @wordpress/compose package. ([27574](https://github.com/WordPress/gutenberg/pull/27574)) +- Add a useFocusReturn hook. ([27572](https://github.com/WordPress/gutenberg/pull/27572)) +- Add a useConstrainedTabbing hook. ([27544](https://github.com/WordPress/gutenberg/pull/27544)) +- Components: Introduce a isDisabled prop to the Disabled component. ([26730](https://github.com/WordPress/gutenberg/pull/26730)) +- Create block: + - Use Block API version 2. ([26098](https://github.com/WordPress/gutenberg/pull/26098)) + - Fix for supporting external templates. ([27784](https://github.com/WordPress/gutenberg/pull/27784)) ([27776](https://github.com/WordPress/gutenberg/pull/27776)) + +### Bug Fixes + +- Widget screen: Fix block select on focus. ([27755](https://github.com/WordPress/gutenberg/pull/27755)) +- [Embed block]: Add html and reusable support back. ([27733](https://github.com/WordPress/gutenberg/pull/27733)) +- Add useCallbackRef to avoid calling the ref multiple times with the same node. ([27710](https://github.com/WordPress/gutenberg/pull/27710)) +- Correct getRedistributedColumnWidths and related tests. ([27681](https://github.com/WordPress/gutenberg/pull/27681)) +- Remove CSS Custom Property in code block. ([27672](https://github.com/WordPress/gutenberg/pull/27672)) +- Fix regression on code block for font-size property ([27862](https://github.com/WordPress/gutenberg/pull/27862)) +- Block crashes if font family is not found. ([27654](https://github.com/WordPress/gutenberg/pull/27654)) +- popover flickering on small screens. ([27648](https://github.com/WordPress/gutenberg/pull/27648)) +- Adding single use block from main inserter causes focus loss and menu to be stuck open. ([27641](https://github.com/WordPress/gutenberg/pull/27641)) +- Changelog for 9.5.2. ([27638](https://github.com/WordPress/gutenberg/pull/27638)) +- Uncaught error with a custom generic store without a unsubscribe function in useSelect. ([27634](https://github.com/WordPress/gutenberg/pull/27634)) +- Revert date changes from branch 'replace-moment'. ([27550](https://github.com/WordPress/gutenberg/pull/27550)) +- useMediaQuery: Make it safe for SSR environments without window. ([27542](https://github.com/WordPress/gutenberg/pull/27542)) +- Fixes the width on the circle color picker popover. ([27523](https://github.com/WordPress/gutenberg/pull/27523)) +- ComboboxControl/FormTokenField: Fix iOS zooming for input. ([27471](https://github.com/WordPress/gutenberg/pull/27471)) +- Fallback to regular subscribe if the store doesn't exist in useSelect. ([27466](https://github.com/WordPress/gutenberg/pull/27466)) +- Global Styles getPresetVariable uses a wrong variable; Remove GLOBAL_CONTEXT. ([27450](https://github.com/WordPress/gutenberg/pull/27450)) +- Popover: Fix issue with undefined getBoundingClientRect. ([27445](https://github.com/WordPress/gutenberg/pull/27445)) +- Try fixing combobox a11y issues. ([27431](https://github.com/WordPress/gutenberg/pull/27431)) +- Support gradients with omitted stop positions in CustomGradientPicker. ([27413](https://github.com/WordPress/gutenberg/pull/27413)) +- Fix combobox suggestion list closure when clicking scrollbar. ([27367](https://github.com/WordPress/gutenberg/pull/27367)) +- Video Block: Let the video fill the container. ([27328](https://github.com/WordPress/gutenberg/pull/27328)) +- Media & Text “crop image to fill” to work with linked media. ([27211](https://github.com/WordPress/gutenberg/pull/27211)) +- Give editable fields in blocks better aria-labels. ([26582](https://github.com/WordPress/gutenberg/pull/26582)) +- Replace function should handle empty string callback return in the shortcode parser. ([16358](https://github.com/WordPress/gutenberg/pull/16358)) + +### Performance + +- Split core blocks assets loading. ([25220](https://github.com/WordPress/gutenberg/pull/25220)) + +### Experiments + +- Add a useDialog hook and replace the duplicated PopoverWrapper. ([27643](https://github.com/WordPress/gutenberg/pull/27643)) +- Refactor withFocusOutside to hook. ([27369](https://github.com/WordPress/gutenberg/pull/27369)) +- FSE: Block Navigation: update Navigation block placeholder. ([27018](https://github.com/WordPress/gutenberg/pull/27018)) +- FSE: Block Query + - Add new post link to Query. ([27732](https://github.com/WordPress/gutenberg/pull/27732)) + - Allow Query Loop only inside Query block. ([27637](https://github.com/WordPress/gutenberg/pull/27637)) + - Adjust mobile margins for the Query block's grid view. ([27619](https://github.com/WordPress/gutenberg/pull/27619)) + - Query block: Allow inheriting the global query arguments. ([27128](https://github.com/WordPress/gutenberg/pull/27128)) +- FSE: Blocks + - Add comment-form block styles. ([27673](https://github.com/WordPress/gutenberg/pull/27673)) + - Tag Cloud block: Adjust styles for the different block alignments. ([27342](https://github.com/WordPress/gutenberg/pull/27342)) + - Site Logo: + - Remove line height. ([27623](https://github.com/WordPress/gutenberg/pull/27623)) + - Add a rounded block style. ([27621](https://github.com/WordPress/gutenberg/pull/27621)) +- FSE: Infrastructure + - Apply hover class in outline mode. ([27714](https://github.com/WordPress/gutenberg/pull/27714)) + - Update documentation to show how a theme can have FSE automatically enabled. ([27680](https://github.com/WordPress/gutenberg/pull/27680)) + - Make the inserter in the site editor behave as a popover. ([27502](https://github.com/WordPress/gutenberg/pull/27502)) + - Add an outline mode and use it both Site Editor and Template mode. ([27499](https://github.com/WordPress/gutenberg/pull/27499)) + - Load the block patterns in the site editor. ([27497](https://github.com/WordPress/gutenberg/pull/27497)) + - Move the templates prePersist logic to core-data. ([27464](https://github.com/WordPress/gutenberg/pull/27464)) + - Expand the multi-entity saving panel by default. ([27437](https://github.com/WordPress/gutenberg/pull/27437)) + - Reveal block boundaries on hover in the Site Editor. ([27271](https://github.com/WordPress/gutenberg/pull/27271)) + - Site Editor - add query args for current context. ([27124](https://github.com/WordPress/gutenberg/pull/27124)) + - Full Site Editing: Introduce a template editing mode inside the post editor. ([26355](https://github.com/WordPress/gutenberg/pull/26355)) + - Remove optimistic updates to solve the template revert issue ([27797](https://github.com/WordPress/gutenberg/pull/27797)) +- FSE: Style System + - Fix: Font Weight and Style don't work independently on global styles. ([27659](https://github.com/WordPress/gutenberg/pull/27659)) + - Add custom units in BoxControl. ([27626](https://github.com/WordPress/gutenberg/pull/27626)) + - Remove Font style, weight, decoration, and transform presets. ([27555](https://github.com/WordPress/gutenberg/pull/27555)) + - Make client preset metadata match server. ([27453](https://github.com/WordPress/gutenberg/pull/27453)) + - Do not pass selectors and supports information to the client. ([27449](https://github.com/WordPress/gutenberg/pull/27449)) + - Add border radius support. ([25791](https://github.com/WordPress/gutenberg/pull/25791)) + - Update font-weight names. ([27718](https://github.com/WordPress/gutenberg/pull/27718)) + - Update performance of global styles code ([27779](https://github.com/WordPress/gutenberg/pull/27779)) + +### Documentation + +- Add missing dependency to code example. ([27742](https://github.com/WordPress/gutenberg/pull/27742)) +- Precise that element ref returned by the hooks that return a ref can change between function or object. ([27610](https://github.com/WordPress/gutenberg/pull/27610)) +- Add escaping functions to code examples. ([27603](https://github.com/WordPress/gutenberg/pull/27603)) +- Add missing @wordpress/components/CHANGELOG.md entry. ([27576](https://github.com/WordPress/gutenberg/pull/27576)) +- Minor changes to release documentation for clarity. ([27571](https://github.com/WordPress/gutenberg/pull/27571)) +- Capitalize JavaScript in accordance with the word mark. ([27539](https://github.com/WordPress/gutenberg/pull/27539)) +- Fix typo in attributes.md. ([27440](https://github.com/WordPress/gutenberg/pull/27440)) +- Try: Update readme screenshot. ([27223](https://github.com/WordPress/gutenberg/pull/27223)) +- Document the useBlockWrapper hook in the block registration documentation. ([26592](https://github.com/WordPress/gutenberg/pull/26592)) +- Add a document explaining the different block API versions. ([26277](https://github.com/WordPress/gutenberg/pull/26277)) +- Update the registration examples to use apiVersion 2. ([26100](https://github.com/WordPress/gutenberg/pull/26100)) + +### Code Quality + +- Remove: Missed unused weights and style translation code. ([27739](https://github.com/WordPress/gutenberg/pull/27739)) +- useDialog: Remove mousedown propagation stopping. ([27725](https://github.com/WordPress/gutenberg/pull/27725)) +- Try: Simplify focus return. ([27705](https://github.com/WordPress/gutenberg/pull/27705)) +- Popover/Modal: Remove and deprecate IsolatedEventContainer. ([27703](https://github.com/WordPress/gutenberg/pull/27703)) +- Popover: Use focus outside hook. ([27700](https://github.com/WordPress/gutenberg/pull/27700)) +- refactor: Tooltip component from classical to functional with hooks. ([27682](https://github.com/WordPress/gutenberg/pull/27682)) +- Template-part padding: Use variables. ([27679](https://github.com/WordPress/gutenberg/pull/27679)) +- Scope image block style variations to only the image block. ([27649](https://github.com/WordPress/gutenberg/pull/27649)) +- Refactor the EditorProvider component and extract hooks. ([27605](https://github.com/WordPress/gutenberg/pull/27605)) +- Use store definition instead of string for notices packages. ([27548](https://github.com/WordPress/gutenberg/pull/27548)) +- Merge RootContainer with BlockList. ([27531](https://github.com/WordPress/gutenberg/pull/27531)) +- Block wrapper: Isolate functionality into smaller hooks. ([27503](https://github.com/WordPress/gutenberg/pull/27503)) +- Writing flow: Consider events only from DOM descendents. ([27489](https://github.com/WordPress/gutenberg/pull/27489)) +- Writing flow: Isolate multi select focus element. ([27482](https://github.com/WordPress/gutenberg/pull/27482)) +- Multi selection: Move hook to WritingFlow with other multi selection logic. ([27479](https://github.com/WordPress/gutenberg/pull/27479)) +- Insertion indicator: Render after last block if none is specified. ([27472](https://github.com/WordPress/gutenberg/pull/27472)) +- Rewrite selection clearer in Block editor. ([27468](https://github.com/WordPress/gutenberg/pull/27468)) +- Move block focus listener to block props hook. ([27463](https://github.com/WordPress/gutenberg/pull/27463)) +- Block editor: Refactor effect.js to controls. ([27298](https://github.com/WordPress/gutenberg/pull/27298)) +- Animate: Type getAnimateClassName. ([27123](https://github.com/WordPress/gutenberg/pull/27123)) +- Refactor image block's image editing tools into separate components. ([27089](https://github.com/WordPress/gutenberg/pull/27089)) +- Drop zone provider: Option to avoid wrapper element. ([27079](https://github.com/WordPress/gutenberg/pull/27079)) +- Audit variables stylesheet. ([26827](https://github.com/WordPress/gutenberg/pull/26827)) +- group block padding: Use variables. ([27676](https://github.com/WordPress/gutenberg/pull/27676)) + +### Tools + +- Release script: Set draft status, and only remove after uploading asset. ([27713](https://github.com/WordPress/gutenberg/pull/27713)) +- CI: Run date test timezone and locale variations using bash script. ([27600](https://github.com/WordPress/gutenberg/pull/27600)) +- Upgrade Babel packages to 7.12.x. ([27553](https://github.com/WordPress/gutenberg/pull/27553)) +- CI: Run package/date unit tests in different timezones. ([27552](https://github.com/WordPress/gutenberg/pull/27552)) +- Avoid cancelling other end-to-end test jobs when one fails. ([27541](https://github.com/WordPress/gutenberg/pull/27541)) +- Add webpack 5 support to dependency-extraction-webpack-plugin. ([27533](https://github.com/WordPress/gutenberg/pull/27533)) +- Add GitHub support document. ([27524](https://github.com/WordPress/gutenberg/pull/27524)) +- Stabilize adding blocks end to end test. ([27493](https://github.com/WordPress/gutenberg/pull/27493)) +- GitHub Actions: Use a build matrix for the end-to-end tests GH action. ([27487](https://github.com/WordPress/gutenberg/pull/27487)) +- Packages: Make it possible to select minimum version bump for publishing. ([27459](https://github.com/WordPress/gutenberg/pull/27459)) +- Upgrade wp-prettier to 2.2.1. ([27441](https://github.com/WordPress/gutenberg/pull/27441)) +- Testing: Make image size test more stable. ([27439](https://github.com/WordPress/gutenberg/pull/27439)) +- Packages: Improve the script that automates version bumps. ([27436](https://github.com/WordPress/gutenberg/pull/27436)) +- CI: Update bundle size workflow to use the latest version. ([27435](https://github.com/WordPress/gutenberg/pull/27435)) +- wp-env: Xdebug support. ([27346](https://github.com/WordPress/gutenberg/pull/27346)) +- Make zip-based URL parsing more general. ([27019](https://github.com/WordPress/gutenberg/pull/27019)) +- Add inserter performance measures. ([26634](https://github.com/WordPress/gutenberg/pull/26634)) + +### Various + +- Verse Block: + - Add support for custom padding. ([27341](https://github.com/WordPress/gutenberg/pull/27341)) + - Add support for font family. ([27332](https://github.com/WordPress/gutenberg/pull/27332)) + - Add support for font size. ([27735](https://github.com/WordPress/gutenberg/pull/27735)) + - Update CSS for frontend and editor. ([27734](https://github.com/WordPress/gutenberg/pull/27734)) +- Popover: Use a11y hooks instead of HoCs. ([27707](https://github.com/WordPress/gutenberg/pull/27707)) +- Refactor focus on mount. ([27699](https://github.com/WordPress/gutenberg/pull/27699)) +- Search block: Use em values for padding. ([27678](https://github.com/WordPress/gutenberg/pull/27678)) +- Button: Is-busy state candybar animation fixed. ([27592](https://github.com/WordPress/gutenberg/pull/27592)) +- Preformatted block: Add support for font sizes. ([27584](https://github.com/WordPress/gutenberg/pull/27584)) +- Remove autoFocus prop from URLInput and from the inserter search form. ([27578](https://github.com/WordPress/gutenberg/pull/27578)) +- Package lock: Update ws. ([27532](https://github.com/WordPress/gutenberg/pull/27532)) +- Update block-patterns.md. ([27520](https://github.com/WordPress/gutenberg/pull/27520)) +- Update wp-env codeowners. ([27491](https://github.com/WordPress/gutenberg/pull/27491)) +- Update the backup icon to better align with WordPress icon package dna. ([27465](https://github.com/WordPress/gutenberg/pull/27465)) +- Update the rich text control titles to sentence case structure. ([27447](https://github.com/WordPress/gutenberg/pull/27447)) +- Search Block: Remove the button only option from the UI. ([27379](https://github.com/WordPress/gutenberg/pull/27379)) +- Add icons for template parts. ([27378](https://github.com/WordPress/gutenberg/pull/27378)) +- Increase radio dimensions to match checkboxes. ([27377](https://github.com/WordPress/gutenberg/pull/27377)) +- Adjusts settings modal height to 90%. ([27362](https://github.com/WordPress/gutenberg/pull/27362)) +- Change the Labels of the Vertical Align options. ([27356](https://github.com/WordPress/gutenberg/pull/27356)) +- Add element selector to template-part block. ([27101](https://github.com/WordPress/gutenberg/pull/27101)) +- Add explicit dismiss button and on dismiss callback to snackbar. ([26952](https://github.com/WordPress/gutenberg/pull/26952)) +- Make social list block align right able on published page & preview. ([26861](https://github.com/WordPress/gutenberg/pull/26861)) +- Update media-text focalPoint conditional rendering. ([25968](https://github.com/WordPress/gutenberg/pull/25968)) +- Remove default icon from PluginBlockSettingsMenuItem. ([21392](https://github.com/WordPress/gutenberg/pull/21392)) +- Add example preview to video block. ([20703](https://github.com/WordPress/gutenberg/pull/20703)) -Changelog creation in progress. = 9.5.2 = diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 46560971890650..ffe0981510eb16 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -222,7 +222,7 @@ example: { #### variations (optional) -- **Type:** `Object[]` +- **Type:** `Object[]` Similarly to how the block's style variations can be declared, a block type can define block variations that the user can pick from. The difference is that, rather than changing only the visual appearance, this field provides a way to apply initial custom attributes and inner blocks at the time when a block is inserted. @@ -256,19 +256,20 @@ variations: [ An object describing a variation defined for the block type can contain the following fields: -- `name` (type `string`) – The unique and machine-readable name. -- `title` (type `string`) – A human-readable variation title. -- `description` (optional, type `string`) – A detailed variation description. -- `icon` (optional, type `string` | `Object`) – An icon helping to visualize the variation. It can have the same shape as the block type. -- `isDefault` (optional, type `boolean`) – Indicates whether the current variation is the default one. Defaults to `false`. -- `attributes` (optional, type `Object`) – Values that override block attributes. -- `innerBlocks` (optional, type `Array[]`) – Initial configuration of nested blocks. -- `example` (optional, type `Object`) – Example provides structured data for the block preview. You can set to `undefined` to disable the preview shown for the block type. -- `scope` (optional, type `WPBlockVariationScope[]`) - the list of scopes where the variation is applicable. When not provided, it defaults to `block` and `inserter`. Available options: - - `inserter` - Block Variation is shown on the inserter. - - `block` - Used by blocks to filter specific block variations. Mostly used in Placeholder patterns like `Columns` block. - - `transform` - Block Variation will be shown in the component for Block Variations transformations. -- `keywords` (optional, type `string[]`) - An array of terms (which can be translated) that help users discover the variation while searching. +- `name` (type `string`) – The unique and machine-readable name. +- `title` (type `string`) – A human-readable variation title. +- `description` (optional, type `string`) – A detailed variation description. +- `icon` (optional, type `string` | `Object`) – An icon helping to visualize the variation. It can have the same shape as the block type. +- `isDefault` (optional, type `boolean`) – Indicates whether the current variation is the default one. Defaults to `false`. +- `attributes` (optional, type `Object`) – Values that override block attributes. +- `innerBlocks` (optional, type `Array[]`) – Initial configuration of nested blocks. +- `example` (optional, type `Object`) – Example provides structured data for the block preview. You can set to `undefined` to disable the preview shown for the block type. +- `scope` (optional, type `WPBlockVariationScope[]`) - the list of scopes where the variation is applicable. When not provided, it defaults to `block` and `inserter`. Available options: + - `inserter` - Block Variation is shown on the inserter. + - `block` - Used by blocks to filter specific block variations. Mostly used in Placeholder patterns like `Columns` block. + - `transform` - Block Variation will be shown in the component for Block Variations transformations. +- `keywords` (optional, type `string[]`) - An array of terms (which can be translated) that help users discover the variation while searching. +- `isActive` (optional, type `Function`) - A function that accepts a block's attributes and the variation's attributes and determines if a variation is active. This function doesn't try to find a match dynamically based on all block's attributes, as in many cases some attributes are irrelevant. An example would be for `embed` block where we only care about `providerNameSlug` attribute's value. It's also possible to override the default block style variation using the `className` attribute when defining block variations. @@ -278,15 +279,17 @@ variations: [ name: 'blue', title: __( 'Blue Quote' ), isDefault: true, - attributes: { className: 'is-style-blue-quote' }, + attributes: { color: 'blue', className: 'is-style-blue-quote' }, icon: 'format-quote', + isActive: ( blockAttributes, variationAttributes ) => + blockAttributes.color === variationAttributes.color }, ], ``` #### supports (optional) -- ***Type:*** `Object` +- **_Type:_** `Object` Supports contains as set of options to control features used in the editor. See the [the supports documentation](/docs/designers-developers/developers/block-api/block-supports.md) for more details. diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php index 0fc357fb8a7d36..a94dbb1fde86d6 100644 --- a/lib/class-wp-theme-json-resolver.php +++ b/lib/class-wp-theme-json-resolver.php @@ -237,7 +237,7 @@ private static function get_user_origin() { $config = $decoded_data; } } - self::$user = new WP_Theme_JSON( $config ); + self::$user = new WP_Theme_JSON( $config, true ); return self::$user; } @@ -270,7 +270,6 @@ private static function get_user_origin() { * @return WP_Theme_JSON */ public function get_origin( $theme_support_data = array(), $origin = 'user', $merged = true ) { - if ( ( 'user' === $origin ) && $merged ) { $result = new WP_Theme_JSON(); $result->merge( self::get_core_origin() ); @@ -301,10 +300,6 @@ public function get_origin( $theme_support_data = array(), $origin = 'user', $me * Registers a Custom Post Type to store the user's origin config. */ public static function register_user_custom_post_type() { - if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { - return; - } - $args = array( 'label' => __( 'Global Styles', 'gutenberg' ), 'description' => 'CPT to store user design tokens', diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index 8f0071f0478abe..95344313853c8f 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -135,8 +135,8 @@ class WP_Theme_JSON { 'dropCap' => null, 'fontFamilies' => null, 'fontSizes' => null, - 'customFontStyle' => null, - 'customFontWeight' => null, + 'customFontStyle' => null, + 'customFontWeight' => null, 'customTextDecorations' => null, 'customTextTransforms' => null, ), @@ -301,9 +301,10 @@ class WP_Theme_JSON { /** * Constructor. * - * @param array $contexts A structure that follows the theme.json schema. + * @param array $contexts A structure that follows the theme.json schema. + * @param boolean $should_escape_styles Whether the incoming styles should be escaped. */ - public function __construct( $contexts = array() ) { + public function __construct( $contexts = array(), $should_escape_styles = false ) { $this->contexts = array(); if ( ! is_array( $contexts ) ) { @@ -324,8 +325,9 @@ public function __construct( $contexts = array() ) { // Process styles subtree. $this->process_key( 'styles', $context, self::SCHEMA ); if ( isset( $context['styles'] ) ) { - $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'] ); - $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'] ); + $this->process_key( 'color', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); + $this->process_key( 'spacing', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); + $this->process_key( 'typography', $context['styles'], self::SCHEMA['styles'], $should_escape_styles ); if ( empty( $context['styles'] ) ) { unset( $context['styles'] ); @@ -337,6 +339,7 @@ public function __construct( $contexts = array() ) { // Process settings subtree. $this->process_key( 'settings', $context, self::SCHEMA ); if ( isset( $context['settings'] ) ) { + $this->process_key( 'border', $context['settings'], self::SCHEMA['settings'] ); $this->process_key( 'color', $context['settings'], self::SCHEMA['settings'] ); $this->process_key( 'spacing', $context['settings'], self::SCHEMA['settings'] ); $this->process_key( 'typography', $context['settings'], self::SCHEMA['settings'] ); @@ -469,11 +472,12 @@ private static function get_blocks_metadata() { * This function modifies the given input by removing * the nodes that aren't valid per the schema. * - * @param string $key Key of the subtree to normalize. - * @param array $input Whole tree to normalize. - * @param array $schema Schema to use for normalization. + * @param string $key Key of the subtree to normalize. + * @param array $input Whole tree to normalize. + * @param array $schema Schema to use for normalization. + * @param boolean $should_escape Whether the subproperties should be escaped. */ - private static function process_key( $key, &$input, $schema ) { + private static function process_key( $key, &$input, $schema, $should_escape = false ) { if ( ! isset( $input[ $key ] ) ) { return; } @@ -493,6 +497,21 @@ private static function process_key( $key, &$input, $schema ) { $schema[ $key ] ); + if ( $should_escape ) { + $subtree = $input[ $key ]; + foreach ( $subtree as $property => $value ) { + $name = 'background-color'; + if ( 'gradient' === $property ) { + $name = 'background'; + } + $result = safecss_filter_attr( "$name: $value" ); + + if ( '' === $result ) { + unset( $input[ $key ][ $property ] ); + } + } + } + if ( 0 === count( $input[ $key ] ) ) { unset( $input[ $key ] ); } diff --git a/lib/global-styles.php b/lib/global-styles.php index 6c62d867fc967d..800048bdcb219f 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -162,6 +162,10 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree, $type = 'al * and enqueues the resulting stylesheet. */ function gutenberg_experimental_global_styles_enqueue_assets() { + if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { + return; + } + $settings = gutenberg_get_common_block_editor_settings(); $theme_support_data = gutenberg_experimental_global_styles_get_theme_support_settings( $settings ); @@ -196,45 +200,74 @@ function gutenberg_experimental_global_styles_settings( $settings ) { unset( $settings['gradients'] ); $resolver = new WP_Theme_JSON_Resolver(); - $all = $resolver->get_origin( $theme_support_data ); - $base = $resolver->get_origin( $theme_support_data, 'theme' ); + $origin = 'theme'; + if ( + gutenberg_experimental_global_styles_has_theme_json_support() && + gutenberg_is_fse_theme() + ) { + // Only lookup for the user data if we need it. + $origin = 'user'; + } + $tree = $resolver->get_origin( $theme_support_data, $origin ); // STEP 1: ADD FEATURES - // These need to be added to settings always. - $settings['__experimentalFeatures'] = $all->get_settings(); + // + // These need to be always added to the editor settings, + // even for themes that don't support theme.json. + // An example of this is that the presets are configured + // from the theme support data. + $settings['__experimentalFeatures'] = $tree->get_settings(); // STEP 2 - IF EDIT-SITE, ADD DATA REQUIRED FOR GLOBAL STYLES SIDEBAR - // The client needs some information to be able to access/update the user styles. - // We only do this if the theme has support for theme.json, though, - // as an indicator that the theme will know how to combine this with its stylesheet. + // + // In the site editor, the user can change styles, so the client + // needs the ability to create them. Hence, we pass it some data + // for this: base styles (core+theme) and the ID of the user CPT. $screen = get_current_screen(); if ( ! empty( $screen ) && function_exists( 'gutenberg_is_edit_site_page' ) && gutenberg_is_edit_site_page( $screen->id ) && - gutenberg_experimental_global_styles_has_theme_json_support() + gutenberg_experimental_global_styles_has_theme_json_support() && + gutenberg_is_fse_theme() ) { - $settings['__experimentalGlobalStylesUserEntityId'] = WP_Theme_JSON_Resolver::get_user_custom_post_type_id(); - $settings['__experimentalGlobalStylesBaseStyles'] = $base->get_raw_data(); - } else { - // STEP 3 - OTHERWISE, ADD STYLES + $user_cpt_id = WP_Theme_JSON_Resolver::get_user_custom_post_type_id(); + $base_styles = $resolver->get_origin( $theme_support_data, 'theme' )->get_raw_data(); + + $settings['__experimentalGlobalStylesUserEntityId'] = $user_cpt_id; + $settings['__experimentalGlobalStylesBaseStyles'] = $base_styles; + } elseif ( gutenberg_experimental_global_styles_has_theme_json_support() ) { + // STEP 3 - ADD STYLES IF THEME HAS SUPPORT // // If we are in a block editor context, but not in edit-site, - // we need to add the styles via the settings. This is because - // we want them processed as if they were added via add_editor_styles, - // which adds the editor wrapper class. + // we add the styles via the settings, so the editor knows that + // some of these should be added the wrapper class, + // as if they were added via add_editor_styles. $settings['styles'][] = array( - 'css' => gutenberg_experimental_global_styles_get_stylesheet( $all, 'css_variables' ), + 'css' => gutenberg_experimental_global_styles_get_stylesheet( $tree, 'css_variables' ), '__experimentalNoWrapper' => true, ); $settings['styles'][] = array( - 'css' => gutenberg_experimental_global_styles_get_stylesheet( $all, 'block_styles' ), + 'css' => gutenberg_experimental_global_styles_get_stylesheet( $tree, 'block_styles' ), ); } return $settings; } -add_action( 'init', array( 'WP_Theme_JSON_Resolver', 'register_user_custom_post_type' ) ); +/** + * Register CPT to store/access user data. + * + * @return array|undefined + */ +function gutenberg_experimental_global_styles_register_user_cpt() { + if ( ! gutenberg_experimental_global_styles_has_theme_json_support() ) { + return; + } + + WP_Theme_JSON_Resolver::register_user_custom_post_type(); +} + +add_action( 'init', 'gutenberg_experimental_global_styles_register_user_cpt' ); add_filter( 'block_editor_settings', 'gutenberg_experimental_global_styles_settings', PHP_INT_MAX ); add_action( 'wp_enqueue_scripts', 'gutenberg_experimental_global_styles_enqueue_assets' ); diff --git a/package-lock.json b/package-lock.json index 9cbe9a44bc33b9..f78a00ff81b75d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "9.6.0-rc.1", + "version": "9.6.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4e72c0e1e5b1dd..bee26b829b584e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "9.6.0-rc.1", + "version": "9.6.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 7ee086234ecd0d..95611432d3e1bb 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -19,12 +19,12 @@ import { BlockEditorProvider, BlockList, WritingFlow, - ObserveTyping + ObserveTyping, } from '@wordpress/block-editor'; import { SlotFillProvider, Popover } from '@wordpress/components'; import { useState } from '@wordpress/element'; -function MyEditorComponent () { +function MyEditorComponent() { const [ blocks, updateBlocks ] = useState( [] ); return ( @@ -568,6 +568,26 @@ _Related_ - +# **useBlockDisplayInformation** + +Hook used to try to find a matching block variation and return +the appropriate information for display reasons. In order to +to try to find a match we need to things: +1\. Block's client id to extract it's current attributes. +2\. A block variation should have set `isActive` prop to a proper function. + +If for any reason a block variaton match cannot be found, +the returned information come from the Block Type. +If no blockType is found with the provided clientId, returns null. + +_Parameters_ + +- _clientId_ `string`: Block's client id. + +_Returns_ + +- `?WPBlockDisplayInformation`: Block's display information, or `null` when the block or its type not found. + # **useBlockEditContext** Undocumented declaration. diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js index c209ea39b5c73e..60f8616b5f49dd 100644 --- a/packages/block-editor/src/components/block-card/index.js +++ b/packages/block-editor/src/components/block-card/index.js @@ -1,9 +1,20 @@ +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + /** * Internal dependencies */ import BlockIcon from '../block-icon'; -function BlockCard( { blockType: { icon, title, description } } ) { +function BlockCard( { title, icon, description, blockType } ) { + if ( blockType ) { + deprecated( '`blockType` property in `BlockCard component`', { + alternative: '`title, icon and description` properties', + } ); + ( { title, icon, description } = blockType ); + } return (
diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index be2433edbe39da..dd6edc46f5be04 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -25,6 +25,8 @@ import BlockStyles from '../block-styles'; import MultiSelectionInspector from '../multi-selection-inspector'; import DefaultStylePicker from '../default-style-picker'; import BlockVariationTransforms from '../block-variation-transforms'; +import useBlockDisplayInformation from '../use-block-display-information'; + const BlockInspector = ( { blockType, count, @@ -64,22 +66,36 @@ const BlockInspector = ( { } return null; } + return ( + + ); +}; +const BlockInspectorSingleBlock = ( { + clientId, + blockName, + hasBlockStyles, + bubblesVirtually, +} ) => { + const blockInformation = useBlockDisplayInformation( clientId ); return (
- - + + { hasBlockStyles && (
- + { hasBlockSupport( - blockType.name, + blockName, 'defaultStylePicker', true - ) && ( - - ) } + ) && }
) } diff --git a/packages/block-editor/src/components/block-mover/index.native.js b/packages/block-editor/src/components/block-mover/index.native.js index aa1892c33e8042..d64509ff17726e 100644 --- a/packages/block-editor/src/components/block-mover/index.native.js +++ b/packages/block-editor/src/components/block-mover/index.native.js @@ -2,29 +2,52 @@ * External dependencies */ import { first, last, partial, castArray } from 'lodash'; +import { Platform } from 'react-native'; /** * WordPress dependencies */ -import { ToolbarButton } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { Picker, ToolbarButton } from '@wordpress/components'; import { withInstanceId, compose } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { useRef, useState } from '@wordpress/element'; /** * Internal dependencies */ import { getMoversSetup } from './mover-description'; -const BlockMover = ( { +export const BLOCK_MOVER_DIRECTION_TOP = 'blockPageMoverOptions-moveToTop'; +export const BLOCK_MOVER_DIRECTION_BOTTOM = + 'blockPageMoverOptions-moveToBottom'; + +export const BlockMover = ( { isFirst, isLast, isLocked, onMoveDown, onMoveUp, + onLongMove, firstIndex, + numberOfBlocks, rootClientId, isStackedHorizontally, } ) => { + const pickerRef = useRef(); + const [ blockPageMoverState, setBlockPageMoverState ] = useState( + undefined + ); + const showBlockPageMover = ( direction ) => () => { + if ( ! pickerRef.current ) { + setBlockPageMoverState( undefined ); + return; + } + + setBlockPageMoverState( direction ); + pickerRef.current.presentPicker(); + }; + const { description: { backwardButtonHint, @@ -36,6 +59,32 @@ const BlockMover = ( { title: { backward: backwardButtonTitle, forward: forwardButtonTitle }, } = getMoversSetup( isStackedHorizontally, { firstIndex } ); + const blockPageMoverOptions = [ + { + icon: backwardButtonIcon, + label: __( 'Move to top' ), + value: BLOCK_MOVER_DIRECTION_TOP, + onSelect: () => { + onLongMove()( 0 ); + }, + }, + { + icon: forwardButtonIcon, + label: __( 'Move to bottom' ), + value: BLOCK_MOVER_DIRECTION_BOTTOM, + onSelect: () => { + onLongMove()( numberOfBlocks ); + }, + }, + ].filter( ( el ) => el.value === blockPageMoverState ); + + const onPickerSelect = ( value ) => { + const option = blockPageMoverOptions.find( + ( el ) => el.value === value + ); + if ( option && option.onSelect ) option.onSelect(); + }; + if ( isLocked || ( isFirst && isLast && ! rootClientId ) ) { return null; } @@ -46,6 +95,7 @@ const BlockMover = ( { title={ ! isFirst ? backwardButtonTitle : firstBlockTitle } isDisabled={ isFirst } onClick={ onMoveUp } + onLongPress={ showBlockPageMover( BLOCK_MOVER_DIRECTION_TOP ) } icon={ backwardButtonIcon } extraProps={ { hint: backwardButtonHint } } /> @@ -54,11 +104,23 @@ const BlockMover = ( { title={ ! isLast ? forwardButtonTitle : lastBlockTitle } isDisabled={ isLast } onClick={ onMoveDown } + onLongPress={ showBlockPageMover( + BLOCK_MOVER_DIRECTION_BOTTOM + ) } icon={ forwardButtonIcon } extraProps={ { hint: forwardButtonHint, } } /> + + ); }; @@ -83,6 +145,7 @@ export default compose( return { firstIndex, + numberOfBlocks: blockOrder.length - 1, isFirst: firstIndex === 0, isLast: lastIndex === blockOrder.length - 1, isLocked: getTemplateLock( rootClientId ) === 'all', @@ -90,12 +153,19 @@ export default compose( }; } ), withDispatch( ( dispatch, { clientIds, rootClientId } ) => { - const { moveBlocksDown, moveBlocksUp } = dispatch( + const { moveBlocksDown, moveBlocksUp, moveBlocksToPosition } = dispatch( 'core/block-editor' ); return { onMoveDown: partial( moveBlocksDown, clientIds, rootClientId ), onMoveUp: partial( moveBlocksUp, clientIds, rootClientId ), + onLongMove: ( targetIndex ) => + partial( + moveBlocksToPosition, + clientIds, + rootClientId, + targetIndex + ), }; } ), withInstanceId diff --git a/packages/block-editor/src/components/block-mover/test/__snapshots__/index.native.js.snap b/packages/block-editor/src/components/block-mover/test/__snapshots__/index.native.js.snap new file mode 100644 index 00000000000000..65269234f413f3 --- /dev/null +++ b/packages/block-editor/src/components/block-mover/test/__snapshots__/index.native.js.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Block Mover Picker should match snapshot 1`] = ` + + + + + } + onLongPress={[Function]} + title="Move block up from row NaN to row NaN" + /> + + + + } + onLongPress={[Function]} + title="Move block down from row NaN to row NaN" + /> + + +`; diff --git a/packages/block-editor/src/components/block-mover/test/index.native.js b/packages/block-editor/src/components/block-mover/test/index.native.js new file mode 100644 index 00000000000000..113d61e84cf254 --- /dev/null +++ b/packages/block-editor/src/components/block-mover/test/index.native.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { BlockMover } from '../index'; + +describe( 'Block Mover Picker', () => { + it( 'renders without crashing', () => { + const wrapper = shallow( , { + context: { + isFirst: false, + isLast: true, + isLocked: false, + numberOfBlocks: 2, + firstIndex: 1, + + onMoveDown: jest.fn(), + onMoveUp: jest.fn(), + onLongPress: jest.fn(), + + rootClientId: '', + isStackedHorizontally: true, + }, + } ); + expect( wrapper ).toBeTruthy(); + } ); + + it( 'should match snapshot', () => { + const wrapper = shallow( , { + context: { + isFirst: false, + isLast: true, + isLocked: false, + numberOfBlocks: 2, + firstIndex: 1, + + onMoveDown: jest.fn(), + onMoveUp: jest.fn(), + onLongPress: jest.fn(), + + rootClientId: '', + isStackedHorizontally: true, + }, + } ); + expect( wrapper ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/block-editor/src/components/block-navigation/block-select-button.js b/packages/block-editor/src/components/block-navigation/block-select-button.js index 2e9956d5387e18..e6f40a1ed62092 100644 --- a/packages/block-editor/src/components/block-navigation/block-select-button.js +++ b/packages/block-editor/src/components/block-navigation/block-select-button.js @@ -19,12 +19,13 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import BlockIcon from '../block-icon'; +import useBlockDisplayInformation from '../use-block-display-information'; import { getBlockPositionDescription } from './utils'; function BlockNavigationBlockSelectButton( { className, - block, + block: { clientId, name, attributes }, isSelected, onClick, position, @@ -38,12 +39,15 @@ function BlockNavigationBlockSelectButton( }, ref ) { - const { name, attributes } = block; - - const blockType = getBlockType( name ); - const blockDisplayName = getBlockLabel( blockType, attributes ); + const blockInformation = useBlockDisplayInformation( clientId ); const instanceId = useInstanceId( BlockNavigationBlockSelectButton ); const descriptionId = `block-navigation-block-select-button__${ instanceId }`; + const blockType = getBlockType( name ); + const blockLabel = getBlockLabel( blockType, attributes ); + // If label is defined we prioritize it over possible possible + // block variation match title. + const blockDisplayName = + blockLabel !== blockType.title ? blockLabel : blockInformation?.title; const blockPositionDescription = getBlockPositionDescription( position, siblingBlockCount, @@ -66,7 +70,7 @@ function BlockNavigationBlockSelectButton( onDragEnd={ onDragEnd } draggable={ draggable } > - + { blockDisplayName } { isSelected && ( diff --git a/packages/block-editor/src/components/block-switcher/block-styles-menu.js b/packages/block-editor/src/components/block-switcher/block-styles-menu.js index e600f062423df0..7ca6336c5f9176 100644 --- a/packages/block-editor/src/components/block-switcher/block-styles-menu.js +++ b/packages/block-editor/src/components/block-switcher/block-styles-menu.js @@ -4,6 +4,8 @@ import { __ } from '@wordpress/i18n'; import { MenuGroup } from '@wordpress/components'; import { useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { cloneBlock, getBlockFromExample } from '@wordpress/blocks'; /** * Internal dependencies @@ -11,8 +13,16 @@ import { useState } from '@wordpress/element'; import BlockStyles from '../block-styles'; import PreviewBlockPopover from './preview-block-popover'; -export default function BlockStylesMenu( { hoveredBlock, onSwitch } ) { +export default function BlockStylesMenu( { + hoveredBlock: { name, clientId }, + onSwitch, +} ) { const [ hoveredClassName, setHoveredClassName ] = useState(); + const blockType = useSelect( + ( select ) => select( 'core/blocks' ).getBlockType( name ), + [ name ] + ); + return ( { hoveredClassName && ( ) } { + const [ + hoveredTransformItemName, + setHoveredTransformItemName, + ] = useState(); return ( + { hoveredTransformItemName && ( + + ) } { possibleBlockTransformations.map( ( item ) => { const { name, icon, title, isDisabled } = item; return ( @@ -28,6 +46,12 @@ const BlockTransformationsMenu = ( { onSelect( name ); } } disabled={ isDisabled } + onMouseLeave={ () => + setHoveredTransformItemName( null ) + } + onMouseEnter={ () => + setHoveredTransformItemName( name ) + } > { title } diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index b789dd7348c146..9428084a21c57b 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -129,6 +129,7 @@ const BlockSwitcher = ( { clientIds } ) => { possibleBlockTransformations={ possibleBlockTransformations } + blocks={ blocks } onSelect={ ( name ) => { onTransform( name ); onClose(); diff --git a/packages/block-editor/src/components/block-switcher/preview-block-popover.js b/packages/block-editor/src/components/block-switcher/preview-block-popover.js index 4e8e0cd0b66842..53adb87f337ad7 100644 --- a/packages/block-editor/src/components/block-switcher/preview-block-popover.js +++ b/packages/block-editor/src/components/block-switcher/preview-block-popover.js @@ -3,21 +3,13 @@ */ import { __ } from '@wordpress/i18n'; import { Popover } from '@wordpress/components'; -import { - getBlockType, - cloneBlock, - getBlockFromExample, -} from '@wordpress/blocks'; + /** * Internal dependencies */ import BlockPreview from '../block-preview'; -export default function PreviewBlockPopover( { - hoveredBlock, - hoveredClassName, -} ) { - const hoveredBlockType = getBlockType( hoveredBlock.name ); +export default function PreviewBlockPopover( { blocks } ) { return (
@@ -30,25 +22,7 @@ export default function PreviewBlockPopover( {
{ __( 'Preview' ) }
- +
diff --git a/packages/block-editor/src/components/block-title/index.js b/packages/block-editor/src/components/block-title/index.js index 3a36d53e12e17c..c7701527e07a09 100644 --- a/packages/block-editor/src/components/block-title/index.js +++ b/packages/block-editor/src/components/block-title/index.js @@ -12,6 +12,11 @@ import { __experimentalGetBlockLabel as getBlockLabel, } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import useBlockDisplayInformation from '../use-block-display-information'; + /** * Renders the block's configured title as a string, or empty if the title * cannot be determined. @@ -44,22 +49,16 @@ export default function BlockTitle( { clientId } ) { [ clientId ] ); - if ( ! name ) { - return null; - } - + const blockInformation = useBlockDisplayInformation( clientId ); + if ( ! name || ! blockInformation ) return null; const blockType = getBlockType( name ); - if ( ! blockType ) { - return null; - } - - const { title } = blockType; const label = getBlockLabel( blockType, attributes ); - - // Label will often fall back to the title if no label is defined for the + // Label will fallback to the title if no label is defined for the // current label context. We do not want "Paragraph: Paragraph". - if ( label !== title ) { - return `${ title }: ${ truncate( label, { length: 15 } ) }`; + // If label is defined we prioritize it over possible possible + // block variation match title. + if ( label !== blockType.title ) { + return `${ blockType.title }: ${ truncate( label, { length: 15 } ) }`; } - return title; + return blockInformation.title; } diff --git a/packages/block-editor/src/components/block-title/test/index.js b/packages/block-editor/src/components/block-title/test/index.js index d268699d09668d..23af6288130e01 100644 --- a/packages/block-editor/src/components/block-title/test/index.js +++ b/packages/block-editor/src/components/block-title/test/index.js @@ -45,6 +45,15 @@ jest.mock( '@wordpress/blocks', () => { }; } ); +jest.mock( '../../use-block-display-information', () => { + const resultsMap = { + 'id-name-exists': { title: 'Block Title' }, + 'id-name-with-label': { title: 'Block With Label' }, + 'id-name-with-long-label': { title: 'Block With Long Label' }, + }; + return jest.fn( ( clientId ) => resultsMap[ clientId ] ); +} ); + jest.mock( '@wordpress/data/src/components/use-select', () => { // This allows us to tweak the returned value on each test const mock = jest.fn(); @@ -81,9 +90,7 @@ describe( 'BlockTitle', () => { attributes: null, } ) ); - const wrapper = shallow( - - ); + const wrapper = shallow( ); expect( wrapper.text() ).toBe( 'Block Title' ); } ); @@ -94,9 +101,7 @@ describe( 'BlockTitle', () => { attributes: null, } ) ); - const wrapper = shallow( - - ); + const wrapper = shallow( ); expect( wrapper.text() ).toBe( 'Block With Label: Test Label' ); } ); @@ -108,7 +113,7 @@ describe( 'BlockTitle', () => { } ) ); const wrapper = shallow( - + ); expect( wrapper.text() ).toBe( diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 3e845204bed823..551f83f8b4d663 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -118,6 +118,7 @@ export { export { default as Warning } from './warning'; export { default as WritingFlow } from './writing-flow'; export { useCanvasClickRedirect as __unstableUseCanvasClickRedirect } from './use-canvas-click-redirect'; +export { default as useBlockDisplayInformation } from './use-block-display-information'; /* * State Related Components diff --git a/packages/block-editor/src/components/inserter/preview-panel.js b/packages/block-editor/src/components/inserter/preview-panel.js index 11c87d647097d6..3242ca306ffaeb 100644 --- a/packages/block-editor/src/components/inserter/preview-panel.js +++ b/packages/block-editor/src/components/inserter/preview-panel.js @@ -16,11 +16,13 @@ import BlockCard from '../block-card'; import BlockPreview from '../block-preview'; function InserterPreviewPanel( { item } ) { - const hoveredItemBlockType = getBlockType( item.name ); + const { name, title, icon, description, initialAttributes } = item; + const hoveredItemBlockType = getBlockType( name ); + const isReusable = isReusableBlock( item ); return (
- { isReusableBlock( item ) || hoveredItemBlockType.example ? ( + { isReusable || hoveredItemBlockType.example ? (
@@ -53,7 +52,13 @@ function InserterPreviewPanel( { item } ) {
) }
- { ! isReusableBlock( item ) && } + { ! isReusable && ( + + ) }
); } diff --git a/packages/block-editor/src/components/link-control/link-preview.js b/packages/block-editor/src/components/link-control/link-preview.js index 673ba1a7a7449a..ef6bd09fcf270f 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -17,7 +17,8 @@ import { ViewerSlot } from './viewer-slot'; export default function LinkPreview( { value, onEditClick } ) { const displayURL = - ( value && filterURLForDisplay( safeDecodeURI( value.url ) ) ) || ''; + ( value && filterURLForDisplay( safeDecodeURI( value.url ), 16 ) ) || + ''; return (
{ expect( onChange ).not.toHaveBeenCalled(); expect( onInput ).not.toHaveBeenCalled(); expect( resetBlocks ).not.toHaveBeenCalled(); - expect( replaceInnerBlocks ).toHaveBeenCalledWith( 'test', testBlocks ); + // We can't check the args because the blocks are cloned. + expect( replaceInnerBlocks ).toHaveBeenCalled(); } ); it( 'does not add the controlled blocks to the block-editor store if the store already contains them', async () => { @@ -200,7 +201,6 @@ describe( 'useBlockSync hook', () => { it( 'calls onInput when a non-persistent block change occurs', async () => { const onChange = jest.fn(); const onInput = jest.fn(); - const value1 = [ { clientId: 'a', innerBlocks: [], attributes: { foo: 1 } }, ]; diff --git a/packages/block-editor/src/components/provider/use-block-sync.js b/packages/block-editor/src/components/provider/use-block-sync.js index 6a2f04c3f86b7c..91a906f837a238 100644 --- a/packages/block-editor/src/components/provider/use-block-sync.js +++ b/packages/block-editor/src/components/provider/use-block-sync.js @@ -8,6 +8,7 @@ import { last, noop } from 'lodash'; */ import { useEffect, useRef } from '@wordpress/element'; import { useRegistry } from '@wordpress/data'; +import { cloneBlock } from '@wordpress/blocks'; /** * A function to call when the block value has been updated in the block-editor @@ -83,6 +84,7 @@ export default function useBlockSync( { const { getBlockName, getBlocks } = registry.select( 'core/block-editor' ); const pendingChanges = useRef( { incoming: null, outgoing: [] } ); + const subscribed = useRef( false ); const setControlledBlocks = () => { if ( ! controlledBlocks ) { @@ -96,8 +98,17 @@ export default function useBlockSync( { if ( clientId ) { setHasControlledInnerBlocks( clientId, true ); __unstableMarkNextChangeAsNotPersistent(); - replaceInnerBlocks( clientId, controlledBlocks ); + const storeBlocks = controlledBlocks.map( ( block ) => + cloneBlock( block ) + ); + if ( subscribed.current ) { + pendingChanges.current.incoming = storeBlocks; + } + replaceInnerBlocks( clientId, storeBlocks ); } else { + if ( subscribed.current ) { + pendingChanges.current.incoming = controlledBlocks; + } resetBlocks( controlledBlocks ); } }; @@ -113,6 +124,37 @@ export default function useBlockSync( { onChangeRef.current = onChange; }, [ onInput, onChange ] ); + // Determine if blocks need to be reset when they change. + useEffect( () => { + if ( pendingChanges.current.outgoing.includes( controlledBlocks ) ) { + // Skip block reset if the value matches expected outbound sync + // triggered by this component by a preceding change detection. + // Only skip if the value matches expectation, since a reset should + // still occur if the value is modified (not equal by reference), + // to allow that the consumer may apply modifications to reflect + // back on the editor. + if ( + last( pendingChanges.current.outgoing ) === controlledBlocks + ) { + pendingChanges.current.outgoing = []; + } + } else if ( getBlocks( clientId ) !== controlledBlocks ) { + // Reset changing value in all other cases than the sync described + // above. Since this can be reached in an update following an out- + // bound sync, unset the outbound value to avoid considering it in + // subsequent renders. + pendingChanges.current.outgoing = []; + setControlledBlocks(); + + if ( controlledSelectionStart && controlledSelectionEnd ) { + resetSelection( + controlledSelectionStart, + controlledSelectionEnd + ); + } + } + }, [ controlledBlocks, clientId ] ); + useEffect( () => { const { getSelectionStart, @@ -125,6 +167,7 @@ export default function useBlockSync( { let isPersistent = isLastBlockChangePersistent(); let previousAreBlocksDifferent = false; + subscribed.current = true; const unsubscribe = registry.subscribe( () => { // Sometimes, when changing block lists, lingering subscriptions // might trigger before they are cleaned up. If the block for which @@ -184,36 +227,4 @@ export default function useBlockSync( { return () => unsubscribe(); }, [ registry, clientId ] ); - - // Determine if blocks need to be reset when they change. - useEffect( () => { - if ( pendingChanges.current.outgoing.includes( controlledBlocks ) ) { - // Skip block reset if the value matches expected outbound sync - // triggered by this component by a preceding change detection. - // Only skip if the value matches expectation, since a reset should - // still occur if the value is modified (not equal by reference), - // to allow that the consumer may apply modifications to reflect - // back on the editor. - if ( - last( pendingChanges.current.outgoing ) === controlledBlocks - ) { - pendingChanges.current.outgoing = []; - } - } else if ( getBlocks( clientId ) !== controlledBlocks ) { - // Reset changing value in all other cases than the sync described - // above. Since this can be reached in an update following an out- - // bound sync, unset the outbound value to avoid considering it in - // subsequent renders. - pendingChanges.current.outgoing = []; - pendingChanges.current.incoming = controlledBlocks; - setControlledBlocks(); - - if ( controlledSelectionStart && controlledSelectionEnd ) { - resetSelection( - controlledSelectionStart, - controlledSelectionEnd - ); - } - } - }, [ controlledBlocks, clientId ] ); } diff --git a/packages/block-editor/src/components/use-block-display-information/README.md b/packages/block-editor/src/components/use-block-display-information/README.md new file mode 100644 index 00000000000000..6d52ee452cc48a --- /dev/null +++ b/packages/block-editor/src/components/use-block-display-information/README.md @@ -0,0 +1,42 @@ +# useBlockDisplayInformation + +A React Hook that tries to find a matching block variation and returns the appropriate information for display reasons. In order to try to find a match we need two things: + +1. Block's client id to extract its current attributes. +2. A block variation has `isActive` prop defined with a matcher function. + +If for any reason a block variaton match cannot be found, the returned information come from the Block Type. + +### Usage + +The hook returns an object which contains the block's title, icon, and description. If no block type is found for the provided `clientId`, it returns `null`. + +```jsx +import { + BlockIcon, + useBlockDisplayInformation, +} from '@wordpress/block-editor'; + +function DemoBlockCard( { clientId } ) { + const blockInformation = useBlockDisplayInformation( clientId ); + const { title, icon, description } = blockInformation; + return ( +
+ +

{ title }

+

{ description }

+
+ ); +} +``` + +## Props + +The hook accepts the following props. + +### clientId + +A block's clientId + +- Type: `String` +- Required: Yes diff --git a/packages/block-editor/src/components/use-block-display-information/index.js b/packages/block-editor/src/components/use-block-display-information/index.js new file mode 100644 index 00000000000000..7ee0d350ffef8a --- /dev/null +++ b/packages/block-editor/src/components/use-block-display-information/index.js @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +/** @typedef {import('@wordpress/blocks').WPIcon} WPIcon */ + +/** + * Contains basic block's information for display reasons. + * + * @typedef {Object} WPBlockDisplayInformation + * + * @property {string} title Human-readable block type label. + * @property {WPIcon} icon Block type icon. + * @property {string} description A detailed block type description. + */ + +/** + * Hook used to try to find a matching block variation and return + * the appropriate information for display reasons. In order to + * to try to find a match we need to things: + * 1. Block's client id to extract it's current attributes. + * 2. A block variation should have set `isActive` prop to a proper function. + * + * If for any reason a block variaton match cannot be found, + * the returned information come from the Block Type. + * If no blockType is found with the provided clientId, returns null. + * + * @param {string} clientId Block's client id. + * @return {?WPBlockDisplayInformation} Block's display information, or `null` when the block or its type not found. + */ + +export default function useBlockDisplayInformation( clientId ) { + return useSelect( + ( select ) => { + if ( ! clientId ) return null; + const { getBlockName, getBlockAttributes } = select( + blockEditorStore + ); + const { getBlockType, getBlockVariations } = select( blocksStore ); + const blockName = getBlockName( clientId ); + const blockType = getBlockType( blockName ); + if ( ! blockType ) return null; + const variations = getBlockVariations( blockName ); + const blockTypeInfo = { + title: blockType.title, + icon: blockType.icon, + description: blockType.description, + }; + if ( ! variations?.length ) return blockTypeInfo; + const attributes = getBlockAttributes( clientId ); + const match = variations.find( ( variation ) => + variation.isActive?.( attributes, variation.attributes ) + ); + if ( ! match ) return blockTypeInfo; + return { + title: match.title || blockType.title, + icon: match.icon || blockType.icon, + description: match.description || blockType.description, + }; + }, + [ clientId ] + ); +} diff --git a/packages/block-library/src/code/style.scss b/packages/block-library/src/code/style.scss index f40e238f6b21bd..35cab12be3c1eb 100644 --- a/packages/block-library/src/code/style.scss +++ b/packages/block-library/src/code/style.scss @@ -1,10 +1,6 @@ // Provide a minimum of overflow handling. -.wp-block-code { - font-size: 0.9em; - - code { - display: block; - white-space: pre-wrap; - overflow-wrap: break-word; - } +.wp-block-code code { + display: block; + white-space: pre-wrap; + overflow-wrap: break-word; } diff --git a/packages/block-library/src/embed/variations.js b/packages/block-library/src/embed/variations.js index 34a1b8f66f43c9..02c68d0cb88cad 100644 --- a/packages/block-library/src/embed/variations.js +++ b/packages/block-library/src/embed/variations.js @@ -339,4 +339,16 @@ const variations = [ }, ]; +/** + * Add `isActive` function to all `embed` variations, if not defined. + * `isActive` function is used to find a variation match from a created + * Block by providing its attributes. + */ +variations.forEach( ( variation ) => { + if ( variation.isActive ) return; + variation.isActive = ( blockAttributes, variationAttributes ) => + blockAttributes.providerNameSlug === + variationAttributes.providerNameSlug; +} ); + export default variations; diff --git a/packages/block-library/src/query-loop/edit.js b/packages/block-library/src/query-loop/edit.js index 4d62b24356bb59..370eb45e08a7d5 100644 --- a/packages/block-library/src/query-loop/edit.js +++ b/packages/block-library/src/query-loop/edit.js @@ -82,17 +82,19 @@ export default function QueryLoopEdit( { // When you insert this block outside of the edit site then store // does not exist therefore we check for its existence. + // TODO: remove this code, edit-site shouldn't be called in block-library. + // This creates a cycle dependency. if ( inherit && select( 'core/edit-site' ) ) { // This should be passed from the context exposed by edit site. - const { getTemplateId, getTemplateType } = select( + const { getEditedPostType, getEditedPostId } = select( 'core/edit-site' ); - if ( 'wp_template' === getTemplateType() ) { + if ( 'wp_template' === getEditedPostType() ) { const { slug } = select( 'core' ).getEntityRecord( 'postType', 'wp_template', - getTemplateId() + getEditedPostId() ); // Change the post-type if needed. diff --git a/packages/block-library/src/social-link/variations.js b/packages/block-library/src/social-link/variations.js index 4297ced3c41718..a2f259feaaabbd 100644 --- a/packages/block-library/src/social-link/variations.js +++ b/packages/block-library/src/social-link/variations.js @@ -304,4 +304,15 @@ const variations = [ }, ]; +/** + * Add `isActive` function to all `social link` variations, if not defined. + * `isActive` function is used to find a variation match from a created + * Block by providing its attributes. + */ +variations.forEach( ( variation ) => { + if ( variation.isActive ) return; + variation.isActive = ( blockAttributes, variationAttributes ) => + blockAttributes.service === variationAttributes.service; +} ); + export default variations; diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index d628cdfae0306c..7c1ce2c1643384 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -97,6 +97,14 @@ import { store as blocksStore } from '../store'; * @property {string[]} [keywords] An array of terms (which can be translated) * that help users discover the variation * while searching. + * @property {Function} [isActive] A function that accepts a block's attributes + * and the variation's attributes and determines + * if a variation is active. This function doesn't + * try to find a match dynamically based on all + * block's attributes, as in many cases some + * attributes are irrelevant. An example would + * be for `embed` block where we only care about + * `providerNameSlug` attribute's value. */ /** diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d82af59b153430..1748317bc6bbf6 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,6 +4,9 @@ ## 12.0.0 (2020-12-17) +### Enhancements +- ComboboxControl: Deburr option labels before filter + ### Breaking Change - Introduce support for other units and advanced CSS properties on `FontSizePicker`. Provided the value passed to the `FontSizePicker` is a string or one of the size options passed is a string, onChange will start to be called with a string value instead of a number. On WordPress usage, font size options are now automatically converted to strings with the default "px" unit added. diff --git a/packages/components/src/combobox-control/index.js b/packages/components/src/combobox-control/index.js index d1434c1787cfd6..fdd406530ee297 100644 --- a/packages/components/src/combobox-control/index.js +++ b/packages/components/src/combobox-control/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; - +import { deburr } from 'lodash'; /** * WordPress dependencies */ @@ -66,9 +66,11 @@ function ComboboxControl( { const matchingSuggestions = useMemo( () => { const startsWithMatch = []; const containsMatch = []; - const match = inputValue.toLocaleLowerCase(); + const match = deburr( inputValue.toLocaleLowerCase() ); options.forEach( ( option ) => { - const index = option.label.toLocaleLowerCase().indexOf( match ); + const index = deburr( option.label ) + .toLocaleLowerCase() + .indexOf( match ); if ( index === 0 ) { startsWithMatch.push( option ); } else if ( index > 0 ) { diff --git a/packages/components/src/combobox-control/stories/index.js b/packages/components/src/combobox-control/stories/index.js index a7f250e38573e1..8bca43c948fd34 100644 --- a/packages/components/src/combobox-control/stories/index.js +++ b/packages/components/src/combobox-control/stories/index.js @@ -276,17 +276,8 @@ function ComboboxControlWithState() { onChange={ setValue } label="Select a country" options={ filteredOptions } - onFilterValueChange={ ( filter ) => - setFilteredOptions( - countries - .filter( ( country ) => - country.name - .toLowerCase() - .startsWith( filter.toLowerCase() ) - ) - .slice( 0, 20 ) - .map( mapCountryOption ) - ) + onFilterValueChange={ () => + setFilteredOptions( countries.map( mapCountryOption ) ) } />

Value: { value }

diff --git a/packages/e2e-tests/plugins/block-variations/index.js b/packages/e2e-tests/plugins/block-variations/index.js index 3a76ae7d42db66..219eedd6bec5e9 100644 --- a/packages/e2e-tests/plugins/block-variations/index.js +++ b/packages/e2e-tests/plugins/block-variations/index.js @@ -40,6 +40,8 @@ }, icon: 'yes-alt', scope: [ 'inserter' ], + isActive: ( { backgroundColor }, variationAttributes ) => + backgroundColor === variationAttributes.backgroundColor, } ); registerBlockVariation( 'core/paragraph', { @@ -52,6 +54,8 @@ }, icon: 'warning', scope: [ 'inserter' ], + isActive: ( { backgroundColor }, variationAttributes ) => + backgroundColor === variationAttributes.backgroundColor, } ); registerBlockVariation( 'core/columns', { diff --git a/packages/e2e-tests/specs/editor/plugins/block-variations.js b/packages/e2e-tests/specs/editor/plugins/block-variations.js index ef4a17360284a3..81862f42a5e304 100644 --- a/packages/e2e-tests/specs/editor/plugins/block-variations.js +++ b/packages/e2e-tests/specs/editor/plugins/block-variations.js @@ -7,6 +7,8 @@ import { deactivatePlugin, insertBlock, searchForBlock, + pressKeyWithModifier, + openDocumentSettingsSidebar, } from '@wordpress/e2e-test-utils'; describe( 'Block variations', () => { @@ -103,4 +105,75 @@ describe( 'Block variations', () => { ) ).toHaveLength( 4 ); } ); + // @see @wordpres/block-editor/src/components/use-block-display-information (`useBlockDisplayInformation` hook). + describe( 'testing block display information with matching variations', () => { + const getActiveBreadcrumb = async () => + page.evaluate( + () => + document.querySelector( + '.block-editor-block-breadcrumb__current' + ).textContent + ); + const getFirstNavigationItem = async () => { + await pressKeyWithModifier( 'access', 'o' ); + // This also returns the visually hidden text `(selected block)`. + // For example `Paragraph(selected block)`. In order to hide this + // implementation detail and search for childNodes, we choose to + // test with `String.prototype.startsWith()`. + return page.evaluate( + () => + document.querySelector( + '.block-editor-block-navigation-block-select-button' + ).textContent + ); + }; + const getBlockCardDescription = async () => { + await openDocumentSettingsSidebar(); + return page.evaluate( + () => + document.querySelector( + '.block-editor-block-card__description' + ).textContent + ); + }; + + it( 'should show block information when no matching variation is found', async () => { + await insertBlock( 'Large Quote' ); + const breadcrumb = await getActiveBreadcrumb(); + expect( breadcrumb ).toEqual( 'Quote' ); + const navigationItem = await getFirstNavigationItem(); + expect( navigationItem.startsWith( 'Quote' ) ).toBeTruthy(); + const description = await getBlockCardDescription(); + expect( description ).toEqual( + 'Give quoted text visual emphasis. "In quoting others, we cite ourselves." — Julio Cortázar' + ); + } ); + it( 'should display variations info if all declared', async () => { + await insertBlock( 'Success Message' ); + const breadcrumb = await getActiveBreadcrumb(); + expect( breadcrumb ).toEqual( 'Success Message' ); + const navigationItem = await getFirstNavigationItem(); + expect( + navigationItem.startsWith( 'Success Message' ) + ).toBeTruthy(); + const description = await getBlockCardDescription(); + expect( description ).toEqual( + 'This block displays a success message. This description overrides the default one provided for the Paragraph block.' + ); + } ); + it( 'should display mixed block and variation match information', async () => { + // Warning Message variation is missing the `description`. + await insertBlock( 'Warning Message' ); + const breadcrumb = await getActiveBreadcrumb(); + expect( breadcrumb ).toEqual( 'Warning Message' ); + const navigationItem = await getFirstNavigationItem(); + expect( + navigationItem.startsWith( 'Warning Message' ) + ).toBeTruthy(); + const description = await getBlockCardDescription(); + expect( description ).toEqual( + 'Start with the building block of all narrative.' + ); + } ); + } ); } ); diff --git a/packages/e2e-tests/specs/editor/various/font-size-picker.test.js b/packages/e2e-tests/specs/editor/various/font-size-picker.test.js index c197b9adabeba9..84edbd564b5352 100644 --- a/packages/e2e-tests/specs/editor/various/font-size-picker.test.js +++ b/packages/e2e-tests/specs/editor/various/font-size-picker.test.js @@ -17,8 +17,6 @@ describe( 'Font Size Picker', () => { const FONT_SIZE_LABEL_SELECTOR = "//label[contains(text(), 'Font size')]"; const CUSTOM_FONT_SIZE_LABEL_SELECTOR = "//fieldset[legend[contains(text(),'Font size')]]//label[contains(text(), 'Custom')]"; - const FONT_SIZE_RESET_BUTTON_SELECTOR = - "//fieldset[legend[contains(text(),'Font size')]]//button[//span[contains(text(), 'Reset')] or contains(text(), 'Reset')]"; beforeEach( async () => { await createNewPost(); } ); @@ -59,6 +57,7 @@ describe( 'Font Size Picker', () => { await first( await page.$x( CUSTOM_FONT_SIZE_LABEL_SELECTOR ) ).click(); // This should be the "small" font-size of the editor defaults. await page.keyboard.type( '13' ); + await page.keyboard.press( 'Enter' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); @@ -72,6 +71,7 @@ describe( 'Font Size Picker', () => { await first( await page.$x( CUSTOM_FONT_SIZE_LABEL_SELECTOR ) ).click(); await page.keyboard.type( '23' ); + await page.keyboard.press( 'Enter' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); @@ -88,8 +88,10 @@ describe( 'Font Size Picker', () => { await first( await page.$x( FONT_SIZE_LABEL_SELECTOR ) ).click(); await pressKeyTimes( 'ArrowDown', 2 ); await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); - await first( await page.$x( FONT_SIZE_RESET_BUTTON_SELECTOR ) ).click(); + await page.keyboard.press( 'Enter' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); @@ -110,6 +112,7 @@ describe( 'Font Size Picker', () => { await first( await page.$x( CUSTOM_FONT_SIZE_LABEL_SELECTOR ) ).click(); await pressKeyTimes( 'ArrowRight', 5 ); await pressKeyTimes( 'Backspace', 5 ); + await page.keyboard.press( 'Enter' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); @@ -123,12 +126,12 @@ describe( 'Font Size Picker', () => { await first( await page.$x( CUSTOM_FONT_SIZE_LABEL_SELECTOR ) ).click(); await page.keyboard.type( '23' ); - - await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Enter' ); await first( await page.$x( CUSTOM_FONT_SIZE_LABEL_SELECTOR ) ).click(); await page.keyboard.press( 'Backspace' ); await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Enter' ); // Ensure content matches snapshot. const content = await getEditedPostContent(); diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 61313977e33361..aff93bd440ca07 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -25,12 +25,12 @@ import { SidebarInspectorFill } from '../sidebar'; export default function BlockEditor( { setIsInserterOpen } ) { const { settings, templateType, page } = useSelect( ( select ) => { - const { getSettings, getTemplateType, getPage } = select( + const { getSettings, getEditedPostType, getPage } = select( 'core/edit-site' ); return { settings: getSettings( setIsInserterOpen ), - templateType: getTemplateType(), + templateType: getEditedPostType(), page: getPage(), }; }, diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index bcd180fe66735c..2ffe544739c753 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -72,23 +72,15 @@ function Editor() { isInserterOpened, __experimentalGetPreviewDeviceType, getSettings, - getTemplateId, - getTemplatePartId, - getTemplateType, + getEditedPostType, + getEditedPostId, getPage, isNavigationOpened, } = select( 'core/edit-site' ); - const _templateId = getTemplateId(); - const _templatePartId = getTemplatePartId(); - const _templateType = getTemplateType(); + const postType = getEditedPostType(); + const postId = getEditedPostId(); // The currently selected entity to display. Typically template or template part. - let _entityId; - if ( _templateType ) { - _entityId = - _templateType === 'wp_template' ? _templateId : _templatePartId; - } - return { isInserterOpen: isInserterOpened(), isFullscreenActive: isFeatureActive( 'fullscreenMode' ), @@ -97,17 +89,16 @@ function Editor() { 'core/interface' ).getActiveComplementaryArea( 'core/edit-site' ), settings: getSettings(), - templateType: _templateType, + templateType: postType, page: getPage(), - template: - _templateType && _entityId - ? select( 'core' ).getEntityRecord( - 'postType', - _templateType, - _entityId - ) - : null, - entityId: _entityId, + template: postId + ? select( 'core' ).getEntityRecord( + 'postType', + postType, + postId + ) + : null, + entityId: postId, isNavigationOpen: isNavigationOpened(), }; }, [] ); diff --git a/packages/edit-site/src/components/header/index.js b/packages/edit-site/src/components/header/index.js index 2567a3cfea10f7..487b2ea757af11 100644 --- a/packages/edit-site/src/components/header/index.js +++ b/packages/edit-site/src/components/header/index.js @@ -36,9 +36,8 @@ export default function Header( { openEntitiesSavedStates } ) { const { __experimentalGetPreviewDeviceType, isFeatureActive, - getTemplateId, - getTemplatePartId, - getTemplateType, + getEditedPostType, + getEditedPostId, isInserterOpened, } = select( 'core/edit-site' ); const { getEntityRecord } = select( 'core' ); @@ -46,9 +45,8 @@ export default function Header( { openEntitiesSavedStates } ) { 'core/editor' ); - const postType = getTemplateType(); - const postId = - postType === 'wp_template' ? getTemplateId() : getTemplatePartId(); + const postType = getEditedPostType(); + const postId = getEditedPostId(); const record = getEntityRecord( 'postType', postType, postId ); const _entityTitle = 'wp_template' === postType diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js index 55bcbf88439ea0..d20c32286c52a0 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/templates-navigation.js @@ -19,34 +19,25 @@ import MainDashboardButton from '../../main-dashboard-button'; import { MENU_ROOT, MENU_TEMPLATE_PARTS, MENU_TEMPLATES } from './constants'; export default function TemplatesNavigation() { - const { templateId, templatePartId, templateType, activeMenu } = useSelect( - ( select ) => { - const { - getTemplateId, - getTemplatePartId, - getTemplateType, - getNavigationPanelActiveMenu, - } = select( 'core/edit-site' ); + const { postId, postType, activeMenu } = useSelect( ( select ) => { + const { + getEditedPostType, + getEditedPostId, + getNavigationPanelActiveMenu, + } = select( 'core/edit-site' ); - return { - templateId: getTemplateId(), - templatePartId: getTemplatePartId(), - templateType: getTemplateType(), - activeMenu: getNavigationPanelActiveMenu(), - }; - }, - [] - ); + return { + postId: getEditedPostId(), + postType: getEditedPostType(), + activeMenu: getNavigationPanelActiveMenu(), + }; + }, [] ); const { setNavigationPanelActiveMenu } = useDispatch( 'core/edit-site' ); return ( diff --git a/packages/edit-site/src/components/url-query-controller/index.js b/packages/edit-site/src/components/url-query-controller/index.js index e7685a1f1ab13a..3ff703bf0debd4 100644 --- a/packages/edit-site/src/components/url-query-controller/index.js +++ b/packages/edit-site/src/components/url-query-controller/index.js @@ -47,28 +47,19 @@ export default function URLQueryController() { function useCurrentPageContext() { return useSelect( ( select ) => { - const { - getTemplateId, - getTemplatePartId, - getTemplateType, - getPage, - } = select( 'core/edit-site' ); + const { getEditedPostType, getEditedPostId, getPage } = select( + 'core/edit-site' + ); const page = getPage(); - const templateType = getTemplateType(); - const templateId = getTemplateId(); - const templatePartId = getTemplatePartId(); - - let _postId, _postType; + let _postId = getEditedPostId(), + _postType = getEditedPostType(); + // This doesn't seem right to me, + // we shouldn't be using the "page" and the "template" in the same way. + // This need to be investigated. if ( page?.context?.postId && page?.context?.postType ) { _postId = page.context.postId; _postType = page.context.postType; - } else if ( templateType === 'wp_template' && templateId ) { - _postId = templateId; - _postType = templateType; - } else if ( templateType === 'wp_template_part' && templatePartId ) { - _postId = templatePartId; - _postType = templateType; } if ( _postId && _postType ) { diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index dcc57c8bc3a9af..0a89862d23ec87 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -69,79 +69,28 @@ export function settings( state = {}, action ) { } /** - * Reducer returning the template ID. + * Reducer keeping track of the currently edited Post Type, + * Post Id and the context provided to fill the content of the block editor. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ -export function templateId( state, action ) { +export function editedPost( state = {}, action ) { switch ( action.type ) { case 'SET_TEMPLATE': case 'SET_PAGE': - return action.templateId; - case 'SET_TEMPLATE_PART': - return undefined; - } - - return state; -} - -/** - * Reducer returning the template part ID. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function templatePartId( state, action ) { - switch ( action.type ) { - case 'SET_TEMPLATE_PART': - return action.templatePartId; - case 'SET_TEMPLATE': - case 'SET_PAGE': - return undefined; - } - - return state; -} - -/** - * Reducer returning the template type. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function templateType( state = 'wp_template', action ) { - switch ( action.type ) { - case 'SET_TEMPLATE': - case 'SET_PAGE': - return 'wp_template'; - case 'SET_TEMPLATE_PART': - return 'wp_template_part'; - } - - return state; -} - -/** - * Reducer returning the page being edited. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function page( state, action ) { - switch ( action.type ) { - case 'SET_PAGE': - return action.page; + return { + type: 'wp_template', + id: action.templateId, + page: action.page, + }; case 'SET_TEMPLATE_PART': - return undefined; + return { + type: 'wp_template_part', + id: action.templatePartId, + }; } return state; @@ -231,10 +180,7 @@ export default combineReducers( { preferences, deviceType, settings, - templateId, - templatePartId, - templateType, - page, + editedPost, homeTemplateId, navigationPanel, blockInserterPanel, diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index 2fc2ba5d43614f..4bcfd07dc2926e 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -96,36 +96,25 @@ export function getHomeTemplateId( state ) { } /** - * Returns the current template ID. + * Returns the current edited post type (wp_template or wp_template_part). * * @param {Object} state Global application state. * * @return {number?} Template ID. */ -export function getTemplateId( state ) { - return state.templateId; +export function getEditedPostType( state ) { + return state.editedPost.type; } /** - * Returns the current template part ID. + * Returns the ID of the currently edited template or template part. * * @param {Object} state Global application state. * - * @return {number?} Template part ID. + * @return {number?} Post ID. */ -export function getTemplatePartId( state ) { - return state.templatePartId; -} - -/** - * Returns the current template type. - * - * @param {Object} state Global application state. - * - * @return {string?} Template type. - */ -export function getTemplateType( state ) { - return state.templateType; +export function getEditedPostId( state ) { + return state.editedPost.id; } /** @@ -136,7 +125,7 @@ export function getTemplateType( state ) { * @return {Object} Page. */ export function getPage( state ) { - return state.page; + return state.editedPost.page; } /** diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js index ff725b0c2e861d..9f8263786464bd 100644 --- a/packages/edit-site/src/store/test/reducer.js +++ b/packages/edit-site/src/store/test/reducer.js @@ -10,10 +10,7 @@ import { preferences, settings, homeTemplateId, - templateId, - templatePartId, - templateType, - page, + editedPost, navigationPanel, blockInserterPanel, } from '../reducer'; @@ -87,108 +84,51 @@ describe( 'state', () => { } ); } ); - describe( 'templateId()', () => { + describe( 'editedPost()', () => { it( 'should apply default state', () => { - expect( templateId( undefined, {} ) ).toEqual( undefined ); + expect( editedPost( undefined, {} ) ).toEqual( {} ); } ); it( 'should default to returning the same state', () => { const state = {}; - expect( templateId( state, {} ) ).toBe( state ); + expect( editedPost( state, {} ) ).toBe( state ); } ); it( 'should update when a template is set', () => { expect( - templateId( 1, { - type: 'SET_TEMPLATE', - templateId: 2, - } ) - ).toEqual( 2 ); - } ); - - it( 'should update when a page is set', () => { - expect( - templateId( 1, { - type: 'SET_PAGE', - templateId: 2, - } ) - ).toEqual( 2 ); - } ); - } ); - - describe( 'templatePartId()', () => { - it( 'should apply default state', () => { - expect( templatePartId( undefined, {} ) ).toEqual( undefined ); - } ); - - it( 'should default to returning the same state', () => { - const state = {}; - expect( templatePartId( state, {} ) ).toBe( state ); - } ); - - it( 'should update when a template part is set', () => { - expect( - templatePartId( 1, { - type: 'SET_TEMPLATE_PART', - templatePartId: 2, - } ) - ).toEqual( 2 ); - } ); - } ); - - describe( 'templateType()', () => { - it( 'should apply default state', () => { - expect( templateType( undefined, {} ) ).toEqual( 'wp_template' ); - } ); - - it( 'should default to returning the same state', () => { - const state = {}; - expect( templateType( state, {} ) ).toBe( state ); - } ); - - it( 'should update when a template is set', () => { - expect( - templateType( undefined, { - type: 'SET_TEMPLATE', - } ) - ).toEqual( 'wp_template' ); + editedPost( + { id: 1, type: 'wp_template' }, + { + type: 'SET_TEMPLATE', + templateId: 2, + } + ) + ).toEqual( { id: 2, type: 'wp_template' } ); } ); it( 'should update when a page is set', () => { expect( - templateType( undefined, { - type: 'SET_PAGE', - } ) - ).toEqual( 'wp_template' ); + editedPost( + { id: 1, type: 'wp_template' }, + { + type: 'SET_PAGE', + templateId: 2, + page: {}, + } + ) + ).toEqual( { id: 2, type: 'wp_template', page: {} } ); } ); it( 'should update when a template part is set', () => { expect( - templateType( undefined, { - type: 'SET_TEMPLATE_PART', - } ) - ).toEqual( 'wp_template_part' ); - } ); - } ); - - describe( 'page()', () => { - it( 'should apply default state', () => { - expect( page( undefined, {} ) ).toEqual( undefined ); - } ); - - it( 'should default to returning the same state', () => { - const state = {}; - expect( page( state, {} ) ).toBe( state ); - } ); - - it( 'should set the page', () => { - const newPage = {}; - expect( - page( undefined, { - type: 'SET_PAGE', - page: newPage, - } ) - ).toBe( newPage ); + editedPost( + { id: 1, type: 'wp_template' }, + { + type: 'SET_TEMPLATE_PART', + templatePartId: 2, + } + ) + ).toEqual( { id: 2, type: 'wp_template_part' } ); } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 85b4ac71219d86..e24b640f07afb1 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -6,9 +6,8 @@ import { getCanUserCreateMedia, getSettings, getHomeTemplateId, - getTemplateId, - getTemplatePartId, - getTemplateType, + getEditedPostType, + getEditedPostId, getPage, getNavigationPanelActiveMenu, isNavigationOpened, @@ -119,31 +118,25 @@ describe( 'selectors', () => { } ); } ); - describe( 'getTemplateId', () => { + describe( 'getEditedPostId', () => { it( 'returns the template ID', () => { - const state = { templateId: {} }; - expect( getTemplateId( state ) ).toBe( state.templateId ); + const state = { editedPost: { id: 10 } }; + expect( getEditedPostId( state ) ).toBe( 10 ); } ); } ); - describe( 'getTemplatePartId', () => { - it( 'returns the template part ID', () => { - const state = { templatePartId: {} }; - expect( getTemplatePartId( state ) ).toBe( state.templatePartId ); - } ); - } ); - - describe( 'getTemplateType', () => { + describe( 'getEditedPostType', () => { it( 'returns the template type', () => { - const state = { templateType: {} }; - expect( getTemplateType( state ) ).toBe( state.templateType ); + const state = { editedPost: { type: 'wp_template' } }; + expect( getEditedPostType( state ) ).toBe( 'wp_template' ); } ); } ); describe( 'getPage', () => { it( 'returns the page object', () => { - const state = { page: {} }; - expect( getPage( state ) ).toBe( state.page ); + const page = {}; + const state = { editedPost: { page } }; + expect( getPage( state ) ).toBe( page ); } ); } ); diff --git a/packages/format-library/src/text-color/index.js b/packages/format-library/src/text-color/index.js index c923c73939dad5..b3083b35b79965 100644 --- a/packages/format-library/src/text-color/index.js +++ b/packages/format-library/src/text-color/index.js @@ -87,7 +87,10 @@ function TextColorEdit( { onClose={ disableIsAddingColor } activeAttributes={ activeAttributes } value={ value } - onChange={ onChange } + onChange={ ( ...args ) => { + onChange( ...args ); + disableIsAddingColor(); + } } contentRef={ contentRef } /> ) } diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index f912598e0f498b..6c94893d143df9 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -166,7 +166,9 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void gutenbergDidSendButtonPressedAction(String buttonType); - void onAddMention(Consumer onSuccess); + void onShowUserSuggestions(Consumer onResult); + + void onShowXpostSuggestions(Consumer onResult); void setStarterPageTemplatesTooltipShown(boolean tooltipShown); diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 4a6b844b2f5c10..d00dc5a566a74a 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -323,8 +323,13 @@ private OtherMediaOptionsReceivedCallback getNewOtherMediaReceivedCallback(final } @ReactMethod - public void addMention(Promise promise) { - mGutenbergBridgeJS2Parent.onAddMention(promise::resolve); + public void showUserSuggestions(Promise promise) { + mGutenbergBridgeJS2Parent.onShowUserSuggestions(promise::resolve); + } + + @ReactMethod + public void showXpostSuggestions(Promise promise) { + mGutenbergBridgeJS2Parent.onShowXpostSuggestions(promise::resolve); } @ReactMethod diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/AddMentionUtil.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/AddMentionUtil.java deleted file mode 100644 index 837f10e1562835..00000000000000 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/AddMentionUtil.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.wordpress.mobile.WPAndroidGlue; - -import androidx.core.util.Consumer; - -public interface AddMentionUtil { - void getMention(Consumer onResult); -} diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index f9f65dd9c20838..7049bc099c8221 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -5,6 +5,7 @@ import android.os.Bundle data class GutenbergProps @JvmOverloads constructor( val enableMediaFilesCollectionBlocks: Boolean, val enableMentions: Boolean, + val enableXPosts: Boolean, val enableUnsupportedBlockEditor: Boolean, val canEnableUnsupportedBlockEditor: Boolean, val localeSlug: String, @@ -38,6 +39,7 @@ data class GutenbergProps @JvmOverloads constructor( fun getUpdatedCapabilitiesProps() = Bundle().apply { putBoolean(PROP_CAPABILITIES_MENTIONS, enableMentions) + putBoolean(PROP_CAPABILITIES_XPOSTS, enableXPosts) putBoolean(PROP_CAPABILITIES_MEDIAFILES_COLLECTION_BLOCK, enableMediaFilesCollectionBlocks) putBoolean(PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR, enableUnsupportedBlockEditor) putBoolean(PROP_CAPABILITIES_CAN_ENABLE_UNSUPPORTED_BLOCK_EDITOR, canEnableUnsupportedBlockEditor) @@ -68,6 +70,7 @@ data class GutenbergProps @JvmOverloads constructor( const val PROP_CAPABILITIES = "capabilities" const val PROP_CAPABILITIES_MEDIAFILES_COLLECTION_BLOCK = "mediaFilesCollectionBlock" const val PROP_CAPABILITIES_MENTIONS = "mentions" + const val PROP_CAPABILITIES_XPOSTS = "xposts" const val PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR = "unsupportedBlockEditor" const val PROP_CAPABILITIES_CAN_ENABLE_UNSUPPORTED_BLOCK_EDITOR = "canEnableUnsupportedBlockEditor" const val PROP_CAPABILITIES_MODAL_LAYOUT_PICKER = "modalLayoutPicker" diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/ShowSuggestionsUtil.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/ShowSuggestionsUtil.java new file mode 100644 index 00000000000000..cd78ded5b6b242 --- /dev/null +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/ShowSuggestionsUtil.java @@ -0,0 +1,8 @@ +package org.wordpress.mobile.WPAndroidGlue; + +import androidx.core.util.Consumer; + +public interface ShowSuggestionsUtil { + void showUserSuggestions(Consumer onResult); + void showXpostSuggestions(Consumer onResult); +} diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 9c1de6ab757722..6feeb61581b287 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -109,7 +109,7 @@ public class WPAndroidGlueCode { private CountDownLatch mGetContentCountDownLatch; private WeakReference mLastFocusedView = null; private RequestExecutor mRequestExecutor; - private AddMentionUtil mAddMentionUtil; + private ShowSuggestionsUtil mShowSuggestionsUtil; private @Nullable Bundle mEditorTheme = null; private static OkHttpHeaderInterceptor sAddCookiesInterceptor = new OkHttpHeaderInterceptor(); @@ -411,8 +411,12 @@ public void gutenbergDidSendButtonPressedAction(String buttonType) { } @Override - public void onAddMention(Consumer onSuccess) { - mAddMentionUtil.getMention(onSuccess); + public void onShowUserSuggestions(Consumer onResult) { + mShowSuggestionsUtil.showUserSuggestions(onResult); + } + + @Override public void onShowXpostSuggestions(Consumer onResult) { + mShowSuggestionsUtil.showXpostSuggestions(onResult); } @Override @@ -531,7 +535,7 @@ public void attachToContainer(ViewGroup viewGroup, OnLogGutenbergUserEventListener onLogGutenbergUserEventListener, OnGutenbergDidRequestUnsupportedBlockFallbackListener onGutenbergDidRequestUnsupportedBlockFallbackListener, OnGutenbergDidSendButtonPressedActionListener onGutenbergDidSendButtonPressedActionListener, - AddMentionUtil addMentionUtil, + ShowSuggestionsUtil showSuggestionsUtil, OnStarterPageTemplatesTooltipShownEventListener onStarterPageTemplatesTooltipListener, OnMediaFilesCollectionBasedBlockEditorListener onMediaFilesCollectionBasedBlockEditorListener, boolean isDarkMode) { @@ -549,7 +553,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnLogGutenbergUserEventListener = onLogGutenbergUserEventListener; mOnGutenbergDidRequestUnsupportedBlockFallbackListener = onGutenbergDidRequestUnsupportedBlockFallbackListener; mOnGutenbergDidSendButtonPressedActionListener = onGutenbergDidSendButtonPressedActionListener; - mAddMentionUtil = addMentionUtil; + mShowSuggestionsUtil = showSuggestionsUtil; mOnStarterPageTemplatesTooltipShownListener = onStarterPageTemplatesTooltipListener; mOnMediaFilesCollectionBasedBlockEditorListener = onMediaFilesCollectionBasedBlockEditorListener; diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index f7d76c91d53cd5..e5bc1db51a0ab2 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -279,8 +279,12 @@ export function logUserEvent( event, properties ) { return RNReactNativeGutenbergBridge.logUserEvent( event, properties ); } -export function addMention() { - return RNReactNativeGutenbergBridge.addMention(); +export function showUserSuggestions() { + return RNReactNativeGutenbergBridge.showUserSuggestions(); +} + +export function showXpostSuggestions() { + return RNReactNativeGutenbergBridge.showXpostSuggestions(); } export function requestStarterPageTemplatesTooltipShown( callback ) { diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index 2e624791346908..753515f600a7e3 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -18,6 +18,7 @@ public struct MediaInfo: Encodable { public enum Capabilities: String { case mediaFilesCollectionBlock case mentions + case xposts case unsupportedBlockEditor case canEnableUnsupportedBlockEditor case modalLayoutPicker @@ -225,6 +226,10 @@ public protocol GutenbergBridgeDelegate: class { /// - Parameter callback: Completion handler to be called with an user mention or an error func gutenbergDidRequestMention(callback: @escaping (Swift.Result) -> Void) + /// Tells the delegate that the editor requested a mention + /// - Parameter callback: Completion handler to be called with an xpost or an error + func gutenbergDidRequestXpost(callback: @escaping (Swift.Result) -> Void) + /// Tells the delegate that the editor requested to show the tooltip func gutenbergDidRequestStarterPageTemplatesTooltipShown() -> Bool diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index aafdce5c869aa0..9443cae473c1f3 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -20,7 +20,8 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(requestMediaEditor:(NSString *)mediaUrl callback:(RCTResponseSenderBlock)callback) RCT_EXTERN_METHOD(logUserEvent:(NSString *)event properties:(NSDictionary *)properties) RCT_EXTERN_METHOD(requestUnsupportedBlockFallback:(NSString *)content blockId:(NSString *)blockId blockName:(NSString *)blockName blockTitle:(NSString *)blockTitle) -RCT_EXTERN_METHOD(addMention:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)rejecter) +RCT_EXTERN_METHOD(showUserSuggestions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)rejecter) +RCT_EXTERN_METHOD(showXpostSuggestions:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)rejecter) RCT_EXTERN_METHOD(requestStarterPageTemplatesTooltipShown:(RCTResponseSenderBlock)callback) RCT_EXTERN_METHOD(setStarterPageTemplatesTooltipShown:(BOOL)tooltipShown) RCT_EXTERN_METHOD(requestMediaFilesEditorLoad:(NSArray *)mediaFiles blockId:(NSString *)blockId) diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index 3d8168ba17e8d4..927a59eb8e0fbe 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -267,7 +267,7 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { } @objc - func addMention(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { + func showUserSuggestions(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { self.delegate?.gutenbergDidRequestMention(callback: { (result) in switch result { case .success(let mention): @@ -278,6 +278,18 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { }) } + @objc + func showXpostSuggestions(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) { + self.delegate?.gutenbergDidRequestXpost(callback: { (result) in + switch result { + case .success(let mention): + resolver([mention]) + case .failure(let error): + rejecter(error.domain, "\(error.code)", error) + } + }) + } + @objc func requestStarterPageTemplatesTooltipShown(_ callback: @escaping RCTResponseSenderBlock) { callback([self.delegate?.gutenbergDidRequestStarterPageTemplatesTooltipShown() ?? false]) diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index be2d7594088176..095ed5bbc8fd57 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -13,14 +13,21 @@ For each user feature we should also add a importance categorization label to i * [***] Full-width and wide alignment support for Columns -## 1.43.0 (2020-12-17) +## 1.43.0 +* [***] New Block: File [#27228] +* [**] Fix issue where a blocks would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [#27583] +## 1.42.0 * [***] Adding support for selecting different unit of value in Cover and Columns blocks [#26161] * [**] Button block - Add link picker to the block settings [#26206] * [**] Support to render background/text colors in Group, Paragraph and Quote blocks [#25994] * [*] Fix theme colors syncing with the editor [#26821] * [**] Fix issue where a blocks would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [#27583] +## 1.44.0 + +* [***] Add support for cross-posting between sites + ## 1.41.0 * [***] Faster editor start and overall operation on Android [#26732] diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index 2b08b695935b6a..f3cdbac9565cf4 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -245,7 +245,9 @@ class EditorPage { blockAccessibilityLabel ); const size = await this.driver.getWindowSize(); - const height = size.height - 5; + // The virtual home button covers the bottom 34 in portrait and 21 on landscape on iOS. + // We start dragging a bit above it to not trigger home button. + const height = size.height - 50; while ( ! ( await blockButton.isDisplayed() ) ) { await this.driver.execute( 'mobile: dragFromToForDuration', { diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java index 83bbea9e5d5a15..ed3fdb7616676a 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainActivity.java @@ -29,6 +29,7 @@ protected Bundle getLaunchOptions() { Bundle bundle = new Bundle(); Bundle capabilities = new Bundle(); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_MENTIONS, true); + capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_XPOSTS, true); capabilities.putBoolean(GutenbergProps.PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR, true); bundle.putBundle(GutenbergProps.PROP_CAPABILITIES, capabilities); return bundle; diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index 628feb8b8b6dbb..afa269ee7ce1c6 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -189,8 +189,13 @@ public void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockC } @Override - public void onAddMention(Consumer onSuccess) { - onSuccess.accept("matt"); + public void onShowUserSuggestions(Consumer onResult) { + onResult.accept("matt"); + } + + @Override + public void onShowXpostSuggestions(Consumer onResult) { + onResult.accept("ma.tt"); } @Override diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index 06ba6a6704bb32..b0002e0e7b5917 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -225,6 +225,10 @@ extension GutenbergViewController: GutenbergBridgeDelegate { callback(.success("matt")) } + func gutenbergDidRequestXpost(callback: @escaping (Result) -> Void) { + callback(.success("ma.tt")) + } + func gutenbergDidRequestStarterPageTemplatesTooltipShown() -> Bool { return false; } @@ -298,6 +302,7 @@ extension GutenbergViewController: GutenbergBridgeDataSource { func gutenbergCapabilities() -> [Capabilities : Bool] { return [ .mentions: true, + .xposts: true, .unsupportedBlockEditor: unsupportedBlockEnabled, .canEnableUnsupportedBlockEditor: unsupportedBlockCanBeActivated, .mediaFilesCollectionBlock: true, diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 39b82482fe5dc7..5735431de050df 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -21,7 +21,7 @@ PODS: - DoubleConversion - glog - glog (0.3.5) - - Gutenberg (1.42.1): + - Gutenberg (1.43.0): - React-Core (= 0.61.5) - React-CoreModules (= 0.61.5) - React-RCTImage (= 0.61.5) @@ -253,7 +253,7 @@ PODS: - React-Core - RNSVG (9.13.6-gb): - React-Core - - RNTAztecView (1.42.1): + - RNTAztecView (1.43.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.3) - WordPress-Aztec-iOS (1.19.3) @@ -402,7 +402,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 118d0d177724c2d67f08a59136eb29ef5943ec75 Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51 glog: 1f3da668190260b06b429bb211bfbee5cd790c28 - Gutenberg: d4257899f1def887029385c03eeadb20f8769506 + Gutenberg: 0bf0e3308e75b4c4693447d3ce672410e978616e RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1 RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320 React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78 @@ -435,7 +435,7 @@ SPEC CHECKSUMS: RNReanimated: f05baf4cd76b6eab2e4d7e2b244424960b968918 RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b RNSVG: 46c4b680fe18237fa01eb7d7b311d77618fde31f - RNTAztecView: 38c4acd97d152a125eba9318125c3a75ea99e1db + RNTAztecView: 2b2423ec5349be4170212c6d81345525b4d9f41c WordPress-Aztec-iOS: b7ac8b30f746992e85d9668453ac87c2cdcecf4f Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 3ecede565bbde8..2d3f3819dbac13 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -8,7 +8,10 @@ */ import RCTAztecView from '@wordpress/react-native-aztec'; import { View, Platform } from 'react-native'; -import { addMention } from '@wordpress/react-native-bridge'; +import { + showUserSuggestions, + showXpostSuggestions, +} from '@wordpress/react-native-bridge'; import { get, pickBy, debounce } from 'lodash'; import memize from 'memize'; @@ -17,14 +20,13 @@ import memize from 'memize'; */ import { BlockFormatControls } from '@wordpress/block-editor'; import { Component } from '@wordpress/element'; -import { Toolbar, ToolbarButton } from '@wordpress/components'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { withSelect } from '@wordpress/data'; import { childrenBlock } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; import { isURL } from '@wordpress/url'; -import { Icon, atSymbol } from '@wordpress/icons'; +import { atSymbol, plus } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; /** @@ -43,6 +45,7 @@ import { removeLineSeparator } from '../remove-line-separator'; import { isCollapsed } from '../is-collapsed'; import { remove } from '../remove'; import styles from './style.scss'; +import ToolbarButtonWithOptions from './toolbar-button-with-options'; import { store as richTextStore } from '../store'; const unescapeSpaces = ( text ) => { @@ -83,7 +86,6 @@ export class RichText extends Component { this.onKeyDown = this.onKeyDown.bind( this ); this.handleEnter = this.handleEnter.bind( this ); this.handleDelete = this.handleDelete.bind( this ); - this.handleMention = this.handleMention.bind( this ); this.onPaste = this.onPaste.bind( this ); this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); @@ -101,7 +103,16 @@ export class RichText extends Component { ); this.valueToFormat = this.valueToFormat.bind( this ); this.getHtmlToRender = this.getHtmlToRender.bind( this ); - this.showMention = this.showMention.bind( this ); + this.handleSuggestionFunc = this.handleSuggestionFunc.bind( this ); + this.handleUserSuggestion = this.handleSuggestionFunc( + showUserSuggestions, + '@' + ).bind( this ); + this.handleXpostSuggestion = this.handleSuggestionFunc( + showXpostSuggestions, + '+' + ).bind( this ); + this.suggestionOptions = this.suggestionOptions.bind( this ); this.insertString = this.insertString.bind( this ); this.state = { activeFormats: [], @@ -322,7 +333,7 @@ export class RichText extends Component { this.handleDelete( event ); this.handleEnter( event ); - this.handleMention( event ); + this.handleTriggerKeyCodes( event ); } handleEnter( event ) { @@ -401,33 +412,66 @@ export class RichText extends Component { this.lastAztecEventType = 'input'; } - handleMention( event ) { + handleTriggerKeyCodes( event ) { const { keyCode } = event; + const triggeredOption = this.suggestionOptions().find( ( option ) => { + const triggeredKeyCode = option.triggerChar.charCodeAt( 0 ); + return triggeredKeyCode === keyCode; + } ); - if ( keyCode !== '@'.charCodeAt( 0 ) ) { - return; - } - const record = this.getRecord(); - const text = getTextContent( record ); - // Only start the mention UI if the selection is on the start of text or the character before is a space - if ( - text.length === 0 || - record.start === 0 || - text.charAt( record.start - 1 ) === ' ' - ) { - this.showMention(); - } else { - this.insertString( record, '@' ); + if ( triggeredOption ) { + const record = this.getRecord(); + const text = getTextContent( record ); + // Only respond to the trigger if the selection is on the start of text or line + // or if the character before is a space + const useTrigger = + text.length === 0 || + record.start === 0 || + text.charAt( record.start - 1 ) === '\n' || + text.charAt( record.start - 1 ) === ' '; + + if ( useTrigger && triggeredOption.onClick ) { + triggeredOption.onClick(); + } else { + this.insertString( record, triggeredOption.triggerChar ); + } } } - showMention() { - const record = this.getRecord(); - addMention() - .then( ( mentionUserId ) => { - this.insertString( record, `@${ mentionUserId } ` ); - } ) - .catch( () => {} ); + suggestionOptions() { + const { areMentionsSupported, areXPostsSupported } = this.props; + const allOptions = [ + { + supported: areMentionsSupported, + title: __( 'Insert mention' ), + onClick: this.handleUserSuggestion, + triggerChar: '@', + value: 'mention', + label: __( 'Mention' ), + icon: atSymbol, + }, + { + supported: areXPostsSupported, + title: __( 'Insert crosspost' ), + onClick: this.handleXpostSuggestion, + triggerChar: '+', + value: 'crosspost', + label: __( 'Crosspost' ), + icon: plus, + }, + ]; + return allOptions.filter( ( op ) => op.supported ); + } + + handleSuggestionFunc( suggestionFunction, prefix ) { + return () => { + const record = this.getRecord(); + suggestionFunction() + .then( ( suggestion ) => { + this.insertString( record, `${ prefix }${ suggestion } ` ); + } ) + .catch( () => {} ); + }; } /** @@ -758,7 +802,6 @@ export class RichText extends Component { withoutInteractiveFormatting, accessibilityLabel, disableEditingMenu = false, - isMentionsSupported, } = this.props; const record = this.getRecord(); @@ -874,11 +917,9 @@ export class RichText extends Component { onFocus={ this.onFocus } onBlur={ this.onBlur } onKeyDown={ this.onKeyDown } - triggerKeyCodes={ - disableEditingMenu === false && isMentionsSupported - ? [ '@' ] - : [] - } + triggerKeyCodes={ this.suggestionOptions().map( + ( op ) => op.triggerChar + ) } onPaste={ this.onPaste } activeFormats={ this.getActiveFormatNames( record ) } onContentSizeChange={ this.onContentSizeChange } @@ -919,18 +960,9 @@ export class RichText extends Component { onFocus={ () => {} } /> - { - // eslint-disable-next-line no-undef - isMentionsSupported && ( - - } - onClick={ this.showMention } - /> - - ) - } + ) } @@ -957,8 +989,9 @@ export default compose( [ return { formatTypes: select( richTextStore ).getFormatTypes(), - isMentionsSupported: + areMentionsSupported: getSettings( 'capabilities' ).mentions === true, + areXPostsSupported: getSettings( 'capabilities' ).xposts === true, ...{ parentBlockStyles }, }; } ), diff --git a/packages/rich-text/src/component/toolbar-button-with-options.native.js b/packages/rich-text/src/component/toolbar-button-with-options.native.js new file mode 100644 index 00000000000000..81c80f3825c1ac --- /dev/null +++ b/packages/rich-text/src/component/toolbar-button-with-options.native.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ +import { Picker, ToolbarGroup, ToolbarButton } from '@wordpress/components'; +import { useRef } from '@wordpress/element'; +import { Icon } from '@wordpress/icons'; + +/** + * Toolbar button component that, upon a long press, opens a Picker + * to allow selecting from among multiple options. + */ +function ToolbarButtonWithOptions( { options } ) { + const picker = useRef(); + + function presentPicker() { + if ( picker.current ) { + picker.current.presentPicker(); + } + } + + function onValueSelected( selectedValue ) { + const selectedOption = options.find( + ( op ) => op.value === selectedValue + ); + if ( selectedOption ) { + selectedOption.onClick(); + } + } + + if ( ! options || options.length === 0 ) { + return null; + } + const firstOption = options[ 0 ]; + const enablePicker = options.length > 1; + + return ( + <> + + } + onClick={ firstOption.onClick } + onLongPress={ enablePicker && presentPicker } + /> + + { enablePicker && ( + + ) } + + ); +} + +export default ToolbarButtonWithOptions; diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index e31ef4fedde793..7dc8a515a74b0a 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Feature + +- Add optional argument `maxLength` for truncating URL in `filterURLForDisplay` + ## 2.16.0 (2020-06-15) ### New Feature diff --git a/packages/url/README.md b/packages/url/README.md index c0df7acf89e227..d369f6ed7cd62d 100644 --- a/packages/url/README.md +++ b/packages/url/README.md @@ -96,11 +96,13 @@ _Usage_ ```js const displayUrl = filterURLForDisplay( 'https://www.wordpress.org/gutenberg/' ); // wordpress.org/gutenberg +const imageUrl = filterURLForDisplay( 'https://www.wordpress.org/wp-content/uploads/img.png', 20 ); // …ent/uploads/img.png ``` _Parameters_ - _url_ `string`: Original URL. +- _maxLength_ `(number|null)`: URL length. _Returns_ diff --git a/packages/url/src/filter-url-for-display.js b/packages/url/src/filter-url-for-display.js index 978594aced536c..fd3eccc99cbf74 100644 --- a/packages/url/src/filter-url-for-display.js +++ b/packages/url/src/filter-url-for-display.js @@ -2,22 +2,53 @@ * Returns a URL for display. * * @param {string} url Original URL. + * @param {number|null} maxLength URL length. * * @example * ```js * const displayUrl = filterURLForDisplay( 'https://www.wordpress.org/gutenberg/' ); // wordpress.org/gutenberg + * const imageUrl = filterURLForDisplay( 'https://www.wordpress.org/wp-content/uploads/img.png', 20 ); // …ent/uploads/img.png * ``` * * @return {string} Displayed URL. */ -export function filterURLForDisplay( url ) { +export function filterURLForDisplay( url, maxLength = null ) { // Remove protocol and www prefixes. - const filteredURL = url.replace( /^(?:https?:)\/\/(?:www\.)?/, '' ); + let filteredURL = url.replace( /^(?:https?:)\/\/(?:www\.)?/, '' ); // Ends with / and only has that single slash, strip it. if ( filteredURL.match( /^[^\/]+\/$/ ) ) { - return filteredURL.replace( '/', '' ); + filteredURL = filteredURL.replace( '/', '' ); } - return filteredURL; + const mediaRegexp = /([\w|:])*\.(?:jpg|jpeg|gif|png|svg)/; + + if ( + ! maxLength || + filteredURL.length <= maxLength || + ! filteredURL.match( mediaRegexp ) + ) { + return filteredURL; + } + + // If the file is not greater than max length, return last portion of URL. + filteredURL = filteredURL.split( '?' )[ 0 ]; + const urlPieces = filteredURL.split( '/' ); + const file = urlPieces[ urlPieces.length - 1 ]; + if ( file.length <= maxLength ) { + return '…' + filteredURL.slice( -maxLength ); + } + + // If the file is greater than max length, truncate the file. + const index = file.lastIndexOf( '.' ); + const [ fileName, extension ] = [ + file.slice( 0, index ), + file.slice( index + 1 ), + ]; + const truncatedFile = fileName.slice( -3 ) + '.' + extension; + return ( + file.slice( 0, maxLength - truncatedFile.length - 1 ) + + '…' + + truncatedFile + ); } diff --git a/packages/url/src/test/index.test.js b/packages/url/src/test/index.test.js index 9a2eddc478a551..132c68b34169e0 100644 --- a/packages/url/src/test/index.test.js +++ b/packages/url/src/test/index.test.js @@ -876,6 +876,54 @@ describe( 'filterURLForDisplay', () => { const url = filterURLForDisplay( 'http://www.wordpress.org/something' ); expect( url ).toBe( 'wordpress.org/something' ); } ); + it( 'should preserve the original url if no argument max length', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/wp-content/uploads/myimage.jpg' + ); + expect( url ).toBe( 'wordpress.org/wp-content/uploads/myimage.jpg' ); + } ); + it( 'should preserve the original url if the url is short enough', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/ig.jpg', + 20 + ); + expect( url ).toBe( 'wordpress.org/ig.jpg' ); + } ); + it( 'should return ellipsis, upper level pieces url, and filename when the url is long enough but filename is short enough', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/wp-content/uploads/myimage.jpg', + 20 + ); + expect( url ).toBe( '…/uploads/myimage.jpg' ); + } ); + it( 'should return filename split by ellipsis plus three characters when filename is long enough', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/wp-content/uploads/superlongtitlewithextension.jpeg', + 20 + ); + expect( url ).toBe( 'superlongti…ion.jpeg' ); + } ); + it( 'should remove query arguments', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/wp-content/uploads/myimage.jpeg?query_args=a', + 20 + ); + expect( url ).toBe( '…uploads/myimage.jpeg' ); + } ); + it( 'should preserve the original url when it is not a file', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/wp-content/url/', + 20 + ); + expect( url ).toBe( 'wordpress.org/wp-content/url/' ); + } ); + it( 'should return file split by ellipsis when the file name has multiple periods', () => { + const url = filterURLForDisplay( + 'http://www.wordpress.org/wp-content/uploads/filename.2020.12.20.png', + 20 + ); + expect( url ).toBe( 'filename.202….20.png' ); + } ); } ); describe( 'cleanForSlug', () => { diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 8b5193a7f3a3ee..d7573d21dac2da 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -8,6 +8,39 @@ class WP_Theme_JSON_Test extends WP_UnitTestCase { + function test_user_data_is_escaped() { + $theme_json = new WP_Theme_JSON( + array( + 'global' => array( + 'styles' => array( + 'color' => array( + 'background' => 'green', + 'gradient' => 'linear-gradient(10deg,rgba(6,147,227,1) 0%,rgb(61,132,163) 37%,rgb(155,81,224) 100%)', + 'link' => 'linear-gradient(10deg,rgba(6,147,227,1) 0%,rgb(61,132,163) 37%,rgb(155,81,224) 100%)', + 'text' => 'var:preset|color|dark-gray', + ), + ), + ), + ), + true + ); + $result = $theme_json->get_raw_data(); + + $expected = array( + 'global' => array( + 'styles' => array( + 'color' => array( + 'background' => 'green', + 'gradient' => 'linear-gradient(10deg,rgba(6,147,227,1) 0%,rgb(61,132,163) 37%,rgb(155,81,224) 100%)', + 'text' => 'var:preset|color|dark-gray', + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $result ); + } + function test_contexts_not_valid_are_skipped() { $theme_json = new WP_Theme_JSON( array( @@ -298,13 +331,13 @@ public function test_merge_incoming_data() { ), ), 'typography' => array( - 'fontSizes' => array( + 'fontSizes' => array( array( 'slug' => 'fontSize', 'size' => 'fontSize', ), ), - 'fontFamilies' => array( + 'fontFamilies' => array( array( 'slug' => 'fontFamily', 'fontFamily' => 'fontFamily', @@ -335,13 +368,13 @@ public function test_merge_incoming_data() { ), ), 'typography' => array( - 'fontSizes' => array( + 'fontSizes' => array( array( 'slug' => 'fontSize', 'size' => 'fontSize', ), ), - 'fontFamilies' => array( + 'fontFamilies' => array( array( 'slug' => 'fontFamily', 'fontFamily' => 'fontFamily', diff --git a/readme.txt b/readme.txt index 2afcfd3cf18d9c..9b45224bac4356 100644 --- a/readme.txt +++ b/readme.txt @@ -57,4 +57,4 @@ View release page. +To read the changelog for Gutenberg 9.6.1, please navigate to the release page.