From 5c08da07c78bfa6b5ca2f724b3cc1aec6f342812 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 13 May 2022 13:52:37 +0800 Subject: [PATCH 01/88] Fix pull-request-automation CI check (temporarily until an upstream fix lands) (#41044) * Bump some versions * temporarily use current branch to test workflow * Revert "Bump some versions" This reverts commit a6f517182c4f5740d1bcd8db3934c17eb9cfa439. * Fix version of `@actions/core` * Revert "temporarily use current branch to test workflow" This reverts commit 5eea292dfe9bcaf8b7863520e679743b5a25a115. --- package-lock.json | 2 +- packages/project-management-automation/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70ba2680fec2f..2b5e39139bcf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18163,7 +18163,7 @@ "version": "file:packages/project-management-automation", "dev": true, "requires": { - "@actions/core": "^1.4.0", + "@actions/core": "1.8.0", "@actions/github": "^5.0.0", "@babel/runtime": "^7.16.0", "@octokit/request-error": "^2.1.0", diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json index b043fc4b15520..466946b9f8dbe 100644 --- a/packages/project-management-automation/package.json +++ b/packages/project-management-automation/package.json @@ -22,7 +22,7 @@ "main": "lib/index.js", "types": "build-types", "dependencies": { - "@actions/core": "^1.4.0", + "@actions/core": "1.8.0", "@actions/github": "^5.0.0", "@babel/runtime": "^7.16.0", "@octokit/request-error": "^2.1.0", From 921f5c0a629a53d6bcb37f95323f22c309620572 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 13 May 2022 15:16:06 +0800 Subject: [PATCH 02/88] Update package-lock.json (#41045) * Update package-lock.json * Update main package.json dependency --- package-lock.json | 11 +++++++---- package.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b5e39139bcf9..7d6071eadea2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,10 +5,13 @@ "requires": true, "dependencies": { "@actions/core": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.4.0.tgz", - "integrity": "sha512-CGx2ilGq5i7zSLgiiGUtBCxhRRxibJYU6Fim0Q1Wg2aQL2LTnF27zbqZOrxfvFQ55eSBW0L8uVStgtKMpa0Qlg==", - "dev": true + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", + "dev": true, + "requires": { + "@actions/http-client": "^1.0.11" + } }, "@actions/github": { "version": "5.0.0", diff --git a/package.json b/package.json index c0f837101bd51..ae15e4b44c499 100755 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@wordpress/wordcount": "file:packages/wordcount" }, "devDependencies": { - "@actions/core": "1.4.0", + "@actions/core": "1.8.0", "@actions/github": "5.0.0", "@babel/core": "7.16.0", "@babel/plugin-syntax-jsx": "7.16.0", From 9601a33e30ba41bac98579c8d822af63dd961488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Fri, 13 May 2022 10:04:55 +0200 Subject: [PATCH 03/88] CheckboxControl: Convert component to TypeScript (#40915) * CheckboxControl: Covert component to TypeScript * Update CHANGELOG.md * Update README.md * Update storybook * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> * Remove className * Use example as the default template Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --- packages/components/CHANGELOG.md | 13 ++-- .../components/src/checkbox-control/README.md | 18 +++-- .../checkbox-control/{index.js => index.tsx} | 63 +++++++++++---- .../stories/{index.js => index.tsx} | 76 ++++++++++++------- .../components/src/checkbox-control/types.ts | 36 +++++++++ packages/components/tsconfig.json | 1 + 6 files changed, 152 insertions(+), 55 deletions(-) rename packages/components/src/checkbox-control/{index.js => index.tsx} (61%) rename packages/components/src/checkbox-control/stories/{index.js => index.tsx} (50%) create mode 100644 packages/components/src/checkbox-control/types.ts diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 720a9f12e8bd0..a7b533663cdd5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,6 +13,7 @@ - `DateTimePicker`: Convert to TypeScript ([#40775](https://github.com/WordPress/gutenberg/pull/40775)). - `DateTimePicker`: Convert unit tests to TypeScript ([#40957](https://github.com/WordPress/gutenberg/pull/40957)). +- `CheckboxControl`: Convert to TypeScript ([#40915](https://github.com/WordPress/gutenberg/pull/40915)). ## 19.10.0 (2022-05-04) @@ -29,15 +30,15 @@ - `UnitControl`: Add `__next36pxDefaultSize` flag for larger default size ([#40627](https://github.com/WordPress/gutenberg/pull/40627)). - `SelectControl`: Improved TypeScript support ([#40737](https://github.com/WordPress/gutenberg/pull/40737)). - `ToggleControlGroup`: Switch to internal `Icon` component for dashicon support ([40717](https://github.com/WordPress/gutenberg/pull/40717)). -- Improve `ToolsPanel` accessibility. ([#40716](https://github.com/WordPress/gutenberg/pull/40716)) +- Improve `ToolsPanel` accessibility. ([#40716](https://github.com/WordPress/gutenberg/pull/40716)) ### Bug Fix -- The `Button` component now displays the label as the tooltip for icon only buttons. ([#40716](https://github.com/WordPress/gutenberg/pull/40716)) -- Use fake timers and fix usage of async methods from `@testing-library/user-event`. ([#40790](https://github.com/WordPress/gutenberg/pull/40790)) -- UnitControl: avoid calling onChange callback twice when unit changes. ([#40796](https://github.com/WordPress/gutenberg/pull/40796)) -- `UnitControl`: show unit label when units prop has only one unit. ([#40784](https://github.com/WordPress/gutenberg/pull/40784)) -- `AnglePickerControl`: Fix closing of gradient popover when the angle control is clicked. ([#40735](https://github.com/WordPress/gutenberg/pull/40735)) +- The `Button` component now displays the label as the tooltip for icon only buttons. ([#40716](https://github.com/WordPress/gutenberg/pull/40716)) +- Use fake timers and fix usage of async methods from `@testing-library/user-event`. ([#40790](https://github.com/WordPress/gutenberg/pull/40790)) +- UnitControl: avoid calling onChange callback twice when unit changes. ([#40796](https://github.com/WordPress/gutenberg/pull/40796)) +- `UnitControl`: show unit label when units prop has only one unit. ([#40784](https://github.com/WordPress/gutenberg/pull/40784)) +- `AnglePickerControl`: Fix closing of gradient popover when the angle control is clicked. ([#40735](https://github.com/WordPress/gutenberg/pull/40735)) ### Internal diff --git a/packages/components/src/checkbox-control/README.md b/packages/components/src/checkbox-control/README.md index 0aa98eae854c7..4bc19ed9bfe5f 100644 --- a/packages/components/src/checkbox-control/README.md +++ b/packages/components/src/checkbox-control/README.md @@ -77,37 +77,39 @@ const MyCheckboxControl = () => { The set of props accepted by the component will be specified below. Props not included in this set will be applied to the input element. -#### label +#### `label`: `string` A label for the input field, that appears at the side of the checkbox. The prop will be rendered as content a label element. If no prop is passed an empty label is rendered. -- Type: `String` - Required: No -#### help +#### `help`: `string|WPElement` If this property is added, a help text will be generated using help property as the content. -- Type: `String|WPElement` - Required: No -#### checked +#### `checked`: `boolean` If checked is true the checkbox will be checked. If checked is false the checkbox will be unchecked. If no value is passed the checkbox will be unchecked. -- Type: `Boolean` - Required: No -#### onChange +#### `onChange`: `function` A function that receives the checked state (boolean) as input. -- Type: `function` - Required: Yes +#### `indeterminate`: `boolean` + +If indeterminate is true the state of the checkbox will be indeterminate. + +- Required: No + ## Related components - To select one option from a set, and you want to show all the available options at once, use the `RadioControl` component. diff --git a/packages/components/src/checkbox-control/index.js b/packages/components/src/checkbox-control/index.tsx similarity index 61% rename from packages/components/src/checkbox-control/index.js rename to packages/components/src/checkbox-control/index.tsx index d5c3506a06d5c..3b4e6eb2dc5e5 100644 --- a/packages/components/src/checkbox-control/index.js +++ b/packages/components/src/checkbox-control/index.tsx @@ -2,6 +2,7 @@ * External dependencies */ import classnames from 'classnames'; +import type { ChangeEvent } from 'react'; /** * WordPress dependencies @@ -15,17 +16,48 @@ import { Icon, check, reset } from '@wordpress/icons'; * Internal dependencies */ import BaseControl from '../base-control'; +import type { CheckboxControlProps } from './types'; +import type { WordPressComponentProps } from '../ui/context'; + +/** + * Checkboxes allow the user to select one or more items from a set. + * + * ```jsx + * import { CheckboxControl } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const MyCheckboxControl = () => { + * const [ isChecked, setChecked ] = useState( true ); + * return ( + * + * ); + * }; + * ``` + */ +export function CheckboxControl( + // ref is omitted until we have `WordPressComponentPropsWithoutRef` or add + // ref forwarding to CheckboxControl. + props: Omit< + WordPressComponentProps< CheckboxControlProps, 'input', false >, + 'ref' + > +) { + const { + label, + className, + heading, + checked, + indeterminate, + help, + onChange, + ...additionalProps + } = props; -export default function CheckboxControl( { - label, - className, - heading, - checked, - indeterminate, - help, - onChange, - ...props -} ) { if ( heading ) { deprecated( '`heading` prop in `CheckboxControl`', { alternative: 'a separate element to implement a heading', @@ -38,9 +70,9 @@ export default function CheckboxControl( { false ); - // Run the following callback everytime the `ref` (and the additional + // Run the following callback every time the `ref` (and the additional // dependencies) change. - const ref = useRefEffect( + const ref = useRefEffect< HTMLInputElement >( ( node ) => { if ( ! node ) { return; @@ -56,7 +88,8 @@ export default function CheckboxControl( { ); const instanceId = useInstanceId( CheckboxControl ); const id = `inspector-checkbox-control-${ instanceId }`; - const onChangeValue = ( event ) => onChange( event.target.checked ); + const onChangeValue = ( event: ChangeEvent< HTMLInputElement > ) => + onChange( event.target.checked ); return ( { showIndeterminateIcon ? ( ); } + +export default CheckboxControl; diff --git a/packages/components/src/checkbox-control/stories/index.js b/packages/components/src/checkbox-control/stories/index.tsx similarity index 50% rename from packages/components/src/checkbox-control/stories/index.js rename to packages/components/src/checkbox-control/stories/index.tsx index f70ac4323abfc..6a43713a0161c 100644 --- a/packages/components/src/checkbox-control/stories/index.js +++ b/packages/components/src/checkbox-control/stories/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { text } from '@storybook/addon-knobs'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; /** * WordPress dependencies @@ -11,42 +11,60 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import CheckboxControl from '../'; +import CheckboxControl from '..'; -export default { - title: 'Components/CheckboxControl', +const meta: ComponentMeta< typeof CheckboxControl > = { component: CheckboxControl, + title: 'Components/CheckboxControl', + argTypes: { + onChange: { + action: 'onChange', + }, + checked: { + control: { type: null }, + }, + help: { control: { type: 'text' } }, + }, parameters: { - knobs: { disable: false }, + controls: { + expanded: true, + exclude: [ 'heading' ], + }, + docs: { source: { state: 'open' } }, }, }; +export default meta; -const CheckboxControlWithState = ( { checked, ...props } ) => { - const [ isChecked, setChecked ] = useState( checked ); +const DefaultTemplate: ComponentStory< typeof CheckboxControl > = ( { + onChange, + ...args +} ) => { + const [ isChecked, setChecked ] = useState( true ); return ( { + setChecked( v ); + onChange( v ); + } } /> ); }; -export const _default = () => { - const label = text( 'Label', 'Is author' ); - - return ; +export const Default: ComponentStory< + typeof CheckboxControl +> = DefaultTemplate.bind( {} ); +Default.args = { + label: 'Is author', + help: 'Is the user an author or not?', }; -export const all = () => { - const label = text( 'Label', 'Is author' ); - const help = text( 'Help', 'Is the user an author or not?' ); - - return ; -}; - -export const Indeterminate = () => { +export const Indeterminate: ComponentStory< typeof CheckboxControl > = ( { + onChange, + ...args +} ) => { const [ fruits, setFruits ] = useState( { apple: false, orange: false } ); const isAllChecked = Object.values( fruits ).every( Boolean ); @@ -56,15 +74,16 @@ export const Indeterminate = () => { return ( <> + onChange={ ( v ) => { setFruits( { - apple: newValue, - orange: newValue, - } ) - } + apple: v, + orange: v, + } ); + onChange( v ); + } } /> { ); }; +Indeterminate.args = { + label: 'Select all', +}; diff --git a/packages/components/src/checkbox-control/types.ts b/packages/components/src/checkbox-control/types.ts new file mode 100644 index 0000000000000..2946147120292 --- /dev/null +++ b/packages/components/src/checkbox-control/types.ts @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; + +/** + * Internal dependencies + */ +import type { BaseControlProps } from '../base-control/types'; + +export type CheckboxControlProps = Pick< BaseControlProps, 'help' > & { + /** + * A function that receives the checked state (boolean) as input. + */ + onChange: ( value: boolean ) => void; + /** + * A label for the input field, that appears at the side of the checkbox. + * The prop will be rendered as content a label element. If no prop is + * passed an empty label is rendered. + */ + label?: string; + /** + * If checked is true the checkbox will be checked. If checked is false the + * checkbox will be unchecked. If no value is passed the checkbox will be + * unchecked. + */ + checked?: boolean; + /** + * If indeterminate is true the state of the checkbox will be indeterminate. + */ + indeterminate?: boolean; + /** + * @deprecated + */ + heading?: ReactNode; +}; diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index dbc64e1aac7b0..eea391d1200dc 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -39,6 +39,7 @@ "src/button/**/*", "src/button-group/**/*", "src/card/**/*", + "src/checkbox-control/**/*", "src/circular-option-picker/**/*", "src/color-indicator/**/*", "src/color-palette/**/*", From 3383af2e4a6547987db2186f18d45b34b60e8543 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Fri, 13 May 2022 12:26:54 +0300 Subject: [PATCH 04/88] [Block Library - Query Loop]: Add `parents` filter (#40933) * [Block Library - Query Loop]: Add `parents` filter * move `isPostTypeHierarchical` in inspector controls component + reset parents on post type change * orderBy relevance * address feedback * feedback --- lib/compat/wordpress-6.0/blocks.php | 3 + .../block-library/src/post-template/edit.js | 5 + packages/block-library/src/query/block.json | 3 +- .../query/edit/inspector-controls/index.js | 24 ++++ .../edit/inspector-controls/parent-control.js | 133 ++++++++++++++++++ packages/block-library/src/query/utils.js | 22 +++ .../fixtures/blocks/core__query.json | 3 +- 7 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 packages/block-library/src/query/edit/inspector-controls/parent-control.js diff --git a/lib/compat/wordpress-6.0/blocks.php b/lib/compat/wordpress-6.0/blocks.php index 7a3f6b728eebc..be2d739329d02 100644 --- a/lib/compat/wordpress-6.0/blocks.php +++ b/lib/compat/wordpress-6.0/blocks.php @@ -119,6 +119,9 @@ function gutenberg_build_query_vars_from_query_block( $block, $page ) { if ( ! empty( $block->context['query']['search'] ) ) { $query['s'] = $block->context['query']['search']; } + if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { + $query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) ); + } } return $query; } diff --git a/packages/block-library/src/post-template/edit.js b/packages/block-library/src/post-template/edit.js index 2422f242aa690..0d21a98c477d2 100644 --- a/packages/block-library/src/post-template/edit.js +++ b/packages/block-library/src/post-template/edit.js @@ -84,6 +84,7 @@ export default function PostTemplateEdit( { sticky, inherit, taxQuery, + parents, } = {}, queryContext = [ { page: 1 } ], templateSlug, @@ -138,6 +139,9 @@ export default function PostTemplateEdit( { if ( exclude?.length ) { query.exclude = exclude; } + if ( parents?.length ) { + query.parent = parents; + } // If sticky is not set, it will return all posts in the results. // If sticky is set to `only`, it will limit the results to sticky posts only. // If it is anything else, it will exclude sticky posts from results. For the record the value stored is `exclude`. @@ -172,6 +176,7 @@ export default function PostTemplateEdit( { inherit, templateSlug, taxQuery, + parents, ] ); const blockContexts = useMemo( diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index 426283966f8d1..6267e53f24c12 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -24,7 +24,8 @@ "exclude": [], "sticky": "", "inherit": true, - "taxQuery": null + "taxQuery": null, + "parents": [] } }, "tagName": { diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 23bb0674f38b2..14b4864efc4fa 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -17,15 +17,28 @@ import { import { __ } from '@wordpress/i18n'; import { InspectorControls } from '@wordpress/block-editor'; import { useEffect, useState, useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ import OrderControl from './order-control'; import AuthorControl from './author-control'; +import ParentControl from './parent-control'; import TaxonomyControls from './taxonomy-controls'; import { usePostTypes } from '../../utils'; +function useIsPostTypeHierarchical( postType ) { + return useSelect( + ( select ) => { + const type = select( coreStore ).getPostType( postType ); + return type?.viewable && type?.hierarchical; + }, + [ postType ] + ); +} + const stickyOptions = [ { label: __( 'Include' ), value: '' }, { label: __( 'Exclude' ), value: 'exclude' }, @@ -45,9 +58,11 @@ export default function QueryInspectorControls( { sticky, inherit, taxQuery, + parents, } = query; const [ showSticky, setShowSticky ] = useState( postType === 'post' ); const { postTypesTaxonomiesMap, postTypesSelectOptions } = usePostTypes(); + const isPostTypeHierarchical = useIsPostTypeHierarchical( postType ); useEffect( () => { setShowSticky( postType === 'post' ); }, [ postType ] ); @@ -72,6 +87,8 @@ export default function QueryInspectorControls( { if ( newValue !== 'post' ) { updateQuery.sticky = ''; } + // We need to reset `parents` because they are tied to each post type. + updateQuery.parents = []; setQuery( updateQuery ); }; const [ querySearch, setQuerySearch ] = useState( query.search ); @@ -156,6 +173,13 @@ export default function QueryInspectorControls( { value={ querySearch } onChange={ setQuerySearch } /> + { isPostTypeHierarchical && ( + + ) } ) } diff --git a/packages/block-library/src/query/edit/inspector-controls/parent-control.js b/packages/block-library/src/query/edit/inspector-controls/parent-control.js new file mode 100644 index 0000000000000..b6be0940d6ab0 --- /dev/null +++ b/packages/block-library/src/query/edit/inspector-controls/parent-control.js @@ -0,0 +1,133 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { FormTokenField } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useState, useEffect, useMemo } from '@wordpress/element'; +import { useDebounce } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { getEntitiesInfo, mapToIHasNameAndId } from '../../utils'; + +const EMPTY_ARRAY = []; +const BASE_QUERY = { + order: 'asc', + _fields: 'id,title', + context: 'view', +}; + +function ParentControl( { parents, postType, onChange } ) { + const [ search, setSearch ] = useState( '' ); + const [ value, setValue ] = useState( EMPTY_ARRAY ); + const [ suggestions, setSuggestions ] = useState( EMPTY_ARRAY ); + const debouncedSearch = useDebounce( setSearch, 250 ); + const { searchResults, searchHasResolved } = useSelect( + ( select ) => { + if ( ! search ) { + return { searchResults: EMPTY_ARRAY, searchHasResolved: true }; + } + const { getEntityRecords, hasFinishedResolution } = select( + coreStore + ); + const selectorArgs = [ + 'postType', + postType, + { + ...BASE_QUERY, + search, + orderby: 'relevance', + exclude: parents, + per_page: 20, + }, + ]; + return { + searchResults: getEntityRecords( ...selectorArgs ), + searchHasResolved: hasFinishedResolution( + 'getEntityRecords', + selectorArgs + ), + }; + }, + [ search, parents ] + ); + const currentParents = useSelect( + ( select ) => { + if ( ! parents?.length ) return EMPTY_ARRAY; + const { getEntityRecords } = select( coreStore ); + return getEntityRecords( 'postType', postType, { + ...BASE_QUERY, + include: parents, + per_page: parents.length, + } ); + }, + [ parents ] + ); + // Update the `value` state only after the selectors are resolved + // to avoid emptying the input when we're changing parents. + useEffect( () => { + if ( ! parents?.length ) { + setValue( EMPTY_ARRAY ); + } + if ( ! currentParents?.length ) return; + const currentParentsInfo = getEntitiesInfo( + mapToIHasNameAndId( currentParents, 'title.rendered' ) + ); + // Returns only the existing entity ids. This prevents the component + // from crashing in the editor, when non existing ids are provided. + const sanitizedValue = parents.reduce( ( accumulator, id ) => { + const entity = currentParentsInfo.mapById[ id ]; + if ( entity ) { + accumulator.push( { + id, + value: entity.name, + } ); + } + return accumulator; + }, [] ); + setValue( sanitizedValue ); + }, [ parents, currentParents ] ); + + const entitiesInfo = useMemo( () => { + if ( ! searchResults?.length ) return EMPTY_ARRAY; + return getEntitiesInfo( + mapToIHasNameAndId( searchResults, 'title.rendered' ) + ); + }, [ searchResults ] ); + // Update suggestions only when the query has resolved. + useEffect( () => { + if ( ! searchHasResolved ) return; + setSuggestions( entitiesInfo.names ); + }, [ entitiesInfo.names, searchHasResolved ] ); + + const getIdByValue = ( entitiesMappedByName, entity ) => { + const id = entity?.id || entitiesMappedByName?.[ entity ]?.id; + if ( id ) return id; + }; + const onParentChange = ( newValue ) => { + const ids = Array.from( + newValue.reduce( ( accumulator, entity ) => { + // Verify that new values point to existing entities. + const id = getIdByValue( entitiesInfo.mapByName, entity ); + if ( id ) accumulator.add( id ); + return accumulator; + }, new Set() ) + ); + setSuggestions( EMPTY_ARRAY ); + onChange( { parents: ids } ); + }; + return ( + + ); +} + +export default ParentControl; diff --git a/packages/block-library/src/query/utils.js b/packages/block-library/src/query/utils.js index 76a5465d365c7..4fd81566378e2 100644 --- a/packages/block-library/src/query/utils.js +++ b/packages/block-library/src/query/utils.js @@ -1,9 +1,15 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + /** * WordPress dependencies */ import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; +import { decodeEntities } from '@wordpress/html-entities'; /** * @typedef IHasNameAndId @@ -47,6 +53,22 @@ export const getEntitiesInfo = ( entities ) => { }; }; +/** + * Helper util to map records to add a `name` prop from a + * provided path, in order to handle all entities in the same + * fashion(implementing`IHasNameAndId` interface). + * + * @param {Object[]} entities The array of entities. + * @param {string} path The path to map a `name` property from the entity. + * @return {IHasNameAndId[]} An array of enitities that now implement the `IHasNameAndId` interface. + */ +export const mapToIHasNameAndId = ( entities, path ) => { + return ( entities || [] ).map( ( entity ) => ( { + ...entity, + name: decodeEntities( get( entity, path ) ), + } ) ); +}; + /** * Returns a helper object that contains: * 1. An `options` object from the available post types, to be passed to a `SelectControl`. diff --git a/test/integration/fixtures/blocks/core__query.json b/test/integration/fixtures/blocks/core__query.json index cf8b1567442d9..4c7ce920a0450 100644 --- a/test/integration/fixtures/blocks/core__query.json +++ b/test/integration/fixtures/blocks/core__query.json @@ -15,7 +15,8 @@ "exclude": [], "sticky": "", "inherit": true, - "taxQuery": null + "taxQuery": null, + "parents": [] }, "tagName": "div", "displayLayout": { From 27ec8f65c929e7e96ecb73aa1097fad5f55e0d1c Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 13 May 2022 18:00:14 +0800 Subject: [PATCH 05/88] Avoid persisting preference every time the sidebar tab is changed (#40923) * Turn complementary areas into a standard action/selector/reducer, unhooking it from the preferences store * Handle visibility in selectors, and resolve issues with actions and reducer * Update test description * My site editor general sidebar active by default * Revert "My site editor general sidebar active by default" This reverts commit 334734ce3d1f1347888dab903a5881bf6520f83b. * Preserve site editor behavior as best as possible by denoting a default area * Use an explicit action when setting up the site editor app to denote the default action * Migrate any stored data * Remove DISABLE_COMPLEMENTARY_AREA handling in interface reducer --- .../edit-site/src/components/sidebar/index.js | 2 + packages/edit-site/src/index.js | 6 ++ packages/interface/src/store/actions.js | 49 +++++++++-- packages/interface/src/store/index.js | 3 +- packages/interface/src/store/reducer.js | 35 ++++++++ packages/interface/src/store/selectors.js | 21 ++++- packages/interface/src/store/test/actions.js | 2 +- packages/preferences-persistence/src/index.js | 5 +- .../convert-complementary-areas.js | 16 ++++ .../preferences-package-data/index.js | 8 ++ .../test/convert-complementary-areas.js | 36 ++++++++ .../preferences-package-data/test/index.js | 84 +++++++++++++++++++ 12 files changed, 254 insertions(+), 13 deletions(-) create mode 100644 packages/interface/src/store/reducer.js create mode 100644 packages/preferences-persistence/src/migrations/preferences-package-data/convert-complementary-areas.js create mode 100644 packages/preferences-persistence/src/migrations/preferences-package-data/index.js create mode 100644 packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-complementary-areas.js create mode 100644 packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index b08e2b15f1539..74c64cdbcb13d 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -46,6 +46,7 @@ export function SidebarComplementaryAreaFills() { [] ); const { enableComplementaryArea } = useDispatch( interfaceStore ); + useEffect( () => { if ( ! isEditorSidebarOpened ) return; if ( hasBlockSelection ) { @@ -54,6 +55,7 @@ export function SidebarComplementaryAreaFills() { enableComplementaryArea( STORE_NAME, SIDEBAR_TEMPLATE ); } }, [ hasBlockSelection, isEditorSidebarOpened ] ); + let sidebarName = sidebar; if ( ! isEditorSidebarOpened ) { sidebarName = hasBlockSelection ? SIDEBAR_BLOCK : SIDEBAR_TEMPLATE; diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 2451f4cff3592..8129241ad45a4 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -13,6 +13,7 @@ import { __experimentalFetchUrlData as fetchUrlData, } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; +import { store as interfaceStore } from '@wordpress/interface'; import { store as preferencesStore } from '@wordpress/preferences'; import { __ } from '@wordpress/i18n'; import { store as viewportStore } from '@wordpress/viewport'; @@ -77,6 +78,11 @@ export function reinitializeEditor( target, settings ) { dispatch( editSiteStore ).setIsListViewOpened( true ); } + dispatch( interfaceStore ).setDefaultComplementaryArea( + 'core/edit-site', + 'edit-site/template' + ); + dispatch( editSiteStore ).updateSettings( settings ); // Keep the defaultTemplateTypes in the core/editor settings too, diff --git a/packages/interface/src/store/actions.js b/packages/interface/src/store/actions.js index d130d62b74308..52d1fc67552ba 100644 --- a/packages/interface/src/store/actions.js +++ b/packages/interface/src/store/actions.js @@ -4,21 +4,50 @@ import deprecated from '@wordpress/deprecated'; import { store as preferencesStore } from '@wordpress/preferences'; +/** + * Set a default complementary area. + * + * @param {string} scope Complementary area scope. + * @param {string} area Area identifier. + * + * @return {Object} Action object. + */ +export const setDefaultComplementaryArea = ( scope, area ) => ( { + type: 'SET_DEFAULT_COMPLEMENTARY_AREA', + scope, + area, +} ); + /** * Enable the complementary area. * * @param {string} scope Complementary area scope. * @param {string} area Area identifier. */ -export const enableComplementaryArea = ( scope, area ) => ( { registry } ) => { +export const enableComplementaryArea = ( scope, area ) => ( { + registry, + dispatch, +} ) => { // Return early if there's no area. if ( ! area ) { return; } - registry - .dispatch( preferencesStore ) - .set( scope, 'complementaryArea', area ); + const isComplementaryAreaVisible = registry + .select( preferencesStore ) + .get( scope, 'isComplementaryAreaVisible' ); + + if ( ! isComplementaryAreaVisible ) { + registry + .dispatch( preferencesStore ) + .set( scope, 'isComplementaryAreaVisible', true ); + } + + dispatch( { + type: 'ENABLE_COMPLEMENTARY_AREA', + scope, + area, + } ); }; /** @@ -27,9 +56,15 @@ export const enableComplementaryArea = ( scope, area ) => ( { registry } ) => { * @param {string} scope Complementary area scope. */ export const disableComplementaryArea = ( scope ) => ( { registry } ) => { - registry - .dispatch( preferencesStore ) - .set( scope, 'complementaryArea', null ); + const isComplementaryAreaVisible = registry + .select( preferencesStore ) + .get( scope, 'isComplementaryAreaVisible' ); + + if ( isComplementaryAreaVisible ) { + registry + .dispatch( preferencesStore ) + .set( scope, 'isComplementaryAreaVisible', false ); + } }; /** diff --git a/packages/interface/src/store/index.js b/packages/interface/src/store/index.js index a08420a02f542..817cdc22767e5 100644 --- a/packages/interface/src/store/index.js +++ b/packages/interface/src/store/index.js @@ -8,6 +8,7 @@ import { createReduxStore, register } from '@wordpress/data'; */ import * as actions from './actions'; import * as selectors from './selectors'; +import reducer from './reducer'; import { STORE_NAME } from './constants'; /** @@ -18,7 +19,7 @@ import { STORE_NAME } from './constants'; * @type {Object} */ export const store = createReduxStore( STORE_NAME, { - reducer: () => {}, + reducer, actions, selectors, } ); diff --git a/packages/interface/src/store/reducer.js b/packages/interface/src/store/reducer.js new file mode 100644 index 0000000000000..433f71d15bcc5 --- /dev/null +++ b/packages/interface/src/store/reducer.js @@ -0,0 +1,35 @@ +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; + +export function complementaryAreas( state = {}, action ) { + switch ( action.type ) { + case 'SET_DEFAULT_COMPLEMENTARY_AREA': { + const { scope, area } = action; + + // If there's already an area, don't overwrite it. + if ( state[ scope ] ) { + return state; + } + + return { + ...state, + [ scope ]: area, + }; + } + case 'ENABLE_COMPLEMENTARY_AREA': { + const { scope, area } = action; + return { + ...state, + [ scope ]: area, + }; + } + } + + return state; +} + +export default combineReducers( { + complementaryAreas, +} ); diff --git a/packages/interface/src/store/selectors.js b/packages/interface/src/store/selectors.js index 84312676f7017..13b5915e17aac 100644 --- a/packages/interface/src/store/selectors.js +++ b/packages/interface/src/store/selectors.js @@ -11,11 +11,28 @@ import { store as preferencesStore } from '@wordpress/preferences'; * @param {Object} state Global application state. * @param {string} scope Item scope. * - * @return {string} The complementary area that is active in the given scope. + * @return {string | null | undefined} The complementary area that is active in the given scope. */ export const getActiveComplementaryArea = createRegistrySelector( ( select ) => ( state, scope ) => { - return select( preferencesStore ).get( scope, 'complementaryArea' ); + const isComplementaryAreaVisible = select( preferencesStore ).get( + scope, + 'isComplementaryAreaVisible' + ); + + // Return `undefined` to indicate that the user has never toggled + // visibility, this is the vanilla default. Other code relies on this + // nuance in the return value. + if ( isComplementaryAreaVisible === undefined ) { + return undefined; + } + + // Return `null` to indicate the user hid the complementary area. + if ( ! isComplementaryAreaVisible ) { + return null; + } + + return state?.complementaryAreas?.[ scope ]; } ); diff --git a/packages/interface/src/store/test/actions.js b/packages/interface/src/store/test/actions.js index 2ee4075a53e63..91a57c0f1fcb2 100644 --- a/packages/interface/src/store/test/actions.js +++ b/packages/interface/src/store/test/actions.js @@ -54,7 +54,7 @@ describe( 'actions', () => { } ); describe( 'disableComplementaryArea', () => { - it( 'removes any assignment to a complementary area', () => { + it( 'results in the complementary area being inactive', () => { registry .dispatch( interfaceStore ) .enableComplementaryArea( 'my-plugin', 'custom-sidebar' ); diff --git a/packages/preferences-persistence/src/index.js b/packages/preferences-persistence/src/index.js index a28fc411ece77..2b1bcdd10fb03 100644 --- a/packages/preferences-persistence/src/index.js +++ b/packages/preferences-persistence/src/index.js @@ -3,6 +3,7 @@ */ import create from './create'; import convertLegacyLocalStorageData from './migrations/legacy-local-storage-data'; +import convertPreferencesPackageData from './migrations/preferences-package-data'; export { create }; @@ -34,9 +35,9 @@ export function __unstableCreatePersistenceLayer( serverData, userId ) { let preloadedData; if ( serverData && serverModified >= localModified ) { - preloadedData = serverData; + preloadedData = convertPreferencesPackageData( serverData ); } else if ( localData ) { - preloadedData = localData; + preloadedData = convertPreferencesPackageData( localData ); } else { // Check if there is data in the legacy format from the old persistence system. preloadedData = convertLegacyLocalStorageData( userId ); diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/convert-complementary-areas.js b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-complementary-areas.js new file mode 100644 index 0000000000000..8db83db84e3d8 --- /dev/null +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/convert-complementary-areas.js @@ -0,0 +1,16 @@ +export default function convertComplementaryAreas( state ) { + return Object.keys( state ).reduce( ( stateAccumulator, scope ) => { + const scopeData = state[ scope ]; + + // If a complementary area is truthy, convert it to the `isComplementaryAreaVisible` boolean. + if ( scopeData?.complementaryArea ) { + const updatedScopeData = { ...scopeData }; + delete updatedScopeData.complementaryArea; + updatedScopeData.isComplementaryAreaVisible = true; + stateAccumulator[ scope ] = updatedScopeData; + return stateAccumulator; + } + + return stateAccumulator; + }, state ); +} diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/index.js b/packages/preferences-persistence/src/migrations/preferences-package-data/index.js new file mode 100644 index 0000000000000..91efe2dac88f7 --- /dev/null +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/index.js @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import convertComplementaryAreas from './convert-complementary-areas'; + +export default function convertPreferencesPackageData( data ) { + return convertComplementaryAreas( data ); +} diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-complementary-areas.js b/packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-complementary-areas.js new file mode 100644 index 0000000000000..fcb31c3dc38dd --- /dev/null +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/test/convert-complementary-areas.js @@ -0,0 +1,36 @@ +/** + * Internal dependencies + */ +import convertComplementaryAreas from '../convert-complementary-areas'; + +describe( 'convertComplementaryAreas', () => { + it( 'converts the `complementaryArea` property in each scope to an `isComplementaryAreaVisible` boolean', () => { + const input = { + 'core/customize-widgets': { + complementaryArea: 'edit-post/block', + }, + 'core/edit-site': { + complementaryArea: 'edit-site/template', + }, + 'core/edit-post': { + complementaryArea: 'edit-post/block', + }, + 'core/edit-widgets': {}, + }; + + const expectedOutput = { + 'core/customize-widgets': { + isComplementaryAreaVisible: true, + }, + 'core/edit-site': { + isComplementaryAreaVisible: true, + }, + 'core/edit-post': { + isComplementaryAreaVisible: true, + }, + 'core/edit-widgets': {}, + }; + + expect( convertComplementaryAreas( input ) ).toEqual( expectedOutput ); + } ); +} ); diff --git a/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js b/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js new file mode 100644 index 0000000000000..c96f8d1b0ab35 --- /dev/null +++ b/packages/preferences-persistence/src/migrations/preferences-package-data/test/index.js @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import convertPreferencesPackageData from '../'; + +const input = { + 'core/customize-widgets': { + welcomeGuide: false, + fixedToolbar: true, + }, + 'core/edit-widgets': { + welcomeGuide: false, + fixedToolbar: true, + showBlockBreadcrumbs: false, + complementaryArea: 'edit-widgets/block-areas', + }, + 'core/edit-post': { + welcomeGuide: false, + fixedToolbar: true, + fullscreenMode: false, + hiddenBlockTypes: [ 'core/audio', 'core/cover' ], + editorMode: 'visual', + preferredStyleVariations: { + 'core/quote': 'large', + }, + inactivePanels: [], + openPanels: [ 'post-status' ], + pinnedItems: { + 'my-sidebar-plugin/title-sidebar': false, + }, + }, + 'core/edit-site': { + welcomeGuide: false, + welcomeGuideStyles: false, + fixedToolbar: true, + complementaryArea: 'edit-site/global-styles', + }, +}; + +describe( 'convertPreferencesPackageData', () => { + it( 'converts data to the expected format', () => { + expect( convertPreferencesPackageData( input ) ) + .toMatchInlineSnapshot( ` + Object { + "core/customize-widgets": Object { + "fixedToolbar": true, + "welcomeGuide": false, + }, + "core/edit-post": Object { + "editorMode": "visual", + "fixedToolbar": true, + "fullscreenMode": false, + "hiddenBlockTypes": Array [ + "core/audio", + "core/cover", + ], + "inactivePanels": Array [], + "openPanels": Array [ + "post-status", + ], + "pinnedItems": Object { + "my-sidebar-plugin/title-sidebar": false, + }, + "preferredStyleVariations": Object { + "core/quote": "large", + }, + "welcomeGuide": false, + }, + "core/edit-site": Object { + "fixedToolbar": true, + "isComplementaryAreaVisible": true, + "welcomeGuide": false, + "welcomeGuideStyles": false, + }, + "core/edit-widgets": Object { + "fixedToolbar": true, + "isComplementaryAreaVisible": true, + "showBlockBreadcrumbs": false, + "welcomeGuide": false, + }, + } + ` ); + } ); +} ); From 0f73fa2ce28e2721d94578ad89776a96085739c8 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Fri, 13 May 2022 17:47:10 +0300 Subject: [PATCH 06/88] [List v2]: Focus new list item added from sibling inserter (#41051) --- packages/block-library/src/list/v2/edit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/list/v2/edit.js b/packages/block-library/src/list/v2/edit.js index fc885cca7eb31..6bffee6844357 100644 --- a/packages/block-library/src/list/v2/edit.js +++ b/packages/block-library/src/list/v2/edit.js @@ -134,6 +134,7 @@ function Edit( { attributes, setAttributes, clientId } ) { const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: [ 'core/list-item' ], template: TEMPLATE, + templateInsertUpdatesSelection: true, } ); useMigrateOnLoad( attributes, clientId ); const { ordered, reversed, start } = attributes; From 239a7cecccaf6b64a56f1b1cb0682010f3f4cc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20K=C3=A4gy?= Date: Fri, 13 May 2022 17:02:10 +0200 Subject: [PATCH 07/88] Add @fabiankaegy as a code owner for the docs/ directory (#41057) * Add @fbiankaegy as a code owner for the docs/ directory * Update .github/CODEOWNERS Co-authored-by: George Mamadashvili Co-authored-by: George Mamadashvili --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8bc716803f106..2c588ae6adcc3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # Documentation -/docs @ajitbohra @ryanwelcher @juanmaguitar +/docs @ajitbohra @ryanwelcher @juanmaguitar @fabiankaegy # Schemas /schemas/json @ajlende From b83ce72de13b0a0f4b84733bd61a4f91271fee95 Mon Sep 17 00:00:00 2001 From: Siobhan Bamber Date: Fri, 13 May 2022 17:33:45 +0100 Subject: [PATCH 08/88] [RNMobile] Improve text read by screen readers for BottomSheetSelectControl (#41036) Improves the text that's read by screen readers by the BottomSheetSelectControl component, adding extra context and making its purpose clearer. --- .../mobile/bottom-sheet-select-control/index.native.js | 9 +++++++-- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js index 6d88c349b7834..cc0a340104f86 100644 --- a/packages/components/src/mobile/bottom-sheet-select-control/index.native.js +++ b/packages/components/src/mobile/bottom-sheet-select-control/index.native.js @@ -57,11 +57,16 @@ const BottomSheetSelectControl = ( { value={ selectedOption.label } onPress={ openSubSheet } accessibilityRole={ 'button' } - accessibilityLabel={ selectedOption.label } + accessibilityLabel={ sprintf( + // translators: %1$s: Select control button label e.g. "Button width". %2$s: Select control option value e.g: "Auto, 25%". + __( '%1$s. Currently selected: %2$s' ), + label, + selectedOption.label + ) } accessibilityHint={ sprintf( // translators: %s: Select control button label e.g. "Button width" __( 'Navigates to select %s' ), - selectedOption.label + label ) } > diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 34b2157e09f90..137beeddf4d8f 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased +- [*] [a11y] Improve text read by screen readers for BottomSheetSelectControl [#41036] ## 1.76.0 From 610e449c8e9c3697d628a93e099b9c32643c4e7b Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Fri, 13 May 2022 21:33:25 +0100 Subject: [PATCH 09/88] Theme Export: Use basename when determining the theme slug (#41058) --- .../class-gutenberg-rest-edit-site-export-controller.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/compat/wordpress-6.0/class-gutenberg-rest-edit-site-export-controller.php b/lib/compat/wordpress-6.0/class-gutenberg-rest-edit-site-export-controller.php index 13862bc97a197..e4cffb2bff268 100644 --- a/lib/compat/wordpress-6.0/class-gutenberg-rest-edit-site-export-controller.php +++ b/lib/compat/wordpress-6.0/class-gutenberg-rest-edit-site-export-controller.php @@ -76,9 +76,7 @@ public function export() { return $filename; } - $stylesheet = get_stylesheet(); - $stylesheet_directories = explode( '/', $stylesheet ); - $theme_name = end( $stylesheet_directories ); + $theme_name = basename( get_stylesheet() ); header( 'Content-Type: application/zip' ); header( 'Content-Disposition: attachment; filename=' . $theme_name . '.zip' ); From 0a982ce71d7958684f147038d670d073f93e1f01 Mon Sep 17 00:00:00 2001 From: Jos Date: Mon, 16 May 2022 10:33:48 +0800 Subject: [PATCH 10/88] [RNMobile] - E2E Simplify heading and lists blocks functions (#40670) * update tests using paragraph, heading and list blocks * fix slash inserter tests to work in ci * lint fixes * wait for ordered list to appear * lint fixes * extra click only on local env * wait to get backspace click reflected * re-add extra click only for local env * add wait to wait for backspace key to be reflected * lint fixes * break function, set position to get list block * lint fixes * use correct params, update function name * lint fixes * make maxIteration a parameter for isElementVisible * update xpath for list block * utilize waitForVisible for isElementVisible * lint fixes * add wait to getNumberOfParagraphBlocks and update xpath for android list block * update edit text xpath to be read from any level Co-authored-by: jos <17252150+jostnes@users.noreply.github.com> --- .../gutenberg-editor-heading-@canary.test.js | 29 ++-- .../gutenberg-editor-lists-@canary.test.js | 34 ++-- .../gutenberg-editor-lists-end.test.js | 21 +-- .../gutenberg-editor-lists.test.js | 31 ++-- .../gutenberg-editor-paragraph.test.js | 20 +-- .../gutenberg-editor-paste.test.js | 14 +- .../gutenberg-editor-rotation.test.js | 19 +-- ...berg-editor-slash-inserter-@canary.test.js | 78 +++------ .../__device-tests__/helpers/utils.js | 94 ++++++++--- .../__device-tests__/pages/editor-page.js | 157 +++++++++--------- 10 files changed, 233 insertions(+), 264 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js index 17f58bfec5f71..f806b43d2f8e4 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-heading-@canary.test.js @@ -2,26 +2,18 @@ * Internal dependencies */ import { blockNames } from './pages/editor-page'; -import { isAndroid } from './helpers/utils'; import testData from './helpers/test-data'; describe( 'Gutenberg Editor tests', () => { it( 'should be able to create a post with heading and paragraph blocks', async () => { await editorPage.addNewBlock( blockNames.heading ); - let headingBlockElement = await editorPage.getBlockAtPosition( - blockNames.heading, - 1, - { - useWaitForVisible: true, - } + let headingBlockElement = await editorPage.getTextBlockAtPosition( + blockNames.heading ); - if ( isAndroid() ) { - await headingBlockElement.click(); - } - await editorPage.sendTextToHeadingBlock( + + await editorPage.typeTextToTextBlock( headingBlockElement, - testData.heading, - false + testData.heading ); await editorPage.addNewBlock( blockNames.paragraph ); @@ -29,7 +21,7 @@ describe( 'Gutenberg Editor tests', () => { blockNames.paragraph, 2 ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.mediumText ); @@ -39,7 +31,7 @@ describe( 'Gutenberg Editor tests', () => { blockNames.paragraph, 3 ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.mediumText ); @@ -49,7 +41,7 @@ describe( 'Gutenberg Editor tests', () => { blockNames.heading, 4 ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( headingBlockElement, testData.heading ); @@ -59,9 +51,12 @@ describe( 'Gutenberg Editor tests', () => { blockNames.paragraph, 5 ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.mediumText ); + + // Assert that even though there are 5 blocks, there should only be 3 paragraph blocks + expect( await editorPage.getNumberOfParagraphBlocks() ).toEqual( 3 ); } ); } ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js index 80939bd9e5801..970fd66967528 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-@canary.test.js @@ -8,27 +8,26 @@ import testData from './helpers/test-data'; describe( 'Gutenberg Editor tests for List block', () => { it( 'should be able to add a new List block', async () => { await editorPage.addNewBlock( blockNames.list ); - const listBlockElement = await editorPage.getBlockAtPosition( - blockNames.list - ); - // Click List block on Android to force EditText focus - if ( isAndroid() ) { - await listBlockElement.click(); - } + let listBlockElement = await editorPage.getListBlockAtPosition( 1, { + isEmptyBlock: true, + } ); - // Send the first list item text. - await editorPage.sendTextToListBlock( + await editorPage.typeTextToTextBlock( listBlockElement, - testData.listItem1 + testData.listItem1, + false ); + listBlockElement = await editorPage.getListBlockAtPosition(); + // Send an Enter. - await editorPage.sendTextToListBlock( listBlockElement, '\n' ); + await editorPage.typeTextToTextBlock( listBlockElement, '\n', false ); // Send the second list item text. - await editorPage.sendTextToListBlock( + await editorPage.typeTextToTextBlock( listBlockElement, - testData.listItem2 + testData.listItem2, + false ); // Switch to html and verify html. @@ -38,12 +37,11 @@ describe( 'Gutenberg Editor tests for List block', () => { // This test depends on being run immediately after 'should be able to add a new List block' it( 'should update format to ordered list, using toolbar button', async () => { - let listBlockElement = await editorPage.getBlockAtPosition( - blockNames.list - ); + let listBlockElement = await editorPage.getListBlockAtPosition(); - // Click List block to force EditText focus. - await listBlockElement.click(); + if ( isAndroid() ) { + await listBlockElement.click(); + } // Send a click on the order list format button. await editorPage.clickOrderedListToolBarButton(); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js index d113be5371a38..e242f2f6f99e0 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists-end.test.js @@ -2,32 +2,21 @@ * Internal dependencies */ import { blockNames } from './pages/editor-page'; -import { isAndroid } from './helpers/utils'; import testData from './helpers/test-data'; describe( 'Gutenberg Editor tests for List block (end)', () => { it( 'should be able to end a List block', async () => { await editorPage.addNewBlock( blockNames.list ); - const listBlockElement = await editorPage.getBlockAtPosition( - blockNames.list - ); - - // Click List block on Android to force EditText focus - if ( isAndroid() ) { - await listBlockElement.click(); - } + const listBlockElement = await editorPage.getListBlockAtPosition(); - // Send the first list item text. - await editorPage.sendTextToListBlock( + await editorPage.typeTextToTextBlock( listBlockElement, - testData.listItem1 + testData.listItem1, + false ); // Send an Enter. - await editorPage.sendTextToListBlock( listBlockElement, '\n' ); - - // Send an Enter. - await editorPage.sendTextToListBlock( listBlockElement, '\n' ); + await editorPage.typeTextToTextBlock( listBlockElement, '\n\n', false ); const html = await editorPage.getHtmlContent(); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js index 12537bfcae535..6b2cb1c04509f 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-lists.test.js @@ -2,28 +2,33 @@ * Internal dependencies */ import { blockNames } from './pages/editor-page'; -import { backspace, isAndroid } from './helpers/utils'; +import { waitIfAndroid, backspace } from './helpers/utils'; describe( 'Gutenberg Editor tests for List block', () => { // Prevent regression of https://github.com/wordpress-mobile/gutenberg-mobile/issues/871 it( 'should handle spaces in a list', async () => { await editorPage.addNewBlock( blockNames.list ); - let listBlockElement = await editorPage.getBlockAtPosition( - blockNames.list - ); - // Click List block on Android to force EditText focus - if ( isAndroid() ) { - await listBlockElement.click(); - } + let listBlockElement = await editorPage.getListBlockAtPosition(); // Send the list item text. - await editorPage.sendTextToListBlock( listBlockElement, ' a' ); + await editorPage.typeTextToTextBlock( listBlockElement, ' a', false ); // Send an Enter. - await editorPage.sendTextToListBlock( listBlockElement, '\n' ); + await editorPage.typeTextToTextBlock( listBlockElement, '\n', false ); + + // Instead of introducing separate conditions for local and CI environment, add this wait for Android to accomodate both environments + await waitIfAndroid(); // Send a backspace. - await editorPage.sendTextToListBlock( listBlockElement, backspace ); + await editorPage.typeTextToTextBlock( + listBlockElement, + backspace, + false + ); + + // There is a delay in Sauce Labs when a key is sent + // There isn't an element to check as it's being typed into an element that already exists, workaround is to add this wait until there's a better solution + await waitIfAndroid(); // Switch to html and verify html. const html = await editorPage.getHtmlContent(); @@ -35,9 +40,7 @@ describe( 'Gutenberg Editor tests for List block', () => { ); // Remove list block to reset editor to clean state. - listBlockElement = await editorPage.getBlockAtPosition( - blockNames.list - ); + listBlockElement = await editorPage.getListBlockAtPosition(); await listBlockElement.click(); await editorPage.removeBlockAtPosition( blockNames.list ); } ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js index 0ac9bae0743ef..20d7d690b96bf 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-paragraph.test.js @@ -16,12 +16,12 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.shortText ); await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n', false @@ -44,19 +44,13 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { let paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.shortText ); await clickMiddleOfElement( editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToParagraphBlock( - paragraphBlockElement, - '\n' - ); + await editorPage.typeTextToTextBlock( paragraphBlockElement, '\n' ); const text0 = await editorPage.getTextForParagraphBlockAtPosition( 1 ); const text1 = await editorPage.getTextForParagraphBlockAtPosition( 2 ); @@ -71,7 +65,7 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { paragraphBlockElement ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, backspace ); @@ -112,7 +106,7 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { editorPage.driver, paragraphBlockElement ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, backspace ); @@ -140,7 +134,7 @@ describe( 'Gutenberg Editor tests for Paragraph Block', () => { blockNames.paragraph, 2 ); - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, backspace ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js index 16c291584b8ed..7404b0969d6ad 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-paste.test.js @@ -29,11 +29,8 @@ describe( 'Gutenberg Editor paste tests', () => { const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.pastePlainText ); @@ -59,9 +56,6 @@ describe( 'Gutenberg Editor paste tests', () => { blockNames.paragraph, 2 ); - if ( isAndroid() ) { - await paragraphBlockElement2.click(); - } // Paste into second paragraph block. await longPressMiddleOfElement( @@ -83,9 +77,6 @@ describe( 'Gutenberg Editor paste tests', () => { const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } // Copy content to clipboard. await longPressMiddleOfElement( @@ -108,9 +99,6 @@ describe( 'Gutenberg Editor paste tests', () => { blockNames.paragraph, 2 ); - if ( isAndroid() ) { - await paragraphBlockElement2.click(); - } // Paste into second paragraph block. await longPressMiddleOfElement( diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js index 9056854fa42c7..215b81bf98b4d 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-rotation.test.js @@ -11,11 +11,8 @@ describe( 'Gutenberg Editor tests', () => { let paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.mediumText ); @@ -23,27 +20,21 @@ describe( 'Gutenberg Editor tests', () => { await toggleOrientation( editorPage.driver ); // On Android the keyboard hides the add block button, let's hide it after rotation if ( isAndroid() ) { - await editorPage.driver.hideDeviceKeyboard(); + await editorPage.dismissKeyboard(); } await editorPage.addNewBlock( blockNames.paragraph ); if ( isAndroid() ) { - await editorPage.driver.hideDeviceKeyboard(); + await editorPage.dismissKeyboard(); } paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph, 2 ); - while ( ! paragraphBlockElement ) { - await editorPage.driver.hideDeviceKeyboard(); - paragraphBlockElement = await editorPage.getBlockAtPosition( - blockNames.paragraph, - 2 - ); - } - await editorPage.typeTextToParagraphBlock( + + await editorPage.typeTextToTextBlock( paragraphBlockElement, testData.mediumText ); diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-slash-inserter-@canary.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-slash-inserter-@canary.test.js index 563445768709a..d16e37bc7090f 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-slash-inserter-@canary.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-slash-inserter-@canary.test.js @@ -5,105 +5,68 @@ import { blockNames } from './pages/editor-page'; import { isAndroid } from './helpers/utils'; import { slashInserter, shortText } from './helpers/test-data'; -const ANIMATION_TIME = 200; - -// Helper function for asserting slash inserter presence. -async function assertSlashInserterPresent( checkIsVisible ) { - let areResultsDisplayed; - try { - const foundElements = await editorPage.driver.elementsByAccessibilityId( - 'Slash inserter results' - ); - areResultsDisplayed = !! foundElements.length; - } catch ( e ) { - areResultsDisplayed = false; - } - if ( checkIsVisible ) { - expect( areResultsDisplayed ).toBeTruthy(); - } else { - expect( areResultsDisplayed ).toBeFalsy(); - } -} - -// Due to flakiness, disabling until its more stable -// https://github.com/wordpress-mobile/gutenberg-mobile/issues/3699 -// eslint-disable-next-line jest/no-disabled-tests -describe.skip( 'Gutenberg Editor Slash Inserter tests', () => { +describe( 'Gutenberg Editor Slash Inserter tests', () => { it( 'should show the menu after typing /', async () => { await editorPage.addNewBlock( blockNames.paragraph ); - const paragraphBlockElement = await editorPage.getBlockAtPosition( + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, slashInserter ); - await editorPage.driver.sleep( ANIMATION_TIME ); - - assertSlashInserterPresent( true ); + expect( await editorPage.assertSlashInserterPresent() ).toBe( true ); await editorPage.removeBlockAtPosition( blockNames.paragraph ); } ); it( 'should hide the menu after deleting the / character', async () => { await editorPage.addNewBlock( blockNames.paragraph ); - const paragraphBlockElement = await editorPage.getBlockAtPosition( + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, slashInserter ); - await editorPage.driver.sleep( ANIMATION_TIME ); - assertSlashInserterPresent( true ); + expect( await editorPage.assertSlashInserterPresent() ).toBe( true ); // Remove / character. if ( isAndroid() ) { - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, `${ shortText }`, true ); } else { - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, `\b ${ shortText }`, false ); } - await editorPage.driver.sleep( ANIMATION_TIME ); // Check if the slash inserter UI no longer exists. - assertSlashInserterPresent( false ); + expect( await editorPage.assertSlashInserterPresent() ).toBe( false ); await editorPage.removeBlockAtPosition( blockNames.paragraph ); } ); it( 'should add an Image block after tying /image and tapping on the Image block button', async () => { await editorPage.addNewBlock( blockNames.paragraph ); - const paragraphBlockElement = await editorPage.getBlockAtPosition( + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, `${ slashInserter }image` ); - await editorPage.driver.sleep( ANIMATION_TIME ); - assertSlashInserterPresent( true ); + expect( await editorPage.assertSlashInserterPresent() ).toBe( true ); // Find Image block button. const imageButtonElement = await editorPage.driver.elementByAccessibilityId( @@ -120,30 +83,27 @@ describe.skip( 'Gutenberg Editor Slash Inserter tests', () => { ).toBe( true ); // Slash inserter UI should not be present after adding a block. - assertSlashInserterPresent( false ); + expect( await editorPage.assertSlashInserterPresent() ).toBe( false ); // Remove image block. await editorPage.removeBlockAtPosition( blockNames.image ); } ); - it( 'should insert an image block with "/img" + enter', async () => { + it( 'should insert an embed image block with "/img" + enter', async () => { await editorPage.addNewBlock( blockNames.paragraph ); - const paragraphBlockElement = await editorPage.getBlockAtPosition( + const paragraphBlockElement = await editorPage.getTextBlockAtPosition( blockNames.paragraph ); - if ( isAndroid() ) { - await paragraphBlockElement.click(); - } - await editorPage.typeTextToParagraphBlock( + await editorPage.typeTextToTextBlock( paragraphBlockElement, '/img\n', false ); expect( - await editorPage.hasBlockAtPosition( 1, blockNames.image ) + await editorPage.hasBlockAtPosition( 1, blockNames.embed ) ).toBe( true ); - await editorPage.removeBlockAtPosition( blockNames.image ); + await editorPage.removeBlockAtPosition( blockNames.embed ); } ); } ); diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 03f3002c59b14..1658d553c9c7a 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -464,48 +464,102 @@ const waitForMediaLibrary = async ( driver ) => { await waitForVisible( driver, locator ); }; -const waitForVisible = async ( driver, elementLocator, iteration = 0 ) => { - const maxIteration = 25; +/** + * @param {string} driver + * @param {string} elementLocator + * @param {number} maxIteration - Default value is 25 + * @param {number} iteration - Default value is 0 + * @return {string} - Returns the first element found, empty string if not found + */ +const waitForVisible = async ( + driver, + elementLocator, + maxIteration = 25, + iteration = 0 +) => { const timeout = 1000; if ( iteration >= maxIteration ) { - throw new Error( + // if element not found, print error and return empty string + // eslint-disable-next-line no-console + console.error( `"${ elementLocator }" is still not visible after ${ iteration } retries!` ); + return ''; } else if ( iteration !== 0 ) { // wait before trying to locate element again await driver.sleep( timeout ); } - const locator = await driver.elementsByXPath( elementLocator ); - if ( locator.length !== 1 ) { + const element = await driver.elementsByXPath( elementLocator ); + if ( element.length !== 1 ) { // if locator is not visible, try again - return waitForVisible( driver, elementLocator, iteration + 1 ); + return waitForVisible( + driver, + elementLocator, + maxIteration, + iteration + 1 + ); + } + + return element[ 0 ]; +}; + +/** + * @param {string} driver + * @param {string} elementLocator + * @param {number} maxIteration - Default value is 25, can be adjusted to be less to wait for element to not be visible + * @return {boolean} - Returns true if element is found, false otherwise + */ +const isElementVisible = async ( + driver, + elementLocator, + maxIteration = 25 +) => { + const element = await waitForVisible( + driver, + elementLocator, + maxIteration + ); + + // if there is no element, return false + if ( ! element ) { + return false; + } + + return true; +}; + +// Only for Android +const waitIfAndroid = async () => { + if ( isAndroid() ) { + await editorPage.driver.sleep( 1000 ); } - return locator[ 0 ]; }; module.exports = { backspace, - timer, - setupDriver, - isLocalEnvironment, - isAndroid, - typeString, - clickMiddleOfElement, clickBeginningOfElement, + clickMiddleOfElement, + doubleTap, + isAndroid, + isEditorVisible, + isElementVisible, + isLocalEnvironment, longPressMiddleOfElement, - tapSelectAllAboveElement, - tapCopyAboveElement, - tapPasteAboveElement, + setupDriver, + stopDriver, swipeDown, - swipeUp, swipeFromTo, - stopDriver, + swipeUp, + tapCopyAboveElement, + tapPasteAboveElement, + tapSelectAllAboveElement, + timer, toggleHtmlMode, toggleOrientation, - doubleTap, - isEditorVisible, + typeString, waitForMediaLibrary, waitForVisible, + waitIfAndroid, }; 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 f408d8baf0fc8..440e256566ef9 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -2,17 +2,18 @@ * Internal dependencies */ const { + doubleTap, + isAndroid, + isEditorVisible, + isElementVisible, + longPressMiddleOfElement, setupDriver, stopDriver, - isAndroid, - swipeUp, swipeDown, - typeString, - toggleHtmlMode, swipeFromTo, - longPressMiddleOfElement, - doubleTap, - isEditorVisible, + swipeUp, + toggleHtmlMode, + typeString, waitForVisible, } = require( '../helpers/utils' ); @@ -45,11 +46,15 @@ class EditorPage { return await this.driver.hasElementByAccessibilityId( 'block-list' ); } - // For text blocks, e.g. Paragraph, Heading + // =============================== + // Text blocks functions + // E.g. Paragraph, Heading blocks + // =============================== async getTextBlockAtPosition( blockName, position = 1 ) { - // iOS needs a click before + // iOS needs a click to get the text element if ( ! isAndroid() ) { const textBlockLocator = `(//XCUIElementTypeButton[contains(@name, "${ blockName } Block. Row ${ position }")])`; + const textBlock = await waitForVisible( this.driver, textBlockLocator @@ -64,6 +69,10 @@ class EditorPage { return await waitForVisible( this.driver, blockLocator ); } + async typeTextToTextBlock( block, text, clear ) { + await typeString( this.driver, block, text, clear ); + } + // Finds the wd element for new block that was added and sets the element attribute // and accessibilityId attributes on this object and selects the block // position uses one based numbering. @@ -506,10 +515,6 @@ class EditorPage { // Paragraph Block functions // ========================= - async typeTextToParagraphBlock( block, text, clear ) { - await typeString( this.driver, block, text, clear ); - } - async sendTextToParagraphBlock( position, text, clear ) { const paragraphs = text.split( '\n' ); for ( let i = 0; i < paragraphs.length; i++ ) { @@ -522,13 +527,9 @@ class EditorPage { await block.click(); } - await this.typeTextToParagraphBlock( - block, - paragraphs[ i ], - clear - ); + await this.typeTextToTextBlock( block, paragraphs[ i ], clear ); if ( i !== paragraphs.length - 1 ) { - await this.typeTextToParagraphBlock( block, '\n', false ); + await this.typeTextToTextBlock( block, '\n', false ); } } } @@ -542,35 +543,68 @@ class EditorPage { return await blockLocator.text(); } + async getNumberOfParagraphBlocks() { + const paragraphBlockLocator = isAndroid() + ? `//android.view.ViewGroup[contains(@content-desc, "Paragraph Block. Row")]//android.widget.EditText` + : `(//XCUIElementTypeButton[contains(@name, "Paragraph Block. Row")])`; + + const locator = await this.driver.elementsByXPath( + paragraphBlockLocator + ); + return locator.length; + } + + async assertSlashInserterPresent() { + const slashInserterLocator = isAndroid() + ? '//android.widget.HorizontalScrollView[@content-desc="Slash inserter results"]/android.view.ViewGroup' + : '(//XCUIElementTypeOther[@name="Slash inserter results"])[1]'; + + return await isElementVisible( this.driver, slashInserterLocator, 5 ); + } + // ========================= // List Block functions // ========================= - async getTextViewForListBlock( block ) { - let textViewElementName = 'XCUIElementTypeTextView'; - if ( isAndroid() ) { - textViewElementName = 'android.widget.EditText'; - } + async getListBlockAtPosition( + position = 1, + options = { isEmptyBlock: false } + ) { + // iOS needs a few extra steps to get the text element + if ( ! isAndroid() ) { + // Wait for and click the list in the correct position + let listBlock = await waitForVisible( + this.driver, + `(//XCUIElementTypeOther[contains(@name, "List Block. Row ${ position }")])[1]` + ); + await listBlock.click(); - const accessibilityId = await block.getAttribute( - this.accessibilityIdKey - ); - const blockLocator = `//*[@${ - this.accessibilityIdXPathAttrib - }=${ JSON.stringify( accessibilityId ) }]//${ textViewElementName }`; - return await this.driver.elementByXPath( blockLocator ); - } + const listBlockLocator = options.isEmptyBlock + ? `(//XCUIElementTypeStaticText[contains(@name, "List")])` + : `//XCUIElementTypeButton[contains(@name, "List")]`; - async sendTextToListBlock( block, text ) { - const textViewElement = await this.getTextViewForListBlock( block ); + // Wait for and click the list to get the text element + listBlock = await waitForVisible( this.driver, listBlockLocator ); + await listBlock.click(); + } - // Cannot clear list blocks because it messes up the list bullet. - const clear = false; + const listBlockTextLocatorIOS = options.isEmptyBlock + ? `(//XCUIElementTypeStaticText[contains(@name, "List")])` + : `//XCUIElementTypeButton[contains(@name, "List")]//XCUIElementTypeTextView`; - return await typeString( this.driver, textViewElement, text, clear ); + const listBlockTextLocator = isAndroid() + ? `//android.view.ViewGroup[contains(@content-desc, "List Block. Row ${ position }")]//android.widget.EditText` + : listBlockTextLocatorIOS; + + return await waitForVisible( this.driver, listBlockTextLocator ); } async clickOrderedListToolBarButton() { + const toolBarLocator = isAndroid() + ? `//android.widget.Button[@content-desc="${ this.orderedListButtonName }"]` + : `//XCUIElementTypeButton[@name="${ this.orderedListButtonName }"]`; + + await waitForVisible( this.driver, toolBarLocator ); await this.clickToolBarButton( this.orderedListButtonName ); } @@ -609,34 +643,6 @@ class EditorPage { await typeString( this.driver, imageBlockCaptionField, caption, clear ); } - // ========================= - // Heading Block functions - // ========================= - - // Inner element changes on iOS if Heading Block is empty - async getTextViewForHeadingBlock( block, empty ) { - let textViewElementName = empty - ? 'XCUIElementTypeStaticText' - : 'XCUIElementTypeTextView'; - if ( isAndroid() ) { - textViewElementName = 'android.widget.EditText'; - } - - const accessibilityId = await block.getAttribute( - this.accessibilityIdKey - ); - const blockLocator = `//*[@${ this.accessibilityIdXPathAttrib }="${ accessibilityId }"]//${ textViewElementName }`; - return await this.driver.elementByXPath( blockLocator ); - } - - async sendTextToHeadingBlock( block, text, clear = true ) { - const textViewElement = await this.getTextViewForHeadingBlock( - block, - true - ); - return await typeString( this.driver, textViewElement, text, clear ); - } - async closePicker() { if ( isAndroid() ) { // Wait for media block picker to load before closing @@ -760,34 +766,25 @@ class EditorPage { async sauceJobStatus( allPassed ) { await this.driver.sauceJobStatus( allPassed ); } - - async getNumberOfParagraphBlocks() { - const paragraphBlockLocator = isAndroid() - ? `//android.view.ViewGroup[contains(@content-desc, "Paragraph Block. Row")]//android.widget.EditText` - : `(//XCUIElementTypeButton[contains(@name, "Paragraph Block. Row")])`; - const locator = await this.driver.elementsByXPath( - paragraphBlockLocator - ); - return locator.length; - } } const blockNames = { - paragraph: 'Paragraph', - gallery: 'Gallery', + audio: 'Audio', columns: 'Columns', cover: 'Cover', + embed: 'Embed', + file: 'File', + gallery: 'Gallery', heading: 'Heading', image: 'Image', latestPosts: 'Latest Posts', list: 'List', more: 'More', + paragraph: 'Paragraph', + search: 'Search', separator: 'Separator', spacer: 'Spacer', verse: 'Verse', - file: 'File', - audio: 'Audio', - search: 'Search', }; module.exports = { initializeEditorPage, blockNames }; From 1e2f974385043cd2ce41c42c1f9638f7b24a43a5 Mon Sep 17 00:00:00 2001 From: Mustaque Ahmed Date: Mon, 16 May 2022 14:31:14 +0530 Subject: [PATCH 11/88] fix typo in the test description didPostSaveRequestSucceed (#41064) --- packages/editor/src/store/test/selectors.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index ef0ba07e720c6..a79d7dbb3fb23 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -2149,7 +2149,7 @@ describe( 'selectors', () => { expect( didPostSaveRequestSucceed( state ) ).toBe( true ); } ); - it( 'should return true if the post save request has failed', () => { + it( 'should return false if the post save request has failed', () => { const state = { saving: { successful: false, @@ -2171,7 +2171,7 @@ describe( 'selectors', () => { expect( didPostSaveRequestFail( state ) ).toBe( true ); } ); - it( 'should return true if the post save request is successful', () => { + it( 'should return false if the post save request is successful', () => { const state = { saving: { error: false, From 3a2a5559b60540ae2098b633f9b54d7676a61ba5 Mon Sep 17 00:00:00 2001 From: James Koster Date: Mon, 16 May 2022 10:43:43 +0100 Subject: [PATCH 12/88] Reduce active tab accent weight (#40998) Reduces the weight of the accent on active tabs from 4px to 1.5px. --- packages/base-styles/_variables.scss | 2 +- packages/components/src/tab-panel/style.scss | 2 +- .../edit-post/src/components/sidebar/settings-header/style.scss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss index d1d6cb848876e..cc922ba5a035a 100644 --- a/packages/base-styles/_variables.scss +++ b/packages/base-styles/_variables.scss @@ -84,7 +84,7 @@ $widget-area-width: 700px; $block-toolbar-height: $grid-unit-60; $border-width: 1px; $border-width-focus: 2px; // This exists as a fallback, and is ideally overridden by var(--wp-admin-border-width-focus) unless in some SASS math cases. -$border-width-tab: 4px; +$border-width-tab: 1.5px; $helptext-font-size: 12px; $radius-round: 50%; $radius-block-ui: 2px; diff --git a/packages/components/src/tab-panel/style.scss b/packages/components/src/tab-panel/style.scss index 40950b09ffdd6..6d902445f88ef 100644 --- a/packages/components/src/tab-panel/style.scss +++ b/packages/components/src/tab-panel/style.scss @@ -59,6 +59,6 @@ } &.is-active:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 -$border-width-tab 0 0 var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 - #{$border-width-tab * 2} 0 0 var(--wp-admin-theme-color); } } diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss index ca340185e8357..469920ece7b78 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/style.scss +++ b/packages/edit-post/src/components/sidebar/settings-header/style.scss @@ -48,6 +48,6 @@ } &.is-active:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 -$border-width-tab 0 0 var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 - #{$border-width-tab * 2} 0 0 var(--wp-admin-theme-color); } } From bb9ec8bcd5a11c057147cfca3bfa4b085ffb73c9 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu Date: Mon, 16 May 2022 14:46:23 +0300 Subject: [PATCH 13/88] prevent navigation on url input suggestion selection via enter key (#40906) --- packages/block-editor/src/components/url-input/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/block-editor/src/components/url-input/index.js b/packages/block-editor/src/components/url-input/index.js index 51132f07179fd..bd6aa16740740 100644 --- a/packages/block-editor/src/components/url-input/index.js +++ b/packages/block-editor/src/components/url-input/index.js @@ -303,6 +303,7 @@ class URLInput extends Component { // Submitting while loading should trigger onSubmit. case ENTER: { + event.preventDefault(); if ( this.props.onSubmit ) { this.props.onSubmit( null, event ); } @@ -350,6 +351,7 @@ class URLInput extends Component { break; } case ENTER: { + event.preventDefault(); if ( this.state.selectedSuggestion !== null ) { this.selectLink( suggestion ); From e120d07cb51016638a5844bee79efbdf08ed2aac Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Mon, 16 May 2022 14:21:12 +0200 Subject: [PATCH 14/88] Remove mid-paragraph line breaks from 2-building-a-list-of-pages.md markdown (#41025) --- .../data-basics/2-building-a-list-of-pages.md | 84 ++++++------------- 1 file changed, 25 insertions(+), 59 deletions(-) diff --git a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md index 4832e46005a04..c830e9d3b21bd 100644 --- a/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md +++ b/docs/how-to-guides/data-basics/2-building-a-list-of-pages.md @@ -1,7 +1,6 @@ # Building a list of pages -In this part, we will build a filterable list of all WordPress pages. This is what the app will look like at the end of -this section: +In this part, we will build a filterable list of all WordPress pages. This is what the app will look like at the end of this section: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/part1-finished.jpg) @@ -30,42 +29,31 @@ function PagesList( { pages } ) { } ``` -Note that this component does not fetch any data yet, only presents the hardcoded list of pages. When you refresh the page, -you should see the following: +Note that this component does not fetch any data yet, only presents the hardcoded list of pages. When you refresh the page, you should see the following: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/simple-list.jpg) ## Step 2: Fetch the data -The hard-coded sample page isn’t very useful. We want to display your actual WordPress pages so let’s fetch the actual -list of pages from the [WordPress REST API](https://developer.wordpress.org/rest-api/). +The hard-coded sample page isn’t very useful. We want to display your actual WordPress pages so let’s fetch the actual list of pages from the [WordPress REST API](https://developer.wordpress.org/rest-api/). -Before we start, let’s confirm we actually have some pages to fetch. Within WPAdmin, Navigate to Pages using the sidebar menu and -ensure it shows at least four or five Pages: +Before we start, let’s confirm we actually have some pages to fetch. Within WPAdmin, Navigate to Pages using the sidebar menu and ensure it shows at least four or five Pages: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/pages-list.jpg) -If it doesn’t, go ahead and create a few pages – you can use the same titles as on the screenshot above. Be sure to _ -publish_ and not just _save_ them. +If it doesn’t, go ahead and create a few pages – you can use the same titles as on the screenshot above. Be sure to _publish_ and not just _save_ them. -Now that we have the data to work with, let’s dive into the code. We will take advantage of the [`@wordpress/core-data` package](https://github.com/WordPress/gutenberg/tree/trunk/packages/core-data) -package which provides resolvers, selectors, and actions to work with the WordPress core API. `@wordpress/core-data` builds on top -of the [`@wordpress/data` package](https://github.com/WordPress/gutenberg/tree/trunk/packages/data). +Now that we have the data to work with, let’s dive into the code. We will take advantage of the [`@wordpress/core-data` package](https://github.com/WordPress/gutenberg/tree/trunk/packages/core-data) package which provides resolvers, selectors, and actions to work with the WordPress core API. `@wordpress/core-data` builds on top of the [`@wordpress/data` package](https://github.com/WordPress/gutenberg/tree/trunk/packages/data). -To fetch the list of pages, we will use -the [`getEntityRecords`](/docs/reference-guides/data/data-core/#getentityrecords) selector. In broad strokes, it will -issue the correct API request, cache the results, and return the list of the records we need. Here’s how to use it: +To fetch the list of pages, we will use the [`getEntityRecords`](/docs/reference-guides/data/data-core/#getentityrecords) selector. In broad strokes, it will issue the correct API request, cache the results, and return the list of the records we need. Here’s how to use it: ```js wp.data.select( 'core' ).getEntityRecords( 'postType', 'page' ) ``` -If you run that following snippet in your browser’s dev tools, you will see it returns `null`. Why? The pages are only -requested by the `getEntityRecords` resolver after first running the _selector_. If you wait a moment and re-run it, it -will return the list of all pages. +If you run that following snippet in your browser’s dev tools, you will see it returns `null`. Why? The pages are only requested by the `getEntityRecords` resolver after first running the _selector_. If you wait a moment and re-run it, it will return the list of all pages. -Similarly, the `MyFirstApp` component needs to re-run the selector once the data is available. That’s exactly what -the `useSelect` hook does: +Similarly, the `MyFirstApp` component needs to re-run the selector once the data is available. That’s exactly what the `useSelect` hook does: ```js import { useSelect } from '@wordpress/data'; @@ -83,9 +71,7 @@ function MyFirstApp() { Note that we use an `import` statement inside index.js. This enables the plugin to automatically load the dependencies using `wp_enqueue_script`. Any references to `coreDataStore` are compiled to the same `wp.data` reference we use in browser's devtools. -`useSelect` takes two arguments: a callback and dependencies. In broad strokes, it re-runs the callback whenever either -the dependencies or the underlying data store changes. You can learn more about [useSelect](/packages/data/README.md#useselect) in -the [data module documentation](/packages/data/README.md#useselect). +`useSelect` takes two arguments: a callback and dependencies. In broad strokes, it re-runs the callback whenever either the dependencies or the underlying data store changes. You can learn more about [useSelect](/packages/data/README.md#useselect) in the [data module documentation](/packages/data/README.md#useselect). Putting it together, we get the following code: @@ -149,8 +135,7 @@ function PagesList( { pages } ) { ## Step 4: Add a search box -The list of pages is short for now; however, the longer it grows, the harder it is to work with. WordPress admins -typically solves this problem with a search box – let’s implement one too! +The list of pages is short for now; however, the longer it grows, the harder it is to work with. WordPress admins typically solves this problem with a search box – let’s implement one too! Let’s start by adding a search field: @@ -173,29 +158,21 @@ function MyFirstApp() { } ``` -Note that instead of using an `input` tag, we took advantage of -the [SearchControl](https://developer.wordpress.org/block-editor/reference-guides/components/search-control/) component. -This is what it looks like: +Note that instead of using an `input` tag, we took advantage of the [SearchControl](https://developer.wordpress.org/block-editor/reference-guides/components/search-control/) component. This is what it looks like: ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/filter-field.jpg) -The field starts empty, and the contents are stored in the `searchTerm` state value. If you aren’t familiar with -the [useState](https://reactjs.org/docs/hooks-state.html) hook, you can learn more -in [React’s documentation](https://reactjs.org/docs/hooks-state.html). +The field starts empty, and the contents are stored in the `searchTerm` state value. If you aren’t familiar with the [useState](https://reactjs.org/docs/hooks-state.html) hook, you can learn more in [React’s documentation](https://reactjs.org/docs/hooks-state.html). We can now request only the pages matching the `searchTerm`. -After checking with the [WordPress API documentation](https://developer.wordpress.org/rest-api/reference/pages/), we -see that the [/wp/v2/pages](https://developer.wordpress.org/rest-api/reference/pages/) endpoint accepts a `search` -query parameter and uses it to _limit results to those matching a string_. But how can we use it? We can pass custom query -parameters as the third argument to `getEntityRecords` as below: +After checking with the [WordPress API documentation](https://developer.wordpress.org/rest-api/reference/pages/), we see that the [/wp/v2/pages](https://developer.wordpress.org/rest-api/reference/pages/) endpoint accepts a `search` query parameter and uses it to _limit results to those matching a string_. But how can we use it? We can pass custom query parameters as the third argument to `getEntityRecords` as below: ```js wp.data.select( 'core' ).getEntityRecords( 'postType', 'page', { search: 'home' } ) ``` -Running that snippet in your browser’s dev tools will trigger a request to `/wp/v2/pages?search=home` instead of -just `/wp/v2/pages`. +Running that snippet in your browser’s dev tools will trigger a request to `/wp/v2/pages?search=home` instead of just `/wp/v2/pages`. Let’s mirror this in our `useSelect` call as follows: @@ -219,8 +196,7 @@ function MyFirstApp() { } ``` -The `searchTerm` is now used as a `search` query parameter when provided. Note that `searchTerm` is also specified -inside the list of `useSelect` dependencies to make sure `getEntityRecords` is re-run when the `searchTerm` changes. +The `searchTerm` is now used as a `search` query parameter when provided. Note that `searchTerm` is also specified inside the list of `useSelect` dependencies to make sure `getEntityRecords` is re-run when the `searchTerm` changes. Finally, here’s how `MyFirstApp` looks once we wire it all together: @@ -276,21 +252,15 @@ function MyFirstApp() { Working outside of core-data, we would need to solve two problems here. -Firstly, out-of-order updates. Searching for „About” would trigger five API requests filtering for `A`, `Ab`, `Abo`, `Abou`, and -`About`. Theese requests could finish in a different order than they started. It is possible that _search=A_ would resolve after _ -search=About_ and thus we’d display the wrong data. +Firstly, out-of-order updates. Searching for „About” would trigger five API requests filtering for `A`, `Ab`, `Abo`, `Abou`, and `About`. Theese requests could finish in a different order than they started. It is possible that _search=A_ would resolve after _ search=About_ and thus we’d display the wrong data. -Gutenberg data helps by handling the asynchronous part behind the scenes. `useSelect` remembers the most recent call and -returns only the data we expect. +Gutenberg data helps by handling the asynchronous part behind the scenes. `useSelect` remembers the most recent call and returns only the data we expect. -Secondly, every keystroke would trigger an API request. If you typed `About`, deleted it, and retyped it, it would -issue 10 requests in total even though we could reuse the data. +Secondly, every keystroke would trigger an API request. If you typed `About`, deleted it, and retyped it, it would issue 10 requests in total even though we could reuse the data. -Gutenberg data helps by caching the responses to API requests triggered by `getEntityRecords()` and reuses them on -subsequent calls. This is especially important when other components rely on the same entity records. +Gutenberg data helps by caching the responses to API requests triggered by `getEntityRecords()` and reuses them on subsequent calls. This is especially important when other components rely on the same entity records. -All in all, the utilities built into core-data are designed to solve the typical problems so that you can focus on your application -instead. +All in all, the utilities built into core-data are designed to solve the typical problems so that you can focus on your application instead. ## Step 5: Loading Indicator @@ -298,8 +268,7 @@ There is one problem with our search feature. We can’t be quite sure whether i ![](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/how-to-guides/data-basics/media/list-of-pages/unclear-status.jpg) -A few messages like _Loading…_ or _No results_ would clear it up. Let’s implement them! First, `PagesList` has to be -aware of the current status: +A few messages like _Loading…_ or _No results_ would clear it up. Let’s implement them! First, `PagesList` has to be aware of the current status: ```js import { SearchControl, Spinner } from '@wordpress/components'; @@ -325,16 +294,13 @@ function MyFirstApp() { } ``` -Note that instead of building a custom loading indicator, we took advantage of -the [Spinner](https://developer.wordpress.org/block-editor/reference-guides/components/spinner/) component. +Note that instead of building a custom loading indicator, we took advantage of the [Spinner](https://developer.wordpress.org/block-editor/reference-guides/components/spinner/) component. -We still need to know whether the pages selector `hasResolved` or not. We can find out using -the `hasFinishedResolution` selector: +We still need to know whether the pages selector `hasResolved` or not. We can find out using the `hasFinishedResolution` selector: `wp.data.select('core').hasFinishedResolution( 'getEntityRecords', [ 'postType', 'page', { search: 'home' } ] )` -It takes the name of the selector and the _exact same arguments you passed to that selector_ and returns either `true` if the data was already loaded or `false` -if we’re still waiting. Let’s add it to `useSelect`: +It takes the name of the selector and the _exact same arguments you passed to that selector_ and returns either `true` if the data was already loaded or `false` if we’re still waiting. Let’s add it to `useSelect`: ```js import { useSelect } from '@wordpress/data'; From 3a9e168495bc5dc811d5b893b8166a90df4a9626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Mon, 16 May 2022 14:50:28 +0200 Subject: [PATCH 15/88] Packages: Add more npm release types to the GitHub workflow (#41046) * Packages: Add more npm release types in GitHub workflow * Remove code duplication * Add some final tweaks to the workflow --- .github/workflows/publish-npm-packages.yml | 37 +++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index 71f72bdb08e5c..b0644c7368e63 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -3,9 +3,17 @@ name: Publish npm packages on: workflow_dispatch: inputs: - wp_version: - description: 'WordPress major version' + release_type: + description: 'Release type' required: true + type: choice + default: 'development' + options: + - development + - bugfix + - wp + wp_version: + description: 'WordPress major version (`wp` only)' type: string # Cancels all previous workflow runs for pull requests that have not completed. @@ -16,8 +24,8 @@ concurrency: cancel-in-progress: true jobs: - wp-release: - name: WordPress major bugfix release + release: + name: Release - ${{ github.event.inputs.release_type }} runs-on: ubuntu-latest environment: WordPress packages steps: @@ -47,7 +55,26 @@ jobs: node-version-file: 'main/.nvmrc' registry-url: 'https://registry.npmjs.org' - - name: Publish packages to npm ("wp/${{ github.event.inputs.wp_version }}" dist-tag) + - name: Publish development packages to npm ("next" dist-tag) + if: ${{ github.event.inputs.release_type == 'development' }} + run: | + cd main + npm ci + ./bin/plugin/cli.js npm-next --ci --repository-path ../publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish packages to npm with bug fixes ("latest" dist-tag) + if: ${{ github.event.inputs.release_type == 'bugfix' }} + run: | + cd main + npm ci + ./bin/plugin/cli.js npm-bugfix --ci --repository-path ../publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish packages to npm for WP major ("wp/${{ github.event.inputs.wp_version || 'X.Y' }}" dist-tag) + if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} run: | cd main npm ci From 4d2e8b4bd05dc29e3d52b64794c96309c5f76f01 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 16 May 2022 19:42:25 +0000 Subject: [PATCH 16/88] Bump plugin version to 13.2.1 --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index c711855f7cc6d..33b46896b4961 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the new block editor in core. * Requires at least: 5.8 * Requires PHP: 5.6 - * Version: 13.2.0 + * Version: 13.2.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 7d6071eadea2f..61a7c8a683198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "13.2.0", + "version": "13.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ae15e4b44c499..d33e933c1682c 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "13.2.0", + "version": "13.2.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From c1375a2d4756d276064d68b33d4085206c0cab54 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Mon, 16 May 2022 19:54:52 +0000 Subject: [PATCH 17/88] Update Changelog for 13.2.1 --- changelog.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.txt b/changelog.txt index 694c5439300bf..4aca1b3b6db67 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,14 @@ == Changelog == += 13.2.1 = + +## Changelog + +### Bug Fixes + +- Fix the position of the block inserter in between blocks ([40919](https://github.com/WordPress/gutenberg/pull/40919)) + + = 13.2.0 = ## Changelog From c088c448b52674b121e18b3eff3fb250547cdbbd Mon Sep 17 00:00:00 2001 From: Brent Nef Date: Mon, 16 May 2022 14:51:45 -0700 Subject: [PATCH 18/88] env :: support ssh protocol for github repos. (#40451) * env :: support ssh protocol for github repos. * Parse git ssh url + add tests. * Update documentation, expand test. * Update the changelog. Co-authored-by: Noah Allen Co-authored-by: brent Co-authored-by: Noah Allen --- packages/env/CHANGELOG.md | 3 ++ packages/env/README.md | 13 +++--- packages/env/lib/config/parse-config.js | 27 +++++++++++ packages/env/test/parse-config.js | 61 +++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 packages/env/test/parse-config.js diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 129dff7df6813..d8d61acb91830 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +### Enhancement +- Added SSH protocol support for git sources + ## 4.2.0 (2022-01-27) ### Enhancement diff --git a/packages/env/README.md b/packages/env/README.md index d3f4a0ae5bc0a..7bb62f5b263a8 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -460,12 +460,13 @@ _Note: the port number environment variables (`WP_ENV_PORT` and `WP_ENV_TESTS_PO Several types of strings can be passed into the `core`, `plugins`, `themes`, and `mappings` fields. -| Type | Format | Example(s) | -| ----------------- | ----------------------------- | -------------------------------------------------------- | -| Relative path | `.\|~` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` | -| Absolute path | `/\|:\` | `"/a/directory"`, `"C:\\a\\directory"` | -| GitHub repository | `/[#]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#trunk"` | -| ZIP File | `http[s]:///.zip` | `"https://wordpress.org/wordpress-5.4-beta2.zip"` | +| Type | Format | Example(s) | +| ----------------- | -------------------------------------------- | -------------------------------------------------------- | +| Relative path | `.\|~` | `"./a/directory"`, `"../a/directory"`, `"~/a/directory"` | +| Absolute path | `/\|:\` | `"/a/directory"`, `"C:\\a\\directory"` | +| GitHub repository | `/[#]` | `"WordPress/WordPress"`, `"WordPress/gutenberg#trunk"` | +| SSH repository | `ssh://user@host//.git[#]` | `"ssh://git@github.com/WordPress/WordPress.git"` | +| ZIP File | `http[s]:///.zip` | `"https://wordpress.org/wordpress-5.4-beta2.zip"` | Remote sources will be downloaded into a temporary directory located in `~/.wp-env`. diff --git a/packages/env/lib/config/parse-config.js b/packages/env/lib/config/parse-config.js index e278ca1ba57dc..90f3f04134900 100644 --- a/packages/env/lib/config/parse-config.js +++ b/packages/env/lib/config/parse-config.js @@ -114,6 +114,33 @@ function parseSourceString( sourceString, { workDirectoryPath } ) { }; } + // SSH URLs (git) + const supportedProtocols = [ 'ssh:', 'git+ssh:' ]; + try { + const sshUrl = new URL( sourceString ); + if ( supportedProtocols.includes( sshUrl.protocol ) ) { + const pathElements = sshUrl.pathname + .split( '/' ) + .filter( ( e ) => !! e ); + const basename = pathElements + .slice( -1 )[ 0 ] + .replace( /\.git/, '' ); + const workingPath = path.resolve( + workDirectoryPath, + ...pathElements.slice( 0, -1 ), + basename + ); + return { + type: 'git', + url: sshUrl.href.split( '#' )[ 0 ], + ref: sshUrl.hash.slice( 1 ) || 'master', + path: workingPath, + clonePath: workingPath, + basename, + }; + } + } catch ( err ) {} + const gitHubFields = sourceString.match( /^([^\/]+)\/([^#\/]+)(\/([^#]+))?(?:#(.+))?$/ ); diff --git a/packages/env/test/parse-config.js b/packages/env/test/parse-config.js new file mode 100644 index 0000000000000..123f1cd4b38fe --- /dev/null +++ b/packages/env/test/parse-config.js @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +const _path = require( 'path' ); + +/** + * Internal dependencies + */ +const { parseSourceString } = require( '../lib/config/parse-config' ); + +const parseSourceStringOptions = { workDirectoryPath: '.' }; +const currentDirectory = _path.resolve( '.' ); + +describe( 'parseSourceString', () => { + it( 'returns null for null source', () => { + const wpSource = parseSourceString( null, {} ); + expect( wpSource ).toBeNull(); + } ); +} ); + +const gitTests = [ + { + sourceString: 'ssh://git@github.com/short.git', + url: 'ssh://git@github.com/short.git', + ref: 'master', + path: currentDirectory + '/short', + clonePath: currentDirectory + '/short', + basename: 'short', + }, + { + sourceString: 'ssh://git@github.com/owner/long/path/repo.git', + url: 'ssh://git@github.com/owner/long/path/repo.git', + ref: 'master', + path: currentDirectory + '/owner/long/path/repo', + clonePath: currentDirectory + '/owner/long/path/repo', + basename: 'repo', + }, + { + sourceString: 'git+ssh://git@github.com/owner/repo.git#kitchen-sink', + url: 'git+ssh://git@github.com/owner/repo.git', + ref: 'kitchen-sink', + path: currentDirectory + '/owner/repo', + clonePath: currentDirectory + '/owner/repo', + basename: 'repo', + }, +]; + +describe.each( gitTests )( 'parseSourceString', ( source ) => { + it( `parses ${ source.sourceString }`, () => { + const { type, url, ref, path, clonePath, basename } = parseSourceString( + source.sourceString, + parseSourceStringOptions + ); + expect( type ).toBe( 'git' ); + expect( url ).toBe( source.url ); + expect( ref ).toBe( source.ref ); + expect( path ).toBe( source.path ); + expect( clonePath ).toBe( source.clonePath ); + expect( basename ).toBe( source.basename ); + } ); +} ); From 82ecd50d27ebdb79e64ce703466dd18f83325cae Mon Sep 17 00:00:00 2001 From: Glen Davies Date: Tue, 17 May 2022 10:30:41 +1200 Subject: [PATCH 19/88] Gallery block: Fix bug with initial image size defaulting to full size (#41079) Co-authored-by: Glen Davies --- packages/block-library/src/gallery/edit.js | 3 ++- packages/block-library/src/gallery/shared.js | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 2d8f07536b4f9..9f434c6044f85 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -204,10 +204,11 @@ function GalleryEdit( props ) { : undefined; } return { - ...pickRelevantMediaFiles( imageAttributes, sizeSlug ), + ...pickRelevantMediaFiles( image, sizeSlug ), ...getHrefAndDestination( image, linkTo ), ...getUpdatedLinkTargetSettings( linkTarget, attributes ), className: newClassName, + caption: imageAttributes.caption, sizeSlug, }; } diff --git a/packages/block-library/src/gallery/shared.js b/packages/block-library/src/gallery/shared.js index 6ebd8c8b37f3e..34cc104acbe6c 100644 --- a/packages/block-library/src/gallery/shared.js +++ b/packages/block-library/src/gallery/shared.js @@ -8,11 +8,12 @@ export function defaultColumnsNumber( imageCount ) { } export const pickRelevantMediaFiles = ( image, sizeSlug = 'large' ) => { - const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] ); + const imageProps = pick( image, [ 'alt', 'id', 'link' ] ); imageProps.url = get( image, [ 'sizes', sizeSlug, 'url' ] ) || get( image, [ 'media_details', 'sizes', sizeSlug, 'source_url' ] ) || - image.url; + image.url || + image.source_url; const fullUrl = get( image, [ 'sizes', 'full', 'url' ] ) || get( image, [ 'media_details', 'sizes', 'full', 'source_url' ] ); From c541147520cebffb055536362a408733f4e1566f Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Tue, 17 May 2022 10:09:50 +1000 Subject: [PATCH 20/88] Use TypeScript and controls for `DateTimePicker`'s stories (#40986) * Use TypeScript and controls for DateTimePicker, DatePicker and TimePicker's stories * DRY up daysFromNow and isWeekend * Show control for events param * Make DateTimePicker stories more interactive * Pass onChange up to storybook --- packages/components/src/date-time/date.tsx | 21 ++++- packages/components/src/date-time/index.tsx | 3 +- .../components/src/date-time/stories/date.js | 17 ---- .../components/src/date-time/stories/date.tsx | 73 +++++++++++++++ .../components/src/date-time/stories/index.js | 91 ------------------- .../src/date-time/stories/index.tsx | 75 +++++++++++++++ .../components/src/date-time/stories/time.js | 32 ------- .../components/src/date-time/stories/time.tsx | 51 +++++++++++ .../components/src/date-time/stories/utils.ts | 9 ++ packages/components/src/date-time/time.tsx | 20 ++++ packages/components/src/date-time/types.ts | 20 ++-- packages/components/src/date-time/utils.ts | 13 +-- 12 files changed, 261 insertions(+), 164 deletions(-) delete mode 100644 packages/components/src/date-time/stories/date.js create mode 100644 packages/components/src/date-time/stories/date.tsx delete mode 100644 packages/components/src/date-time/stories/index.js create mode 100644 packages/components/src/date-time/stories/index.tsx delete mode 100644 packages/components/src/date-time/stories/time.js create mode 100644 packages/components/src/date-time/stories/time.tsx create mode 100644 packages/components/src/date-time/stories/utils.ts diff --git a/packages/components/src/date-time/date.tsx b/packages/components/src/date-time/date.tsx index fffba036a3a8c..9399d6afbab4a 100644 --- a/packages/components/src/date-time/date.tsx +++ b/packages/components/src/date-time/date.tsx @@ -80,7 +80,26 @@ function DatePickerDay( { day, events = [] }: DatePickerDayProps ) { ); } -function DatePicker( { +/** + * DatePicker is a React component that renders a calendar for date selection. + * + * ```jsx + * import { DatePicker } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const MyDatePicker = () => { + * const [ date, setDate ] = useState( new Date() ); + * + * return ( + * setDate( newDate ) } + * /> + * ); + * }; + * ``` + */ +export function DatePicker( { currentDate, onChange, events, diff --git a/packages/components/src/date-time/index.tsx b/packages/components/src/date-time/index.tsx index 9801d4c9407fa..85d9bdaf218e4 100644 --- a/packages/components/src/date-time/index.tsx +++ b/packages/components/src/date-time/index.tsx @@ -174,7 +174,6 @@ function UnforwardedDateTimePicker( * date and time selection. The calendar and clock components can be accessed * individually using the `DatePicker` and `TimePicker` components respectively. * - * @example * ```jsx * import { DateTimePicker } from '@wordpress/components'; * import { useState } from '@wordpress/element'; @@ -186,7 +185,7 @@ function UnforwardedDateTimePicker( * setDate( newDate ) } - * is12Hour={ true } + * is12Hour * /> * ); * }; diff --git a/packages/components/src/date-time/stories/date.js b/packages/components/src/date-time/stories/date.js deleted file mode 100644 index 6859eec76bd71..0000000000000 --- a/packages/components/src/date-time/stories/date.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Internal dependencies - */ -import DatePicker from '../date'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -export default { title: 'Components/DatePicker', component: DatePicker }; - -export const _default = () => { - const [ date, setDate ] = useState(); - - return ; -}; diff --git a/packages/components/src/date-time/stories/date.tsx b/packages/components/src/date-time/stories/date.tsx new file mode 100644 index 0000000000000..b469635fe3e3d --- /dev/null +++ b/packages/components/src/date-time/stories/date.tsx @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DatePicker from '../date'; +import { daysFromNow, isWeekend } from './utils'; + +const meta: ComponentMeta< typeof DatePicker > = { + title: 'Components/DatePicker', + component: DatePicker, + argTypes: { + currentDate: { control: 'date' }, + onChange: { action: 'onChange', control: { type: null } }, + }, + parameters: { + controls: { expanded: true }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const Template: ComponentStory< typeof DatePicker > = ( { + currentDate, + onChange, + ...args +} ) => { + const [ date, setDate ] = useState( currentDate ); + useEffect( () => { + setDate( currentDate ); + }, [ currentDate ] ); + return ( + { + setDate( newDate ); + onChange?.( newDate ); + } } + /> + ); +}; + +export const Default: ComponentStory< typeof DatePicker > = Template.bind( {} ); + +export const WithEvents: ComponentStory< typeof DatePicker > = Template.bind( + {} +); +WithEvents.args = { + currentDate: new Date(), + events: [ + { date: daysFromNow( 2 ) }, + { date: daysFromNow( 4 ) }, + { date: daysFromNow( 6 ) }, + { date: daysFromNow( 8 ) }, + ], +}; + +export const WithInvalidDates: ComponentStory< + typeof DatePicker +> = Template.bind( {} ); +WithInvalidDates.args = { + currentDate: new Date(), + isInvalidDate: isWeekend, +}; diff --git a/packages/components/src/date-time/stories/index.js b/packages/components/src/date-time/stories/index.js deleted file mode 100644 index 9c3aa2607764a..0000000000000 --- a/packages/components/src/date-time/stories/index.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * External dependencies - */ -import { boolean, button } from '@storybook/addon-knobs'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import DateTimePicker from '../'; - -export default { - title: 'Components/DateTimePicker', - component: DateTimePicker, - parameters: { - knobs: { disable: false }, - }, -}; - -const DateTimePickerWithState = ( { is12Hour } ) => { - const [ date, setDate ] = useState(); - - return ( - - ); -}; - -export const _default = () => { - const is12Hour = boolean( 'Is 12 hour (shows AM/PM)', false ); - return ; -}; - -// Date utils, for demo purposes. -const DAY_IN_MS = 24 * 60 * 60 * 1000; -const aFewDaysAfter = ( date ) => { - // eslint-disable-next-line no-restricted-syntax - return new Date( date.getTime() + ( 1 + Math.random() * 5 ) * DAY_IN_MS ); -}; - -const now = new Date(); - -export const WithDaysHighlighted = () => { - const [ date, setDate ] = useState( now ); - - const [ highlights, setHighlights ] = useState( [ - { date: aFewDaysAfter( now ) }, - ] ); - - button( 'Add random highlight', () => { - const lastHighlight = highlights[ highlights.length - 1 ]; - setHighlights( [ - ...highlights, - { date: aFewDaysAfter( lastHighlight.date ) }, - ] ); - } ); - - return ( - - ); -}; - -/** - * You can mark particular dates as invalid using the `isInvalidDate` prop. This - * prevents the user from being able to select it. - */ -export const WithInvalidDates = () => { - const [ currentDate, setCurrentDate ] = useState( now ); - - return ( - - // Mark Saturdays and Sundays as invalid. - date.getDay() === 0 || date.getDay() === 6 - } - /> - ); -}; diff --git a/packages/components/src/date-time/stories/index.tsx b/packages/components/src/date-time/stories/index.tsx new file mode 100644 index 0000000000000..fb126edb81f4e --- /dev/null +++ b/packages/components/src/date-time/stories/index.tsx @@ -0,0 +1,75 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import DateTimePicker from '..'; +import { daysFromNow, isWeekend } from './utils'; + +const meta: ComponentMeta< typeof DateTimePicker > = { + title: 'Components/DateTimePicker', + component: DateTimePicker, + argTypes: { + currentDate: { control: 'date' }, + onChange: { action: 'onChange', control: { type: null } }, + }, + parameters: { + controls: { expanded: true }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const Template: ComponentStory< typeof DateTimePicker > = ( { + currentDate, + onChange, + ...args +} ) => { + const [ date, setDate ] = useState( currentDate ); + useEffect( () => { + setDate( currentDate ); + }, [ currentDate ] ); + return ( + { + setDate( newDate ); + onChange?.( newDate ); + } } + /> + ); +}; + +export const Default: ComponentStory< typeof DateTimePicker > = Template.bind( + {} +); + +export const WithEvents: ComponentStory< + typeof DateTimePicker +> = Template.bind( {} ); +WithEvents.args = { + currentDate: new Date(), + events: [ + { date: daysFromNow( 2 ) }, + { date: daysFromNow( 4 ) }, + { date: daysFromNow( 6 ) }, + { date: daysFromNow( 8 ) }, + ], +}; + +export const WithInvalidDates: ComponentStory< + typeof DateTimePicker +> = Template.bind( {} ); +WithInvalidDates.args = { + currentDate: new Date(), + isInvalidDate: isWeekend, +}; diff --git a/packages/components/src/date-time/stories/time.js b/packages/components/src/date-time/stories/time.js deleted file mode 100644 index 9a184c1940ee9..0000000000000 --- a/packages/components/src/date-time/stories/time.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Internal dependencies - */ -import TimePicker from '../time'; - -/** - * External dependencies - */ -import { date, boolean } from '@storybook/addon-knobs'; -import { noop } from 'lodash'; - -export default { - title: 'Components/TimePicker', - component: TimePicker, - parameters: { - knobs: { disable: false }, - }, -}; - -export const _default = () => { - return ( - - ); -}; diff --git a/packages/components/src/date-time/stories/time.tsx b/packages/components/src/date-time/stories/time.tsx new file mode 100644 index 0000000000000..9fc72086075f7 --- /dev/null +++ b/packages/components/src/date-time/stories/time.tsx @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import TimePicker from '../time'; + +const meta: ComponentMeta< typeof TimePicker > = { + title: 'Components/TimePicker', + component: TimePicker, + argTypes: { + currentTime: { control: 'date' }, + onChange: { action: 'onChange', control: { type: null } }, + }, + parameters: { + controls: { expanded: true }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const Template: ComponentStory< typeof TimePicker > = ( { + currentTime, + onChange, + ...args +} ) => { + const [ time, setTime ] = useState( currentTime ); + useEffect( () => { + setTime( currentTime ); + }, [ currentTime ] ); + return ( + { + setTime( newTime ); + onChange?.( newTime ); + } } + /> + ); +}; + +export const Default: ComponentStory< typeof TimePicker > = Template.bind( {} ); diff --git a/packages/components/src/date-time/stories/utils.ts b/packages/components/src/date-time/stories/utils.ts new file mode 100644 index 0000000000000..ccdac56c38135 --- /dev/null +++ b/packages/components/src/date-time/stories/utils.ts @@ -0,0 +1,9 @@ +export function daysFromNow( days: number ) { + const date = new Date(); + date.setDate( date.getDate() + days ); + return date; +} + +export function isWeekend( date: Date ) { + return date.getDay() === 0 || date.getDay() === 6; +} diff --git a/packages/components/src/date-time/time.tsx b/packages/components/src/date-time/time.tsx index 10b673b7c83d8..9d0c1cfa4c394 100644 --- a/packages/components/src/date-time/time.tsx +++ b/packages/components/src/date-time/time.tsx @@ -80,6 +80,26 @@ function UpdateOnBlurAsIntegerField( { } ); } +/** + * TimePicker is a React component that renders a clock for time selection. + * + * ```jsx + * import { TimePicker } from '@wordpress/components'; + * import { useState } from '@wordpress/element'; + * + * const MyTimePicker = () => { + * const [ time, setTime ] = useState( new Date() ); + * + * return ( + * setTime( newTime ) } + * is12Hour + * /> + * ); + * }; + * ``` + */ export function TimePicker( { is12Hour, currentTime, diff --git a/packages/components/src/date-time/types.ts b/packages/components/src/date-time/types.ts index 7ceddca3e70d2..1293721f3e27d 100644 --- a/packages/components/src/date-time/types.ts +++ b/packages/components/src/date-time/types.ts @@ -21,15 +21,11 @@ export type UpdateOnBlurAsIntegerFieldProps = { children?: ReactNode; }; -export type DateTimeString = string; - -export type ValidDateTimeInput = Date | string | number | null; - export type TimePickerProps = { /** * The initial current time the time picker should render. */ - currentTime?: ValidDateTimeInput; + currentTime?: Date | string | number | null; /** * Whether we use a 12-hour clock. With a 12-hour clock, an AM/PM widget is @@ -42,7 +38,7 @@ export type TimePickerProps = { * The function called when a new time has been selected. It is passed the * time as an argument. */ - onChange?: ( time: DateTimeString ) => void; + onChange?: ( time: string ) => void; }; export type DatePickerEvent = { @@ -71,13 +67,13 @@ export type DatePickerProps = { * The current date and time at initialization. Optionally pass in a `null` * value to specify no date is currently selected. */ - currentDate?: ValidDateTimeInput; + currentDate?: Date | string | number | null; /** * The function called when a new date has been selected. It is passed the * date as an argument. */ - onChange?: ( date: DateTimeString ) => void; + onChange?: ( date: string ) => void; /** * A callback function which receives a Date object representing a day as an @@ -91,7 +87,7 @@ export type DatePickerProps = { * picker. The callback receives the new month date in the ISO format as an * argument. */ - onMonthPreviewed?: ( date: DateTimeString ) => void; + onMonthPreviewed?: ( date: string ) => void; /** * List of events to show in the date picker. Each event will appear as a @@ -100,11 +96,11 @@ export type DatePickerProps = { events?: DatePickerEvent[]; }; -export type DateTimePickerProps = DatePickerProps & - TimePickerProps & { +export type DateTimePickerProps = Omit< DatePickerProps, 'onChange' > & + Omit< TimePickerProps, 'currentTime' | 'onChange' > & { /** * The function called when a new date or time has been selected. It is * passed the date and time as an argument. */ - onChange?: ( date: DateTimeString | null ) => void; + onChange?: ( date: string | null ) => void; }; diff --git a/packages/components/src/date-time/utils.ts b/packages/components/src/date-time/utils.ts index 6c4f4fd217cb7..25bb63a84d012 100644 --- a/packages/components/src/date-time/utils.ts +++ b/packages/components/src/date-time/utils.ts @@ -3,21 +3,16 @@ */ import moment from 'moment'; -/** - * Internal dependencies - */ -import type { ValidDateTimeInput } from './types'; - /** * Create a Moment object from a date string. With no date supplied, default to * a Moment object representing now. If a null value is passed, return a null * value. * - * @param {ValidDateTimeInput} [date] Date representing the currently selected - * date or null to signify no selection. - * @return {?moment.Moment} Moment object for selected date or null. + * @param [date] Date representing the currently selected + * date or null to signify no selection. + * @return Moment object for selected date or null. */ -export const getMomentDate = ( date?: ValidDateTimeInput ) => { +export const getMomentDate = ( date?: Date | string | number | null ) => { if ( null === date ) { return null; } From c5ac0311632e6d5ac432f1add9a5ba3402a1347b Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 17 May 2022 11:11:28 +0900 Subject: [PATCH 21/88] Fix: env unit test fails on Windows (#41070) * Fix: env config unit test fails on Windows * remove blank lines * update readme * polish codes --- packages/env/lib/config/test/config.js | 111 ++++++++++++++++--------- packages/scripts/CHANGELOG.md | 4 + 2 files changed, 75 insertions(+), 40 deletions(-) diff --git a/packages/env/lib/config/test/config.js b/packages/env/lib/config/test/config.js index 5a79cb6d4f724..1d5060f11ed79 100644 --- a/packages/env/lib/config/test/config.js +++ b/packages/env/lib/config/test/config.js @@ -4,6 +4,7 @@ */ const { readFile, stat } = require( 'fs' ).promises; const os = require( 'os' ); +const { join, resolve } = require( 'path' ); /** * Internal dependencies @@ -101,13 +102,17 @@ describe( 'readConfig', () => { process.env.WP_ENV_HOME = 'here/is/a/path'; const configWith = await readConfig( '.wp-env.json' ); expect( - configWith.workDirectoryPath.includes( 'here/is/a/path' ) + configWith.workDirectoryPath.includes( + join( 'here', 'is', 'a', 'path' ) + ) ).toBe( true ); process.env.WP_ENV_HOME = undefined; const configWithout = await readConfig( '.wp-env.json' ); expect( - configWithout.workDirectoryPath.includes( 'here/is/a/path' ) + configWithout.workDirectoryPath.includes( + join( 'here', 'is', 'a', 'path' ) + ) ).toBe( false ); process.env.WP_ENV_HOME = oldEnvHome; @@ -126,13 +131,17 @@ describe( 'readConfig', () => { process.env.WP_ENV_HOME = 'here/is/a/path'; const configWith = await readConfig( '.wp-env.json' ); expect( - configWith.workDirectoryPath.includes( 'here/is/a/path' ) + configWith.workDirectoryPath.includes( + join( 'here', 'is', 'a', 'path' ) + ) ).toBe( true ); process.env.WP_ENV_HOME = undefined; const configWithout = await readConfig( '.wp-env.json' ); expect( - configWithout.workDirectoryPath.includes( 'here/is/a/path' ) + configWithout.workDirectoryPath.includes( + join( 'here', 'is', 'a', 'path' ) + ) ).toBe( false ); process.env.WP_ENV_HOME = oldEnvHome; @@ -242,26 +251,31 @@ describe( 'readConfig', () => { readFile.mockImplementation( () => Promise.resolve( JSON.stringify( { - plugins: [ './relative', '../parent', '~/home' ], + plugins: [ + './relative', + '../parent', + `${ os.homedir() }/home`, + ], } ) ) ); const config = await readConfig( '.wp-env.json' ); + expect( config.env.development ).toMatchObject( { pluginSources: [ { type: 'local', - path: expect.stringMatching( /^\/.*relative$/ ), + path: expect.stringMatching( /^(\/||\\).*relative$/ ), basename: 'relative', }, { type: 'local', - path: expect.stringMatching( /^\/.*parent$/ ), + path: expect.stringMatching( /^(\/||\\).*parent$/ ), basename: 'parent', }, { type: 'local', - path: expect.stringMatching( /^\/.*home$/ ), + path: expect.stringMatching( /^(\/||\\).*home$/ ), basename: 'home', }, ], @@ -270,17 +284,17 @@ describe( 'readConfig', () => { pluginSources: [ { type: 'local', - path: expect.stringMatching( /^\/.*relative$/ ), + path: expect.stringMatching( /^(\/||\\).*relative$/ ), basename: 'relative', }, { type: 'local', - path: expect.stringMatching( /^\/.*parent$/ ), + path: expect.stringMatching( /^(\/||\\).*parent$/ ), basename: 'parent', }, { type: 'local', - path: expect.stringMatching( /^\/.*home$/ ), + path: expect.stringMatching( /^(\/||\\).*home$/ ), basename: 'home', }, ], @@ -310,28 +324,28 @@ describe( 'readConfig', () => { expect( config.env.development.pluginSources ).toEqual( [ { type: 'local', - path: expect.stringMatching( /^\/.*test1a$/ ), + path: expect.stringMatching( /^(\/||\\).*test1a$/ ), basename: 'test1a', }, ] ); expect( config.env.development.themeSources ).toEqual( [ { type: 'local', - path: expect.stringMatching( /^\/.*test2a$/ ), + path: expect.stringMatching( /^(\/||\\).*test2a$/ ), basename: 'test2a', }, ] ); expect( config.env.tests.pluginSources ).toEqual( [ { type: 'local', - path: expect.stringMatching( /^\/.*test1b$/ ), + path: expect.stringMatching( /^(\/||\\).*test1b$/ ), basename: 'test1b', }, ] ); expect( config.env.tests.themeSources ).toEqual( [ { type: 'local', - path: expect.stringMatching( /^\/.*test2b$/ ), + path: expect.stringMatching( /^(\/||\\).*test2b$/ ), basename: 'test2b', }, ] ); @@ -345,15 +359,19 @@ describe( 'readConfig', () => { expect( config.env.development ).toMatchObject( { coreSource: { type: 'local', - path: expect.stringMatching( /^\/.*relative$/ ), - testsPath: expect.stringMatching( /^\/.*tests-relative$/ ), + path: expect.stringMatching( /^(\/||\\).*relative$/ ), + testsPath: expect.stringMatching( + /^(\/||\\).*tests-relative$/ + ), }, } ); expect( config.env.tests ).toMatchObject( { coreSource: { type: 'local', - path: expect.stringMatching( /^\/.*relative$/ ), - testsPath: expect.stringMatching( /^\/.*tests-relative$/ ), + path: expect.stringMatching( /^(\/||\\).*relative$/ ), + testsPath: expect.stringMatching( + /^(\/||\\).*tests-relative$/ + ), }, } ); } ); @@ -378,21 +396,21 @@ describe( 'readConfig', () => { type: 'git', url: 'https://github.com/WordPress/gutenberg.git', ref: 'master', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, { type: 'git', url: 'https://github.com/WordPress/gutenberg.git', ref: 'trunk', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, { type: 'git', url: 'https://github.com/WordPress/gutenberg.git', ref: '5.0', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, { @@ -401,7 +419,7 @@ describe( 'readConfig', () => { 'https://github.com/WordPress/theme-experiments.git', ref: 'tt1-blocks@0.4.3', path: expect.stringMatching( - /^\/.*theme-experiments\/tt1-blocks$/ + /^(\/||\\).*theme-experiments(\/||\\)tt1-blocks$/ ), basename: 'tt1-blocks', }, @@ -431,28 +449,32 @@ describe( 'readConfig', () => { type: 'zip', url: 'https://downloads.wordpress.org/plugin/gutenberg.zip', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, { type: 'zip', url: 'https://downloads.wordpress.org/plugin/gutenberg.8.1.0.zip', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, { type: 'zip', url: 'https://downloads.wordpress.org/theme/twentytwenty.zip', - path: expect.stringMatching( /^\/.*twentytwenty$/ ), + path: expect.stringMatching( + /^(\/||\\).*twentytwenty$/ + ), basename: 'twentytwenty', }, { type: 'zip', url: 'https://downloads.wordpress.org/theme/twentytwenty.1.3.zip', - path: expect.stringMatching( /^\/.*twentytwenty$/ ), + path: expect.stringMatching( + /^(\/||\\).*twentytwenty$/ + ), basename: 'twentytwenty', }, ], @@ -482,34 +504,42 @@ describe( 'readConfig', () => { type: 'zip', url: 'https://www.example.com/test/path/to/gutenberg.zip', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, { type: 'zip', url: 'https://www.example.com/test/path/to/gutenberg.8.1.0.zip', - path: expect.stringMatching( /^\/.*gutenberg.8.1.0$/ ), + path: expect.stringMatching( + /^(\/||\\).*gutenberg.8.1.0$/ + ), basename: 'gutenberg.8.1.0', }, { type: 'zip', url: 'https://www.example.com/test/path/to/twentytwenty.zip', - path: expect.stringMatching( /^\/.*twentytwenty$/ ), + path: expect.stringMatching( + /^(\/||\\).*twentytwenty$/ + ), basename: 'twentytwenty', }, { type: 'zip', url: 'https://www.example.com/test/path/to/twentytwenty.1.3.zip', - path: expect.stringMatching( /^\/.*twentytwenty.1.3$/ ), + path: expect.stringMatching( + /^(\/||\\).*twentytwenty.1.3$/ + ), basename: 'twentytwenty.1.3', }, { type: 'zip', url: 'https://example.com/twentytwenty.1.3.zip', - path: expect.stringMatching( /^\/.*twentytwenty.1.3$/ ), + path: expect.stringMatching( + /^(\/||\\).*twentytwenty.1.3$/ + ), basename: 'twentytwenty.1.3', }, ], @@ -550,12 +580,12 @@ describe( 'readConfig', () => { const matchObj = { test: { type: 'local', - path: expect.stringMatching( /^\/.*relative$/ ), + path: expect.stringMatching( /^(\/||\\).*relative$/ ), basename: 'relative', }, test2: { type: 'git', - path: expect.stringMatching( /^\/.*gutenberg$/ ), + path: expect.stringMatching( /^(\/||\\).*gutenberg$/ ), basename: 'gutenberg', }, }; @@ -653,24 +683,25 @@ describe( 'readConfig', () => { expect( config.env.development.mappings ).toEqual( { test1: { basename: 'test1', - path: '/test1', + // resolve is required to remove drive letters on Windows. + path: resolve( '/test1' ), type: 'local', }, test3: { basename: 'test3', - path: '/test3', + path: resolve( '/test3' ), type: 'local', }, } ); expect( config.env.tests.mappings ).toEqual( { test1: { basename: 'test1', - path: '/test1', + path: resolve( '/test1' ), type: 'local', }, test2: { basename: 'test2', - path: '/test2', + path: resolve( '/test2' ), type: 'local', }, } ); @@ -702,14 +733,14 @@ describe( 'readConfig', () => { expect( config.env.development.mappings ).toEqual( { test: { basename: 'test3', - path: '/test3', + path: resolve( '/test3' ), type: 'local', }, } ); expect( config.env.tests.mappings ).toEqual( { test: { basename: 'test2', - path: '/test2', + path: resolve( '/test2' ), type: 'local', }, } ); diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 45e0edb006469..41440738212cb 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -6,6 +6,10 @@ - Enable by default code formatting for JSON files in the `format` command ([#40994](https://github.com/WordPress/gutenberg/pull/40994)). You can opt-out of this behavior by providing a custom file matcher, example: `wp-scripts format src/**/*.js`. +### Bug Fixes + +- Fix: env unit test fails on Windows ([#41070](https://github.com/WordPress/gutenberg/pull/41070)) + ## 23.0.0 (2022-05-04) ### Breaking Changes From 78375e9912a1b6cc6bd6f387b7ed584ccce0506d Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 17 May 2022 08:13:54 +0400 Subject: [PATCH 22/88] Fix Storybook builds (#41089) --- packages/components/src/tab-panel/style.scss | 2 +- .../edit-post/src/components/sidebar/settings-header/style.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/tab-panel/style.scss b/packages/components/src/tab-panel/style.scss index 6d902445f88ef..74e3aa2a48379 100644 --- a/packages/components/src/tab-panel/style.scss +++ b/packages/components/src/tab-panel/style.scss @@ -59,6 +59,6 @@ } &.is-active:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 - #{$border-width-tab * 2} 0 0 var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 -#{$border-width-tab * 2} 0 0 var(--wp-admin-theme-color); } } diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss index 469920ece7b78..eefbb56ca2417 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/style.scss +++ b/packages/edit-post/src/components/sidebar/settings-header/style.scss @@ -48,6 +48,6 @@ } &.is-active:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 - #{$border-width-tab * 2} 0 0 var(--wp-admin-theme-color); + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 -#{$border-width-tab * 2} 0 0 var(--wp-admin-theme-color); } } From c3e86c4e264077a7dbac1ee220fbc9ee325b0285 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 17 May 2022 12:00:51 +0400 Subject: [PATCH 23/88] FlatTermSelector: Avoid errors when returned terms aren't iterable (#41099) --- .../src/components/post-taxonomies/flat-term-selector.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/post-taxonomies/flat-term-selector.js b/packages/editor/src/components/post-taxonomies/flat-term-selector.js index 73332b7737dec..6fac4dc9de99b 100644 --- a/packages/editor/src/components/post-taxonomies/flat-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/flat-term-selector.js @@ -177,7 +177,7 @@ function FlatTermSelector( { slug } ) { // while core data makes REST API requests. useEffect( () => { if ( hasResolvedTerms ) { - const newValues = terms.map( ( term ) => + const newValues = ( terms ?? [] ).map( ( term ) => unescapeString( term.name ) ); @@ -202,7 +202,10 @@ function FlatTermSelector( { slug } ) { } function onChange( termNames ) { - const availableTerms = [ ...terms, ...( searchResults ?? [] ) ]; + const availableTerms = [ + ...( terms ?? [] ), + ...( searchResults ?? [] ), + ]; const uniqueTerms = uniqBy( termNames, ( term ) => term.toLowerCase() ); const newTermNames = uniqueTerms.filter( ( termName ) => From b02a0d603c38868cab4e0c55f503f28a5538d014 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 17 May 2022 12:10:46 +0400 Subject: [PATCH 24/88] Code Editor: Don't commit 'non-dirty' changes (#41092) --- .../editor/src/components/post-text-editor/index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index b4e380190692a..b771c74605526 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -51,6 +51,7 @@ export default function PostTextEditor() { editPost( { content: newValue } ); setValue( newValue ); setIsDirty( true ); + valueRef.current = newValue; }; /** @@ -66,15 +67,13 @@ export default function PostTextEditor() { } }; - useEffect( () => { - valueRef.current = value; - }, [ value ] ); - // Ensure changes aren't lost when component unmounts. useEffect( () => { return () => { - const blocks = parse( valueRef.current ); - resetEditorBlocks( blocks ); + if ( valueRef.current ) { + const blocks = parse( valueRef.current ); + resetEditorBlocks( blocks ); + } }; }, [] ); From 380377799478d2177e0da3fd7f19ba82fb761cc5 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 17 May 2022 12:02:04 +0300 Subject: [PATCH 25/88] [Block Library - Query Loop]: Move sticky control to separate file (#41101) --- .../query/edit/inspector-controls/index.js | 14 ++--------- .../edit/inspector-controls/sticky-control.js | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 packages/block-library/src/query/edit/inspector-controls/sticky-control.js diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 14b4864efc4fa..10c6eb31a6248 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -27,6 +27,7 @@ import OrderControl from './order-control'; import AuthorControl from './author-control'; import ParentControl from './parent-control'; import TaxonomyControls from './taxonomy-controls'; +import StickyControl from './sticky-control'; import { usePostTypes } from '../../utils'; function useIsPostTypeHierarchical( postType ) { @@ -39,12 +40,6 @@ function useIsPostTypeHierarchical( postType ) { ); } -const stickyOptions = [ - { label: __( 'Include' ), value: '' }, - { label: __( 'Exclude' ), value: 'exclude' }, - { label: __( 'Only' ), value: 'only' }, -]; - export default function QueryInspectorControls( { attributes: { query, displayLayout }, setQuery, @@ -153,14 +148,9 @@ export default function QueryInspectorControls( { /> ) } { showSticky && ( - setQuery( { sticky: value } ) } - help={ __( - 'Blog posts can be "stickied", a feature that places them at the top of the front page of posts, keeping it there until new sticky posts are published.' - ) } /> ) } diff --git a/packages/block-library/src/query/edit/inspector-controls/sticky-control.js b/packages/block-library/src/query/edit/inspector-controls/sticky-control.js new file mode 100644 index 0000000000000..dcb83b4076a34 --- /dev/null +++ b/packages/block-library/src/query/edit/inspector-controls/sticky-control.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { SelectControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +const stickyOptions = [ + { label: __( 'Include' ), value: '' }, + { label: __( 'Exclude' ), value: 'exclude' }, + { label: __( 'Only' ), value: 'only' }, +]; + +export default function StickyControl( { value, onChange } ) { + return ( + + ); +} From b8ee22a1f48a29192f2c7faf56bd787192b81458 Mon Sep 17 00:00:00 2001 From: Carolina Nymark Date: Tue, 17 May 2022 14:17:56 +0200 Subject: [PATCH 26/88] Template editor: Show the inserter if the template part is empty (#41024) * Show the inserter if the template part is empty * Update index.js * Use the button block appender * Revert to the default appender * Unset the visual editor height while in focus mode * Revert CSS change --- packages/edit-site/src/components/block-editor/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 29f22a64a548d..4ec7b95ca6103 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -112,6 +112,7 @@ export default function BlockEditor( { setIsInserterOpen } ) { const { clearSelectedBlock } = useDispatch( blockEditorStore ); const isTemplatePart = templateType === 'wp_template_part'; + const hasBlocks = blocks.length !== 0; const NavMenuSidebarToggle = () => ( @@ -186,7 +187,9 @@ export default function BlockEditor( { setIsInserterOpen } ) { <__unstableBlockSettingsMenuFirstItem> From 72a348b10c4723e170db9275662c8d1f10c85d85 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Tue, 17 May 2022 14:22:38 +0200 Subject: [PATCH 27/88] Send registered patterns in HTML and combine them with REST ones (#40818) * Send registered patterns in HTML and combine them with REST ones * Fix formatting * Use the outside_init_only param when getting patterns in edit-site page * Add patterns to settings only if enabled by filter * Remove filter and rename settings fields instead * Support all settings forms in both WP 5.9 and 6.0 --- lib/compat/wordpress-5.9/edit-site-page.php | 4 + .../wordpress-6.0/block-editor-settings.php | 15 ---- .../src/components/block-editor/index.js | 90 +++++++++++-------- .../provider/use-block-editor-settings.js | 40 ++++++--- 4 files changed, 86 insertions(+), 63 deletions(-) diff --git a/lib/compat/wordpress-5.9/edit-site-page.php b/lib/compat/wordpress-5.9/edit-site-page.php index bd9bb17286a38..3706232b4b838 100644 --- a/lib/compat/wordpress-5.9/edit-site-page.php +++ b/lib/compat/wordpress-5.9/edit-site-page.php @@ -122,6 +122,10 @@ static function( $classes ) { '__unstableHomeTemplate' => gutenberg_resolve_home_template(), ); + // Add additional back-compat patterns registered by `current_screen` et al. + $custom_settings['__experimentalAdditionalBlockPatterns'] = WP_Block_Patterns_Registry::get_instance()->get_all_registered( true ); + $custom_settings['__experimentalAdditionalBlockPatternCategories'] = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered( true ); + /** * Make the WP Screen object aware that this is a block editor page. * Since custom blocks check whether the screen is_block_editor, diff --git a/lib/compat/wordpress-6.0/block-editor-settings.php b/lib/compat/wordpress-6.0/block-editor-settings.php index e76f6a40a6230..9030998aed250 100644 --- a/lib/compat/wordpress-6.0/block-editor-settings.php +++ b/lib/compat/wordpress-6.0/block-editor-settings.php @@ -193,18 +193,3 @@ function gutenberg_get_block_editor_settings( $settings ) { } add_filter( 'block_editor_settings_all', 'gutenberg_get_block_editor_settings', PHP_INT_MAX ); - -/** - * Removes the unwanted block patterns fields from block editor settings. - * - * @param array $settings Existing block editor settings. - * - * @return array New block editor settings. - */ -function gutenberg_remove_block_patterns_settings( $settings ) { - unset( $settings['__experimentalBlockPatterns'] ); - unset( $settings['__experimentalBlockPatternCategories'] ); - return $settings; -} - -add_filter( 'block_editor_settings_all', 'gutenberg_remove_block_patterns_settings', PHP_INT_MAX ); diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index 4ec7b95ca6103..252cf56d2a15f 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -2,12 +2,13 @@ * External dependencies */ import classnames from 'classnames'; +import { omit, unionBy } from 'lodash'; /** * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useCallback, useRef, Fragment } from '@wordpress/element'; +import { useCallback, useMemo, useRef, Fragment } from '@wordpress/element'; import { useEntityBlockEditor, store as coreStore } from '@wordpress/core-data'; import { BlockList, @@ -48,44 +49,17 @@ const LAYOUT = { }; export default function BlockEditor( { setIsInserterOpen } ) { - const { settings } = useSelect( + const { storedSettings, templateType, templateId, page } = useSelect( ( select ) => { - let storedSettings = select( editSiteStore ).getSettings( - setIsInserterOpen - ); - - if ( ! storedSettings.__experimentalBlockPatterns ) { - storedSettings = { - ...storedSettings, - __experimentalBlockPatterns: select( - coreStore - ).getBlockPatterns(), - }; - } - - if ( ! storedSettings.__experimentalBlockPatternCategories ) { - storedSettings = { - ...storedSettings, - __experimentalBlockPatternCategories: select( - coreStore - ).getBlockPatternCategories(), - }; - } - - return { - settings: storedSettings, - }; - }, - [ setIsInserterOpen ] - ); - - const { templateType, templateId, page } = useSelect( - ( select ) => { - const { getEditedPostType, getEditedPostId, getPage } = select( - editSiteStore - ); + const { + getSettings, + getEditedPostType, + getEditedPostId, + getPage, + } = select( editSiteStore ); return { + storedSettings: getSettings( setIsInserterOpen ), templateType: getEditedPostType(), templateId: getEditedPostId(), page: getPage(), @@ -94,6 +68,50 @@ export default function BlockEditor( { setIsInserterOpen } ) { [ setIsInserterOpen ] ); + const settingsBlockPatterns = + storedSettings.__experimentalAdditionalBlockPatterns ?? // WP 6.0 + storedSettings.__experimentalBlockPatterns; // WP 5.9 + const settingsBlockPatternCategories = + storedSettings.__experimentalAdditionalBlockPatternCategories ?? // WP 6.0 + storedSettings.__experimentalBlockPatternCategories; // WP 5.9 + + const { restBlockPatterns, restBlockPatternCategories } = useSelect( + ( select ) => ( { + restBlockPatterns: select( coreStore ).getBlockPatterns(), + restBlockPatternCategories: select( + coreStore + ).getBlockPatternCategories(), + } ), + [] + ); + + const blockPatterns = useMemo( + () => unionBy( settingsBlockPatterns, restBlockPatterns, 'name' ), + [ settingsBlockPatterns, restBlockPatterns ] + ); + + const blockPatternCategories = useMemo( + () => + unionBy( + settingsBlockPatternCategories, + restBlockPatternCategories, + 'name' + ), + [ settingsBlockPatternCategories, restBlockPatternCategories ] + ); + + const settings = useMemo( + () => ( { + ...omit( storedSettings, [ + '__experimentalAdditionalBlockPatterns', + '__experimentalAdditionalBlockPatternCategories', + ] ), + __experimentalBlockPatterns: blockPatterns, + __experimentalBlockPatternCategories: blockPatternCategories, + } ), + [ storedSettings, blockPatterns, blockPatternCategories ] + ); + const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', templateType diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index a3fa78a7b7972..0b52f841ece11 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { pick, defaultTo } from 'lodash'; +import { pick, defaultTo, unionBy } from 'lodash'; /** * WordPress dependencies @@ -61,20 +61,36 @@ function useBlockEditorSettings( settings, hasTemplate ) { }; }, [] ); - const { - __experimentalBlockPatterns: settingsBlockPatterns, - __experimentalBlockPatternCategories: settingsBlockPatternCategories, - } = settings; + const settingsBlockPatterns = + settings.__experimentalAdditionalBlockPatterns ?? // WP 6.0 + settings.__experimentalBlockPatterns; // WP 5.9 + const settingsBlockPatternCategories = + settings.__experimentalAdditionalBlockPatternCategories ?? // WP 6.0 + settings.__experimentalBlockPatternCategories; // WP 5.9 - const { blockPatterns, blockPatternCategories } = useSelect( + const { restBlockPatterns, restBlockPatternCategories } = useSelect( ( select ) => ( { - blockPatterns: - settingsBlockPatterns ?? select( coreStore ).getBlockPatterns(), - blockPatternCategories: - settingsBlockPatternCategories ?? - select( coreStore ).getBlockPatternCategories(), + restBlockPatterns: select( coreStore ).getBlockPatterns(), + restBlockPatternCategories: select( + coreStore + ).getBlockPatternCategories(), } ), - [ settingsBlockPatterns, settingsBlockPatternCategories ] + [] + ); + + const blockPatterns = useMemo( + () => unionBy( settingsBlockPatterns, restBlockPatterns, 'name' ), + [ settingsBlockPatterns, restBlockPatterns ] + ); + + const blockPatternCategories = useMemo( + () => + unionBy( + settingsBlockPatternCategories, + restBlockPatternCategories, + 'name' + ), + [ settingsBlockPatternCategories, restBlockPatternCategories ] ); const { undo } = useDispatch( editorStore ); From da6a7b790adc266ad13e36c64749e62e91716a84 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 17 May 2022 17:26:39 +0400 Subject: [PATCH 28/88] Media Utils: Don't convert error messages into an array (#39448) * Media Utils: Don't convert error messages into an array * Update blocks * Update changelog * Update messages to include file names --- .../block-library/src/cover/edit/index.js | 4 +- .../src/post-featured-image/edit.js | 2 +- packages/block-library/src/site-logo/edit.js | 2 +- packages/media-utils/CHANGELOG.md | 4 ++ .../media-utils/src/utils/upload-media.js | 47 ++++++++++--------- 5 files changed, 33 insertions(+), 26 deletions(-) diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 513e222294dbd..83dd7b7665823 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -130,9 +130,7 @@ function CoverEdit( { const isUploadingMedia = isTemporaryMedia( id, url ); const onUploadError = ( message ) => { - createErrorNotice( Array.isArray( message ) ? message[ 2 ] : message, { - type: 'snackbar', - } ); + createErrorNotice( message, { type: 'snackbar' } ); }; const mediaElement = useRef(); diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 4ee479b42be25..a133b8846a4ee 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -118,7 +118,7 @@ function PostFeaturedImageDisplay( { const { createErrorNotice } = useDispatch( noticesStore ); const onUploadError = ( message ) => { - createErrorNotice( message[ 2 ], { type: 'snackbar' } ); + createErrorNotice( message, { type: 'snackbar' } ); }; const controls = ( diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index 74e721183b0b5..a08e3520af4b3 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -469,7 +469,7 @@ export default function LogoEdit( { const { createErrorNotice } = useDispatch( noticesStore ); const onUploadError = ( message ) => { - createErrorNotice( message[ 2 ], { type: 'snackbar' } ); + createErrorNotice( message, { type: 'snackbar' } ); }; const controls = canUserEdit && logoUrl && ( diff --git a/packages/media-utils/CHANGELOG.md b/packages/media-utils/CHANGELOG.md index 3c99109b55fa3..973e0cd472e96 100644 --- a/packages/media-utils/CHANGELOG.md +++ b/packages/media-utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Change + +- The `onError` now always receives the `message` as a string ([#39448](https://github.com/WordPress/gutenberg/pull/39448)). + ## 3.6.0 (2022-05-04) ## 3.5.0 (2022-04-21) diff --git a/packages/media-utils/src/utils/upload-media.js b/packages/media-utils/src/utils/upload-media.js index e0d484ab76281..1d55327bcb53a 100644 --- a/packages/media-utils/src/utils/upload-media.js +++ b/packages/media-utils/src/utils/upload-media.js @@ -104,17 +104,6 @@ export async function uploadMedia( { return includes( allowedMimeTypesForUser, fileType ); }; - // Build the error message including the filename. - const triggerError = ( error ) => { - error.message = [ - { error.file.name }, - ': ', - error.message, - ]; - - onError( error ); - }; - const validFiles = []; for ( const mediaFile of files ) { @@ -125,10 +114,14 @@ export async function uploadMedia( { mediaFile.type && ! isAllowedMimeTypeForUser( mediaFile.type ) ) { - triggerError( { + onError( { code: 'MIME_TYPE_NOT_ALLOWED_FOR_USER', - message: __( - 'Sorry, you are not allowed to upload this file type.' + message: sprintf( + // translators: %s: file name. + __( + '%s: Sorry, you are not allowed to upload this file type.' + ), + mediaFile.name ), file: mediaFile, } ); @@ -138,9 +131,13 @@ export async function uploadMedia( { // Check if the block supports this mime type. // Defer to the server when type not detected. if ( mediaFile.type && ! isAllowedType( mediaFile.type ) ) { - triggerError( { + onError( { code: 'MIME_TYPE_NOT_SUPPORTED', - message: __( 'Sorry, this file type is not supported here.' ), + message: sprintf( + // translators: %s: file name. + __( '%s: Sorry, this file type is not supported here.' ), + mediaFile.name + ), file: mediaFile, } ); continue; @@ -148,10 +145,14 @@ export async function uploadMedia( { // Verify if file is greater than the maximum file upload size allowed for the site. if ( maxUploadFileSize && mediaFile.size > maxUploadFileSize ) { - triggerError( { + onError( { code: 'SIZE_ABOVE_LIMIT', - message: __( - 'This file exceeds the maximum upload size for this site.' + message: sprintf( + // translators: %s: file name. + __( + '%s: This file exceeds the maximum upload size for this site.' + ), + mediaFile.name ), file: mediaFile, } ); @@ -160,9 +161,13 @@ export async function uploadMedia( { // Don't allow empty files to be uploaded. if ( mediaFile.size <= 0 ) { - triggerError( { + onError( { code: 'EMPTY_FILE', - message: __( 'This file is empty.' ), + message: sprintf( + // translators: %s: file name. + __( '%s: This file is empty.' ), + mediaFile.name + ), file: mediaFile, } ); continue; From 56cd4fdc4fcb1541a1169edfa6105eb3f154781c Mon Sep 17 00:00:00 2001 From: Phill <38789408+SavPhill@users.noreply.github.com> Date: Tue, 17 May 2022 22:17:42 +0700 Subject: [PATCH 29/88] Documentation: Update broken links (#41071) * Updated incorrect links Edited the two incorrect GitHub links as shown within this TRAC ticket https://core.trac.wordpress.org/ticket/55731 * Update Link Title to JSX build As discussed in #41071 comments, we would keep the link title as JSX build --- .../block-tutorial/writing-your-first-block-type.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md index 9ecd7795fd713..48f6726964120 100644 --- a/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md +++ b/docs/how-to-guides/block-tutorial/writing-your-first-block-type.md @@ -152,9 +152,9 @@ When you save the post and view it published, you will see the `Hola mundo (from This shows the most basic static block. The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository has complete examples for both. -- [Basic example with JSX build](https://github.com/WordPress/gutenberg-examples/tree/trunk/01-basic-esnext) +- [Basic Example with JSX build](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-jsx/01-basic-esnext) -- [Basic example plain JavaScript](https://github.com/WordPress/gutenberg-examples/tree/trunk/01-basic), +- [Basic Example Plain JavaScript](https://github.com/WordPress/gutenberg-examples/tree/trunk/blocks-non-jsx/01-basic), **NOTE:** The examples include a more complete block setup with translation features included, it is recommended to follow those examples for a production block. The internationalization features were left out of this guide for simplicity and focusing on the very basics of a block. From 90bc0ecc2f65e429ed1abc5386b5bcd428ec9496 Mon Sep 17 00:00:00 2001 From: JessicaGosselin Date: Tue, 17 May 2022 12:25:04 -0400 Subject: [PATCH 30/88] [Documentation]: Fix typo in `Panel` readme (#41111) * Update README.md Fix typo * Update packages/components/src/panel/README.md Co-authored-by: Nik Tsekouras Co-authored-by: Nik Tsekouras --- packages/components/src/panel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/panel/README.md b/packages/components/src/panel/README.md index 58e59a4f85a04..83f7d66af4bd2 100644 --- a/packages/components/src/panel/README.md +++ b/packages/components/src/panel/README.md @@ -161,7 +161,7 @@ Props that are passed to the `Button` component in the `PanelBodyTitle` within t #### PanelRow -The is a generic container for panel content. Default styles add a top margin and arrange items in a flex row. +The `PanelRow` is a generic container for panel content. Default styles add a top margin and arrange items in a flex row. ##### Props From f83dd103bcfefc5d1d213414b2a687c0db72c448 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 17 May 2022 23:08:16 +0100 Subject: [PATCH 31/88] List block v2: Fixes an issue where pressing enter deletes innerblocks. (#41109) --- packages/block-library/src/list-item/edit.js | 11 +++------ .../src/list-item/hooks/index.js | 1 + .../src/list-item/hooks/use-split.js | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 packages/block-library/src/list-item/hooks/use-split.js diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 46f8f67bbaeba..2240e1eb16a41 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -8,7 +8,6 @@ import { BlockControls, } from '@wordpress/block-editor'; import { isRTL, __ } from '@wordpress/i18n'; -import { createBlock } from '@wordpress/blocks'; import { ToolbarButton } from '@wordpress/components'; import { formatOutdent, @@ -27,6 +26,7 @@ import { useSpace, useIndentListItem, useOutdentListItem, + useSplit, } from './hooks'; function IndentUI( { clientId } ) { @@ -54,7 +54,6 @@ function IndentUI( { clientId } ) { } export default function ListItemEdit( { - name, attributes, setAttributes, mergeBlocks, @@ -69,6 +68,7 @@ export default function ListItemEdit( { const useEnterRef = useEnter( { content, clientId } ); const useBackspaceRef = useBackspace( { clientId } ); const useSpaceRef = useSpace( clientId ); + const onSplit = useSplit( clientId ); return ( <>
  • @@ -86,12 +86,7 @@ export default function ListItemEdit( { value={ content } aria-label={ __( 'List text' ) } placeholder={ placeholder || __( 'List' ) } - onSplit={ ( value ) => { - return createBlock( name, { - ...attributes, - content: value, - } ); - } } + onSplit={ onSplit } onMerge={ mergeBlocks } onReplace={ onReplace } /> diff --git a/packages/block-library/src/list-item/hooks/index.js b/packages/block-library/src/list-item/hooks/index.js index 5fcd802926065..c78702b578176 100644 --- a/packages/block-library/src/list-item/hooks/index.js +++ b/packages/block-library/src/list-item/hooks/index.js @@ -3,3 +3,4 @@ export { default as useIndentListItem } from './use-indent-list-item'; export { default as useEnter } from './use-enter'; export { default as useBackspace } from './use-backspace'; export { default as useSpace } from './use-space'; +export { default as useSplit } from './use-split'; diff --git a/packages/block-library/src/list-item/hooks/use-split.js b/packages/block-library/src/list-item/hooks/use-split.js new file mode 100644 index 0000000000000..58e811f8c6783 --- /dev/null +++ b/packages/block-library/src/list-item/hooks/use-split.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { cloneBlock } from '@wordpress/blocks'; + +export default function useSplit( clientId ) { + const { getBlock } = useSelect( blockEditorStore ); + return useCallback( + ( value, isAfterOriginal ) => { + const block = getBlock( clientId ); + return cloneBlock( + block, + { + content: value, + }, + isAfterOriginal ? [] : block.innerBlocks + ); + }, + [ clientId, getBlock ] + ); +} From 303a423e174ac3ea58655b15d437517f29eb5b60 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 17 May 2022 23:08:51 +0100 Subject: [PATCH 32/88] Add: Raw handling to the new list block. (#39954) --- packages/block-library/src/list/v2/migrate.js | 2 +- .../block-library/src/list/v2/transforms.js | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/list/v2/migrate.js b/packages/block-library/src/list/v2/migrate.js index f9cd6d6419fb0..d0ed33e8f3c44 100644 --- a/packages/block-library/src/list/v2/migrate.js +++ b/packages/block-library/src/list/v2/migrate.js @@ -8,7 +8,7 @@ import { omit } from 'lodash'; */ import { createBlock } from '@wordpress/blocks'; -function createListBlockFromDOMElement( listElement ) { +export function createListBlockFromDOMElement( listElement ) { const listAttributes = { ordered: 'OL' === listElement.tagName, start: listElement.getAttribute( 'start' ) diff --git a/packages/block-library/src/list/v2/transforms.js b/packages/block-library/src/list/v2/transforms.js index 5070bc7c473d1..176a9d74377ed 100644 --- a/packages/block-library/src/list/v2/transforms.js +++ b/packages/block-library/src/list/v2/transforms.js @@ -9,6 +9,32 @@ import { toHTMLString, } from '@wordpress/rich-text'; +/** + * Internal dependencies + */ +import { createListBlockFromDOMElement } from './migrate'; + +function getListContentSchema( { phrasingContentSchema } ) { + const listContentSchema = { + ...phrasingContentSchema, + ul: {}, + ol: { attributes: [ 'type', 'start', 'reversed' ] }, + }; + + // Recursion is needed. + // Possible: ul > li > ul. + // Impossible: ul > ul. + [ 'ul', 'ol' ].forEach( ( tag ) => { + listContentSchema[ tag ].children = { + li: { + children: listContentSchema, + }, + }; + } ); + + return listContentSchema; +} + const transforms = { from: [ { @@ -82,6 +108,15 @@ const transforms = { ); }, } ) ), + { + type: 'raw', + selector: 'ol,ul', + schema: ( args ) => ( { + ol: getListContentSchema( args ).ol, + ul: getListContentSchema( args ).ul, + } ), + transform: createListBlockFromDOMElement, + }, ], to: [ ...[ 'core/paragraph', 'core/heading' ].map( ( block ) => ( { From 14c0c1d02b1d674d4c16f4835133a13cf1f40d68 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Wed, 18 May 2022 09:22:34 +1000 Subject: [PATCH 33/88] Correct resizeable spelling to US resizable (#41100) --- packages/block-library/src/cover/edit/index.js | 2 +- .../src/cover/edit/{resizeable-cover.js => resizable-cover.js} | 0 packages/block-library/src/cover/editor.scss | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/block-library/src/cover/edit/{resizeable-cover.js => resizable-cover.js} (100%) diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 83dd7b7665823..53756a7669de2 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -43,7 +43,7 @@ import useCoverIsDark from './use-cover-is-dark'; import CoverInspectorControls from './inspector-controls'; import CoverBlockControls from './block-controls'; import CoverPlaceholder from './cover-placeholder'; -import ResizableCover from './resizeable-cover'; +import ResizableCover from './resizable-cover'; extend( [ namesPlugin ] ); diff --git a/packages/block-library/src/cover/edit/resizeable-cover.js b/packages/block-library/src/cover/edit/resizable-cover.js similarity index 100% rename from packages/block-library/src/cover/edit/resizeable-cover.js rename to packages/block-library/src/cover/edit/resizable-cover.js diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index f3890c56ceae1..67bdc0e42dbfb 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -10,7 +10,7 @@ min-height: auto !important; padding: 0 !important; - // Resizeable placeholder for placeholder. + // Resizable placeholder for placeholder. .block-library-cover__resize-container { display: none; } From 1eee4656d5cab0951944badea704442100b0e9b8 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 May 2022 12:58:04 +1000 Subject: [PATCH 34/88] Style Engine: add typography and color to frontend (#40665) * Returning inline styles by default. * Adding some dev notes for later brain * Adding a couple of typography styles Updating tests Refactor styles directory * Adding text decoration Fixing camel case style prop name * Adding typography properties letterSpacing.ts and textTransform.ts * Adding color.text and typography.fontWeight * Refactor generic rule generator * Experiment: adding classname generator * Fixing bonkers typos because I copy and paste like I just don't care. It's my PR and I'll fail if I want to. Preferencing `uniq` for new Set() spread Updating tests. * Enable style engine for backgroundColor * Regenerate pullquote fixture since the style engine changes the order of the inline styles rules. Nothing major. * Add constants Updated inline docs Making the generateRule signature the same as generateBoxRules * Update packages/style-engine/src/index.ts Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> * Refactoring generate method to split inline and selector logic Removing styles color text support because we haven't yet come up a with a way to deal with link color styles for elements. We need to reconcile the way we generate text colors with link css var colors and the logic in hooks/style.js Add constants Updated inline docs Making the generateRule signature the same as generateBoxRules * Remove TODO comment. It's something we can do in the future, but it doesn't need to pollute the codebase. * Reinstating color and now parsing style value for `var:`, whereby we'd return CSS vars all the time. * A bit of defensive coding to check for a string type * Testing out generating block color classnames using the style engine. * Also passing the style color value to `getClassnames` so it will add the generic class name if required. Regenerating fixtures due to the re-ordering of color classnames. * Reverting `getClassnames`. It's a bigger change and should be looked at in another PR. * Fix typo in fixture * Flag fontStyle with useEngine: true so that it uses the style engine. Remove the handler for fontFamily to reflect the current situation in the editor whereby no custom fontFamily style values are expected. Removed unused function `getSlugFromPreset` Added tests for elements.link.color.text preset style values. Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --- packages/block-editor/src/hooks/style.js | 5 +- packages/blocks/src/api/constants.js | 9 ++ .../global-styles/use-global-styles-output.js | 5 +- packages/style-engine/src/index.ts | 14 ++- .../src/styles/color/background.ts | 19 ++++ .../style-engine/src/styles/color/gradient.ts | 19 ++++ .../style-engine/src/styles/color/index.ts | 8 ++ .../style-engine/src/styles/color/text.ts | 14 +++ packages/style-engine/src/styles/constants.ts | 3 + packages/style-engine/src/styles/index.ts | 7 +- .../style-engine/src/styles/spacing/index.ts | 7 ++ .../src/styles/{ => spacing}/margin.ts | 4 +- .../src/styles/{ => spacing}/padding.ts | 4 +- .../src/styles/typography/index.ts | 99 +++++++++++++++++ packages/style-engine/src/styles/utils.ts | 69 +++++++++++- packages/style-engine/src/test/index.js | 103 +++++++++++++++++- packages/style-engine/src/types.ts | 15 ++- .../core__button__squared.serialized.html | 2 +- ...__pullquote__custom-colors.serialized.html | 2 +- 19 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 packages/style-engine/src/styles/color/background.ts create mode 100644 packages/style-engine/src/styles/color/gradient.ts create mode 100644 packages/style-engine/src/styles/color/index.ts create mode 100644 packages/style-engine/src/styles/color/text.ts create mode 100644 packages/style-engine/src/styles/constants.ts create mode 100644 packages/style-engine/src/styles/spacing/index.ts rename packages/style-engine/src/styles/{ => spacing}/margin.ts (71%) rename packages/style-engine/src/styles/{ => spacing}/padding.ts (71%) create mode 100644 packages/style-engine/src/styles/typography/index.ts diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index d8b8ad6771a07..400ebd0a84263 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -97,11 +97,8 @@ export function getInlineStyles( styles = {} ) { // The goal is to move everything to server side generated engine styles // This is temporary as we absorb more and more styles into the engine. - const extraRules = getCSSRules( styles, { selector: 'self' } ); + const extraRules = getCSSRules( styles ); extraRules.forEach( ( rule ) => { - if ( rule.selector !== 'self' ) { - throw "This style can't be added as inline style"; - } output[ rule.key ] = rule.value; } ); diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index c583b53d96c0a..a5ed771d72d2f 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -28,6 +28,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'color', 'background' ], support: [ 'color', 'background' ], requiresOptOut: true, + useEngine: true, }, borderColor: { value: [ 'border', 'color' ], @@ -103,6 +104,7 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { value: [ 'color', 'text' ], support: [ 'color', 'text' ], requiresOptOut: true, + useEngine: true, }, filter: { value: [ 'filter', 'duotone' ], @@ -119,18 +121,22 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { fontSize: { value: [ 'typography', 'fontSize' ], support: [ 'typography', 'fontSize' ], + useEngine: true, }, fontStyle: { value: [ 'typography', 'fontStyle' ], support: [ 'typography', '__experimentalFontStyle' ], + useEngine: true, }, fontWeight: { value: [ 'typography', 'fontWeight' ], support: [ 'typography', '__experimentalFontWeight' ], + useEngine: true, }, lineHeight: { value: [ 'typography', 'lineHeight' ], support: [ 'typography', 'lineHeight' ], + useEngine: true, }, margin: { value: [ 'spacing', 'margin' ], @@ -157,14 +163,17 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { textDecoration: { value: [ 'typography', 'textDecoration' ], support: [ 'typography', '__experimentalTextDecoration' ], + useEngine: true, }, textTransform: { value: [ 'typography', 'textTransform' ], support: [ 'typography', '__experimentalTextTransform' ], + useEngine: true, }, letterSpacing: { value: [ 'typography', 'letterSpacing' ], support: [ 'typography', '__experimentalLetterSpacing' ], + useEngine: true, }, '--wp--style--block-gap': { value: [ 'spacing', 'blockGap' ], diff --git a/packages/edit-site/src/components/global-styles/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/use-global-styles-output.js index e477faa83a024..0be47a7000b53 100644 --- a/packages/edit-site/src/components/global-styles/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/use-global-styles-output.js @@ -220,11 +220,8 @@ function getStylesDeclarations( blockStyles = {} ) { // The goal is to move everything to server side generated engine styles // This is temporary as we absorb more and more styles into the engine. - const extraRules = getCSSRules( blockStyles, { selector: 'self' } ); + const extraRules = getCSSRules( blockStyles ); extraRules.forEach( ( rule ) => { - if ( rule.selector !== 'self' ) { - throw "This style can't be added as inline style"; - } const cssProperty = rule.key.startsWith( '--' ) ? rule.key : kebabCase( rule.key ); diff --git a/packages/style-engine/src/index.ts b/packages/style-engine/src/index.ts index 8117e27892140..c78dd753834b6 100644 --- a/packages/style-engine/src/index.ts +++ b/packages/style-engine/src/index.ts @@ -24,6 +24,16 @@ import { styleDefinitions } from './styles'; */ export function generate( style: Style, options: StyleOptions ): string { const rules = getCSSRules( style, options ); + + // If no selector is provided, treat generated rules as inline styles to be returned as a single string. + if ( ! options?.selector ) { + const inlineRules: string[] = []; + rules.forEach( ( rule ) => { + inlineRules.push( `${ kebabCase( rule.key ) }: ${ rule.value };` ); + } ); + return inlineRules.join( ' ' ); + } + const groupedRules = groupBy( rules, 'selector' ); const selectorRules = Object.keys( groupedRules ).reduce( ( acc: string[], subSelector: string ) => { @@ -57,7 +67,9 @@ export function getCSSRules( ): GeneratedCSSRule[] { const rules: GeneratedCSSRule[] = []; styleDefinitions.forEach( ( definition: StyleDefinition ) => { - rules.push( ...definition.generate( style, options ) ); + if ( typeof definition.generate === 'function' ) { + rules.push( ...definition.generate( style, options ) ); + } } ); return rules; diff --git a/packages/style-engine/src/styles/color/background.ts b/packages/style-engine/src/styles/color/background.ts new file mode 100644 index 0000000000000..0df422bae7d58 --- /dev/null +++ b/packages/style-engine/src/styles/color/background.ts @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const background = { + name: 'background', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'color', 'background' ], + 'backgroundColor' + ); + }, +}; + +export default background; diff --git a/packages/style-engine/src/styles/color/gradient.ts b/packages/style-engine/src/styles/color/gradient.ts new file mode 100644 index 0000000000000..4cc0aecffbe63 --- /dev/null +++ b/packages/style-engine/src/styles/color/gradient.ts @@ -0,0 +1,19 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const gradient = { + name: 'gradient', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'color', 'gradient' ], + 'background' + ); + }, +}; + +export default gradient; diff --git a/packages/style-engine/src/styles/color/index.ts b/packages/style-engine/src/styles/color/index.ts new file mode 100644 index 0000000000000..15db28d726a80 --- /dev/null +++ b/packages/style-engine/src/styles/color/index.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import background from './background'; +import gradient from './gradient'; +import text from './text'; + +export default [ text, gradient, background ]; diff --git a/packages/style-engine/src/styles/color/text.ts b/packages/style-engine/src/styles/color/text.ts new file mode 100644 index 0000000000000..e1a6bb3d99b5e --- /dev/null +++ b/packages/style-engine/src/styles/color/text.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const text = { + name: 'text', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( style, options, [ 'color', 'text' ], 'color' ); + }, +}; + +export default text; diff --git a/packages/style-engine/src/styles/constants.ts b/packages/style-engine/src/styles/constants.ts new file mode 100644 index 0000000000000..9df0cf255568e --- /dev/null +++ b/packages/style-engine/src/styles/constants.ts @@ -0,0 +1,3 @@ +export const VARIABLE_REFERENCE_PREFIX = 'var:'; +export const VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE = '|'; +export const VARIABLE_PATH_SEPARATOR_TOKEN_STYLE = '--'; diff --git a/packages/style-engine/src/styles/index.ts b/packages/style-engine/src/styles/index.ts index f7c87a8c7a29b..79f1c43d8d33f 100644 --- a/packages/style-engine/src/styles/index.ts +++ b/packages/style-engine/src/styles/index.ts @@ -1,7 +1,8 @@ /** * Internal dependencies */ -import padding from './padding'; -import margin from './margin'; +import color from './color'; +import spacing from './spacing'; +import typography from './typography'; -export const styleDefinitions = [ margin, padding ]; +export const styleDefinitions = [ ...color, ...spacing, ...typography ]; diff --git a/packages/style-engine/src/styles/spacing/index.ts b/packages/style-engine/src/styles/spacing/index.ts new file mode 100644 index 0000000000000..4f6121951b0bf --- /dev/null +++ b/packages/style-engine/src/styles/spacing/index.ts @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +import padding from './padding'; +import margin from './margin'; + +export default [ margin, padding ]; diff --git a/packages/style-engine/src/styles/margin.ts b/packages/style-engine/src/styles/spacing/margin.ts similarity index 71% rename from packages/style-engine/src/styles/margin.ts rename to packages/style-engine/src/styles/spacing/margin.ts index 2ead5da23f1e7..c8492c378954f 100644 --- a/packages/style-engine/src/styles/margin.ts +++ b/packages/style-engine/src/styles/spacing/margin.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import type { Style, StyleOptions } from '../types'; -import { generateBoxRules } from './utils'; +import type { Style, StyleOptions } from '../../types'; +import { generateBoxRules } from '../utils'; const margin = { name: 'margin', diff --git a/packages/style-engine/src/styles/padding.ts b/packages/style-engine/src/styles/spacing/padding.ts similarity index 71% rename from packages/style-engine/src/styles/padding.ts rename to packages/style-engine/src/styles/spacing/padding.ts index 94b268de35604..a5a3227030e07 100644 --- a/packages/style-engine/src/styles/padding.ts +++ b/packages/style-engine/src/styles/spacing/padding.ts @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import type { Style, StyleOptions } from '../types'; -import { generateBoxRules } from './utils'; +import type { Style, StyleOptions } from '../../types'; +import { generateBoxRules } from '../utils'; const padding = { name: 'padding', diff --git a/packages/style-engine/src/styles/typography/index.ts b/packages/style-engine/src/styles/typography/index.ts new file mode 100644 index 0000000000000..70e32b23cafe7 --- /dev/null +++ b/packages/style-engine/src/styles/typography/index.ts @@ -0,0 +1,99 @@ +/** + * Internal dependencies + */ +import type { Style, StyleOptions } from '../../types'; +import { generateRule } from '../utils'; + +const fontSize = { + name: 'fontSize', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'fontSize' ], + 'fontSize' + ); + }, +}; + +const fontStyle = { + name: 'fontStyle', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'fontStyle' ], + 'fontStyle' + ); + }, +}; + +const fontWeight = { + name: 'fontWeight', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'fontWeight' ], + 'fontWeight' + ); + }, +}; + +const letterSpacing = { + name: 'letterSpacing', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'letterSpacing' ], + 'letterSpacing' + ); + }, +}; + +const lineHeight = { + name: 'letterSpacing', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'lineHeight' ], + 'lineHeight' + ); + }, +}; + +const textDecoration = { + name: 'textDecoration', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'textDecoration' ], + 'textDecoration' + ); + }, +}; + +const textTransform = { + name: 'textTransform', + generate: ( style: Style, options: StyleOptions ) => { + return generateRule( + style, + options, + [ 'typography', 'textTransform' ], + 'textTransform' + ); + }, +}; + +export default [ + fontSize, + fontStyle, + fontWeight, + letterSpacing, + lineHeight, + textDecoration, + textTransform, +]; diff --git a/packages/style-engine/src/styles/utils.ts b/packages/style-engine/src/styles/utils.ts index 5455901e49d1a..928d923c86821 100644 --- a/packages/style-engine/src/styles/utils.ts +++ b/packages/style-engine/src/styles/utils.ts @@ -7,7 +7,51 @@ import { get, upperFirst } from 'lodash'; * Internal dependencies */ import type { GeneratedCSSRule, Style, Box, StyleOptions } from '../types'; +import { + VARIABLE_REFERENCE_PREFIX, + VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE, + VARIABLE_PATH_SEPARATOR_TOKEN_STYLE, +} from './constants'; +/** + * Returns a JSON representation of the generated CSS rules. + * + * @param style Style object. + * @param options Options object with settings to adjust how the styles are generated. + * @param path An array of strings representing the path to the style value in the style object. + * @param ruleKey A CSS property key. + * + * @return GeneratedCSSRule[] CSS rules. + */ +export function generateRule( + style: Style, + options: StyleOptions, + path: string[], + ruleKey: string +) { + const styleValue: string | undefined = get( style, path ); + + return styleValue + ? [ + { + selector: options?.selector, + key: ruleKey, + value: getCSSVarFromStyleValue( styleValue ), + }, + ] + : []; +} + +/** + * Returns a JSON representation of the generated CSS rules taking into account box model properties, top, right, bottom, left. + * + * @param style Style object. + * @param options Options object with settings to adjust how the styles are generated. + * @param path An array of strings representing the path to the style value in the style object. + * @param ruleKey A CSS property key. + * + * @return GeneratedCSSRule[] CSS rules. + */ export function generateBoxRules( style: Style, options: StyleOptions, @@ -22,7 +66,7 @@ export function generateBoxRules( const rules: GeneratedCSSRule[] = []; if ( typeof boxStyle === 'string' ) { rules.push( { - selector: options.selector, + selector: options?.selector, key: ruleKey, value: boxStyle, } ); @@ -32,7 +76,7 @@ export function generateBoxRules( const value: string | undefined = get( boxStyle, [ side ] ); if ( value ) { acc.push( { - selector: options.selector, + selector: options?.selector, key: `${ ruleKey }${ upperFirst( side ) }`, value, } ); @@ -46,3 +90,24 @@ export function generateBoxRules( return rules; } + +/** + * Returns a CSS var value from incoming style value following the pattern `var:description|context|slug`. + * + * @param styleValue A raw style value. + * + * @return string A CSS var value. + */ +export function getCSSVarFromStyleValue( styleValue: string ): string { + if ( + typeof styleValue === 'string' && + styleValue.startsWith( VARIABLE_REFERENCE_PREFIX ) + ) { + const variable = styleValue + .slice( VARIABLE_REFERENCE_PREFIX.length ) + .split( VARIABLE_PATH_SEPARATOR_TOKEN_ATTRIBUTE ) + .join( VARIABLE_PATH_SEPARATOR_TOKEN_STYLE ); + return `var(--wp--${ variable })`; + } + return styleValue; +} diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index bf1ab73566f52..ad2acd401056b 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -8,7 +8,23 @@ describe( 'generate', () => { expect( generate( {}, '.some-selector' ) ).toEqual( '' ); } ); - it( 'should generate spacing styles', () => { + it( 'should generate inline styles where there is no selector', () => { + expect( + generate( { + spacing: { padding: '10px', margin: '12px' }, + color: { + text: '#f1f1f1', + background: '#222222', + gradient: + 'linear-gradient(135deg,rgb(6,147,227) 0%,rgb(143,47,47) 49%,rgb(155,81,224) 100%)', + }, + } ) + ).toEqual( + 'color: #f1f1f1; background: linear-gradient(135deg,rgb(6,147,227) 0%,rgb(143,47,47) 49%,rgb(155,81,224) 100%); background-color: #222222; margin: 12px; padding: 10px;' + ); + } ); + + it( 'should generate styles with an optional selector', () => { expect( generate( { @@ -23,6 +39,10 @@ describe( 'generate', () => { expect( generate( { + color: { + text: '#cccccc', + background: '#111111', + }, spacing: { padding: { top: '10px', bottom: '5px' }, margin: { @@ -32,15 +52,35 @@ describe( 'generate', () => { left: '14px', }, }, + typography: { + fontSize: '2.2rem', + fontStyle: 'italic', + fontWeight: '800', + fontFamily: "'Helvetica Neue',sans-serif", + lineHeight: '3.3', + textDecoration: 'line-through', + letterSpacing: '12px', + textTransform: 'uppercase', + }, }, { selector: '.some-selector', } ) ).toEqual( - '.some-selector { margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; }' + '.some-selector { color: #cccccc; background-color: #111111; margin-top: 11px; margin-right: 12px; margin-bottom: 13px; margin-left: 14px; padding-top: 10px; padding-bottom: 5px; font-size: 2.2rem; font-style: italic; font-weight: 800; letter-spacing: 12px; line-height: 3.3; text-decoration: line-through; text-transform: uppercase; }' ); } ); + + it( 'should parse preset values (use for elements.link.color.text)', () => { + expect( + generate( { + color: { + text: 'var:preset|color|ham-sandwich', + }, + } ) + ).toEqual( 'color: var(--wp--preset--color--ham-sandwich);' ); + } ); } ); describe( 'getCSSRules', () => { @@ -96,16 +136,40 @@ describe( 'getCSSRules', () => { expect( getCSSRules( { + color: { + text: '#dddddd', + background: '#555555', + }, spacing: { padding: { top: '10px', bottom: '5px' }, margin: { right: '2em', left: '1vw' }, }, + typography: { + fontSize: '2.2rem', + fontStyle: 'italic', + fontWeight: '800', + fontFamily: "'Helvetica Neue',sans-serif", + lineHeight: '3.3', + textDecoration: 'line-through', + letterSpacing: '12px', + textTransform: 'uppercase', + }, }, { selector: '.some-selector', } ) ).toEqual( [ + { + selector: '.some-selector', + key: 'color', + value: '#dddddd', + }, + { + selector: '.some-selector', + key: 'backgroundColor', + value: '#555555', + }, { selector: '.some-selector', key: 'marginRight', @@ -126,6 +190,41 @@ describe( 'getCSSRules', () => { key: 'paddingBottom', value: '5px', }, + { + key: 'fontSize', + selector: '.some-selector', + value: '2.2rem', + }, + { + key: 'fontStyle', + selector: '.some-selector', + value: 'italic', + }, + { + key: 'fontWeight', + selector: '.some-selector', + value: '800', + }, + { + key: 'letterSpacing', + selector: '.some-selector', + value: '12px', + }, + { + key: 'lineHeight', + selector: '.some-selector', + value: '3.3', + }, + { + key: 'textDecoration', + selector: '.some-selector', + value: 'line-through', + }, + { + key: 'textTransform', + selector: '.some-selector', + value: 'uppercase', + }, ] ); } ); } ); diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index 48bd9a0d8e5c6..9f60b1b0f6fc0 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -26,6 +26,18 @@ export interface Style { textDecoration?: CSSProperties[ 'textDecoration' ]; textTransform?: CSSProperties[ 'textTransform' ]; }; + color?: { + text?: CSSProperties[ 'color' ]; + background?: CSSProperties[ 'backgroundColor' ]; + gradient?: CSSProperties[ 'background' ]; + }; + elements?: { + link?: { + color?: { + text?: CSSProperties[ 'color' ]; + }; + }; + }; } export type StyleOptions = { @@ -47,5 +59,6 @@ export type GeneratedCSSRule = { export interface StyleDefinition { name: string; - generate: ( style: Style, options: StyleOptions ) => GeneratedCSSRule[]; + generate?: ( style: Style, options: StyleOptions ) => GeneratedCSSRule[]; + getClassNames?: ( style: Style ) => string[]; } diff --git a/test/integration/fixtures/blocks/core__button__squared.serialized.html b/test/integration/fixtures/blocks/core__button__squared.serialized.html index 29e7fed15a510..ec83bb66b4971 100644 --- a/test/integration/fixtures/blocks/core__button__squared.serialized.html +++ b/test/integration/fixtures/blocks/core__button__squared.serialized.html @@ -1,3 +1,3 @@ - + diff --git a/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html b/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html index 3427b0ff08798..d6b9f285fdecb 100644 --- a/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html +++ b/test/integration/fixtures/blocks/core__pullquote__custom-colors.serialized.html @@ -1,3 +1,3 @@ -

    pullquote

    citation
    +

    pullquote

    citation
    From a754e08c48dc7686f8014b015486a3cc5328ba27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Walb=C3=B8=20Johnsg=C3=A5rd?= Date: Wed, 18 May 2022 09:06:53 +0200 Subject: [PATCH 35/88] ButtonGroup: Convert to TypeScript (#41007) --- packages/components/CHANGELOG.md | 1 + packages/components/src/button-group/index.js | 21 --------- .../components/src/button-group/index.tsx | 47 +++++++++++++++++++ .../src/button-group/stories/index.js | 21 --------- .../src/button-group/stories/index.tsx | 41 ++++++++++++++++ packages/components/src/button-group/types.ts | 11 +++++ 6 files changed, 100 insertions(+), 42 deletions(-) delete mode 100644 packages/components/src/button-group/index.js create mode 100644 packages/components/src/button-group/index.tsx delete mode 100644 packages/components/src/button-group/stories/index.js create mode 100644 packages/components/src/button-group/stories/index.tsx create mode 100644 packages/components/src/button-group/types.ts diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index a7b533663cdd5..e111fffc7aa5c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ - `DateTimePicker`: Convert to TypeScript ([#40775](https://github.com/WordPress/gutenberg/pull/40775)). - `DateTimePicker`: Convert unit tests to TypeScript ([#40957](https://github.com/WordPress/gutenberg/pull/40957)). - `CheckboxControl`: Convert to TypeScript ([#40915](https://github.com/WordPress/gutenberg/pull/40915)). +- `ButtonGroup`: Convert to TypeScript ([#41007](https://github.com/WordPress/gutenberg/pull/41007)). ## 19.10.0 (2022-05-04) diff --git a/packages/components/src/button-group/index.js b/packages/components/src/button-group/index.js deleted file mode 100644 index cdd4343d776ed..0000000000000 --- a/packages/components/src/button-group/index.js +++ /dev/null @@ -1,21 +0,0 @@ -// @ts-nocheck -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { forwardRef } from '@wordpress/element'; - -function ButtonGroup( props, ref ) { - const { className, ...restProps } = props; - const classes = classnames( 'components-button-group', className ); - - return ( -
    - ); -} - -export default forwardRef( ButtonGroup ); diff --git a/packages/components/src/button-group/index.tsx b/packages/components/src/button-group/index.tsx new file mode 100644 index 0000000000000..6185792b6e4d9 --- /dev/null +++ b/packages/components/src/button-group/index.tsx @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import type { ForwardedRef } from 'react'; + +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { ButtonGroupProps } from './types'; +import type { WordPressComponentProps } from '../ui/context'; + +function UnforwardedButtonGroup( + props: WordPressComponentProps< ButtonGroupProps, 'div', false >, + ref: ForwardedRef< HTMLDivElement > +) { + const { className, ...restProps } = props; + const classes = classnames( 'components-button-group', className ); + + return ( +
    + ); +} + +/** + * ButtonGroup can be used to group any related buttons together. To emphasize + * related buttons, a group should share a common container. + * + * ```jsx + * import { Button, ButtonGroup } from '@wordpress/components'; + * + * const MyButtonGroup = () => ( + * + * + * + * + * ); + * ``` + */ +export const ButtonGroup = forwardRef( UnforwardedButtonGroup ); + +export default ButtonGroup; diff --git a/packages/components/src/button-group/stories/index.js b/packages/components/src/button-group/stories/index.js deleted file mode 100644 index 3b82a4c86346b..0000000000000 --- a/packages/components/src/button-group/stories/index.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Internal dependencies - */ -import Button from '../../button'; -import ButtonGroup from '../'; - -export default { title: 'Components/ButtonGroup', component: ButtonGroup }; - -export const _default = () => { - const style = { margin: '0 4px' }; - return ( - - - - - ); -}; diff --git a/packages/components/src/button-group/stories/index.tsx b/packages/components/src/button-group/stories/index.tsx new file mode 100644 index 0000000000000..33d7cbfacd35d --- /dev/null +++ b/packages/components/src/button-group/stories/index.tsx @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +/** + * Internal dependencies + */ +import ButtonGroup from '..'; +import Button from '../../button'; + +const meta: ComponentMeta< typeof ButtonGroup > = { + title: 'Components/ButtonGroup', + component: ButtonGroup, + argTypes: { + children: { control: { type: null } }, + }, + parameters: { + controls: { expanded: true }, + docs: { source: { state: 'open' } }, + }, +}; +export default meta; + +const Template: ComponentStory< typeof ButtonGroup > = ( args ) => { + const style = { margin: '0 4px' }; + return ( + + + + + ); +}; + +export const Default: ComponentStory< typeof ButtonGroup > = Template.bind( + {} +); diff --git a/packages/components/src/button-group/types.ts b/packages/components/src/button-group/types.ts new file mode 100644 index 0000000000000..0bc162d5cf1c7 --- /dev/null +++ b/packages/components/src/button-group/types.ts @@ -0,0 +1,11 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; + +export type ButtonGroupProps = { + /** + * The children elements. + */ + children: ReactNode; +}; From b49948774e6d1e451d75c9df2e5763f95cb7e965 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Wed, 18 May 2022 11:26:57 +0400 Subject: [PATCH 36/88] Migrate 'block-locking' to Playwright (#41050) * Migrate 'block-locking' to Playwright * Use correct utility method --- .../editor/various/block-locking.test.js | 120 ------------------ .../editor/various/block-locking.spec.js | 86 +++++++++++++ 2 files changed, 86 insertions(+), 120 deletions(-) delete mode 100644 packages/e2e-tests/specs/editor/various/block-locking.test.js create mode 100644 test/e2e/specs/editor/various/block-locking.spec.js diff --git a/packages/e2e-tests/specs/editor/various/block-locking.test.js b/packages/e2e-tests/specs/editor/various/block-locking.test.js deleted file mode 100644 index 8bb2833a59201..0000000000000 --- a/packages/e2e-tests/specs/editor/various/block-locking.test.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - clickButton, - clickMenuItem, - clickBlockToolbarButton, - getEditedPostContent, - insertBlock, -} from '@wordpress/e2e-test-utils'; - -describe( 'Block Grouping', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - describe( 'General', () => { - it( 'can prevent removal', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Some paragraph' ); - - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Lock' ); - - const [ checkbox ] = await page.$x( - '//label[contains(text(), "Prevent removal")]' - ); - await checkbox.click(); - - await clickButton( 'Apply' ); - - const [ removeBlock ] = await page.$x( - '//*[@role="menu"]//*[text()="Remove Paragraph"]' - ); - - expect( removeBlock ).toBeFalsy(); - } ); - - it( 'can disable movement', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'First paragraph' ); - - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Second paragraph' ); - - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Lock' ); - - const [ checkbox ] = await page.$x( - '//label[contains(text(), "Disable movement")]' - ); - await checkbox.click(); - - await clickButton( 'Apply' ); - - // Hide options. - await clickBlockToolbarButton( 'Options' ); - - const blockMover = await page.$( '.block-editor-block-mover' ); - expect( blockMover ).toBeNull(); - - const [ lockButton ] = await page.$x( - '//button[@aria-label="Unlock Paragraph"]' - ); - expect( lockButton ).toBeTruthy(); - } ); - - it( 'can lock everything', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Some paragraph' ); - - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Lock' ); - - const [ checkbox ] = await page.$x( - '//label//*[contains(text(), "Lock all")]' - ); - await checkbox.click(); - - await clickButton( 'Apply' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

    Some paragraph

    - " - ` ); - } ); - - it( 'can unlock from toolbar', async () => { - await insertBlock( 'Paragraph' ); - await page.keyboard.type( 'Some paragraph' ); - - await clickBlockToolbarButton( 'Options' ); - await clickMenuItem( 'Lock' ); - - const [ lockCheckbox ] = await page.$x( - '//label//*[contains(text(), "Lock all")]' - ); - await lockCheckbox.click(); - - await clickButton( 'Apply' ); - - await clickBlockToolbarButton( 'Unlock Paragraph' ); - - const [ unlockCheckbox ] = await page.$x( - '//label//*[contains(text(), "Lock all")]' - ); - await unlockCheckbox.click(); - - await clickButton( 'Apply' ); - - expect( await getEditedPostContent() ).toMatchInlineSnapshot( ` - " -

    Some paragraph

    - " - ` ); - } ); - } ); -} ); diff --git a/test/e2e/specs/editor/various/block-locking.spec.js b/test/e2e/specs/editor/various/block-locking.spec.js new file mode 100644 index 0000000000000..7398cc6be7b18 --- /dev/null +++ b/test/e2e/specs/editor/various/block-locking.spec.js @@ -0,0 +1,86 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Block Locking', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.describe( 'General', () => { + test( 'can prevent removal', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( 'Some paragraph' ); + + await editor.clickBlockOptionsMenuItem( 'Lock' ); + + await page.click( 'role=checkbox[name="Prevent removal"]' ); + await page.click( 'role=button[name="Apply"]' ); + + await expect( + page.locator( 'role=menuitem[name="Remove Paragraph"]' ) + ).not.toBeVisible(); + } ); + + test( 'can disable movement', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( 'First paragraph' ); + + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( 'Second paragraph' ); + + await editor.clickBlockOptionsMenuItem( 'Lock' ); + + await page.click( 'role=checkbox[name="Disable movement"]' ); + await page.click( 'role=button[name="Apply"]' ); + + // Hide options. + await editor.clickBlockToolbarButton( 'Options' ); + + // Drag handle is hidden. + await expect( + page.locator( 'role=button[name="Drag"]' ) + ).not.toBeVisible(); + + // Movers are hidden. No need to check for both. + await expect( + page.locator( 'role=button[name="Move up"]' ) + ).not.toBeVisible(); + } ); + + test( 'can lock everything', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( 'Some paragraph' ); + + await editor.clickBlockOptionsMenuItem( 'Lock' ); + + await page.click( 'role=checkbox[name="Lock all"]' ); + await page.click( 'role=button[name="Apply"]' ); + + expect( await editor.getEditedPostContent() ) + .toBe( ` +

    Some paragraph

    +` ); + } ); + + test( 'can unlock from toolbar', async ( { editor, page } ) => { + await editor.insertBlock( { name: 'core/paragraph' } ); + await page.keyboard.type( 'Some paragraph' ); + + await editor.clickBlockOptionsMenuItem( 'Lock' ); + + await page.click( 'role=checkbox[name="Lock all"]' ); + await page.click( 'role=button[name="Apply"]' ); + + await editor.clickBlockToolbarButton( 'Unlock Paragraph' ); + await page.click( 'role=checkbox[name="Lock all"]' ); + await page.click( 'role=button[name="Apply"]' ); + + expect( await editor.getEditedPostContent() ) + .toBe( ` +

    Some paragraph

    +` ); + } ); + } ); +} ); From 8b1492131355aa6d42272faba25b841f0cd4509d Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 18 May 2022 08:38:10 +0100 Subject: [PATCH 37/88] Refactor the Popover component to use the floatingUI library (#40740) Co-authored-by: ntsekouras Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --- docs/manifest.json | 6 - package-lock.json | 26 +- .../block-alignment-matrix-control/index.js | 6 +- .../block-alignment-matrix-control/style.scss | 10 - .../src/components/block-popover/inbetween.js | 9 +- .../src/components/block-popover/index.js | 17 +- .../src/components/block-popover/style.scss | 1 + .../src/components/block-switcher/style.scss | 41 +- .../components/colors-gradients/dropdown.js | 13 +- .../components/colors-gradients/style.scss | 20 +- .../src/components/duotone-control/style.scss | 8 +- .../src/components/inserter/style.scss | 3 +- .../components/list-view/drop-indicator.js | 1 - .../src/components/list-view/style.scss | 3 +- .../src/components/navigable-toolbar/index.js | 14 +- .../src/components/preview-options/style.scss | 4 - .../rich-text/format-toolbar-container.js | 1 - .../src/components/rich-text/index.js | 2 +- .../src/components/rich-text/style.scss | 10 +- .../src/components/url-input/index.js | 2 +- packages/block-editor/src/hooks/border.js | 13 +- packages/block-editor/src/hooks/border.scss | 48 -- packages/block-editor/src/style.scss | 1 - packages/block-library/src/image/editor.scss | 2 +- packages/block-library/src/video/editor.scss | 9 +- packages/components/package.json | 1 + .../components/src/autocomplete/style.scss | 2 +- .../component.tsx | 24 +- .../border-box-control/README.md | 24 +- .../border-box-control/component.tsx | 21 +- .../src/border-box-control/types.ts | 26 +- .../border-control-dropdown/component.tsx | 6 +- .../border-control-dropdown/hook.ts | 5 +- .../border-control/border-control/README.md | 7 - .../border-control/component.tsx | 4 +- .../components/src/border-control/styles.ts | 3 +- .../components/src/border-control/types.ts | 10 +- .../components/src/color-palette/index.js | 19 +- .../components/src/color-palette/style.scss | 21 +- packages/components/src/dropdown/index.js | 13 +- packages/components/src/dropdown/style.scss | 2 +- packages/components/src/flyout/context.js | 10 - .../src/flyout/flyout-content/component.js | 53 -- .../src/flyout/flyout-content/index.js | 1 - .../components/src/flyout/flyout/README.md | 98 --- .../components/src/flyout/flyout/component.js | 111 ---- packages/components/src/flyout/flyout/hook.js | 45 -- .../components/src/flyout/flyout/index.js | 2 - packages/components/src/flyout/index.js | 1 - .../components/src/flyout/stories/index.js | 24 - packages/components/src/flyout/styles.ts | 41 -- .../flyout/test/__snapshots__/index.js.snap | 183 ------ packages/components/src/flyout/test/index.js | 103 --- packages/components/src/flyout/types.ts | 84 --- packages/components/src/flyout/utils.js | 21 - packages/components/src/index.js | 1 - .../components/src/navigator/stories/index.js | 20 +- packages/components/src/palette-edit/index.js | 3 +- .../components/src/palette-edit/style.scss | 7 - packages/components/src/popover/README.md | 16 +- packages/components/src/popover/index.js | 613 +++++++----------- packages/components/src/popover/style.scss | 210 +----- .../popover/test/__snapshots__/index.js.snap | 32 +- packages/components/src/popover/test/utils.js | 304 --------- packages/components/src/popover/utils.js | 396 ----------- packages/components/src/tooltip/index.js | 10 +- packages/components/src/tooltip/style.scss | 6 +- .../src/hooks/use-focus-on-mount/index.js | 7 +- .../blocks/__snapshots__/heading.test.js.snap | 6 - .../specs/editor/blocks/heading.test.js | 12 +- .../src/components/header/style.scss | 6 +- .../components/sidebar/post-schedule/index.js | 2 +- .../sidebar/post-schedule/style.scss | 4 +- .../components/global-styles/border-panel.js | 14 +- .../header/document-actions/style.scss | 2 +- .../src/components/sidebar/style.scss | 49 -- .../components/table-of-contents/style.scss | 4 +- .../components/more-menu-dropdown/style.scss | 1 - .../src/components/import-dropdown/style.scss | 2 +- packages/nux/src/components/dot-tip/index.js | 1 - .../nux/src/components/dot-tip/style.scss | 5 +- .../dot-tip/test/__snapshots__/index.js.snap | 1 - .../src/blocks/legacy-widget/edit/form.js | 2 +- 83 files changed, 487 insertions(+), 2484 deletions(-) delete mode 100644 packages/block-editor/src/components/block-alignment-matrix-control/style.scss delete mode 100644 packages/components/src/flyout/context.js delete mode 100644 packages/components/src/flyout/flyout-content/component.js delete mode 100644 packages/components/src/flyout/flyout-content/index.js delete mode 100644 packages/components/src/flyout/flyout/README.md delete mode 100644 packages/components/src/flyout/flyout/component.js delete mode 100644 packages/components/src/flyout/flyout/hook.js delete mode 100644 packages/components/src/flyout/flyout/index.js delete mode 100644 packages/components/src/flyout/index.js delete mode 100644 packages/components/src/flyout/stories/index.js delete mode 100644 packages/components/src/flyout/styles.ts delete mode 100644 packages/components/src/flyout/test/__snapshots__/index.js.snap delete mode 100644 packages/components/src/flyout/test/index.js delete mode 100644 packages/components/src/flyout/types.ts delete mode 100644 packages/components/src/flyout/utils.js delete mode 100644 packages/components/src/popover/test/utils.js delete mode 100644 packages/components/src/popover/utils.js diff --git a/docs/manifest.json b/docs/manifest.json index 7ae12e9743f00..693f4bd420e52 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -815,12 +815,6 @@ "markdown_source": "../packages/components/src/flex/flex/README.md", "parent": "components" }, - { - "title": "Flyout", - "slug": "flyout", - "markdown_source": "../packages/components/src/flyout/flyout/README.md", - "parent": "components" - }, { "title": "FocalPointPicker", "slug": "focal-point-picker", diff --git a/package-lock.json b/package-lock.json index 61a7c8a683198..6d77b8c2bdd18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2724,6 +2724,28 @@ } } }, + "@floating-ui/core": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.6.2.tgz", + "integrity": "sha512-jktYRmZwmau63adUG3GKOAVCofBXkk55S/zQ94XOorAHhwqFIOFAy1rSp2N0Wp6/tGbe9V3u/ExlGZypyY17rg==" + }, + "@floating-ui/dom": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.4.5.tgz", + "integrity": "sha512-b+prvQgJt8pieaKYMSJBXHxX/DYwdLsAWxKYqnO5dO2V4oo/TYBZJAUQCVNjTWWsrs6o4VDrNcP9+E70HAhJdw==", + "requires": { + "@floating-ui/core": "^0.6.2" + } + }, + "@floating-ui/react-dom": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.6.3.tgz", + "integrity": "sha512-hC+pS5D6AgS2wWjbmSQ6UR6Kpy+drvWGJIri6e1EDGADTPsCaa4KzCgmCczHrQeInx9tqs81EyDmbKJYY2swKg==", + "requires": { + "@floating-ui/dom": "^0.4.5", + "use-isomorphic-layout-effect": "^1.1.1" + } + }, "@gar/promisify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", @@ -17266,6 +17288,7 @@ "@emotion/serialize": "^1.0.2", "@emotion/styled": "^11.6.0", "@emotion/utils": "1.0.0", + "@floating-ui/react-dom": "0.6.3", "@use-gesture/react": "^10.2.6", "@wordpress/a11y": "file:packages/a11y", "@wordpress/compose": "file:packages/compose", @@ -57866,8 +57889,7 @@ "use-isomorphic-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", - "integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==", - "dev": true + "integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==" }, "use-latest": { "version": "1.2.0", diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/index.js b/packages/block-editor/src/components/block-alignment-matrix-control/index.js index 6c3f34137d903..1a923fe2e057f 100644 --- a/packages/block-editor/src/components/block-alignment-matrix-control/index.js +++ b/packages/block-editor/src/components/block-alignment-matrix-control/index.js @@ -22,15 +22,11 @@ function BlockAlignmentMatrixControl( props ) { } = props; const icon = ; - const className = 'block-editor-block-alignment-matrix-control'; - const popoverClassName = `${ className }__popover`; - const isAlternate = true; return ( { const openOnArrowDown = ( event ) => { if ( ! isOpen && event.keyCode === DOWN ) { diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/style.scss b/packages/block-editor/src/components/block-alignment-matrix-control/style.scss deleted file mode 100644 index bc2fa4837bece..0000000000000 --- a/packages/block-editor/src/components/block-alignment-matrix-control/style.scss +++ /dev/null @@ -1,10 +0,0 @@ -.block-editor-block-alignment-matrix-control__popover { - .components-popover__content { - min-width: 0; - width: auto; - - > div { - padding: $grid-unit; - } - } -} diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js index 68fdc8955ee8e..de0fb77ea66bc 100644 --- a/packages/block-editor/src/components/block-popover/inbetween.js +++ b/packages/block-editor/src/components/block-popover/inbetween.js @@ -107,6 +107,8 @@ function BlockPopoverInbetween( { left: previousRect ? previousRect.right : nextRect.right, right: previousRect ? previousRect.left : nextRect.left, bottom: nextRect ? nextRect.top : previousRect.bottom, + height: 0, + width: 0, ownerDocument, }; } @@ -116,6 +118,8 @@ function BlockPopoverInbetween( { left: previousRect ? previousRect.left : nextRect.left, right: previousRect ? previousRect.right : nextRect.right, bottom: nextRect ? nextRect.top : previousRect.bottom, + height: 0, + width: 0, ownerDocument, }; } @@ -126,6 +130,8 @@ function BlockPopoverInbetween( { left: previousRect ? previousRect.left : nextRect.right, right: nextRect ? nextRect.right : previousRect.left, bottom: previousRect ? previousRect.bottom : nextRect.bottom, + height: 0, + width: 0, ownerDocument, }; } @@ -135,6 +141,8 @@ function BlockPopoverInbetween( { left: previousRect ? previousRect.right : nextRect.left, right: nextRect ? nextRect.left : previousRect.right, bottom: previousRect ? previousRect.bottom : nextRect.bottom, + height: 0, + width: 0, ownerDocument, }; }, [ previousElement, nextElement ] ); @@ -155,7 +163,6 @@ function BlockPopoverInbetween( { return ( div { - min-width: auto; - display: flex; - background: $white; - padding: 0; - - .components-menu-group { - margin: 0; - } -} - -.block-editor-block-switcher__popover .components-popover__content { - - .block-editor-block-styles { - margin: 0 -3px; // Remove the panel body padding while keeping it for the title. - } - - // Hide the bottom border on the last panel so it stacks with the popover. - .components-panel__body { - border: 0; - - // Elevate this so the hover style is visible. - position: relative; - z-index: 1; - } - - .components-panel__body + .components-panel__body { - border-top: $border-width solid $gray-200; - } -} - .block-editor-block-switcher__popover__preview__parent { .block-editor-block-switcher__popover__preview__container { position: absolute; top: -$grid-unit-15; - left: calc(100% + #{$grid-unit-40}); + left: calc(100% + #{$grid-unit-20}); } } @@ -138,7 +101,6 @@ // Position correctly. Needs specificity. &.components-popover { - margin-left: $grid-unit-05; margin-top: $grid-unit-15 - $border-width; } @@ -151,6 +113,7 @@ border: $border-width solid $gray-900; background: $white; border-radius: $radius-block-ui; + outline: none; } .block-editor-block-switcher__preview { diff --git a/packages/block-editor/src/components/colors-gradients/dropdown.js b/packages/block-editor/src/components/colors-gradients/dropdown.js index 5a04d65b6b5d8..299995d3eb6cc 100644 --- a/packages/block-editor/src/components/colors-gradients/dropdown.js +++ b/packages/block-editor/src/components/colors-gradients/dropdown.js @@ -121,13 +121,16 @@ export default function ColorGradientSettingsDropdown( { __experimentalIsRenderedInSidebar, ...props } ) { - const dropdownPosition = __experimentalIsRenderedInSidebar - ? 'bottom left' - : undefined; - const dropdownClassName = __experimentalIsItemGroup ? 'block-editor-panel-color-gradient-settings__dropdown' : 'block-editor-tools-panel-color-gradient-settings__dropdown'; + let popoverProps; + if ( __experimentalIsRenderedInSidebar ) { + popoverProps = { + placement: 'left-start', + offset: 36, + }; + } return ( // Only wrap with `ItemGroup` if these controls are being rendered @@ -170,7 +173,7 @@ export default function ColorGradientSettingsDropdown( { { ...props } > div { - width: $sidebar-width; - } -} - -@include break-medium() { - .block-editor-panel-color-gradient-settings__dropdown-content { - .components-popover__content { - margin-right: #{ math.div($sidebar-width, 2) + $grid-unit-20 } !important; - } - - &.is-from-top .components-popover__content { - margin-top: #{ -($grid-unit-60 + $grid-unit-15) } !important; - } - - &.is-from-bottom .components-popover__content { - margin-bottom: #{ -($grid-unit-60 + $grid-unit-15) } !important; - } - } + width: $sidebar-width; } .block-editor-panel-color-gradient-settings__dropdown:last-child > div { diff --git a/packages/block-editor/src/components/duotone-control/style.scss b/packages/block-editor/src/components/duotone-control/style.scss index f7bf47ee66c1d..d5b8c24bcdb9d 100644 --- a/packages/block-editor/src/components/duotone-control/style.scss +++ b/packages/block-editor/src/components/duotone-control/style.scss @@ -9,7 +9,7 @@ $popover-padding: $grid-unit-20; $swatch-columns: math.floor(math.div($popover-width + $swatch-gap - 2 * $popover-padding, $swatch-size + $swatch-gap)); .block-editor-duotone-control__popover { - > .components-popover__content > div { + > .components-popover__content { padding: $popover-padding; width: $popover-width; } @@ -34,9 +34,3 @@ $swatch-columns: math.floor(math.div($popover-width + $swatch-gap - 2 * $popover margin: $grid-unit-20 0; font-size: $helptext-font-size; } - -// Better align the popover under the swatch. -// @todo: when the positioning for popovers gets refactored, this can presumably be removed. -.block-editor-duotone-control__popover:not([data-y-axis="middle"][data-x-axis="right"]) > .components-popover__content { - margin-left: -14px; -} diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index da16f256bec2a..802718847648d 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -23,6 +23,7 @@ $block-inserter-tabs-height: 44px; .block-editor-inserter__popover.is-quick { .components-popover__content { border: none; + outline: none; .block-editor-inserter__quick-inserter > * { border-left: $border-width solid $gray-400; @@ -310,7 +311,7 @@ $block-inserter-tabs-height: 44px; border-top: $border-width solid $gray-300; } -.block-editor-inserter__popover.is-quick > .components-popover__content > div { +.block-editor-inserter__popover.is-quick > .components-popover__content { padding: 0; } diff --git a/packages/block-editor/src/components/list-view/drop-indicator.js b/packages/block-editor/src/components/list-view/drop-indicator.js index 15f5d68212a8a..b242ae62ca86f 100644 --- a/packages/block-editor/src/components/list-view/drop-indicator.js +++ b/packages/block-editor/src/components/list-view/drop-indicator.js @@ -110,7 +110,6 @@ export default function ListViewDropIndicator( { return ( .components-popover__content { +.block-editor-list-view-drop-indicator > .components-popover__content { margin-left: 0; border: none; box-shadow: none; + outline: none; } .block-editor-list-view-placeholder { diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 34ffa790f4ce3..4025468f4abbb 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -29,7 +29,12 @@ function hasFocusWithin( container ) { function focusFirstTabbableIn( container ) { const [ firstTabbable ] = focus.tabbable.find( container ); if ( firstTabbable ) { - firstTabbable.focus(); + firstTabbable.focus( { + // When focusing newly mounted toolbars, + // the position of the popover is often not right on the first render + // This prevents the layout shifts when focusing the dialogs. + preventScroll: true, + } ); } } @@ -119,7 +124,12 @@ function useToolbarFocus( const items = getAllToolbarItemsIn( ref.current ); const index = initialIndex || 0; if ( items[ index ] && hasFocusWithin( ref.current ) ) { - items[ index ].focus(); + items[ index ].focus( { + // When focusing newly mounted toolbars, + // the position of the popover is often not right on the first render + // This prevents the layout shifts when focusing the dialogs. + preventScroll: true, + } ); } } ); } diff --git a/packages/block-editor/src/components/preview-options/style.scss b/packages/block-editor/src/components/preview-options/style.scss index 166764b35498f..b25578c1d2051 100644 --- a/packages/block-editor/src/components/preview-options/style.scss +++ b/packages/block-editor/src/components/preview-options/style.scss @@ -11,10 +11,6 @@ } .block-editor-post-preview__dropdown-content { - .components-popover__content { - overflow-y: visible; - } - &.edit-post-post-preview-dropdown { .components-menu-group { &:first-child { diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.js index 7d32b911d9de0..08ea1e7e66fdf 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar-container.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.js @@ -14,7 +14,6 @@ const FormatToolbarContainer = ( { inline, anchorRef } ) => { // Render in popover. return ( ) } +
    .components-popover__content { - width: 360px; -} - .block-library-video-tracks-editor__track-list-track { display: flex; place-content: space-between; @@ -75,9 +71,11 @@ display: block; } -.block-library-video-tracks-editor > .components-popover__content > div { +.block-library-video-tracks-editor > .components-popover__content { + width: 360px; padding: 0; } + .block-library-video-tracks-editor__track-list, .block-library-video-tracks-editor__add-tracks-container { .components-menu-group__label { @@ -85,7 +83,6 @@ } } - .block-library-video-tracks-editor__single-track-editor, .block-library-video-tracks-editor__track-list, .block-library-video-tracks-editor__add-tracks-container { diff --git a/packages/components/package.json b/packages/components/package.json index 15ea54baa7d6b..1e791128d2937 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -36,6 +36,7 @@ "@emotion/serialize": "^1.0.2", "@emotion/styled": "^11.6.0", "@emotion/utils": "1.0.0", + "@floating-ui/react-dom": "0.6.3", "@use-gesture/react": "^10.2.6", "@wordpress/a11y": "file:../a11y", "@wordpress/compose": "file:../compose", diff --git a/packages/components/src/autocomplete/style.scss b/packages/components/src/autocomplete/style.scss index b7060e4a46918..4b8434caacf45 100644 --- a/packages/components/src/autocomplete/style.scss +++ b/packages/components/src/autocomplete/style.scss @@ -1,4 +1,4 @@ -.components-autocomplete__popover .components-popover__content > div { +.components-autocomplete__popover .components-popover__content { padding: $grid-unit-20; min-width: 220px; } diff --git a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx index add245b0d5e94..af6249f7b4ca9 100644 --- a/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx +++ b/packages/components/src/border-box-control/border-box-control-split-controls/component.tsx @@ -2,6 +2,8 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useRef } from '@wordpress/element'; +import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies @@ -25,13 +27,23 @@ const BorderBoxControlSplitControls = ( enableAlpha, enableStyle, onChange, - popoverClassNames, + popoverPlacement, + popoverOffset, value, __experimentalHasMultipleOrigins, __experimentalIsRenderedInSidebar, __next36pxDefaultSize, ...otherProps } = useBorderBoxControlSplitControls( props ); + const containerRef = useRef(); + const mergedRef = useMergeRefs( [ containerRef, forwardedRef ] ); + const popoverProps = popoverPlacement + ? { + placement: popoverPlacement, + offset: popoverOffset, + anchorRef: containerRef, + } + : undefined; const sharedBorderControlProps = { colors, @@ -45,7 +57,7 @@ const BorderBoxControlSplitControls = ( }; return ( - + onChange( newBorder, 'top' ) } - popoverContentClassName={ popoverClassNames?.top } + __unstablePopoverProps={ popoverProps } value={ value?.top } { ...sharedBorderControlProps } /> @@ -63,7 +75,7 @@ const BorderBoxControlSplitControls = ( hideLabelFromVision={ true } label={ __( 'Left border' ) } onChange={ ( newBorder ) => onChange( newBorder, 'left' ) } - popoverContentClassName={ popoverClassNames?.left } + __unstablePopoverProps={ popoverProps } value={ value?.left } { ...sharedBorderControlProps } /> @@ -71,7 +83,7 @@ const BorderBoxControlSplitControls = ( hideLabelFromVision={ true } label={ __( 'Right border' ) } onChange={ ( newBorder ) => onChange( newBorder, 'right' ) } - popoverContentClassName={ popoverClassNames?.right } + __unstablePopoverProps={ popoverProps } value={ value?.right } { ...sharedBorderControlProps } /> @@ -80,7 +92,7 @@ const BorderBoxControlSplitControls = ( hideLabelFromVision={ true } label={ __( 'Bottom border' ) } onChange={ ( newBorder ) => onChange( newBorder, 'bottom' ) } - popoverContentClassName={ popoverClassNames?.bottom } + __unstablePopoverProps={ popoverProps } value={ value?.bottom } { ...sharedBorderControlProps } /> diff --git a/packages/components/src/border-box-control/border-box-control/README.md b/packages/components/src/border-box-control/border-box-control/README.md index 67a9426823f0f..c895c1b853de1 100644 --- a/packages/components/src/border-box-control/border-box-control/README.md +++ b/packages/components/src/border-box-control/border-box-control/README.md @@ -122,23 +122,19 @@ _Note: The will be `undefined` if a user clears all borders._ - Required: Yes -### `popoverClassNames`: `Object` +### `popoverPlacement`: `string` -An object defining CSS classnames for all the inner `BorderControl` popover -content. +The position of the color popover relative to the control wrapper. -Example: -```js -{ - linked: 'linked-border-popover-content', - top: 'top-border-popover-content', - right: 'right-border-popover-content', - bottom: 'bottom-border-popover-content', - left: 'left-border-popover-content', -} -``` +By default, popovers are displayed relative to the button that initiated the popover. By supplying a popover placement, you force the popover to display in a specific location. + +The available base placements are 'top', 'right', 'bottom', 'left'. Each of these base placements has an alignment in the form -start and -end. For example, 'right-start', or 'bottom-end'. These allow you to align the tooltip to the edges of the button, rather than centering it. + +- Required: No + +### `popoverOffset`: `number` -By default, popovers are displayed relative to the button that initiated the popover. By supplying classnames for each individual popover, it is possible to add styling rules to align the popover positions to an unrelated design element, for example, the sidebar inspector in the block editor. +Works in conjunctions with `popoverPlacement` and allows leaving a space between the color popover and the control wrapper. - Required: No diff --git a/packages/components/src/border-box-control/border-box-control/component.tsx b/packages/components/src/border-box-control/border-box-control/component.tsx index c51de26a1dfdd..654bd0ec4ce1f 100644 --- a/packages/components/src/border-box-control/border-box-control/component.tsx +++ b/packages/components/src/border-box-control/border-box-control/component.tsx @@ -2,6 +2,8 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useRef } from '@wordpress/element'; +import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies @@ -51,7 +53,8 @@ const BorderBoxControl = ( linkedValue, onLinkedChange, onSplitChange, - popoverClassNames, + popoverPlacement, + popoverOffset, splitValue, toggleLinked, __experimentalHasMultipleOrigins, @@ -59,9 +62,18 @@ const BorderBoxControl = ( __next36pxDefaultSize = false, ...otherProps } = useBorderBoxControl( props ); + const containerRef = useRef(); + const mergedRef = useMergeRefs( [ containerRef, forwardedRef ] ); + const popoverProps = popoverPlacement + ? { + placement: popoverPlacement, + offset: popoverOffset, + anchorRef: containerRef, + } + : undefined; return ( - + void; /** - * An object defining CSS classnames for all the inner `BorderControl` - * popover content. + * The position of the color popovers compared to the control wrapper. + */ + popoverPlacement?: string; + /** + * The space between the popover and the control wrapper. */ - popoverClassNames?: PopoverClassNames; + popoverOffset?: number; /** * An object representing the current border configuration. * @@ -106,10 +101,13 @@ export type SplitControlsProps = ColorProps & { */ onChange: ( value: Border | undefined, side: BorderSide ) => void; /** - * An object defining CSS classnames for the split side `BorderControl`s' - * popover content. + * The position of the color popovers compared to the control wrapper. + */ + popoverPlacement?: string; + /** + * The space between the popover and the control wrapper. */ - popoverClassNames?: PopoverClassNames; + popoverOffset?: number; /** * An object representing the current border configuration. It contains * properties for each side, with each side an object reflecting the border diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx index 80940164afcd0..6d8ee795497dc 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -147,6 +147,7 @@ const BorderControlDropdown = ( resetButtonClassName, showDropdownHeader, enableStyle = true, + __unstablePopoverProps, ...otherProps } = useBorderControlDropdown( props ); @@ -240,7 +241,10 @@ const BorderControlDropdown = ( diff --git a/packages/components/src/border-control/border-control-dropdown/hook.ts b/packages/components/src/border-control/border-control-dropdown/hook.ts index 25d8652148468..aab058874ae96 100644 --- a/packages/components/src/border-control/border-control-dropdown/hook.ts +++ b/packages/components/src/border-control/border-control-dropdown/hook.ts @@ -20,7 +20,6 @@ export function useBorderControlDropdown( border, className, colors, - contentClassName, onChange, previousStyleSelection, __next36pxDefaultSize, @@ -68,8 +67,8 @@ export function useBorderControlDropdown( }, [ border, cx, __next36pxDefaultSize ] ); const popoverClassName = useMemo( () => { - return cx( styles.borderControlPopover, contentClassName ); - }, [ cx, contentClassName ] ); + return cx( styles.borderControlPopover ); + }, [ cx ] ); const popoverControlsClassName = useMemo( () => { return cx( styles.borderControlPopoverControls ); diff --git a/packages/components/src/border-control/border-control/README.md b/packages/components/src/border-control/border-control/README.md index aa8b439d8326b..a4d0f2017f2d6 100644 --- a/packages/components/src/border-control/border-control/README.md +++ b/packages/components/src/border-control/border-control/README.md @@ -113,13 +113,6 @@ _Note: the value may be `undefined` if a user clears all border properties._ - Required: Yes -### `popoverContentClassName`: `string` - -A custom CSS class name to be assigned to the `BorderControl`'s dropdown -popover content. - -- Required: No - ### `shouldSanitizeBorder`: `boolean` If opted into, sanitizing the border means that if no width or color have been diff --git a/packages/components/src/border-control/border-control/component.tsx b/packages/components/src/border-control/border-control/component.tsx index 59e6743e9e5a1..942e8b00104da 100644 --- a/packages/components/src/border-control/border-control/component.tsx +++ b/packages/components/src/border-control/border-control/component.tsx @@ -43,7 +43,7 @@ const BorderControl = ( onSliderChange, onWidthChange, placeholder, - popoverContentClassName, + __unstablePopoverProps, previousStyleSelection, showDropdownHeader, sliderClassName, @@ -69,7 +69,7 @@ const BorderControl = ( div > div { + && .components-popover__content { padding: 0; + width: 264px; } `; diff --git a/packages/components/src/border-control/types.ts b/packages/components/src/border-control/types.ts index 049c278a5e71f..0605b2e40fe41 100644 --- a/packages/components/src/border-control/types.ts +++ b/packages/components/src/border-control/types.ts @@ -81,10 +81,9 @@ export type BorderControlProps = ColorProps & */ onChange: ( value?: Border ) => void; /** - * A custom CSS class name to be assigned to the border control's - * dropdown popover content. + * An internal prop used to control the visibility of the dropdown. */ - popoverContentClassName?: string; + __unstablePopoverProps?: Record< string, unknown >; /** * If opted into, sanitizing the border means that if no width or color * have been selected, the border style is also cleared and `undefined` @@ -131,10 +130,9 @@ export type DropdownProps = ColorProps & { */ border?: Border; /** - * A custom CSS class name to be assigned to the border control's - * dropdown popover content. + * An internal prop used to control the visibility of the dropdown. */ - contentClassName?: string; + __unstablePopoverProps?: Record< string, unknown >; /** * This controls whether to render border style options. * diff --git a/packages/components/src/color-palette/index.js b/packages/components/src/color-palette/index.js index 9e2745fd57574..4c92fd35b1822 100644 --- a/packages/components/src/color-palette/index.js +++ b/packages/components/src/color-palette/index.js @@ -6,7 +6,6 @@ import { map } from 'lodash'; import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; import a11yPlugin from 'colord/plugins/a11y'; -import classnames from 'classnames'; /** * WordPress dependencies @@ -116,12 +115,12 @@ function MultiplePalettes( { export function CustomColorPickerDropdown( { isRenderedInSidebar, ...props } ) { return ( ); @@ -176,11 +175,6 @@ export default function ColorPalette( { /> ); - let dropdownPosition; - if ( __experimentalIsRenderedInSidebar ) { - dropdownPosition = 'bottom left'; - } - const colordColor = colord( value ); const valueWithoutLeadingHash = value?.startsWith( '#' ) @@ -211,7 +205,6 @@ export default function ColorPalette( { { ! disableCustomColors && ( ( diff --git a/packages/components/src/color-palette/style.scss b/packages/components/src/color-palette/style.scss index 3aab4914deee3..1abb50c6e3528 100644 --- a/packages/components/src/color-palette/style.scss +++ b/packages/components/src/color-palette/style.scss @@ -30,31 +30,16 @@ overflow: visible; box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05); border: none; + outline: none; border-radius: $radius-block-ui; - max-height: fit-content !important; - & > div { - padding: 0; - } + padding: 0; + .react-colorful__saturation { border-top-right-radius: $radius-block-ui; border-top-left-radius: $radius-block-ui; } } -@include break-medium() { - .components-dropdown__content.components-color-palette__custom-color-dropdown-content.is-rendered-in-sidebar { - .components-popover__content.components-popover__content { - margin-right: #{ math.div($sidebar-width, 2) + $grid-unit-20 }; - } - &.is-from-top .components-popover__content { - margin-top: #{ -($grid-unit-60 + $grid-unit-15) }; - } - &.is-from-bottom .components-popover__content { - margin-bottom: #{ -($grid-unit-60 + $grid-unit-15) }; - } - } -} - .components-color-palette__custom-color-name { text-align: left; } diff --git a/packages/components/src/dropdown/index.js b/packages/components/src/dropdown/index.js index d9ebd10fab9ff..ce080c1be0adb 100644 --- a/packages/components/src/dropdown/index.js +++ b/packages/components/src/dropdown/index.js @@ -31,12 +31,12 @@ export default function Dropdown( props ) { const { renderContent, renderToggle, - position = 'bottom right', className, contentClassName, expandOnMobile, headerTitle, focusOnMount, + position, popoverProps, onClose, onToggle, @@ -82,6 +82,10 @@ export default function Dropdown( props ) { } const args = { isOpen, onToggle: toggle, onClose: close }; + const hasAnchorRef = + !! popoverProps?.anchorRef || + !! popoverProps?.getAnchorRect || + !! popoverProps?.anchorRect; return (
    div { + .components-popover__content { padding: $grid-unit-10; } diff --git a/packages/components/src/flyout/context.js b/packages/components/src/flyout/context.js deleted file mode 100644 index f1678c99e3efd..0000000000000 --- a/packages/components/src/flyout/context.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * WordPress dependencies - */ -import { createContext, useContext } from '@wordpress/element'; - -/** - * @type {import('react').Context} - */ -export const FlyoutContext = createContext( {} ); -export const useFlyoutContext = () => useContext( FlyoutContext ); diff --git a/packages/components/src/flyout/flyout-content/component.js b/packages/components/src/flyout/flyout-content/component.js deleted file mode 100644 index e1f415e2dda1b..0000000000000 --- a/packages/components/src/flyout/flyout-content/component.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Internal dependencies - */ -import { useFlyoutContext } from '../context'; -import { FlyoutContentView, CardView } from '../styles'; -import { contextConnect, useContextSystem } from '../../ui/context'; - -/** - * - * @param {import('../../ui/context').WordPressComponentProps} props - * @param {import('react').ForwardedRef} forwardedRef forwardedRef - */ -function FlyoutContent( props, forwardedRef ) { - const { - children, - elevation, - maxWidth, - style = {}, - ...otherProps - } = useContextSystem( props, 'FlyoutContent' ); - - const { label, flyoutState } = useFlyoutContext(); - - if ( ! flyoutState ) { - throw new Error( - '`FlyoutContent` must only be used inside a `Flyout`.' - ); - } - - const showContent = flyoutState.visible || flyoutState.animating; - - return ( - - { showContent && ( - - { children } - - ) } - - ); -} - -const ConnectedFlyoutContent = contextConnect( FlyoutContent, 'FlyoutContent' ); - -export default ConnectedFlyoutContent; diff --git a/packages/components/src/flyout/flyout-content/index.js b/packages/components/src/flyout/flyout-content/index.js deleted file mode 100644 index b404d7fd44a81..0000000000000 --- a/packages/components/src/flyout/flyout-content/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './component'; diff --git a/packages/components/src/flyout/flyout/README.md b/packages/components/src/flyout/flyout/README.md deleted file mode 100644 index 49aa328bd6b1f..0000000000000 --- a/packages/components/src/flyout/flyout/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Flyout - -
    -This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. -
    - -`Flyout` is a component to render a floating content modal. It is similar in purpose to a tooltip, but renders content of any sort, not only simple text. - -## Usage - -```jsx -import { Button, __experimentalFlyout as Flyout, __experimentalText as Text } from '@wordpress/components'; - -function Example() { - return ( - Show/Hide content }> - Code is Poetry - - ); -} -``` - -## Props - -### `state`: `PopoverStateReturn` - -- Required: No - -### `label`: `string` - -- Required: No - -### `animated`: `boolean` - -Determines if `Flyout` has animations. - -- Required: No -- Default: `true` - -### `animationDuration`: `boolean` - -The duration of `Flyout` animations. - -- Required: No -- Default: `160` - -### `baseId`: `string` - -ID that will serve as a base for all the items IDs. See https://reakit.io/docs/popover/#usepopoverstate - -- Required: No -- Default: `160` - -### `elevation`: `number` - -Size of the elevation shadow. For more information, check out [`Card`](/packages/components/src/card/card/README.md#props). - -- Required: No -- Default: `5` - -### `maxWidth`: `CSSProperties[ 'maxWidth' ]` - -Max-width for the `Flyout` element. - -- Required: No -- Default: `360` - -### `onVisibleChange`: `( ...args: any ) => void` - -Callback for when the `visible` state changes. - -- Required: No - -### `trigger`: `FunctionComponentElement< any >` - -Element that triggers the `visible` state of `Flyout` when clicked. - -```jsx -Greet}> - Hi! I'm Olaf! - -``` - -- Required: Yes - -### `visible`: `boolean` - -Whether `Flyout` is visible. See [the `Reakit` docs](https://reakit.io/docs/popover/#usepopoverstate) for more information. - -- Required: No -- Default: `false` - -### `placement`: `PopperPlacement` - -Position of the popover element. See [the `popper` docs](https://popper.js.org/docs/v1/#popperplacements--codeenumcode) for more information. - -- Required: No -- Default: `auto` diff --git a/packages/components/src/flyout/flyout/component.js b/packages/components/src/flyout/flyout/component.js deleted file mode 100644 index 1ad283d0ab1d6..0000000000000 --- a/packages/components/src/flyout/flyout/component.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * External dependencies - */ -// eslint-disable-next-line no-restricted-imports -import { PopoverDisclosure, Portal } from 'reakit'; - -/** - * WordPress dependencies - */ -import { useCallback, useMemo, cloneElement } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { contextConnect } from '../../ui/context'; -import { FlyoutContext } from '../context'; -import { useFlyoutResizeUpdater } from '../utils'; -import FlyoutContent from '../flyout-content'; -import { useUpdateEffect } from '../../utils/hooks'; -import { useFlyout } from './hook'; - -/** - * - * @param {import('../../ui/context').WordPressComponentProps} props - * @param {import('react').ForwardedRef} forwardedRef - */ -function Flyout( props, forwardedRef ) { - const { - children, - elevation, - label, - maxWidth, - onVisibleChange, - trigger, - flyoutState, - ...otherProps - } = useFlyout( props ); - - const resizeListener = useFlyoutResizeUpdater( { - onResize: flyoutState.unstable_update, - } ); - - const uniqueId = `flyout-${ flyoutState.baseId }`; - const labelId = label || uniqueId; - - const contextProps = useMemo( - () => ( { - label: labelId, - flyoutState, - } ), - [ labelId, flyoutState ] - ); - - const triggerContent = useCallback( - ( triggerProps ) => { - return cloneElement( trigger, triggerProps ); - }, - [ trigger ] - ); - - useUpdateEffect( () => { - onVisibleChange?.( flyoutState.visible ); - }, [ flyoutState.visible ] ); - - return ( - - { trigger && ( - - { triggerContent } - - ) } - - - { resizeListener } - { children } - - - - ); -} - -/** - * `Flyout` is a component to render a floating content modal. - * It is similar in purpose to a tooltip, but renders content of any sort, - * not only simple text. - * - * @example - * ```jsx - * import { Button, __experimentalFlyout as Flyout, __experimentalText as } from '@wordpress/components'; - * - * function Example() { - * return ( - * Show/Hide content }> - * Code is Poetry - * - * ); - * } - * ``` - */ -const ConnectedFlyout = contextConnect( Flyout, 'Flyout' ); - -export default ConnectedFlyout; diff --git a/packages/components/src/flyout/flyout/hook.js b/packages/components/src/flyout/flyout/hook.js deleted file mode 100644 index d917c1011f4b9..0000000000000 --- a/packages/components/src/flyout/flyout/hook.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * External dependencies - */ -// eslint-disable-next-line no-restricted-imports -import { usePopoverState } from 'reakit'; - -/** - * Internal dependencies - */ -import { useContextSystem } from '../../ui/context'; - -/** - * @param {import('../../ui/context').WordPressComponentProps} props - */ -export function useFlyout( props ) { - const { - animated = true, - animationDuration = 160, - baseId, - elevation = 5, - id, - maxWidth = 360, - placement, - state, - visible, - ...otherProps - } = useContextSystem( props, 'Flyout' ); - - const _flyoutState = usePopoverState( { - animated: animated ? animationDuration : undefined, - baseId: baseId || id, - placement, - visible, - ...otherProps, - } ); - - const flyoutState = state || _flyoutState; - - return { - ...otherProps, - elevation, - maxWidth, - flyoutState, - }; -} diff --git a/packages/components/src/flyout/flyout/index.js b/packages/components/src/flyout/flyout/index.js deleted file mode 100644 index ef5d7d27828f8..0000000000000 --- a/packages/components/src/flyout/flyout/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './component'; -export { useFlyout } from './hook'; diff --git a/packages/components/src/flyout/index.js b/packages/components/src/flyout/index.js deleted file mode 100644 index 28e34de445ba0..0000000000000 --- a/packages/components/src/flyout/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as Flyout } from './flyout'; diff --git a/packages/components/src/flyout/stories/index.js b/packages/components/src/flyout/stories/index.js deleted file mode 100644 index df8f41b8f279e..0000000000000 --- a/packages/components/src/flyout/stories/index.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Internal dependencies - */ -import { CardBody, CardHeader } from '../../card'; -import Button from '../../button'; -import { Flyout } from '..'; - -export default { - component: Flyout, - title: 'Components (Experimental)/Flyout', -}; - -export const _default = () => { - return ( - Click } - visible - placement="bottom-start" - > - Go - Stuff - - ); -}; diff --git a/packages/components/src/flyout/styles.ts b/packages/components/src/flyout/styles.ts deleted file mode 100644 index 3ac211d808ffd..0000000000000 --- a/packages/components/src/flyout/styles.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * External dependencies - */ -import styled, { StyledComponent } from '@emotion/styled'; -// eslint-disable-next-line no-restricted-imports -import { Popover as ReakitPopover } from 'reakit'; - -/** - * Internal dependencies - */ -import { Card, CardBody } from '../card'; -import * as ZIndex from '../utils/z-index'; -import CONFIG from '../utils/config-values'; - -export const FlyoutContentView: StyledComponent< - React.ComponentPropsWithoutRef< typeof ReakitPopover > -> = styled( ReakitPopover )` - z-index: ${ ZIndex.Flyout }; - box-sizing: border-box; - opacity: 0; - outline: none; - position: relative; - transform-origin: center center; - transition: opacity ${ CONFIG.transitionDurationFastest } linear; - width: 100%; - - &[data-enter] { - opacity: 1; - } - - &::before, - &::after { - display: none; - } -`; - -export const CardView = styled( Card )` - ${ CardBody.selector } { - max-height: 80vh; - } -`; diff --git a/packages/components/src/flyout/test/__snapshots__/index.js.snap b/packages/components/src/flyout/test/__snapshots__/index.js.snap deleted file mode 100644 index 60c6d3fe10500..0000000000000 --- a/packages/components/src/flyout/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,183 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`props should render correctly 1`] = ` -.emotion-1 { - background-color: #fff; - color: #1e1e1e; - position: relative; - box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1); - outline: none; - border-radius: calc(2px - 1px); -} - -.emotion-1 .components-card-body { - max-height: 80vh; -} - -.emotion-3 { - height: 100%; -} - -.emotion-5 { - box-sizing: border-box; - height: auto; - max-height: 100%; - padding: calc(4px * 4) calc(4px * 6); -} - -.emotion-5:first-of-type { - border-top-left-radius: calc(2px - 1px); - border-top-right-radius: calc(2px - 1px); -} - -.emotion-5:last-of-type { - border-bottom-left-radius: calc(2px - 1px); - border-bottom-right-radius: calc(2px - 1px); -} - -.emotion-7 { - background: transparent; - display: block; - margin: 0!important; - pointer-events: none; - position: absolute; - will-change: box-shadow; - border-radius: inherit; - bottom: 0; - box-shadow: 0 1px 2px 0 rgba(0 ,0, 0, 0.05); - opacity: 1; - left: 0; - right: 0; - top: 0; - -webkit-transition: box-shadow 200ms cubic-bezier(0.08, 0.52, 0.52, 1); - transition: box-shadow 200ms cubic-bezier(0.08, 0.52, 0.52, 1); - border-radius: 2px; -} - -@media ( prefers-reduced-motion: reduce ) { - .emotion-7 { - transition-duration: 0ms; - } -} - -.emotion-9 { - background: transparent; - display: block; - margin: 0!important; - pointer-events: none; - position: absolute; - will-change: box-shadow; - border-radius: inherit; - bottom: 0; - box-shadow: 0 5px 10px 0 rgba(0 ,0, 0, 0.25); - opacity: 1; - left: 0; - right: 0; - top: 0; - -webkit-transition: box-shadow 200ms cubic-bezier(0.08, 0.52, 0.52, 1); - transition: box-shadow 200ms cubic-bezier(0.08, 0.52, 0.52, 1); - border-radius: 2px; -} - -@media ( prefers-reduced-motion: reduce ) { - .emotion-9 { - transition-duration: 0ms; - } -} - -
    -
    - -