From 31bd66c18f532d520b7e83a0a5b9bcbf56357690 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 28 Feb 2024 23:02:28 +0900 Subject: [PATCH 01/45] CustomSelectControlV2: Remove legacy adapter layer (#59420) * CustomSelectControlV2: Remove legacy adapter layer * Remove `never` type for `children` prop on legacy * Add changelog * Remove unnecessary `describe` wrapper in tests Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: aaronrobertshaw Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 4 + .../default-component/index.tsx | 5 +- .../src/custom-select-control-v2/index.tsx | 2 +- .../legacy-adapter.tsx | 25 - .../legacy-component/test/index.tsx | 457 ++++++++ .../stories/legacy.story.tsx | 11 +- .../custom-select-control-v2/test/index.tsx | 1028 +++++------------ .../src/custom-select-control-v2/types.ts | 1 - 8 files changed, 750 insertions(+), 783 deletions(-) delete mode 100644 packages/components/src/custom-select-control-v2/legacy-adapter.tsx create mode 100644 packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 769d201e587d07..e20b3444468fe6 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,10 @@ - `SnackbarList`, `Snackbar`: add unit tests ([#59157](https://github.com/WordPress/gutenberg/pull/59157)). +### Experimental + +- `CustomSelectControlV2`: Remove legacy adapter layer ([#59420](https://github.com/WordPress/gutenberg/pull/59420)). + ## 27.0.0 (2024-02-21) ### Breaking Changes diff --git a/packages/components/src/custom-select-control-v2/default-component/index.tsx b/packages/components/src/custom-select-control-v2/default-component/index.tsx index 746861ed03b5a5..e5650202a41605 100644 --- a/packages/components/src/custom-select-control-v2/default-component/index.tsx +++ b/packages/components/src/custom-select-control-v2/default-component/index.tsx @@ -8,8 +8,11 @@ import * as Ariakit from '@ariakit/react'; */ import _CustomSelect from '../custom-select'; import type { CustomSelectProps } from '../types'; +import type { WordPressComponentProps } from '../../context'; -function CustomSelect( props: CustomSelectProps ) { +function CustomSelect( + props: WordPressComponentProps< CustomSelectProps, 'button', false > +) { const { defaultValue, onChange, value, ...restProps } = props; // Forward props + store from v2 implementation const store = Ariakit.useSelectStore( { diff --git a/packages/components/src/custom-select-control-v2/index.tsx b/packages/components/src/custom-select-control-v2/index.tsx index 58ca626be91619..f05191ad8fc0b1 100644 --- a/packages/components/src/custom-select-control-v2/index.tsx +++ b/packages/components/src/custom-select-control-v2/index.tsx @@ -1,5 +1,5 @@ /** * Internal dependencies */ -export { default as CustomSelect } from './legacy-adapter'; +export { default as CustomSelect } from './default-component'; export { default as CustomSelectItem } from './custom-select-item'; diff --git a/packages/components/src/custom-select-control-v2/legacy-adapter.tsx b/packages/components/src/custom-select-control-v2/legacy-adapter.tsx deleted file mode 100644 index ab7fc74d977929..00000000000000 --- a/packages/components/src/custom-select-control-v2/legacy-adapter.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Internal dependencies - */ -import _LegacyCustomSelect from './legacy-component'; -import _NewCustomSelect from './default-component'; -import type { CustomSelectProps, LegacyCustomSelectProps } from './types'; -import type { WordPressComponentProps } from '../context'; - -function isLegacy( props: any ): props is LegacyCustomSelectProps { - return typeof props.options !== 'undefined'; -} - -function CustomSelect( - props: - | LegacyCustomSelectProps - | WordPressComponentProps< CustomSelectProps, 'button', false > -) { - if ( isLegacy( props ) ) { - return <_LegacyCustomSelect { ...props } />; - } - - return <_NewCustomSelect { ...props } />; -} - -export default CustomSelect; diff --git a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx new file mode 100644 index 00000000000000..dbb1ac1d784022 --- /dev/null +++ b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx @@ -0,0 +1,457 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import { click, press, sleep, type, waitFor } from '@ariakit/test'; + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import UncontrolledCustomSelect from '..'; + +const customClass = 'amber-skies'; + +const legacyProps = { + label: 'label!', + options: [ + { + key: 'flower1', + name: 'violets', + }, + { + key: 'flower2', + name: 'crimson clover', + className: customClass, + }, + { + key: 'flower3', + name: 'poppy', + }, + { + key: 'color1', + name: 'amber', + className: customClass, + }, + { + key: 'color2', + name: 'aquamarine', + style: { + backgroundColor: 'rgb(127, 255, 212)', + rotate: '13deg', + }, + }, + ], +}; + +const LegacyControlledCustomSelect = ( { + options, + onChange, + ...restProps +}: React.ComponentProps< typeof UncontrolledCustomSelect > ) => { + const [ value, setValue ] = useState( options[ 0 ] ); + return ( + { + onChange?.( args ); + setValue( args.selectedItem ); + } } + value={ options.find( + ( option: any ) => option.key === value.key + ) } + /> + ); +}; + +describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', LegacyControlledCustomSelect ], +] )( 'CustomSelectControl (%s)', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; + + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + + await click( currentSelectedItem ); + + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( + legacyProps.options[ 0 ].name + ); + } ); + + it( 'Should apply class only to options that have a className defined', async () => { + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ a className added + const itemsWithClass = legacyProps.options.filter( + ( option ) => option.className !== undefined + ); + + // assert against filtered array + itemsWithClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveClass( + customClass + ) + ); + + // return an array of items _without_ a className added + const itemsWithoutClass = legacyProps.options.filter( + ( option ) => option.className === undefined + ); + + // assert against filtered array + itemsWithoutClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass( + customClass + ) + ); + } ); + + it( 'Should apply styles only to options that have styles defined', async () => { + const customStyles = + 'background-color: rgb(127, 255, 212); rotate: 13deg;'; + + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + // return an array of items _with_ styles added + const styledItems = legacyProps.options.filter( + ( option ) => option.style !== undefined + ); + + // assert against filtered array + styledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( + customStyles + ) + ); + + // return an array of items _without_ styles added + const unstyledItems = legacyProps.options.filter( + ( option ) => option.style === undefined + ); + + // assert against filtered array + unstyledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveStyle( + customStyles + ) + ); + } ); + + it( 'does not show selected hint by default', async () => { + render( + + ); + await waitFor( () => + expect( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ).not.toHaveTextContent( 'Hint' ) + ); + } ); + + it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { + render( + + ); + + await waitFor( () => + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveTextContent( /hint/i ) + ); + } ); + + it( 'shows selected hint in list of options when added', async () => { + render( + + ); + + await click( + screen.getByRole( 'combobox', { name: 'Custom select' } ) + ); + + expect( screen.getByRole( 'option', { name: /hint/i } ) ).toBeVisible(); + } ); + + it( 'Should return object onChange', async () => { + const mockOnChange = jest.fn(); + + render( ); + + await click( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 1, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: { key: 'violets', name: 'violets' }, + type: '', + } ) + ); + + await click( + screen.getByRole( 'option', { + name: 'aquamarine', + } ) + ); + + expect( mockOnChange ).toHaveBeenNthCalledWith( + 2, + expect.objectContaining( { + inputValue: '', + isOpen: false, + selectedItem: expect.objectContaining( { + name: 'aquamarine', + } ), + type: '', + } ) + ); + } ); + + it( 'Should return selectedItem object when specified onChange', async () => { + const mockOnChange = jest.fn( + ( { selectedItem } ) => selectedItem.key + ); + + render( ); + + await sleep(); + await press.Tab(); + expect( + screen.getByRole( 'combobox', { + expanded: false, + } ) + ).toHaveFocus(); + + await type( 'p' ); + await press.Enter(); + + expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await press.ArrowDown(); + await press.Enter(); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + } ); + + it( 'Should be able to type characters to select matching options', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await type( 'a' ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); + } ); + + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await type( 'aq' ); + + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); + + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + } ); + + it( 'Should have correct aria-selected value for selections', async () => { + render( ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); + + await click( currentSelectedItem ); + + // get all items except for first option + const unselectedItems = legacyProps.options.filter( + ( { name } ) => name !== legacyProps.options[ 0 ].name + ); + + // assert that all other items have aria-selected="false" + unselectedItems.map( ( { name } ) => + expect( + screen.getByRole( 'option', { name, selected: false } ) + ).toBeVisible() + ); + + // assert that first item has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: true, + } ) + ).toBeVisible(); + + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + + // click combobox to mount listbox with options again + await click( currentSelectedItem ); + + // check that first item is has aria-selected="false" after new selection + expect( + screen.getByRole( 'option', { + name: legacyProps.options[ 0 ].name, + selected: false, + } ) + ).toBeVisible(); + + // check that new selected item now has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'poppy', + selected: true, + } ) + ).toBeVisible(); + } ); + } ); +} ); diff --git a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx index 9faa285cd72abb..f97b2376e9debd 100644 --- a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx +++ b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx @@ -11,12 +11,11 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import _LegacyCustomSelect from '../legacy-component'; -import { CustomSelect } from '..'; +import CustomSelect from '../legacy-component'; -const meta: Meta< typeof _LegacyCustomSelect > = { +const meta: Meta< typeof CustomSelect > = { title: 'Components (Experimental)/CustomSelectControl v2/Legacy', - component: _LegacyCustomSelect, + component: CustomSelect, argTypes: { onChange: { control: { type: null } }, value: { control: { type: null } }, @@ -43,11 +42,11 @@ const meta: Meta< typeof _LegacyCustomSelect > = { }; export default meta; -const Template: StoryFn< typeof _LegacyCustomSelect > = ( props ) => { +const Template: StoryFn< typeof CustomSelect > = ( props ) => { const [ fontSize, setFontSize ] = useState( props.options[ 0 ] ); const onChange: React.ComponentProps< - typeof _LegacyCustomSelect + typeof CustomSelect >[ 'onChange' ] = ( changeObject ) => { setFontSize( changeObject.selectedItem ); props.onChange?.( changeObject ); diff --git a/packages/components/src/custom-select-control-v2/test/index.tsx b/packages/components/src/custom-select-control-v2/test/index.tsx index dc8df0813b169b..fc8552b7a612a7 100644 --- a/packages/components/src/custom-select-control-v2/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen } from '@testing-library/react'; -import { click, press, sleep, type, waitFor } from '@ariakit/test'; +import { click, press, sleep, type } from '@ariakit/test'; /** * WordPress dependencies @@ -13,100 +13,115 @@ import { useState } from '@wordpress/element'; * Internal dependencies */ import { CustomSelect as UncontrolledCustomSelect, CustomSelectItem } from '..'; -import type { CustomSelectProps, LegacyCustomSelectProps } from '../types'; +import type { CustomSelectProps } from '../types'; + +const items = [ + { + key: 'flower1', + value: 'violets', + }, + { + key: 'flower2', + value: 'crimson clover', + }, + { + key: 'flower3', + value: 'poppy', + }, + { + key: 'color1', + value: 'amber', + }, + { + key: 'color2', + value: 'aquamarine', + }, +]; -const customClass = 'amber-skies'; - -const legacyProps = { +const defaultProps = { label: 'label!', - options: [ - { - key: 'flower1', - name: 'violets', - }, - { - key: 'flower2', - name: 'crimson clover', - className: customClass, - }, - { - key: 'flower3', - name: 'poppy', - }, - { - key: 'color1', - name: 'amber', - className: customClass, - }, - { - key: 'color2', - name: 'aquamarine', - style: { - backgroundColor: 'rgb(127, 255, 212)', - rotate: '13deg', - }, - }, - ], + children: items.map( ( { value, key } ) => ( + + ) ), }; -const LegacyControlledCustomSelect = ( { - options, - onChange, - ...restProps -}: LegacyCustomSelectProps ) => { - const [ value, setValue ] = useState( options[ 0 ] ); +const ControlledCustomSelect = ( props: CustomSelectProps ) => { + const [ value, setValue ] = useState< string | string[] >(); return ( { - onChange?.( args ); - setValue( args.selectedItem ); + { ...props } + onChange={ ( nextValue: string | string[] ) => { + setValue( nextValue ); + props.onChange?.( nextValue ); } } - value={ options.find( - ( option: any ) => option.key === value.key - ) } + value={ value } /> ); }; -describe( 'With Legacy Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', LegacyControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; +describe.each( [ + [ 'Uncontrolled', UncontrolledCustomSelect ], + [ 'Controlled', ControlledCustomSelect ], +] )( 'CustomSelectControlV2 (%s)', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; - it( 'Should replace the initial selection when a new item is selected', async () => { - render( ); + it( 'Should replace the initial selection when a new item is selected', async () => { + render( ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); - await click( currentSelectedItem ); + await click( currentSelectedItem ); - await click( - screen.getByRole( 'option', { - name: 'crimson clover', - } ) - ); + await click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); - await click( currentSelectedItem ); + await click( currentSelectedItem ); - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); + await click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + render( ); - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { - render( ); + await sleep(); + await press.Tab(); + await press.Enter(); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await press.Escape(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( items[ 0 ].value ); + } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Should be able to change selection using keyboard', async () => { + render( ); const currentSelectedItem = screen.getByRole( 'combobox', { expanded: false, @@ -114,417 +129,67 @@ describe( 'With Legacy Props', () => { await sleep(); await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); + await press.Enter(); expect( screen.getByRole( 'listbox', { name: 'label!', } ) - ).toBeVisible(); - - await press.Escape(); - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - } ) - ).not.toBeInTheDocument(); - - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); - } ); - - it( 'Should apply class only to options that have a className defined', async () => { - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - // return an array of items _with_ a className added - const itemsWithClass = legacyProps.options.filter( - ( option ) => option.className !== undefined - ); - - // assert against filtered array - itemsWithClass.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveClass( - customClass - ) - ); - - // return an array of items _without_ a className added - const itemsWithoutClass = legacyProps.options.filter( - ( option ) => option.className === undefined - ); - - // assert against filtered array - itemsWithoutClass.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveClass( customClass ) - ); - } ); - - it( 'Should apply styles only to options that have styles defined', async () => { - const customStyles = - 'background-color: rgb(127, 255, 212); rotate: 13deg;'; - - render( ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - // return an array of items _with_ styles added - const styledItems = legacyProps.options.filter( - ( option ) => option.style !== undefined - ); - - // assert against filtered array - styledItems.map( ( { name } ) => - expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( - customStyles - ) - ); - - // return an array of items _without_ styles added - const unstyledItems = legacyProps.options.filter( - ( option ) => option.style === undefined - ); - - // assert against filtered array - unstyledItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name } ) - ).not.toHaveStyle( customStyles ) - ); - } ); - - it( 'does not show selected hint by default', async () => { - render( - - ); - await waitFor( () => - expect( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ).not.toHaveTextContent( 'Hint' ) - ); - } ); - - it( 'shows selected hint when __experimentalShowSelectedHint is set', async () => { - render( - - ); - - await waitFor( () => - expect( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ).toHaveTextContent( /hint/i ) - ); - } ); - - it( 'shows selected hint in list of options when added', async () => { - render( - - ); - - await click( - screen.getByRole( 'combobox', { name: 'Custom select' } ) - ); - - expect( - screen.getByRole( 'option', { name: /hint/i } ) - ).toBeVisible(); - } ); - - it( 'Should return object onChange', async () => { - const mockOnChange = jest.fn(); - - render( - - ); - - await click( - screen.getByRole( 'combobox', { - expanded: false, - } ) - ); - - expect( mockOnChange ).toHaveBeenNthCalledWith( - 1, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: { key: 'violets', name: 'violets' }, - type: '', - } ) - ); + ).toHaveFocus(); - await click( - screen.getByRole( 'option', { - name: 'aquamarine', - } ) - ); + await press.ArrowDown(); + await press.Enter(); - expect( mockOnChange ).toHaveBeenNthCalledWith( - 2, - expect.objectContaining( { - inputValue: '', - isOpen: false, - selectedItem: expect.objectContaining( { - name: 'aquamarine', - } ), - type: '', - } ) - ); + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); } ); - it( 'Should return selectedItem object when specified onChange', async () => { - const mockOnChange = jest.fn( - ( { selectedItem } ) => selectedItem.key - ); + it( 'Should be able to type characters to select matching options', async () => { + render( ); - render( - - ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, + } ); await sleep(); await press.Tab(); + await press.Enter(); expect( - screen.getByRole( 'combobox', { - expanded: false, + screen.getByRole( 'listbox', { + name: 'label!', } ) ).toHaveFocus(); - await type( 'p' ); + await type( 'a' ); await press.Enter(); - - expect( mockOnChange ).toHaveReturnedWith( 'poppy' ); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); } ); - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); - - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); - } ); - - it( 'Should be able to type characters to select matching options', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); - - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await type( 'aq' ); - - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + render( ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should have correct aria-selected value for selections', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // get all items except for first option - const unselectedItems = legacyProps.options.filter( - ( { name } ) => name !== legacyProps.options[ 0 ].name - ); - - // assert that all other items have aria-selected="false" - unselectedItems.map( ( { name } ) => - expect( - screen.getByRole( 'option', { name, selected: false } ) - ).toBeVisible() - ); - - // assert that first item has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: true, - } ) - ).toBeVisible(); - - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + await sleep(); + await press.Tab(); + expect( currentSelectedItem ).toHaveFocus(); - // click combobox to mount listbox with options again - await click( currentSelectedItem ); + await type( 'aq' ); - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: legacyProps.options[ 0 ].name, - selected: false, - } ) - ).toBeVisible(); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); - } ); + await press.Enter(); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); } ); - } ); -} ); - -describe( 'static typing', () => { - <> - { /* @ts-expect-error - when `options` prop is passed, `onChange` should have legacy signature */ } - undefined } - /> - undefined } - /> - undefined } - > - foobar - - { /* @ts-expect-error - when `children` are passed, `onChange` should have new default signature */ } - undefined } - > - foobar - - ; -} ); - -const defaultProps = { - label: 'label!', - children: legacyProps.options.map( ( { name, key } ) => ( - - ) ), -}; - -const ControlledCustomSelect = ( props: CustomSelectProps ) => { - const [ value, setValue ] = useState< string | string[] >(); - return ( - { - setValue( nextValue ); - props.onChange?.( nextValue ); - } } - value={ value } - /> - ); -}; -describe( 'With Default Props', () => { - describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', ControlledCustomSelect ], - ] )( '%s', ( ...modeAndComponent ) => { - const [ , Component ] = modeAndComponent; - - it( 'Should replace the initial selection when a new item is selected', async () => { + it( 'Should have correct aria-selected value for selections', async () => { render( ); const currentSelectedItem = screen.getByRole( 'combobox', { @@ -533,320 +198,134 @@ describe( 'With Default Props', () => { await click( currentSelectedItem ); - await click( + // assert that first item has aria-selected="true" + expect( screen.getByRole( 'option', { - name: 'crimson clover', + name: 'violets', + selected: true, } ) - ); + ).toBeVisible(); - expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + // change the current selection + await click( screen.getByRole( 'option', { name: 'poppy' } ) ); + // click combobox to mount listbox with options again await click( currentSelectedItem ); - await click( - screen.getByRole( 'option', { - name: 'poppy', - } ) - ); - - expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); - } ); - - it( 'Should keep current selection if dropdown is closed without changing selection', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); + // check that first item is has aria-selected="false" after new selection expect( - screen.getByRole( 'listbox', { - name: 'label!', + screen.getByRole( 'option', { + name: 'violets', + selected: false, } ) ).toBeVisible(); - await press.Escape(); + // check that new selected item now has aria-selected="true" expect( - screen.queryByRole( 'listbox', { - name: 'label!', + screen.getByRole( 'option', { + name: 'poppy', + selected: true, } ) - ).not.toBeInTheDocument(); - - expect( currentSelectedItem ).toHaveTextContent( - legacyProps.options[ 0 ].name - ); + ).toBeVisible(); } ); + } ); - describe( 'Keyboard behavior and accessibility', () => { - it( 'Should be able to change selection using keyboard', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); - - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await press.ArrowDown(); - await press.Enter(); - - expect( currentSelectedItem ).toHaveTextContent( - 'crimson clover' - ); - } ); - - it( 'Should be able to type characters to select matching options', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - await press.Enter(); - expect( - screen.getByRole( 'listbox', { - name: 'label!', - } ) - ).toHaveFocus(); - - await type( 'a' ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'amber' ); - } ); - - it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { - render( ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await sleep(); - await press.Tab(); - expect( currentSelectedItem ).toHaveFocus(); + describe( 'Multiple selection', () => { + it( 'Should be able to select multiple items when provided an array', async () => { + const onChangeMock = jest.fn(); - await type( 'aq' ); + // initial selection as defaultValue + const defaultValues = [ + 'incandescent glow', + 'ultraviolet morning light', + ]; - expect( - screen.queryByRole( 'listbox', { - name: 'label!', - hidden: true, - } ) - ).not.toBeInTheDocument(); + render( + + { [ + 'aurora borealis green', + 'flamingo pink sunrise', + 'incandescent glow', + 'rose blush', + 'ultraviolet morning light', + ].map( ( item ) => ( + + { item } + + ) ) } + + ); - await press.Enter(); - expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); - it( 'Should have correct aria-selected value for selections', async () => { - render( ); + // ensure more than one item is selected due to defaultValues + expect( currentSelectedItem ).toHaveTextContent( + `${ defaultValues.length } items selected` + ); - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); + await click( currentSelectedItem ); - await click( currentSelectedItem ); + expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( + 'aria-multiselectable' + ); - // assert that first item has aria-selected="true" + // ensure defaultValues are selected in list of items + defaultValues.forEach( ( value ) => expect( screen.getByRole( 'option', { - name: 'violets', + name: value, selected: true, } ) - ).toBeVisible(); - - // change the current selection - await click( screen.getByRole( 'option', { name: 'poppy' } ) ); - - // click combobox to mount listbox with options again - await click( currentSelectedItem ); + ).toBeVisible() + ); - // check that first item is has aria-selected="false" after new selection - expect( - screen.getByRole( 'option', { - name: 'violets', - selected: false, - } ) - ).toBeVisible(); + // name of next selection + const nextSelectionName = 'rose blush'; - // check that new selected item now has aria-selected="true" - expect( - screen.getByRole( 'option', { - name: 'poppy', - selected: true, - } ) - ).toBeVisible(); + // element for next selection + const nextSelection = screen.getByRole( 'option', { + name: nextSelectionName, } ); - } ); - - describe( 'Multiple selection', () => { - it( 'Should be able to select multiple items when provided an array', async () => { - const onChangeMock = jest.fn(); - - // initial selection as defaultValue - const defaultValues = [ - 'incandescent glow', - 'ultraviolet morning light', - ]; - - render( - - { [ - 'aurora borealis green', - 'flamingo pink sunrise', - 'incandescent glow', - 'rose blush', - 'ultraviolet morning light', - ].map( ( item ) => ( - - { item } - - ) ) } - - ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - // ensure more than one item is selected due to defaultValues - expect( currentSelectedItem ).toHaveTextContent( - `${ defaultValues.length } items selected` - ); - - await click( currentSelectedItem ); - - expect( screen.getByRole( 'listbox' ) ).toHaveAttribute( - 'aria-multiselectable' - ); - - // ensure defaultValues are selected in list of items - defaultValues.forEach( ( value ) => - expect( - screen.getByRole( 'option', { - name: value, - selected: true, - } ) - ).toBeVisible() - ); - - // name of next selection - const nextSelectionName = 'rose blush'; - - // element for next selection - const nextSelection = screen.getByRole( 'option', { - name: nextSelectionName, - } ); - // click next selection to add another item to current selection - await click( nextSelection ); + // click next selection to add another item to current selection + await click( nextSelection ); - // updated array containing defaultValues + the item just selected - const updatedSelection = - defaultValues.concat( nextSelectionName ); + // updated array containing defaultValues + the item just selected + const updatedSelection = defaultValues.concat( nextSelectionName ); - expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); + expect( onChangeMock ).toHaveBeenCalledWith( updatedSelection ); - expect( nextSelection ).toHaveAttribute( 'aria-selected' ); + expect( nextSelection ).toHaveAttribute( 'aria-selected' ); - // expect increased array length for current selection - expect( currentSelectedItem ).toHaveTextContent( - `${ updatedSelection.length } items selected` - ); - } ); - - it( 'Should be able to deselect items when provided an array', async () => { - // initial selection as defaultValue - const defaultValues = [ - 'aurora borealis green', - 'incandescent glow', - 'key lime green', - 'rose blush', - 'ultraviolet morning light', - ]; - - render( - - { defaultValues.map( ( item ) => ( - - { item } - - ) ) } - - ); - - const currentSelectedItem = screen.getByRole( 'combobox', { - expanded: false, - } ); - - await click( currentSelectedItem ); - - // Array containing items to deselect - const nextSelection = [ - 'aurora borealis green', - 'rose blush', - 'incandescent glow', - ]; - - // Deselect some items by clicking them to ensure that changes - // are reflected correctly - await Promise.all( - nextSelection.map( async ( value ) => { - await click( - screen.getByRole( 'option', { name: value } ) - ); - expect( - screen.getByRole( 'option', { - name: value, - selected: false, - } ) - ).toBeVisible(); - } ) - ); - - // expect different array length from defaultValues due to deselecting items - expect( currentSelectedItem ).toHaveTextContent( - `${ - defaultValues.length - nextSelection.length - } items selected` - ); - } ); + // expect increased array length for current selection + expect( currentSelectedItem ).toHaveTextContent( + `${ updatedSelection.length } items selected` + ); } ); - it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { - const renderValue = ( value: string | string[] ) => { - return {; - }; + it( 'Should be able to deselect items when provided an array', async () => { + // initial selection as defaultValue + const defaultValues = [ + 'aurora borealis green', + 'incandescent glow', + 'key lime green', + 'rose blush', + 'ultraviolet morning light', + ]; render( - - - { renderValue( 'april-29' ) } - - - { renderValue( 'july-9' ) } - + + { defaultValues.map( ( item ) => ( + + { item } + + ) ) } ); @@ -854,26 +333,77 @@ describe( 'With Default Props', () => { expanded: false, } ); - expect( currentSelectedItem ).toBeVisible(); + await click( currentSelectedItem ); - // expect that the initial selection renders an image - expect( currentSelectedItem ).toContainElement( - screen.getByRole( 'img', { name: 'april-29' } ) + // Array containing items to deselect + const nextSelection = [ + 'aurora borealis green', + 'rose blush', + 'incandescent glow', + ]; + + // Deselect some items by clicking them to ensure that changes + // are reflected correctly + await Promise.all( + nextSelection.map( async ( value ) => { + await click( + screen.getByRole( 'option', { name: value } ) + ); + expect( + screen.getByRole( 'option', { + name: value, + selected: false, + } ) + ).toBeVisible(); + } ) ); - expect( - screen.queryByRole( 'img', { name: 'july-9' } ) - ).not.toBeInTheDocument(); - - await click( currentSelectedItem ); + // expect different array length from defaultValues due to deselecting items + expect( currentSelectedItem ).toHaveTextContent( + `${ + defaultValues.length - nextSelection.length + } items selected` + ); + } ); + } ); - // expect that the other image is only visible after opening popover with options - expect( - screen.getByRole( 'img', { name: 'july-9' } ) - ).toBeVisible(); - expect( - screen.getByRole( 'option', { name: 'july-9' } ) - ).toBeVisible(); + it( 'Should allow rendering a custom value when using `renderSelectedValue`', async () => { + const renderValue = ( value: string | string[] ) => { + return {; + }; + + render( + + + { renderValue( 'april-29' ) } + + + { renderValue( 'july-9' ) } + + + ); + + const currentSelectedItem = screen.getByRole( 'combobox', { + expanded: false, } ); + + expect( currentSelectedItem ).toBeVisible(); + + // expect that the initial selection renders an image + expect( currentSelectedItem ).toContainElement( + screen.getByRole( 'img', { name: 'april-29' } ) + ); + + expect( + screen.queryByRole( 'img', { name: 'july-9' } ) + ).not.toBeInTheDocument(); + + await click( currentSelectedItem ); + + // expect that the other image is only visible after opening popover with options + expect( screen.getByRole( 'img', { name: 'july-9' } ) ).toBeVisible(); + expect( + screen.getByRole( 'option', { name: 'july-9' } ) + ).toBeVisible(); } ); } ); diff --git a/packages/components/src/custom-select-control-v2/types.ts b/packages/components/src/custom-select-control-v2/types.ts index b32f3ee2113080..51b0cb6b38d6a7 100644 --- a/packages/components/src/custom-select-control-v2/types.ts +++ b/packages/components/src/custom-select-control-v2/types.ts @@ -92,7 +92,6 @@ type LegacyOnChangeObject = { }; export type LegacyCustomSelectProps = { - children?: never; /** * Optional classname for the component. */ From 9840fbe4d0ce266a7d6ea6aa1535845644829b33 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 28 Feb 2024 23:04:34 +0900 Subject: [PATCH 02/45] HStack, VStack: Stop passing invalid props (#59416) * Add tests * HStack, VStack: Stop passing invalid props * useFlex: Fix return types * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: aaronrobertshaw Co-authored-by: t-hamano Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/flex/flex/hook.ts | 2 +- packages/components/src/h-stack/hook.tsx | 3 ++- packages/components/src/h-stack/test/index.tsx | 10 ++++++++++ packages/components/src/v-stack/test/index.tsx | 10 ++++++++++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index e20b3444468fe6..d575fc96f83fc4 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug Fix - `Tooltip`: Explicitly set system font to avoid CSS bleed ([#59307](https://github.com/WordPress/gutenberg/pull/59307)). +- `HStack`, `VStack`: Stop passing invalid props to underlying element ([#59416](https://github.com/WordPress/gutenberg/pull/59416)). - `Button`: Fix focus outline in disabled primary variant ([#59391](https://github.com/WordPress/gutenberg/pull/59391)). ### Internal diff --git a/packages/components/src/flex/flex/hook.ts b/packages/components/src/flex/flex/hook.ts index df53667915999c..f5648c187c7628 100644 --- a/packages/components/src/flex/flex/hook.ts +++ b/packages/components/src/flex/flex/hook.ts @@ -22,7 +22,7 @@ import type { FlexProps } from '../types'; function useDeprecatedProps( props: WordPressComponentProps< FlexProps, 'div' > -): WordPressComponentProps< FlexProps, 'div' > { +): Omit< typeof props, 'isReversed' > { const { isReversed, ...otherProps } = props; if ( typeof isReversed !== 'undefined' ) { diff --git a/packages/components/src/h-stack/hook.tsx b/packages/components/src/h-stack/hook.tsx index b7b8cf92954a1d..bb473a881b9dea 100644 --- a/packages/components/src/h-stack/hook.tsx +++ b/packages/components/src/h-stack/hook.tsx @@ -47,7 +47,8 @@ export function useHStack( props: WordPressComponentProps< Props, 'div' > ) { gap: spacing, }; - const flexProps = useFlex( propsForFlex ); + // Omit `isColumn` because it's not used in HStack. + const { isColumn, ...flexProps } = useFlex( propsForFlex ); return flexProps; } diff --git a/packages/components/src/h-stack/test/index.tsx b/packages/components/src/h-stack/test/index.tsx index ea4f9c94fbe7f8..75352f3dfd22f0 100644 --- a/packages/components/src/h-stack/test/index.tsx +++ b/packages/components/src/h-stack/test/index.tsx @@ -39,4 +39,14 @@ describe( 'props', () => { ); expect( container ).toMatchSnapshot(); } ); + + test( 'should not pass through invalid props to the `as` component', () => { + const AsComponent = ( props: JSX.IntrinsicElements[ 'div' ] ) => { + return
; + }; + + render( foobar ); + + expect( console ).not.toHaveErrored(); + } ); } ); diff --git a/packages/components/src/v-stack/test/index.tsx b/packages/components/src/v-stack/test/index.tsx index cf35691d0858f6..e8ea1ab0ee396f 100644 --- a/packages/components/src/v-stack/test/index.tsx +++ b/packages/components/src/v-stack/test/index.tsx @@ -39,4 +39,14 @@ describe( 'props', () => { ); expect( container ).toMatchSnapshot(); } ); + + test( 'should not pass through invalid props to the `as` component', () => { + const AsComponent = ( props: JSX.IntrinsicElements[ 'div' ] ) => { + return
; + }; + + render( foobar ); + + expect( console ).not.toHaveErrored(); + } ); } ); From a65b156238a514a7fc03c3262f7cac4091d38fb5 Mon Sep 17 00:00:00 2001 From: Bernie Reiter <96308+ockham@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:28:29 +0100 Subject: [PATCH 03/45] Block Hooks: Display toggle for hooked blocks added via filter (#59396) Show the Block Hooks toggle for hooked blocks that were added via the `hooked_block_types` filter (rather than at block registration time, i.e. via the `blockHooks` field in `block.json`), by evaluating the anchor block's `ignoredHookedBlocks` metadata attribute. (This attribute is only present if the containing template/part/pattern has user modifications.) Co-authored-by: ockham Co-authored-by: gziolo Co-authored-by: michalczaplinski Co-authored-by: sirreal --- .../block-editor/src/hooks/block-hooks.js | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index eb84352ab62f09..93bf87f42124b1 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -19,18 +19,28 @@ import { store as blockEditorStore } from '../store'; const EMPTY_OBJECT = {}; -function BlockHooksControlPure( { name, clientId } ) { +function BlockHooksControlPure( { + name, + clientId, + metadata: { ignoredHookedBlocks = [] } = {}, +} ) { const blockTypes = useSelect( ( select ) => select( blocksStore ).getBlockTypes(), [] ); + // A hooked block added via a filter will not be exposed through a block + // type's `blockHooks` property; however, if the containing layout has been + // modified, it will be present in the anchor block's `ignoredHookedBlocks` + // metadata. const hookedBlocksForCurrentBlock = useMemo( () => blockTypes?.filter( - ( { blockHooks } ) => blockHooks && name in blockHooks + ( { name: blockName, blockHooks } ) => + ( blockHooks && name in blockHooks ) || + ignoredHookedBlocks.includes( blockName ) ), - [ blockTypes, name ] + [ blockTypes, name, ignoredHookedBlocks ] ); const { blockIndex, rootClientId, innerBlocksLength } = useSelect( @@ -79,6 +89,16 @@ function BlockHooksControlPure( { name, clientId } ) { // inserted and then moved around a bit by the user. candidates = getBlocks( clientId ); break; + + case undefined: + // If we haven't found a blockHooks field with a relative position for the hooked + // block, it means that it was added by a filter. In this case, we look for the block + // both among the current block's siblings and its children. + candidates = [ + ...getBlocks( rootClientId ), + ...getBlocks( clientId ), + ]; + break; } const hookedBlock = candidates?.find( @@ -151,6 +171,18 @@ function BlockHooksControlPure( { name, clientId } ) { false ); break; + + case undefined: + // If we do not know the relative position, it is because the block was + // added via a filter. In this case, we default to inserting it after the + // current block. + insertBlock( + block, + blockIndex + 1, + rootClientId, // Insert as a child of the current block's parent + false + ); + break; } }; @@ -219,6 +251,7 @@ function BlockHooksControlPure( { name, clientId } ) { export default { edit: BlockHooksControlPure, + attributeKeys: [ 'metadata' ], hasSupport() { return true; }, From 085af14b7e5a295fff2a48dd478ed1714d95a195 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 29 Feb 2024 00:25:47 +0900 Subject: [PATCH 04/45] URLPopover: Fix a problem with the layout of link settings (#58906) Co-authored-by: t-hamano Co-authored-by: getdave --- .../block-editor/src/components/url-popover/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/url-popover/index.js b/packages/block-editor/src/components/url-popover/index.js index b5bbe8f50958bb..d060a464cc306f 100644 --- a/packages/block-editor/src/components/url-popover/index.js +++ b/packages/block-editor/src/components/url-popover/index.js @@ -92,12 +92,12 @@ const URLPopover = forwardRef( /> ) }
- { showSettings && ( -
- { renderSettings() } -
- ) }
+ { showSettings && ( +
+ { renderSettings() } +
+ ) } { additionalControls && ! showSettings && (
{ additionalControls } From faf60a4db005219b2abd607e9e2fdefd5b979dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Wed, 28 Feb 2024 12:56:02 -0300 Subject: [PATCH 05/45] Block Bindings: do not use useSource hook conditionally (#59403) * replace use-binding-attributes with block-binding-support * minor enhancement * minor change * tweak * do not import use-binding-attributes * use isItPossibleToBindBlock() helper * introduce core/entity source handler * rename folder * rename source name * polish post-entity source handler * make core/post-entity more consistent with core-data * make entity source hand;ler more generic * fix entity sour handl;er issues * remove uneeded useValue () hook (crossfingers) * minor jsdoc improvement * clean * rename with updateValue() * remove core/entity binding source handler * move useSource to Connector cmp * move the whole dryining logic to the Connect component * improve jsdoc * rename to blockProps * minor jsdoc improvements * use a single effect to update attr and value * discard useValue. Return value and setValue instead * check wheter updateValue function is defined * check prop value is defined when updating attr * handle `placerholder` * ensure to put attribute in sync when onmount * remove // eslint comment * enable editing for bound with post-meta * move block bindiung processor to hooks/ * ensure update bound attr once when mounting * Update packages/block-editor/src/hooks/block-binding-support/index.js Co-authored-by: Michal * disable editing block attribute * move changes to the use-binding-attributes file * introduce BlockBindingBridge component * update isItPossibleToBindBlock() import path * introduce hasPossibleBlockBinding() helper * use hooks API to extened blocks with bound attts * fix propagating attr value. jsdoc * minor changes * minor code enhancement * not edit bound prop for now * jsdoc * revert using hooks API to extrend block * jsdoc * update internal path * rollback hook utils chnages * tidy * wrap Connector instances with a Fragment * return original Edit instance when no bindings * check whether useSource is defined * Use `useSelect` and move it out of the for loop * check attr value type * iterare when creating BindingConnector instances * rename helper functions * use useSelect to get binding sources * Update packages/block-editor/src/hooks/use-bindings-attributes.js Co-authored-by: Michal * Update packages/block-editor/src/hooks/use-bindings-attributes.js Co-authored-by: Michal * pass prev attr value to compare * improve binding allowed block attributes * sync derevied updates when updating bound attr * improve getting attr source * check properly bindings data * preserve the RichTextData for block attr * comment line just for tesrting purposes * rebasing changes * rollback change foir testing purposes * change cmp prop name. improve jsdoc * simplify checking bindins value * use attr name as key instance * store bound attrs values in a local state * collect and update bound attr in a local state * Refactor block binding functionality from e55f6bc * pick block data from straight props * remove conditional onPropValueChange call * Update e2e tests * Use `useLayoutEffect` instead of `useEffect` --------- Co-authored-by: Michal Co-authored-by: Mario Santos --- .../src/components/rich-text/index.js | 4 +- .../src/hooks/use-bindings-attributes.js | 276 ++++++++++++++---- packages/editor/src/bindings/post-meta.js | 5 +- .../editor/various/block-bindings.spec.js | 6 +- 4 files changed, 223 insertions(+), 68 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 458f5a96609b65..7236e74b2f6d68 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -47,7 +47,7 @@ import { getAllowedFormats } from './utils'; import { Content } from './content'; import { withDeprecations } from './with-deprecations'; import { unlock } from '../../lock-unlock'; -import { BLOCK_BINDINGS_ALLOWED_BLOCKS } from '../../hooks/use-bindings-attributes'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -161,7 +161,7 @@ export function RichTextWrapper( ( select ) => { // Disable Rich Text editing if block bindings specify that. let _disableBoundBlocks = false; - if ( blockBindings && blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS ) { + if ( blockBindings && canBindBlock( blockName ) ) { const blockTypeAttributes = getBlockType( blockName ).attributes; const { getBlockBindingsSource } = unlock( diff --git a/packages/block-editor/src/hooks/use-bindings-attributes.js b/packages/block-editor/src/hooks/use-bindings-attributes.js index 0e5b6614f07cbf..5cd8cb46b3b7e7 100644 --- a/packages/block-editor/src/hooks/use-bindings-attributes.js +++ b/packages/block-editor/src/hooks/use-bindings-attributes.js @@ -4,12 +4,13 @@ import { getBlockType, store as blocksStore } from '@wordpress/blocks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; +import { useLayoutEffect, useCallback, useState } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; +import { RichTextData } from '@wordpress/rich-text'; + /** * Internal dependencies */ -import { store as blockEditorStore } from '../store'; -import { useBlockEditContext } from '../components/block-edit/context'; import { unlock } from '../lock-unlock'; /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ @@ -22,87 +23,238 @@ import { unlock } from '../lock-unlock'; * @return {WPHigherOrderComponent} Higher-order component. */ -export const BLOCK_BINDINGS_ALLOWED_BLOCKS = { +const BLOCK_BINDINGS_ALLOWED_BLOCKS = { 'core/paragraph': [ 'content' ], 'core/heading': [ 'content' ], 'core/image': [ 'url', 'title', 'alt' ], 'core/button': [ 'url', 'text', 'linkTarget' ], }; -const createEditFunctionWithBindingsAttribute = () => - createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { clientId, name: blockName } = useBlockEditContext(); - const blockBindingsSources = unlock( - useSelect( blocksStore ) - ).getAllBlockBindingsSources(); - const { getBlockAttributes } = useSelect( blockEditorStore ); - - const updatedAttributes = getBlockAttributes( clientId ); - if ( updatedAttributes?.metadata?.bindings ) { - Object.entries( updatedAttributes.metadata.bindings ).forEach( - ( [ attributeName, settings ] ) => { - const source = blockBindingsSources[ settings.source ]; - - if ( source && source.useSource ) { - // Second argument (`updateMetaValue`) will be used to update the value in the future. - const { - placeholder, - useValue: [ metaValue = null ] = [], - } = source.useSource( props, settings.args ); - - if ( placeholder && ! metaValue ) { - // If the attribute is `src` or `href`, a placeholder can't be used because it is not a valid url. - // Adding this workaround until attributes and metadata fields types are improved and include `url`. - const htmlAttribute = - getBlockType( blockName ).attributes[ - attributeName - ].attribute; - if ( - htmlAttribute === 'src' || - htmlAttribute === 'href' - ) { - updatedAttributes[ attributeName ] = null; - } else { - updatedAttributes[ attributeName ] = - placeholder; - } - } - - if ( metaValue ) { - updatedAttributes[ attributeName ] = metaValue; - } - } - } - ); +/** + * Based on the given block name, + * check if it is possible to bind the block. + * + * @param {string} blockName - The block name. + * @return {boolean} Whether it is possible to bind the block to sources. + */ +export function canBindBlock( blockName ) { + return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; +} + +/** + * Based on the given block name and attribute name, + * check if it is possible to bind the block attribute. + * + * @param {string} blockName - The block name. + * @param {string} attributeName - The attribute name. + * @return {boolean} Whether it is possible to bind the block attribute. + */ +export function canBindAttribute( blockName, attributeName ) { + return ( + canBindBlock( blockName ) && + BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) + ); +} + +/** + * This component is responsible for detecting and + * propagating data changes from the source to the block. + * + * @param {Object} props - The component props. + * @param {string} props.attrName - The attribute name. + * @param {Object} props.blockProps - The block props with bound attribute. + * @param {Object} props.source - Source handler. + * @param {Object} props.args - The arguments to pass to the source. + * @param {Function} props.onPropValueChange - The function to call when the attribute value changes. + * @return {null} Data-handling component. Render nothing. + */ +const BindingConnector = ( { + args, + attrName, + blockProps, + source, + onPropValueChange, +} ) => { + const { placeholder, value: propValue } = source.useSource( + blockProps, + args + ); + + const { name: blockName } = blockProps; + const attrValue = blockProps.attributes[ attrName ]; + + const updateBoundAttibute = useCallback( + ( newAttrValue, prevAttrValue ) => { + /* + * If the attribute is a RichTextData instance, + * (core/paragraph, core/heading, core/button, etc.) + * compare its HTML representation with the new value. + * + * To do: it looks like a workaround. + * Consider improving the attribute and metadata fields types. + */ + if ( prevAttrValue instanceof RichTextData ) { + // Bail early if the Rich Text value is the same. + if ( prevAttrValue.toHTMLString() === newAttrValue ) { + return; + } + + /* + * To preserve the value type, + * convert the new value to a RichTextData instance. + */ + newAttrValue = RichTextData.fromHTMLString( newAttrValue ); + } + + if ( prevAttrValue === newAttrValue ) { + return; } - return ( + onPropValueChange( { [ attrName ]: newAttrValue } ); + }, + [ attrName, onPropValueChange ] + ); + + useLayoutEffect( () => { + if ( typeof propValue !== 'undefined' ) { + updateBoundAttibute( propValue, attrValue ); + } else if ( placeholder ) { + /* + * Placeholder fallback. + * If the attribute is `src` or `href`, + * a placeholder can't be used because it is not a valid url. + * Adding this workaround until + * attributes and metadata fields types are improved and include `url`. + */ + const htmlAttribute = + getBlockType( blockName ).attributes[ attrName ].attribute; + + if ( htmlAttribute === 'src' || htmlAttribute === 'href' ) { + updateBoundAttibute( null ); + return; + } + + updateBoundAttibute( placeholder ); + } + }, [ + updateBoundAttibute, + propValue, + attrValue, + placeholder, + blockName, + attrName, + ] ); + + return null; +}; + +/** + * BlockBindingBridge acts like a component wrapper + * that connects the bound attributes of a block + * to the source handlers. + * For this, it creates a BindingConnector for each bound attribute. + * + * @param {Object} props - The component props. + * @param {Object} props.blockProps - The BlockEdit props object. + * @param {Object} props.bindings - The block bindings settings. + * @param {Function} props.onPropValueChange - The function to call when the attribute value changes. + * @return {null} Data-handling component. Render nothing. + */ +function BlockBindingBridge( { blockProps, bindings, onPropValueChange } ) { + const blockBindingsSources = unlock( + useSelect( blocksStore ) + ).getAllBlockBindingsSources(); + + return ( + <> + { Object.entries( bindings ).map( + ( [ attrName, boundAttribute ] ) => { + // Bail early if the block doesn't have a valid source handler. + const source = + blockBindingsSources[ boundAttribute.source ]; + if ( ! source?.useSource ) { + return null; + } + + return ( + + ); + } + ) } + + ); +} + +const withBlockBindingSupport = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + /* + * Collect and update the bound attributes + * in a separate state. + */ + const [ boundAttributes, setBoundAttributes ] = useState( {} ); + const updateBoundAttributes = useCallback( + ( newAttributes ) => + setBoundAttributes( ( prev ) => ( { + ...prev, + ...newAttributes, + } ) ), + [] + ); + + /* + * Create binding object filtering + * only the attributes that can be bound. + */ + const bindings = Object.fromEntries( + Object.entries( props.attributes.metadata?.bindings || {} ).filter( + ( [ attrName ] ) => canBindAttribute( props.name, attrName ) + ) + ); + + return ( + <> + { Object.keys( bindings ).length > 0 && ( + + ) } + - ); - }, - 'useBoundAttributes' - ); + + ); + }, + 'withBlockBindingSupport' +); /** * Filters a registered block's settings to enhance a block's `edit` component * to upgrade bound attributes. * - * @param {WPBlockSettings} settings Registered block settings. - * + * @param {WPBlockSettings} settings - Registered block settings. + * @param {string} name - Block name. * @return {WPBlockSettings} Filtered block settings. */ -function shimAttributeSource( settings ) { - if ( ! ( settings.name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { +function shimAttributeSource( settings, name ) { + if ( ! canBindBlock( name ) ) { return settings; } - settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit ); - return settings; + return { + ...settings, + edit: withBlockBindingSupport( settings.edit ), + }; } addFilter( diff --git a/packages/editor/src/bindings/post-meta.js b/packages/editor/src/bindings/post-meta.js index a9a00599b68037..0d0c737d0eaf77 100644 --- a/packages/editor/src/bindings/post-meta.js +++ b/packages/editor/src/bindings/post-meta.js @@ -19,6 +19,7 @@ export default { const postType = context.postType ? context.postType : getCurrentPostType(); + const [ meta, setMeta ] = useEntityProp( 'postType', context.postType, @@ -33,9 +34,11 @@ export default { const updateMetaValue = ( newValue ) => { setMeta( { ...meta, [ metaKey ]: newValue } ); }; + return { placeholder: metaKey, - useValue: [ metaValue, updateMetaValue ], + value: metaValue, + updateValue: updateMetaValue, }; }, }; diff --git a/test/e2e/specs/editor/various/block-bindings.spec.js b/test/e2e/specs/editor/various/block-bindings.spec.js index 419a70faeaf9be..ca19c06beff01a 100644 --- a/test/e2e/specs/editor/various/block-bindings.spec.js +++ b/test/e2e/specs/editor/various/block-bindings.spec.js @@ -1245,7 +1245,7 @@ test.describe( 'Block bindings', () => { const postId = await editor.publishPost(); await page.goto( `/?p=${ postId }` ); await expect( page.locator( '#paragraph-binding' ) ).toHaveText( - 'non_existing_custom_field' + 'fallback value' ); } ); @@ -1276,7 +1276,7 @@ test.describe( 'Block bindings', () => { const postId = await editor.publishPost(); await page.goto( `/?p=${ postId }` ); await expect( page.locator( '#paragraph-binding' ) ).toHaveText( - '_protected_field' + 'fallback value' ); } ); @@ -1309,7 +1309,7 @@ test.describe( 'Block bindings', () => { const postId = await editor.publishPost(); await page.goto( `/?p=${ postId }` ); await expect( page.locator( '#paragraph-binding' ) ).toHaveText( - 'show_in_rest_false_field' + 'fallback value' ); } ); } ); From ed72f4880ce0d5006710d8969800a0afee01393a Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Wed, 28 Feb 2024 16:19:47 +0000 Subject: [PATCH 06/45] Add color and typography presets to Global Styles (#56622) * Add colors and typography to the styles tab * refactor * refactor * Add font family names * add heading and body previews * use palettes * make typograpgy a grid * make the styles look nicer * move component to its own file * move color and type variations to different locations in the UI * revert * revert * revert * revert * code dedupe * Add typesets * reorg typogrpahy * tidy up * tidy up code * restyle color palettes * restyle color palettes * remove typesets * add padding back * refactor * Show theme_name + style where there is no typography font family info. Remove unused file. * Consolidating logic into a hook phase 1 * 2022 borks getFamilyPreviewStyle because `face.fontStyle` doesn't exist * comments * Filtering results * Create new convenience hook `useCurrentMergeThemeStyleVariationsWithUserConfig()` Replace usage with utils created in https://github.com/WordPress/gutenberg/pull/58803 * add a todo * A bit of clean up. Remove comments, adjust margins. * move variation to a new component * make variation a composible component * use the variation component for typography * Update typography preview * Add animation * move variation to a component * move preset to the higher level * Remove unneeded CSS * updawte the design * remove unsed css * Ensuring the cursor for the color previews is a pointer, which is the same as the typography preview Reducing the vheight of the color panel to match typography Some light refactoring * Reinstate removing property from user config before merging with variation. We do this in order that any user config changes with the same property don't "bleed" into variations. The variation should be "pure". For example, if the user has defined a background color, but the variation has not, we shouldn't merge the two and allow the user background color to persist. Rather the variation's color properties should take the place of all color properties. * i18n for letters * Refactor unique font calculation * move color and type variations higher * Use same box shadow as ColorIndicator so that there's a border around colors This is so that theme colors that are the same value as the background are more noticeable --------- Co-authored-by: ramon --- .../global-styles/color-palette-panel.js | 12 +- .../utils/preview-styles.js | 3 +- .../global-styles/preview-colors.js | 216 ++++++++++++++++++ .../components/global-styles/screen-colors.js | 14 +- .../global-styles/screen-typography.js | 19 +- .../style-variations-container.js | 103 +-------- .../src/components/global-styles/style.scss | 11 + .../src/components/global-styles/variation.js | 93 ++++++++ .../global-styles/variations-color.js | 30 +++ .../global-styles/variations-typography.js | 162 +++++++++++++ .../use-theme-style-variations-by-property.js | 173 ++++++++++++++ .../use-theme-style-variations-by-property.js | 69 +++++- 12 files changed, 809 insertions(+), 96 deletions(-) create mode 100644 packages/edit-site/src/components/global-styles/preview-colors.js create mode 100644 packages/edit-site/src/components/global-styles/variation.js create mode 100644 packages/edit-site/src/components/global-styles/variations-color.js create mode 100644 packages/edit-site/src/components/global-styles/variations-typography.js diff --git a/packages/edit-site/src/components/global-styles/color-palette-panel.js b/packages/edit-site/src/components/global-styles/color-palette-panel.js index 03401cab9f80b5..9e6bb5dbe2c77b 100644 --- a/packages/edit-site/src/components/global-styles/color-palette-panel.js +++ b/packages/edit-site/src/components/global-styles/color-palette-panel.js @@ -13,6 +13,8 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; * Internal dependencies */ import { unlock } from '../../lock-unlock'; +import ColorVariations from './variations-color'; +import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../../hooks/use-theme-style-variations/use-theme-style-variations-by-property'; const { useGlobalSetting } = unlock( blockEditorPrivateApis ); const mobilePopoverProps = { placement: 'bottom-start', offset: 8 }; @@ -45,7 +47,12 @@ export default function ColorPalettePanel( { name } ) { 'color.defaultPalette', name ); - + const colorVariations = useCurrentMergeThemeStyleVariationsWithUserConfig( { + property: 'color', + filter: ( variation ) => + variation?.settings?.color && + Object.keys( variation?.settings?.color ).length, + } ); const isMobileViewport = useViewportMatch( 'small', '<' ); const popoverProps = isMobileViewport ? mobilePopoverProps : undefined; @@ -78,6 +85,9 @@ export default function ColorPalettePanel( { name } ) { popoverProps={ popoverProps } /> ) } + { !! colorVariations.length && ( + + ) } face.fontStyle.toLowerCase() === 'normal' + ( face ) => + face?.fontStyle && face.fontStyle.toLowerCase() === 'normal' ); if ( normalFaces.length > 0 ) { style.fontStyle = 'normal'; diff --git a/packages/edit-site/src/components/global-styles/preview-colors.js b/packages/edit-site/src/components/global-styles/preview-colors.js new file mode 100644 index 00000000000000..88885baf300626 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/preview-colors.js @@ -0,0 +1,216 @@ +/** + * WordPress dependencies + */ +import { + __unstableIframe as Iframe, + __unstableEditorStyles as EditorStyles, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { + __unstableMotion as motion, + __experimentalHStack as HStack, +} from '@wordpress/components'; +import { + useThrottle, + useReducedMotion, + useResizeObserver, +} from '@wordpress/compose'; +import { useLayoutEffect, useState, useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { useStylesPreviewColors } from './hooks'; + +const { useGlobalStyle, useGlobalStylesOutput } = unlock( + blockEditorPrivateApis +); + +const firstFrame = { + start: { + scale: 1, + opacity: 1, + }, + hover: { + scale: 0, + opacity: 0, + }, +}; + +const normalizedWidth = 248; +const normalizedHeight = 152; + +const normalizedColorSwatchSize = 66; + +// Throttle options for useThrottle. Must be defined outside of the component, +// so that the object reference is the same on each render. +const THROTTLE_OPTIONS = { + leading: true, + trailing: true, +}; + +const StylesPreviewColors = ( { label, isFocused, withHoverView } ) => { + const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); + const [ gradientValue ] = useGlobalStyle( 'color.gradient' ); + const [ styles ] = useGlobalStylesOutput(); + const disableMotion = useReducedMotion(); + const [ isHovered, setIsHovered ] = useState( false ); + const [ containerResizeListener, { width } ] = useResizeObserver(); + const [ throttledWidth, setThrottledWidthState ] = useState( width ); + const [ ratioState, setRatioState ] = useState(); + + const setThrottledWidth = useThrottle( + setThrottledWidthState, + 250, + THROTTLE_OPTIONS + ); + + // Must use useLayoutEffect to avoid a flash of the iframe at the wrong + // size before the width is set. + useLayoutEffect( () => { + if ( width ) { + setThrottledWidth( width ); + } + }, [ width, setThrottledWidth ] ); + + // Must use useLayoutEffect to avoid a flash of the iframe at the wrong + // size before the width is set. + useLayoutEffect( () => { + const newRatio = throttledWidth ? throttledWidth / normalizedWidth : 1; + const ratioDiff = newRatio - ( ratioState || 0 ); + + // Only update the ratio state if the difference is big enough + // or if the ratio state is not yet set. This is to avoid an + // endless loop of updates at particular viewport heights when the + // presence of a scrollbar causes the width to change slightly. + const isRatioDiffBigEnough = Math.abs( ratioDiff ) > 0.1; + + if ( isRatioDiffBigEnough || ! ratioState ) { + setRatioState( newRatio ); + } + }, [ throttledWidth, ratioState ] ); + + // Set a fallbackRatio to use before the throttled ratio has been set. + const fallbackRatio = width ? width / normalizedWidth : 1; + /* + * Use the throttled ratio if it has been calculated, otherwise + * use the fallback ratio. The throttled ratio is used to avoid + * an endless loop of updates at particular viewport heights. + * See: https://github.com/WordPress/gutenberg/issues/55112 + */ + const ratio = ratioState ? ratioState : fallbackRatio; + + const { highlightedColors } = useStylesPreviewColors(); + + /* + * Reset leaked styles from WP common.css and remove main content layout padding and border. + * Add pointer cursor to the body to indicate the iframe is interactive, + * similar to Typography variation previews. + */ + const editorStyles = useMemo( () => { + if ( styles ) { + return [ + ...styles, + { + css: 'html{overflow:hidden}body{min-width: 0;padding: 0;border: none;cursor: pointer;}', + isGlobalStyles: true, + }, + ]; + } + + return styles; + }, [ styles ] ); + const isReady = !! width; + + return ( + <> +
+ { containerResizeListener } +
+ { isReady && ( + + ) } + + ); +}; + +export default StylesPreviewColors; diff --git a/packages/edit-site/src/components/global-styles/screen-colors.js b/packages/edit-site/src/components/global-styles/screen-colors.js index a19bd16c8839f2..c9ddef9c6230d1 100644 --- a/packages/edit-site/src/components/global-styles/screen-colors.js +++ b/packages/edit-site/src/components/global-styles/screen-colors.js @@ -11,6 +11,8 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import ScreenHeader from './header'; import Palette from './palette'; import { unlock } from '../../lock-unlock'; +import ColorVariations from './variations-color'; +import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../../hooks/use-theme-style-variations/use-theme-style-variations-by-property'; const { useGlobalStyle, @@ -29,6 +31,12 @@ function ScreenColors() { const [ rawSettings ] = useGlobalSetting( '' ); const settings = useSettingsForBlockElement( rawSettings ); + const colorVariations = useCurrentMergeThemeStyleVariationsWithUserConfig( { + property: 'color', + filter: ( variation ) => + variation?.settings?.color && + Object.keys( variation?.settings?.color ).length, + } ); return ( <>
- + + { !! colorVariations.length && ( + + ) } + + variation?.settings?.typography?.fontFamilies && + Object.keys( variation?.settings?.typography?.fontFamilies ) + .length, + } ); return ( <> @@ -30,7 +40,14 @@ function ScreenTypography() { />
- { fontLibraryEnabled && } + { !! typographyVariations.length && ( + + ) } + { ! window.__experimentalDisableFontLibrary && ( + + { fontLibraryEnabled && } + + ) }
diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/edit-site/src/components/global-styles/style-variations-container.js index 6cc8b53b800d3a..3ca856dd8fb902 100644 --- a/packages/edit-site/src/components/global-styles/style-variations-container.js +++ b/packages/edit-site/src/components/global-styles/style-variations-container.js @@ -1,104 +1,17 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; -import { useMemo, useContext, useState } from '@wordpress/element'; -import { ENTER } from '@wordpress/keycodes'; +import { useMemo } from '@wordpress/element'; import { __experimentalGrid as Grid } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { mergeBaseAndUserConfigs } from './global-styles-provider'; import StylesPreview from './preview'; -import { unlock } from '../../lock-unlock'; - -const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( - blockEditorPrivateApis -); - -function Variation( { variation } ) { - const [ isFocused, setIsFocused ] = useState( false ); - const { base, user, setUserConfig } = useContext( GlobalStylesContext ); - const context = useMemo( () => { - return { - user: { - settings: variation.settings ?? {}, - styles: variation.styles ?? {}, - }, - base, - merged: mergeBaseAndUserConfigs( base, variation ), - setUserConfig: () => {}, - }; - }, [ variation, base ] ); - - const selectVariation = () => { - setUserConfig( () => { - return { - settings: variation.settings, - styles: variation.styles, - }; - } ); - }; - - const selectOnEnter = ( event ) => { - if ( event.keyCode === ENTER ) { - event.preventDefault(); - selectVariation(); - } - }; - - const isActive = useMemo( () => { - return areGlobalStyleConfigsEqual( user, variation ); - }, [ user, variation ] ); - - let label = variation?.title; - if ( variation?.description ) { - label = sprintf( - /* translators: %1$s: variation title. %2$s variation description. */ - __( '%1$s (%2$s)' ), - variation?.title, - variation?.description - ); - } - - return ( - -
setIsFocused( true ) } - onBlur={ () => setIsFocused( false ) } - > -
- -
-
-
- ); -} +import Variation from './variation'; export default function StyleVariationsContainer() { const variations = useSelect( ( select ) => { @@ -128,7 +41,15 @@ export default function StyleVariationsContainer() { className="edit-site-global-styles-style-variations-container" > { withEmptyVariation.map( ( variation, index ) => ( - + + { ( isFocused ) => ( + + ) } + ) ) } ); diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index f53173437d47d8..d1e0a493847054 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -95,6 +95,7 @@ box-sizing: border-box; // To round the outline in Windows 10 high contrast mode. border-radius: $radius-block-ui; + cursor: pointer; .edit-site-global-styles-variations_item-preview { padding: $border-width * 2; @@ -102,6 +103,10 @@ box-shadow: 0 0 0 $border-width $gray-200; // Shown in Windows 10 high contrast mode. outline: 1px solid transparent; + + .edit-site-global-styles-color-variations & { + padding: $grid-unit-10; + } } &.is-active .edit-site-global-styles-variations_item-preview { @@ -199,3 +204,9 @@ .edit-site-global-styles-sidebar__panel .block-editor-block-icon svg { fill: currentColor; } + +.edit-site-global-styles-type-preview { + font-size: 22px; + line-height: 44px; + text-align: center; +} diff --git a/packages/edit-site/src/components/global-styles/variation.js b/packages/edit-site/src/components/global-styles/variation.js new file mode 100644 index 00000000000000..cc669130ea516d --- /dev/null +++ b/packages/edit-site/src/components/global-styles/variation.js @@ -0,0 +1,93 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useMemo, useContext, useState } from '@wordpress/element'; +import { ENTER } from '@wordpress/keycodes'; +import { __, sprintf } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { mergeBaseAndUserConfigs } from './global-styles-provider'; +import { unlock } from '../../lock-unlock'; + +const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( + blockEditorPrivateApis +); + +export default function Variation( { variation, children } ) { + const [ isFocused, setIsFocused ] = useState( false ); + const { base, user, setUserConfig } = useContext( GlobalStylesContext ); + const context = useMemo( + () => ( { + user: { + settings: variation.settings ?? {}, + styles: variation.styles ?? {}, + }, + base, + merged: mergeBaseAndUserConfigs( base, variation ), + setUserConfig: () => {}, + } ), + [ variation, base ] + ); + + const selectVariation = () => { + setUserConfig( () => ( { + settings: variation.settings, + styles: variation.styles, + } ) ); + }; + + const selectOnEnter = ( event ) => { + if ( event.keyCode === ENTER ) { + event.preventDefault(); + selectVariation(); + } + }; + + const isActive = useMemo( + () => areGlobalStyleConfigsEqual( user, variation ), + [ user, variation ] + ); + + let label = variation?.title; + if ( variation?.description ) { + label = sprintf( + /* translators: %1$s: variation title. %2$s variation description. */ + __( '%1$s (%2$s)' ), + variation?.title, + variation?.description + ); + } + + return ( + +
setIsFocused( true ) } + onBlur={ () => setIsFocused( false ) } + > +
+ { children( isFocused ) } +
+
+
+ ); +} diff --git a/packages/edit-site/src/components/global-styles/variations-color.js b/packages/edit-site/src/components/global-styles/variations-color.js new file mode 100644 index 00000000000000..badb07336d303f --- /dev/null +++ b/packages/edit-site/src/components/global-styles/variations-color.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { + __experimentalGrid as Grid, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Subtitle from './subtitle'; +import Variation from './variation'; +import StylesPreviewColors from './preview-colors'; + +export default function ColorVariations( { variations } ) { + return ( + + { __( 'Presets' ) } + + { variations.map( ( variation, index ) => ( + + { () => } + + ) ) } + + + ); +} diff --git a/packages/edit-site/src/components/global-styles/variations-typography.js b/packages/edit-site/src/components/global-styles/variations-typography.js new file mode 100644 index 00000000000000..cd30ed0b07756e --- /dev/null +++ b/packages/edit-site/src/components/global-styles/variations-typography.js @@ -0,0 +1,162 @@ +/** + * WordPress dependencies + */ +import { useContext } from '@wordpress/element'; +import { + __experimentalGrid as Grid, + __experimentalVStack as VStack, + __unstableMotion as motion, +} from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { mergeBaseAndUserConfigs } from './global-styles-provider'; +import { unlock } from '../../lock-unlock'; +import { getFamilyPreviewStyle } from './font-library-modal/utils/preview-styles'; +import { useCurrentMergeThemeStyleVariationsWithUserConfig } from '../../hooks/use-theme-style-variations/use-theme-style-variations-by-property'; +import Subtitle from './subtitle'; +import Variation from './variation'; + +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); + +function getFontFamilyFromSetting( fontFamilies, setting ) { + if ( ! setting ) { + return null; + } + + const fontFamilyVariable = setting.replace( 'var(', '' ).replace( ')', '' ); + const fontFamilySlug = fontFamilyVariable?.split( '--' ).slice( -1 )[ 0 ]; + + return fontFamilies.find( + ( fontFamily ) => fontFamily.slug === fontFamilySlug + ); +} + +const getFontFamilies = ( themeJson ) => { + const fontFamilies = themeJson?.settings?.typography?.fontFamilies?.theme; // TODO this could not be under theme. + const bodyFontFamilySetting = themeJson?.styles?.typography?.fontFamily; + const bodyFontFamily = getFontFamilyFromSetting( + fontFamilies, + bodyFontFamilySetting + ); + + const headingFontFamilySetting = + themeJson?.styles?.elements?.heading?.typography?.fontFamily; + + let headingFontFamily; + if ( ! headingFontFamilySetting ) { + headingFontFamily = bodyFontFamily; + } else { + headingFontFamily = getFontFamilyFromSetting( + fontFamilies, + themeJson?.styles?.elements?.heading?.typography?.fontFamily + ); + } + + return [ bodyFontFamily, headingFontFamily ]; +}; + +const TypePreview = ( { variation } ) => { + const { base } = useContext( GlobalStylesContext ); + const [ bodyFontFamilies, headingFontFamilies ] = getFontFamilies( + mergeBaseAndUserConfigs( base, variation ) + ); + const bodyPreviewStyle = bodyFontFamilies + ? getFamilyPreviewStyle( bodyFontFamilies ) + : {}; + const headingPreviewStyle = headingFontFamilies + ? getFamilyPreviewStyle( headingFontFamilies ) + : {}; + return ( + + + { _x( 'A', 'Uppercase letter A' ) } + + + { _x( 'a', 'Lowercase letter A' ) } + + + ); +}; + +export default function TypographyVariations() { + const typographyVariations = + useCurrentMergeThemeStyleVariationsWithUserConfig( { + property: 'typography', + filter: ( variation ) => + variation?.settings?.typography?.fontFamilies && + Object.keys( variation?.settings?.typography?.fontFamilies ) + .length, + } ); + + const { base } = useContext( GlobalStylesContext ); + + /* + * Filter duplicate variations based on the font families used in the variation. + */ + const uniqueTypographyVariations = typographyVariations?.length + ? Object.values( + typographyVariations.reduce( ( acc, variation ) => { + const [ bodyFontFamily, headingFontFamily ] = + getFontFamilies( + mergeBaseAndUserConfigs( base, variation ) + ); + if ( + headingFontFamily?.name && + bodyFontFamily?.name && + ! acc[ + `${ headingFontFamily?.name }:${ bodyFontFamily?.name }` + ] + ) { + acc[ + `${ headingFontFamily?.name }:${ bodyFontFamily?.name }` + ] = variation; + } + + return acc; + }, {} ) + ) + : []; + + return ( + + { __( 'Presets' ) } + + { typographyVariations && typographyVariations.length + ? uniqueTypographyVariations.map( ( variation, index ) => { + return ( + + { () => ( + + ) } + + ); + } ) + : null } + + + ); +} diff --git a/packages/edit-site/src/hooks/use-theme-style-variations/test/use-theme-style-variations-by-property.js b/packages/edit-site/src/hooks/use-theme-style-variations/test/use-theme-style-variations-by-property.js index ced849f8fb1213..23db164b24895d 100644 --- a/packages/edit-site/src/hooks/use-theme-style-variations/test/use-theme-style-variations-by-property.js +++ b/packages/edit-site/src/hooks/use-theme-style-variations/test/use-theme-style-variations-by-property.js @@ -8,6 +8,7 @@ import { renderHook } from '@testing-library/react'; */ import useThemeStyleVariationsByProperty, { filterObjectByProperty, + removePropertyFromObject, } from '../use-theme-style-variations-by-property'; describe( 'filterObjectByProperty', () => { @@ -962,3 +963,175 @@ describe( 'useThemeStyleVariationsByProperty', () => { ] ); } ); } ); + +describe( 'removePropertyFromObject', () => { + const mockBaseVariation = { + settings: { + typography: { + fontFamilies: { + custom: [ + { + name: 'ADLaM Display', + fontFamily: 'ADLaM Display, system-ui', + slug: 'adlam-display', + fontFace: [ + { + src: 'adlam.woff2', + fontWeight: '400', + fontStyle: 'normal', + fontFamily: 'ADLaM Display', + }, + ], + }, + ], + }, + fontSizes: [ + { + name: 'Base small', + slug: 'base-small', + size: '1px', + }, + { + name: 'Base medium', + slug: 'base-medium', + size: '2px', + }, + { + name: 'Base large', + slug: 'base-large', + size: '3px', + }, + ], + }, + color: { + palette: { + custom: [ + { + color: '#c42727', + name: 'Color 1', + slug: 'custom-color-1', + }, + { + color: '#3b0f0f', + name: 'Color 2', + slug: 'custom-color-2', + }, + ], + }, + }, + layout: { + wideSize: '1137px', + contentSize: '400px', + }, + }, + styles: { + typography: { + fontSize: '12px', + lineHeight: '1.5', + }, + color: { + backgroundColor: 'cheese', + color: 'lettuce', + }, + elements: { + cite: { + color: { + text: 'white', + }, + typography: { + letterSpacing: 'white', + }, + }, + }, + blocks: { + 'core/quote': { + color: { + text: 'hello', + background: 'dolly', + }, + typography: { + fontSize: '111111px', + }, + }, + 'core/group': { + typography: { + fontFamily: 'var:preset|font-family|system-sans-serif', + }, + }, + }, + }, + }; + + it( 'should return with no property', () => { + const object = { test: 'me' }; + expect( removePropertyFromObject( object, undefined ) ).toEqual( + object + ); + } ); + + it( 'should return with non-string property', () => { + const object = { test: 'you' }; + expect( removePropertyFromObject( object, true ) ).toEqual( object ); + } ); + + it( 'should return with empty object', () => { + const object = {}; + expect( removePropertyFromObject( object, 'color' ) ).toEqual( object ); + } ); + + it( 'should remove the specified property from the object', () => { + expect( + removePropertyFromObject( + { + ...mockBaseVariation, + }, + 'typography' + ) + ).toEqual( { + settings: { + color: { + palette: { + custom: [ + { + color: '#c42727', + name: 'Color 1', + slug: 'custom-color-1', + }, + { + color: '#3b0f0f', + name: 'Color 2', + slug: 'custom-color-2', + }, + ], + }, + }, + layout: { + wideSize: '1137px', + contentSize: '400px', + }, + }, + styles: { + color: { + backgroundColor: 'cheese', + color: 'lettuce', + }, + elements: { + cite: { + color: { + text: 'white', + }, + }, + }, + blocks: { + 'core/quote': { + color: { + text: 'hello', + background: 'dolly', + }, + }, + 'core/group': {}, + }, + }, + } ); + } ); +} ); diff --git a/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js b/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js index b9c1b40ec7c1d3..d15b624c9eb44e 100644 --- a/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js +++ b/packages/edit-site/src/hooks/use-theme-style-variations/use-theme-style-variations-by-property.js @@ -1,13 +1,76 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useContext, useMemo } from '@wordpress/element'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; /** * Internal dependencies */ import { mergeBaseAndUserConfigs } from '../../components/global-styles/global-styles-provider'; import cloneDeep from '../../utils/clone-deep'; +import { unlock } from '../../lock-unlock'; + +const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); + +/** + * Removes all instances of a property from an object. + * + * @param {Object} object The object to remove the property from. + * @param {string} property The property to remove. + * @return {Object} The modified object. + */ +export function removePropertyFromObject( object, property ) { + if ( ! property || typeof property !== 'string' ) { + return object; + } + + if ( typeof object !== 'object' || ! Object.keys( object ).length ) { + return object; + } + + for ( const key in object ) { + if ( key === property ) { + delete object[ key ]; + } else if ( typeof object[ key ] === 'object' ) { + removePropertyFromObject( object[ key ], property ); + } + } + return object; +} + +/** + * A convenience wrapper for `useThemeStyleVariationsByProperty()` that fetches the current theme style variations, + * and user-defined global style/settings object. + * + * @param {Object} props Object of hook args. + * @param {string} props.property The property to filter by. + * @param {Function} props.filter Optional. The filter function to apply to the variations. + * @return {Object[]|*} The merged object. + */ +export function useCurrentMergeThemeStyleVariationsWithUserConfig( { + property, + filter, +} ) { + const variations = useSelect( ( select ) => { + return select( + coreStore + ).__experimentalGetCurrentThemeGlobalStylesVariations(); + }, [] ); + const { user: baseVariation } = useContext( GlobalStylesContext ); + + return useThemeStyleVariationsByProperty( { + variations, + property, + filter, + baseVariation: removePropertyFromObject( + cloneDeep( baseVariation ), + property + ), + } ); +} /** * Returns a new object, with properties specified in `property`, @@ -40,6 +103,10 @@ export const filterObjectByProperty = ( object, property ) => { /** * Returns a new object with only the properties specified in `property`. + * Optional merges the baseVariation object with the variation object. + * Note: this function will only overwrite the specified property in baseVariation if it exists. + * The baseVariation will not be otherwise modified. To strip a property from the baseVariation object, use `removePropertyFromObject`. + * See useCurrentMergeThemeStyleVariationsWithUserConfig for an example of how to use this function. * * @param {Object} props Object of hook args. * @param {Object[]} props.variations The theme style variations to filter. From 2ca6cb43751d643b81bbfb41dc0a16fd02c8bbac Mon Sep 17 00:00:00 2001 From: Grant Kinney Date: Wed, 28 Feb 2024 13:04:36 -0600 Subject: [PATCH 07/45] Font Library: Hide UI elements when user lacks permissions (#59332) Co-authored-by: creativecoder Co-authored-by: matiasbenedetto Co-authored-by: mikachan Co-authored-by: peterwilsoncc Co-authored-by: youknowriad Co-authored-by: swissspidy Co-authored-by: getdave --- .../global-styles/font-library-modal/index.js | 35 +++++++++++-------- .../font-library-modal/installed-fonts.js | 19 +++++++++- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/index.js index dc0fcd7ea373b0..481befaf6e3c0f 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/index.js @@ -6,6 +6,8 @@ import { Modal, privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; import { useContext } from '@wordpress/element'; /** @@ -19,16 +21,15 @@ import { unlock } from '../../../lock-unlock'; const { Tabs } = unlock( componentsPrivateApis ); -const DEFAULT_TABS = [ - { - id: 'installed-fonts', - title: __( 'Library' ), - }, - { - id: 'upload-fonts', - title: __( 'Upload' ), - }, -]; +const DEFAULT_TAB = { + id: 'installed-fonts', + title: __( 'Library' ), +}; + +const UPLOAD_TAB = { + id: 'upload-fonts', + title: __( 'Upload' ), +}; const tabsFromCollections = ( collections ) => collections.map( ( { slug, name } ) => ( { @@ -44,11 +45,17 @@ function FontLibraryModal( { defaultTabId = 'installed-fonts', } ) { const { collections, setNotice } = useContext( FontLibraryContext ); + const canUserCreate = useSelect( ( select ) => { + const { canUser } = select( coreStore ); + return canUser( 'create', 'font-families' ); + }, [] ); + + const tabs = [ DEFAULT_TAB ]; - const tabs = [ - ...DEFAULT_TABS, - ...tabsFromCollections( collections || [] ), - ]; + if ( canUserCreate ) { + tabs.push( UPLOAD_TAB ); + tabs.push( ...tabsFromCollections( collections || [] ) ); + } // Reset notice when new tab is selected. const onSelect = () => { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js index 9425cb9c2d27ba..226e9cf5a3eefc 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/installed-fonts.js @@ -18,6 +18,8 @@ import { Spinner, privateApis as componentsPrivateApis, } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; import { useContext, useEffect, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { chevronLeft } from '@wordpress/icons'; @@ -49,9 +51,24 @@ function InstalledFonts() { setNotice, } = useContext( FontLibraryContext ); const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState( false ); + const customFontFamilyId = + libraryFontSelected?.source === 'custom' && libraryFontSelected?.id; + + const canUserDelete = useSelect( + ( select ) => { + const { canUser } = select( coreStore ); + return ( + customFontFamilyId && + canUser( 'delete', 'font-families', customFontFamilyId ) + ); + }, + [ customFontFamilyId ] + ); const shouldDisplayDeleteButton = - !! libraryFontSelected && libraryFontSelected?.source !== 'theme'; + !! libraryFontSelected && + libraryFontSelected?.source !== 'theme' && + canUserDelete; const handleUninstallClick = () => { setIsConfirmDeleteOpen( true ); From 340004ef9b6fb87d37aad242ca354c44a605a85b Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 28 Feb 2024 19:14:24 +0000 Subject: [PATCH 08/45] Bump plugin version to 17.8.0 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 1226a3e24917d5..9c314d47d9ad6d 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.8.0-rc.4 + * Version: 17.8.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 9c03b2e0e1fafa..324736a01403c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.8.0-rc.4", + "version": "17.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.8.0-rc.4", + "version": "17.8.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index f93f37999cc6bd..ce777b3c1b96e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.8.0-rc.4", + "version": "17.8.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From a6f953f29f1a462db3e23a762d9719ec08775e45 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 28 Feb 2024 19:37:09 +0000 Subject: [PATCH 09/45] Update Changelog for 17.8.0 --- changelog.txt | 538 +------------------------------------------------- 1 file changed, 9 insertions(+), 529 deletions(-) diff --git a/changelog.txt b/changelog.txt index 75cfddbf5612e8..fbb5fe66bfdcea 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,23 +1,6 @@ == Changelog == -= 17.8.0-rc.4 = - -## Changelog - -### Tools - -#### Testing -Fix failing Dropdown Menu e2e tests. ([59356](https://github.com/WordPress/gutenberg/pull/59356)) - - -## Contributors - -The following contributors merged PRs in this release: - -@Mamaduka - - -= 17.8.0-rc.3 = += 17.8.0 = ## Changelog @@ -70,10 +53,7 @@ The following contributors merged PRs in this release: - Distraction Free Mode: fix ui toggling bugs. ([59061](https://github.com/WordPress/gutenberg/pull/59061)) - Layout: Refactor responsive logic for grid column spans. ([59057](https://github.com/WordPress/gutenberg/pull/59057)) - Interactivity API: Only add proxies to plain objects inside the store. ([59039](https://github.com/WordPress/gutenberg/pull/59039)) - -#### Block Hooks -- Fix in Navigation block. ([59021](https://github.com/WordPress/gutenberg/pull/59021)) -- Take controlled blocks into account for toggle state. ([59367](https://github.com/WordPress/gutenberg/pull/59367)) +- Cover Block: Restore overflow: Clip rule to allow border radius again. ([59388](https://github.com/WordPress/gutenberg/pull/59388)) #### List View - Editor: Do not open list view by default on mobile. ([59016](https://github.com/WordPress/gutenberg/pull/59016)) @@ -100,6 +80,10 @@ The following contributors merged PRs in this release: - DataViews: Remove second `reset filter` button in filter dialog. ([58960](https://github.com/WordPress/gutenberg/pull/58960)) - Revert footer in pages list with DataViews. ([59151](https://github.com/WordPress/gutenberg/pull/59151)) +#### Block Hooks +- Fix in Navigation block. ([59021](https://github.com/WordPress/gutenberg/pull/59021)) +- Take controlled blocks into account for toggle state. ([59367](https://github.com/WordPress/gutenberg/pull/59367)) + #### Block Editor - After Enter transform, skip other onEnter actions like splitting. ([59064](https://github.com/WordPress/gutenberg/pull/59064)) - Close link preview if collapsed selection when creating link. ([58896](https://github.com/WordPress/gutenberg/pull/58896)) @@ -129,9 +113,9 @@ The following contributors merged PRs in this release: #### Font Library - Fixes installed font families not rendering in the editor or frontend. ([59019](https://github.com/WordPress/gutenberg/pull/59019)) -- Font Libary: Add missing translation functions. ([58104](https://github.com/WordPress/gutenberg/pull/58104)) +- Font Library: Add missing translation functions. ([58104](https://github.com/WordPress/gutenberg/pull/58104)) - Show error message when no fonts found to install. ([58914](https://github.com/WordPress/gutenberg/pull/58914)) -- Create post types on init hook. ([59333](https://github.com/WordPress/gutenberg/pull/59333)) +- Font Library: Create post types on init hook. ([59333](https://github.com/WordPress/gutenberg/pull/59333)) #### Synced Patterns - Fix missing source for binding attributes. ([59194](https://github.com/WordPress/gutenberg/pull/59194)) @@ -142,7 +126,6 @@ The following contributors merged PRs in this release: - Background image support: Fix issue with background position keyboard entry. ([59050](https://github.com/WordPress/gutenberg/pull/59050)) - Cover block: Clear the min height field when aspect ratio is set. ([59191](https://github.com/WordPress/gutenberg/pull/59191)) - Elements: Fix block instance element styles for links applying to buttons. ([59114](https://github.com/WordPress/gutenberg/pull/59114)) -- Cover Block: Restore overflow: Clip rule to allow border radius again. ([59388](https://github.com/WordPress/gutenberg/pull/59388)) #### Components - Modal: Add `box-sizing` reset style. ([58905](https://github.com/WordPress/gutenberg/pull/58905)) @@ -262,6 +245,7 @@ The following contributors merged PRs in this release: - Update test environment default theme versions to latest. ([58955](https://github.com/WordPress/gutenberg/pull/58955)) - Performance tests: Make site editor performance test backwards compatible. ([59266](https://github.com/WordPress/gutenberg/pull/59266)) - Performance tests: Update selectors in site editor pattern loading tests. ([59259](https://github.com/WordPress/gutenberg/pull/59259)) +- Fix failing Dropdown Menu e2e tests. ([59356](https://github.com/WordPress/gutenberg/pull/59356)) #### Build Tooling - Add test:e2e:playwright:debug command to debug Playwright tests. ([58808](https://github.com/WordPress/gutenberg/pull/58808)) @@ -277,510 +261,6 @@ The following contributors merged PRs in this release: @aaronrobertshaw @afercia @ajlende @alexstine @andrewhayward @andrewserong @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @creativecoder @DAreRodz @derekblank @desrosj @draganescu @ellatrix @fabiankaegy @gaambo @glendaviesnz @jameskoster @janboddez @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanfra @kevin940726 @Mamaduka @MarieComet @matiasbenedetto @mirka @noisysocks @ntsekouras @oandregal @ockham @pbking @ramonjd @SantosGuillamot @scruffian @shreyash3087 @t-hamano @talldan @tellthemachines @tyxla @youknowriad -= 17.8.0-rc.2 = - -## Changelog - -### Features - -- Patterns: add bulk export patterns action. ([58897](https://github.com/WordPress/gutenberg/pull/58897)) -- Template editor/inspector: show and select related patterns. ([55091](https://github.com/WordPress/gutenberg/pull/55091)) - -#### Layout -- Add toggle for grid types and stabilise Grid block variation. ([59051](https://github.com/WordPress/gutenberg/pull/59051) and [59116](https://github.com/WordPress/gutenberg/pull/59116)) -- Add support for column and row span in grid children. ([58539](https://github.com/WordPress/gutenberg/pull/58539)) - - -### Enhancements - -- Patterns Page: Make category action button compact. ([59203](https://github.com/WordPress/gutenberg/pull/59203)) -- Block Editor: Use hooks instead of HoC in 'SkipToSelectedBlock'. ([59202](https://github.com/WordPress/gutenberg/pull/59202)) -- Font Library: Adds the ability to use generic() in font family names. ([59103](https://github.com/WordPress/gutenberg/pull/59103) and [59037](https://github.com/WordPress/gutenberg/pull/59037)) -- REST API Global Styles Revisions Controller: Return single revision only when it matches the parent id. ([59049](https://github.com/WordPress/gutenberg/pull/59049)) -- CSS & Styling: Tweak link focus outline styles in HTML anchor and custom CSS. ([59048](https://github.com/WordPress/gutenberg/pull/59048)) -- Data Views: Make 'All pages' view label consistent with template and patterns. ([59009](https://github.com/WordPress/gutenberg/pull/59009)) -- Script Modules API: Script Modules add deregister option. ([58830](https://github.com/WordPress/gutenberg/pull/58830)) - -#### Custom Fields -- Block Bindings: Lock editing of blocks by default. ([58787](https://github.com/WordPress/gutenberg/pull/58787)) -- Style engine: Rename at_rule to rules_groups and update test/docs. ([58922](https://github.com/WordPress/gutenberg/pull/58922)) - -#### Block Library -- Gallery: Set the 'defaultBlock' setting for inner blocks. ([59168](https://github.com/WordPress/gutenberg/pull/59168)) -- Remove the navigation edit button because it leads to a useless screen. ([59211](https://github.com/WordPress/gutenberg/pull/59211)) -- Set the 'defaultBlock' setting for Columns & List blocks. ([59196](https://github.com/WordPress/gutenberg/pull/59196)) -- Update: Increase footnotes meta priority and separate footnotes meta registration. ([58882](https://github.com/WordPress/gutenberg/pull/58882)) - -#### Site Editor -- Editor: Hide template part and post content blocks in some site editor contexts. ([58928](https://github.com/WordPress/gutenberg/pull/58928)) -- Tweak save hub button. ([58917](https://github.com/WordPress/gutenberg/pull/58917) and [59200](https://github.com/WordPress/gutenberg/pull/59200)) - -#### Components -- CustomSelect: Adapt component for legacy props. ([57902](https://github.com/WordPress/gutenberg/pull/57902)) -- Use `Element.scrollIntoView()` instead of `dom-scroll-into-view`. ([59085](https://github.com/WordPress/gutenberg/pull/59085)) - -#### Global Styles -- Global style changes: Refactor output for a more flexible UI and grouping. ([59055](https://github.com/WordPress/gutenberg/pull/59055)) -- Style theme variations: Add property extraction and merge utils. ([58803](https://github.com/WordPress/gutenberg/pull/58803)) - - -### Bug Fixes - -- Distraction Free Mode: fix ui toggling bugs. ([59061](https://github.com/WordPress/gutenberg/pull/59061)) -- Layout: Refactor responsive logic for grid column spans. ([59057](https://github.com/WordPress/gutenberg/pull/59057)) -- Interactivity API: Only add proxies to plain objects inside the store. ([59039](https://github.com/WordPress/gutenberg/pull/59039)) -- Block Hooks: Fix in Navigation block. ([59021](https://github.com/WordPress/gutenberg/pull/59021)) - -#### List View -- Editor: Do not open list view by default on mobile. ([59016](https://github.com/WordPress/gutenberg/pull/59016)) -- Create Block: Add missing `viewScriptModule` field. ([59140](https://github.com/WordPress/gutenberg/pull/59140)) -- Ignore the 'twentytwentyfour' test theme dir created by wp-env. ([59072](https://github.com/WordPress/gutenberg/pull/59072)) -- useEntityBlockEditor: Update 'content' type check. ([59058](https://github.com/WordPress/gutenberg/pull/59058)) - -#### Block Library -- Author, Author Bio, Author Name: Add a fallback for Author Archive Template. ([55451](https://github.com/WordPress/gutenberg/pull/55451)) -- Fix Spacer orientation when inside a block with default flex layout. ([58921](https://github.com/WordPress/gutenberg/pull/58921)) -- Fix WP 6.4/6.3 compat for navigation link variations. ([59126](https://github.com/WordPress/gutenberg/pull/59126)) -- Interactivity API: Fix server side rendering for Search block. ([59029](https://github.com/WordPress/gutenberg/pull/59029)) -- Navigation: Avoid using embedded record from fallback API. ([59076](https://github.com/WordPress/gutenberg/pull/59076)) -- Pagination Numbers: Add `data-wp-key` to pagination numbers if enhanced pagination is enabled. ([58189](https://github.com/WordPress/gutenberg/pull/58189)) -- Revert "Navigation: Refactor mobile overlay breakpoints to JS (#57520)". ([59149](https://github.com/WordPress/gutenberg/pull/59149)) -- Spacer block: Fix `null` label in tooltip when horizontal layout. ([58909](https://github.com/WordPress/gutenberg/pull/58909)) - -#### Data Views -- DataViews: Add loading/no results message in grid view. ([59002](https://github.com/WordPress/gutenberg/pull/59002)) -- DataViews: Correctly display featured image that don't have image sizes. ([59111](https://github.com/WordPress/gutenberg/pull/59111)) -- DataViews: Fix pages list back path. ([59201](https://github.com/WordPress/gutenberg/pull/59201)) -- DataViews: Fix patterns, templates and template parts pagination `z-index`. ([58965](https://github.com/WordPress/gutenberg/pull/58965)) -- DataViews: Fix storybook. ([58842](https://github.com/WordPress/gutenberg/pull/58842)) -- DataViews: Remove second `reset filter` button in filter dialog. ([58960](https://github.com/WordPress/gutenberg/pull/58960)) -- Revert footer in pages list with DataViews. ([59151](https://github.com/WordPress/gutenberg/pull/59151)) - -#### Block Editor -- After Enter transform, skip other onEnter actions like splitting. ([59064](https://github.com/WordPress/gutenberg/pull/59064)) -- Close link preview if collapsed selection when creating link. ([58896](https://github.com/WordPress/gutenberg/pull/58896)) -- Editor: Limit spotlight mode to the editor. ([58817](https://github.com/WordPress/gutenberg/pull/58817)) -- Fix incorrect useAnchor positioning when switching from virtual to rich text elements. ([58900](https://github.com/WordPress/gutenberg/pull/58900)) -- Inserter: Don't select the closest block with 'disabled' editing mode. ([58971](https://github.com/WordPress/gutenberg/pull/58971)) -- Inserter: Fix title condition for media tab previews. ([58993](https://github.com/WordPress/gutenberg/pull/58993)) - -#### Site Editor -- Fix navigation on mobile web. ([59014](https://github.com/WordPress/gutenberg/pull/59014)) -- Fix: Don't render the Transform Into panel if there are no patterns. ([59217](https://github.com/WordPress/gutenberg/pull/59217)) -- Fix: Logical error in filterPatterns on template-panel/hooks.js. ([59218](https://github.com/WordPress/gutenberg/pull/59218)) -- Make command palette string transatables. ([59133](https://github.com/WordPress/gutenberg/pull/59133)) -- Remove left margin on Status help text. ([58775](https://github.com/WordPress/gutenberg/pull/58775)) - -#### Patterns -- Allow editing of image block alt and title attributes in content only mode. ([58998](https://github.com/WordPress/gutenberg/pull/58998)) -- Avoid showing block removal warning when deleting a pattern instance that has overrides. ([59044](https://github.com/WordPress/gutenberg/pull/59044)) -- Block editor: Pass patterns selector as setting. ([58661](https://github.com/WordPress/gutenberg/pull/58661)) -- Fix pattern categories on import. ([58926](https://github.com/WordPress/gutenberg/pull/58926)) -- Site editor: Fix start patterns store selector. ([58813](https://github.com/WordPress/gutenberg/pull/58813)) - -#### Global Styles -- Fix console error in block preview. ([59112](https://github.com/WordPress/gutenberg/pull/59112)) -- Revert "Use all the settings origins for a block that consumes paths with merge #55219" ([58951](https://github.com/WordPress/gutenberg/pull/58951) and [59101](https://github.com/WordPress/gutenberg/pull/59101)) -- Shadows: Don't assume that core provides default shadows. ([58973](https://github.com/WordPress/gutenberg/pull/58973)) - -#### Font Library -- Fixes installed font families not rendering in the editor or frontend. ([59019](https://github.com/WordPress/gutenberg/pull/59019)) -- Font Libary: Add missing translation functions. ([58104](https://github.com/WordPress/gutenberg/pull/58104)) -- Show error message when no fonts found to install. ([58914](https://github.com/WordPress/gutenberg/pull/58914)) - -#### Synced Patterns -- Fix missing source for binding attributes. ([59194](https://github.com/WordPress/gutenberg/pull/59194)) -- Fix resetting individual blocks to empty optional values for Pattern Overrides. ([59170](https://github.com/WordPress/gutenberg/pull/59170)) -- Fix upload button on overridden empty image block in patterns. ([59169](https://github.com/WordPress/gutenberg/pull/59169)) - -#### Design Tools -- Background image support: Fix issue with background position keyboard entry. ([59050](https://github.com/WordPress/gutenberg/pull/59050)) -- Cover block: Clear the min height field when aspect ratio is set. ([59191](https://github.com/WordPress/gutenberg/pull/59191)) -- Elements: Fix block instance element styles for links applying to buttons. ([59114](https://github.com/WordPress/gutenberg/pull/59114)) - -#### Components -- Modal: Add `box-sizing` reset style. ([58905](https://github.com/WordPress/gutenberg/pull/58905)) -- ToolbarButton: Fix text centering for short labels. ([59117](https://github.com/WordPress/gutenberg/pull/59117)) -- Upgrade Floating UI packages, fix nested iframe positioning bug. ([58932](https://github.com/WordPress/gutenberg/pull/58932)) - -#### Post Editor -- Editor: Fix 'useHideBlocksFromInserter' hook filename. ([59150](https://github.com/WordPress/gutenberg/pull/59150)) -- Fix layout for non viewable post types. ([58962](https://github.com/WordPress/gutenberg/pull/58962)) - -#### Rich Text -- Fix link paste for internal paste. ([59063](https://github.com/WordPress/gutenberg/pull/59063)) -- Revert "Rich text: Pad multiple spaces through en/em replacement". ([58792](https://github.com/WordPress/gutenberg/pull/58792)) - -#### Custom Fields -- Block Bindings: Add block context needed for bindings in PHP. ([58554](https://github.com/WordPress/gutenberg/pull/58554)) -- Block Bindings: Fix disable bindings editing when source is undefined. ([58961](https://github.com/WordPress/gutenberg/pull/58961)) - - -### Accessibility - -- Enter editing mode via Enter or Spacebar. ([58795](https://github.com/WordPress/gutenberg/pull/58795)) -- Block Bindings > Image Block:Mark connected controls as 'readonly'. ([59059](https://github.com/WordPress/gutenberg/pull/59059)) -- Details Block: Try double enter to escape inner blocks. ([58903](https://github.com/WordPress/gutenberg/pull/58903)) -- Font Library: Replace infinite scroll by pagination. ([58794](https://github.com/WordPress/gutenberg/pull/58794)) -- Global Styles: Remove menubar role and improve complementary area header semantics. ([58740](https://github.com/WordPress/gutenberg/pull/58740)) - -#### Block Editor -- Block Mover: Unify visual separator when show button label is on. ([59158](https://github.com/WordPress/gutenberg/pull/59158)) -- Make the custom CSS validation error message accessible. ([56690](https://github.com/WordPress/gutenberg/pull/56690)) -- Restore default border and focus style on image URL input field. ([58505](https://github.com/WordPress/gutenberg/pull/58505)) - -### Performance - -- Pattern Block: Batch replacing actions. ([59075](https://github.com/WordPress/gutenberg/pull/59075)) -- Block Editor: Move StopEditingAsBlocksOnOutsideSelect to Root. ([58412](https://github.com/WordPress/gutenberg/pull/58412)) - - -### Documentation - -- Add contributing guidlines around Component versioning. ([58789](https://github.com/WordPress/gutenberg/pull/58789)) -- Clarify the performance reference commit and how to pick it. ([58927](https://github.com/WordPress/gutenberg/pull/58927)) -- DataViews: Update documentation. ([58847](https://github.com/WordPress/gutenberg/pull/58847)) -- Docs: Clarify the status of the wp-block-styles theme support, and its intent. ([58915](https://github.com/WordPress/gutenberg/pull/58915)) -- Fix move interactivity schema to supports property instead of selectors property. ([59166](https://github.com/WordPress/gutenberg/pull/59166)) -- Storybook: Show badges in sidebar. ([58518](https://github.com/WordPress/gutenberg/pull/58518)) -- Theme docs: Update appearance-tools documentation to reflect opt-in for backgroundSize and aspectRatio. ([59165](https://github.com/WordPress/gutenberg/pull/59165)) -- Update richtext.md. ([59089](https://github.com/WordPress/gutenberg/pull/59089)) - -#### Interactivity API -- Interactivity API: Fix WP version, update new store documentation. ([59107](https://github.com/WordPress/gutenberg/pull/59107)) -- Interactivity API: Update documentation guide with new `wp-interactivity` directive implementation. ([59018](https://github.com/WordPress/gutenberg/pull/59018)) -- Add interactivity property to block supports reference documentation. ([59152](https://github.com/WordPress/gutenberg/pull/59152)) - -#### Schemas -- Block JSON schema: Add `viewScriptModule` field. ([59060](https://github.com/WordPress/gutenberg/pull/59060)) -- Block JSON schema: Update `shadow` definition. ([58910](https://github.com/WordPress/gutenberg/pull/58910)) -- JSON schema: Update schema for background support. ([59127](https://github.com/WordPress/gutenberg/pull/59127)) - -### Code Quality - -- Create Block: Remove deprecated viewModule field. ([59198](https://github.com/WordPress/gutenberg/pull/59198)) -- Editor: Remove the 'all' rendering mode. ([58935](https://github.com/WordPress/gutenberg/pull/58935)) -- Editor: Unify the editor commands between post and site editors. ([59005](https://github.com/WordPress/gutenberg/pull/59005)) -- Relocate 'ErrorBoundary' component unit test folders. ([59031](https://github.com/WordPress/gutenberg/pull/59031)) -- Remove obsolete wp-env configuration from package.json (#58877). ([58899](https://github.com/WordPress/gutenberg/pull/58899)) -- Design Tools > Elements: Make editor selector match theme.json and frontend. ([59167](https://github.com/WordPress/gutenberg/pull/59167)) -- Global Styles: Update sprintf calls using `_n`. ([59160](https://github.com/WordPress/gutenberg/pull/59160)) -- Block API: Revert "Block Hooks: Set ignoredHookedBlocks metada attr upon insertion". ([58969](https://github.com/WordPress/gutenberg/pull/58969)) -- Editor > Rich Text: Remove inline toolbar preference. ([58945](https://github.com/WordPress/gutenberg/pull/58945)) -- Style Variations: Remove preferred style variations legacy support. ([58930](https://github.com/WordPress/gutenberg/pull/58930)) -- REST API > Template Revisions: Move from experimental to compat/6.4. ([58920](https://github.com/WordPress/gutenberg/pull/58920)) - -#### Block Editor -- Block-editor: Auto-register block commands. ([59079](https://github.com/WordPress/gutenberg/pull/59079)) -- BlockSettingsMenu: Combine 'block-editor' store selectors. ([59153](https://github.com/WordPress/gutenberg/pull/59153)) -- Clean up link control CSS. ([58934](https://github.com/WordPress/gutenberg/pull/58934)) -- HeadingLevelDropdown: Remove unnecessary isPressed prop. ([56636](https://github.com/WordPress/gutenberg/pull/56636)) -- Move 'ParentSelectorMenuItem' into a separate file. ([59146](https://github.com/WordPress/gutenberg/pull/59146)) -- Remove 'BlockSettingsMenu' styles. ([59147](https://github.com/WordPress/gutenberg/pull/59147)) - -#### Components -- Add Higher Order Function to ignore Input Method Editor (IME) keydowns. ([59081](https://github.com/WordPress/gutenberg/pull/59081)) -- Add lint rules for theme color CSS var usage. ([59022](https://github.com/WordPress/gutenberg/pull/59022)) -- ColorPicker: Style without accessing InputControl internals. ([59069](https://github.com/WordPress/gutenberg/pull/59069)) -- CustomSelectControl (v1 & v2): Fix errors in unit test setup. ([59038](https://github.com/WordPress/gutenberg/pull/59038)) -- CustomSelectControl: Hard deprecate constrained width. ([58974](https://github.com/WordPress/gutenberg/pull/58974)) - -#### Post Editor -- DocumentBar: Fix browser warning error. ([59193](https://github.com/WordPress/gutenberg/pull/59193)) -- DocumentBar: Simplify component, use framer for animation. ([58656](https://github.com/WordPress/gutenberg/pull/58656)) -- Editor: Remove unused selector value from 'PostTitle'. ([59204](https://github.com/WordPress/gutenberg/pull/59204)) -- Editor: Unify Mode Switcher component between post and site editor. ([59100](https://github.com/WordPress/gutenberg/pull/59100)) - -#### Interactivity API -- Refactor to use string instead of an object on `wp-data-interactive`. ([59034](https://github.com/WordPress/gutenberg/pull/59034)) -- Remove `data-wp-interactive` object for core/router. ([59030](https://github.com/WordPress/gutenberg/pull/59030)) -- Use `data_wp_context` helper in core blocks and remove `data-wp-interactive` object. ([58943](https://github.com/WordPress/gutenberg/pull/58943)) - -#### Site Editor -- Add stylelint rule to prevent theme CSS vars outside of wp-components. ([59020](https://github.com/WordPress/gutenberg/pull/59020)) -- Don't memoize the canvas container title. ([59000](https://github.com/WordPress/gutenberg/pull/59000)) -- Remove old patterns list code and styles. ([58966](https://github.com/WordPress/gutenberg/pull/58966)) - - -### Tools - -- Remove reference to CODE_OF_CONDUCT.md in documentation. ([59206](https://github.com/WordPress/gutenberg/pull/59206)) -- Remove repository specific Code of Conduct. ([59027](https://github.com/WordPress/gutenberg/pull/59027)) -- env: Fix mariadb version to LTS. ([59237](https://github.com/WordPress/gutenberg/pull/59237)) - -#### Testing -- Components: Add sleep() before all Tab() to fix flaky tests. ([59012](https://github.com/WordPress/gutenberg/pull/59012)) -- Components: Try fixing some flaky `Composite` and `Tabs` tests. ([58968](https://github.com/WordPress/gutenberg/pull/58968)) -- Migrate `change-detection` to Playwright. ([58767](https://github.com/WordPress/gutenberg/pull/58767)) -- Tabs: Fix flaky unit tests. ([58629](https://github.com/WordPress/gutenberg/pull/58629)) -- Update test environment default theme versions to latest. ([58955](https://github.com/WordPress/gutenberg/pull/58955)) -- Performance tests: Make site editor performance test backwards compatible. ([59266](https://github.com/WordPress/gutenberg/pull/59266)) -- Performance tests: Update selectors in site editor pattern loading tests. ([59259](https://github.com/WordPress/gutenberg/pull/59259)) - -#### Build Tooling -- Add test:e2e:playwright:debug command to debug Playwright tests. ([58808](https://github.com/WordPress/gutenberg/pull/58808)) -- Updating Storybook to v7.6.15 (latest). ([59074](https://github.com/WordPress/gutenberg/pull/59074)) - - - - -## Contributors - -The following contributors merged PRs in this release: - -@aaronrobertshaw @afercia @ajlende @alexstine @andrewhayward @andrewserong @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @DAreRodz @derekblank @desrosj @draganescu @ellatrix @fabiankaegy @gaambo @glendaviesnz @jameskoster @janboddez @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanfra @kevin940726 @Mamaduka @MarieComet @matiasbenedetto @mirka @noisysocks @ntsekouras @oandregal @ockham @pbking @ramonjd @SantosGuillamot @scruffian @shreyash3087 @t-hamano @talldan @tellthemachines @tyxla @youknowriad - - -= 17.8.0-rc.1 = - -## Changelog - -### Features - -- Patterns: add bulk export patterns action. ([58897](https://github.com/WordPress/gutenberg/pull/58897)) -- Template editor/inspector: show and select related patterns. ([55091](https://github.com/WordPress/gutenberg/pull/55091)) - -#### Layout -- Add toggle for grid types and stabilise Grid block variation. ([59051](https://github.com/WordPress/gutenberg/pull/59051) and [59116](https://github.com/WordPress/gutenberg/pull/59116)) -- Add support for column and row span in grid children. ([58539](https://github.com/WordPress/gutenberg/pull/58539)) - - -### Enhancements - -- Patterns Page: Make category action button compact. ([59203](https://github.com/WordPress/gutenberg/pull/59203)) -- Block Editor: Use hooks instead of HoC in 'SkipToSelectedBlock'. ([59202](https://github.com/WordPress/gutenberg/pull/59202)) -- Font Library: Adds the ability to use generic() in font family names. ([59103](https://github.com/WordPress/gutenberg/pull/59103) and [59037](https://github.com/WordPress/gutenberg/pull/59037)) -- REST API Global Styles Revisions Controller: Return single revision only when it matches the parent id. ([59049](https://github.com/WordPress/gutenberg/pull/59049)) -- CSS & Styling: Tweak link focus outline styles in HTML anchor and custom CSS. ([59048](https://github.com/WordPress/gutenberg/pull/59048)) -- Data Views: Make 'All pages' view label consistent with template and patterns. ([59009](https://github.com/WordPress/gutenberg/pull/59009)) -- Script Modules API: Script Modules add deregister option. ([58830](https://github.com/WordPress/gutenberg/pull/58830)) - -#### Custom Fields -- Block Bindings: Lock editing of blocks by default. ([58787](https://github.com/WordPress/gutenberg/pull/58787)) -- Style engine: Rename at_rule to rules_groups and update test/docs. ([58922](https://github.com/WordPress/gutenberg/pull/58922)) - -#### Block Library -- Gallery: Set the 'defaultBlock' setting for inner blocks. ([59168](https://github.com/WordPress/gutenberg/pull/59168)) -- Remove the navigation edit button because it leads to a useless screen. ([59211](https://github.com/WordPress/gutenberg/pull/59211)) -- Set the 'defaultBlock' setting for Columns & List blocks. ([59196](https://github.com/WordPress/gutenberg/pull/59196)) -- Update: Increase footnotes meta priority and separate footnotes meta registration. ([58882](https://github.com/WordPress/gutenberg/pull/58882)) - -#### Site Editor -- Editor: Hide template part and post content blocks in some site editor contexts. ([58928](https://github.com/WordPress/gutenberg/pull/58928)) -- Tweak save hub button. ([58917](https://github.com/WordPress/gutenberg/pull/58917) and [59200](https://github.com/WordPress/gutenberg/pull/59200)) - -#### Components -- CustomSelect: Adapt component for legacy props. ([57902](https://github.com/WordPress/gutenberg/pull/57902)) -- Use `Element.scrollIntoView()` instead of `dom-scroll-into-view`. ([59085](https://github.com/WordPress/gutenberg/pull/59085)) - -#### Global Styles -- Global style changes: Refactor output for a more flexible UI and grouping. ([59055](https://github.com/WordPress/gutenberg/pull/59055)) -- Style theme variations: Add property extraction and merge utils. ([58803](https://github.com/WordPress/gutenberg/pull/58803)) - - -### Bug Fixes - -- Distraction Free Mode: fix ui toggling bugs. ([59061](https://github.com/WordPress/gutenberg/pull/59061)) -- Layout: Refactor responsive logic for grid column spans. ([59057](https://github.com/WordPress/gutenberg/pull/59057)) -- Interactivity API: Only add proxies to plain objects inside the store. ([59039](https://github.com/WordPress/gutenberg/pull/59039)) -- Block Hooks: Fix in Navigation block. ([59021](https://github.com/WordPress/gutenberg/pull/59021)) - -#### List View -- Editor: Do not open list view by default on mobile. ([59016](https://github.com/WordPress/gutenberg/pull/59016)) -- Create Block: Add missing `viewScriptModule` field. ([59140](https://github.com/WordPress/gutenberg/pull/59140)) -- Ignore the 'twentytwentyfour' test theme dir created by wp-env. ([59072](https://github.com/WordPress/gutenberg/pull/59072)) -- useEntityBlockEditor: Update 'content' type check. ([59058](https://github.com/WordPress/gutenberg/pull/59058)) - -#### Block Library -- Author, Author Bio, Author Name: Add a fallback for Author Archive Template. ([55451](https://github.com/WordPress/gutenberg/pull/55451)) -- Fix Spacer orientation when inside a block with default flex layout. ([58921](https://github.com/WordPress/gutenberg/pull/58921)) -- Fix WP 6.4/6.3 compat for navigation link variations. ([59126](https://github.com/WordPress/gutenberg/pull/59126)) -- Interactivity API: Fix server side rendering for Search block. ([59029](https://github.com/WordPress/gutenberg/pull/59029)) -- Navigation: Avoid using embedded record from fallback API. ([59076](https://github.com/WordPress/gutenberg/pull/59076)) -- Pagination Numbers: Add `data-wp-key` to pagination numbers if enhanced pagination is enabled. ([58189](https://github.com/WordPress/gutenberg/pull/58189)) -- Revert "Navigation: Refactor mobile overlay breakpoints to JS (#57520)". ([59149](https://github.com/WordPress/gutenberg/pull/59149)) -- Spacer block: Fix `null` label in tooltip when horizontal layout. ([58909](https://github.com/WordPress/gutenberg/pull/58909)) - -#### Data Views -- DataViews: Add loading/no results message in grid view. ([59002](https://github.com/WordPress/gutenberg/pull/59002)) -- DataViews: Correctly display featured image that don't have image sizes. ([59111](https://github.com/WordPress/gutenberg/pull/59111)) -- DataViews: Fix pages list back path. ([59201](https://github.com/WordPress/gutenberg/pull/59201)) -- DataViews: Fix patterns, templates and template parts pagination `z-index`. ([58965](https://github.com/WordPress/gutenberg/pull/58965)) -- DataViews: Fix storybook. ([58842](https://github.com/WordPress/gutenberg/pull/58842)) -- DataViews: Remove second `reset filter` button in filter dialog. ([58960](https://github.com/WordPress/gutenberg/pull/58960)) -- Revert footer in pages list with DataViews. ([59151](https://github.com/WordPress/gutenberg/pull/59151)) - -#### Block Editor -- After Enter transform, skip other onEnter actions like splitting. ([59064](https://github.com/WordPress/gutenberg/pull/59064)) -- Close link preview if collapsed selection when creating link. ([58896](https://github.com/WordPress/gutenberg/pull/58896)) -- Editor: Limit spotlight mode to the editor. ([58817](https://github.com/WordPress/gutenberg/pull/58817)) -- Fix incorrect useAnchor positioning when switching from virtual to rich text elements. ([58900](https://github.com/WordPress/gutenberg/pull/58900)) -- Inserter: Don't select the closest block with 'disabled' editing mode. ([58971](https://github.com/WordPress/gutenberg/pull/58971)) -- Inserter: Fix title condition for media tab previews. ([58993](https://github.com/WordPress/gutenberg/pull/58993)) - -#### Site Editor -- Fix navigation on mobile web. ([59014](https://github.com/WordPress/gutenberg/pull/59014)) -- Fix: Don't render the Transform Into panel if there are no patterns. ([59217](https://github.com/WordPress/gutenberg/pull/59217)) -- Fix: Logical error in filterPatterns on template-panel/hooks.js. ([59218](https://github.com/WordPress/gutenberg/pull/59218)) -- Make command palette string transatables. ([59133](https://github.com/WordPress/gutenberg/pull/59133)) -- Remove left margin on Status help text. ([58775](https://github.com/WordPress/gutenberg/pull/58775)) - -#### Patterns -- Allow editing of image block alt and title attributes in content only mode. ([58998](https://github.com/WordPress/gutenberg/pull/58998)) -- Avoid showing block removal warning when deleting a pattern instance that has overrides. ([59044](https://github.com/WordPress/gutenberg/pull/59044)) -- Block editor: Pass patterns selector as setting. ([58661](https://github.com/WordPress/gutenberg/pull/58661)) -- Fix pattern categories on import. ([58926](https://github.com/WordPress/gutenberg/pull/58926)) -- Site editor: Fix start patterns store selector. ([58813](https://github.com/WordPress/gutenberg/pull/58813)) - -#### Global Styles -- Fix console error in block preview. ([59112](https://github.com/WordPress/gutenberg/pull/59112)) -- Revert "Use all the settings origins for a block that consumes paths with merge #55219" ([58951](https://github.com/WordPress/gutenberg/pull/58951) and [59101](https://github.com/WordPress/gutenberg/pull/59101)) -- Shadows: Don't assume that core provides default shadows. ([58973](https://github.com/WordPress/gutenberg/pull/58973)) - -#### Font Library -- Fixes installed font families not rendering in the editor or frontend. ([59019](https://github.com/WordPress/gutenberg/pull/59019)) -- Font Libary: Add missing translation functions. ([58104](https://github.com/WordPress/gutenberg/pull/58104)) -- Show error message when no fonts found to install. ([58914](https://github.com/WordPress/gutenberg/pull/58914)) - -#### Synced Patterns -- Fix missing source for binding attributes. ([59194](https://github.com/WordPress/gutenberg/pull/59194)) -- Fix resetting individual blocks to empty optional values for Pattern Overrides. ([59170](https://github.com/WordPress/gutenberg/pull/59170)) -- Fix upload button on overridden empty image block in patterns. ([59169](https://github.com/WordPress/gutenberg/pull/59169)) - -#### Design Tools -- Background image support: Fix issue with background position keyboard entry. ([59050](https://github.com/WordPress/gutenberg/pull/59050)) -- Cover block: Clear the min height field when aspect ratio is set. ([59191](https://github.com/WordPress/gutenberg/pull/59191)) -- Elements: Fix block instance element styles for links applying to buttons. ([59114](https://github.com/WordPress/gutenberg/pull/59114)) - -#### Components -- Modal: Add `box-sizing` reset style. ([58905](https://github.com/WordPress/gutenberg/pull/58905)) -- ToolbarButton: Fix text centering for short labels. ([59117](https://github.com/WordPress/gutenberg/pull/59117)) -- Upgrade Floating UI packages, fix nested iframe positioning bug. ([58932](https://github.com/WordPress/gutenberg/pull/58932)) - -#### Post Editor -- Editor: Fix 'useHideBlocksFromInserter' hook filename. ([59150](https://github.com/WordPress/gutenberg/pull/59150)) -- Fix layout for non viewable post types. ([58962](https://github.com/WordPress/gutenberg/pull/58962)) - -#### Rich Text -- Fix link paste for internal paste. ([59063](https://github.com/WordPress/gutenberg/pull/59063)) -- Revert "Rich text: Pad multiple spaces through en/em replacement". ([58792](https://github.com/WordPress/gutenberg/pull/58792)) - -#### Custom Fields -- Block Bindings: Add block context needed for bindings in PHP. ([58554](https://github.com/WordPress/gutenberg/pull/58554)) -- Block Bindings: Fix disable bindings editing when source is undefined. ([58961](https://github.com/WordPress/gutenberg/pull/58961)) - - -### Accessibility - -- Enter editing mode via Enter or Spacebar. ([58795](https://github.com/WordPress/gutenberg/pull/58795)) -- Block Bindings > Image Block:Mark connected controls as 'readonly'. ([59059](https://github.com/WordPress/gutenberg/pull/59059)) -- Details Block: Try double enter to escape inner blocks. ([58903](https://github.com/WordPress/gutenberg/pull/58903)) -- Font Library: Replace infinite scroll by pagination. ([58794](https://github.com/WordPress/gutenberg/pull/58794)) -- Global Styles: Remove menubar role and improve complementary area header semantics. ([58740](https://github.com/WordPress/gutenberg/pull/58740)) - -#### Block Editor -- Block Mover: Unify visual separator when show button label is on. ([59158](https://github.com/WordPress/gutenberg/pull/59158)) -- Make the custom CSS validation error message accessible. ([56690](https://github.com/WordPress/gutenberg/pull/56690)) -- Restore default border and focus style on image URL input field. ([58505](https://github.com/WordPress/gutenberg/pull/58505)) - -### Performance - -- Pattern Block: Batch replacing actions. ([59075](https://github.com/WordPress/gutenberg/pull/59075)) -- Block Editor: Move StopEditingAsBlocksOnOutsideSelect to Root. ([58412](https://github.com/WordPress/gutenberg/pull/58412)) - - -### Documentation - -- Add contributing guidlines around Component versioning. ([58789](https://github.com/WordPress/gutenberg/pull/58789)) -- Clarify the performance reference commit and how to pick it. ([58927](https://github.com/WordPress/gutenberg/pull/58927)) -- DataViews: Update documentation. ([58847](https://github.com/WordPress/gutenberg/pull/58847)) -- Docs: Clarify the status of the wp-block-styles theme support, and its intent. ([58915](https://github.com/WordPress/gutenberg/pull/58915)) -- Fix move interactivity schema to supports property instead of selectors property. ([59166](https://github.com/WordPress/gutenberg/pull/59166)) -- Storybook: Show badges in sidebar. ([58518](https://github.com/WordPress/gutenberg/pull/58518)) -- Theme docs: Update appearance-tools documentation to reflect opt-in for backgroundSize and aspectRatio. ([59165](https://github.com/WordPress/gutenberg/pull/59165)) -- Update richtext.md. ([59089](https://github.com/WordPress/gutenberg/pull/59089)) - -#### Interactivity API -- Interactivity API: Fix WP version, update new store documentation. ([59107](https://github.com/WordPress/gutenberg/pull/59107)) -- Interactivity API: Update documentation guide with new `wp-interactivity` directive implementation. ([59018](https://github.com/WordPress/gutenberg/pull/59018)) -- Add interactivity property to block supports reference documentation. ([59152](https://github.com/WordPress/gutenberg/pull/59152)) - -#### Schemas -- Block JSON schema: Add `viewScriptModule` field. ([59060](https://github.com/WordPress/gutenberg/pull/59060)) -- Block JSON schema: Update `shadow` definition. ([58910](https://github.com/WordPress/gutenberg/pull/58910)) -- JSON schema: Update schema for background support. ([59127](https://github.com/WordPress/gutenberg/pull/59127)) - -### Code Quality - -- Create Block: Remove deprecated viewModule field. ([59198](https://github.com/WordPress/gutenberg/pull/59198)) -- Editor: Remove the 'all' rendering mode. ([58935](https://github.com/WordPress/gutenberg/pull/58935)) -- Editor: Unify the editor commands between post and site editors. ([59005](https://github.com/WordPress/gutenberg/pull/59005)) -- Relocate 'ErrorBoundary' component unit test folders. ([59031](https://github.com/WordPress/gutenberg/pull/59031)) -- Remove obsolete wp-env configuration from package.json (#58877). ([58899](https://github.com/WordPress/gutenberg/pull/58899)) -- Design Tools > Elements: Make editor selector match theme.json and frontend. ([59167](https://github.com/WordPress/gutenberg/pull/59167)) -- Global Styles: Update sprintf calls using `_n`. ([59160](https://github.com/WordPress/gutenberg/pull/59160)) -- Block API: Revert "Block Hooks: Set ignoredHookedBlocks metada attr upon insertion". ([58969](https://github.com/WordPress/gutenberg/pull/58969)) -- Editor > Rich Text: Remove inline toolbar preference. ([58945](https://github.com/WordPress/gutenberg/pull/58945)) -- Style Variations: Remove preferred style variations legacy support. ([58930](https://github.com/WordPress/gutenberg/pull/58930)) -- REST API > Template Revisions: Move from experimental to compat/6.4. ([58920](https://github.com/WordPress/gutenberg/pull/58920)) - -#### Block Editor -- Block-editor: Auto-register block commands. ([59079](https://github.com/WordPress/gutenberg/pull/59079)) -- BlockSettingsMenu: Combine 'block-editor' store selectors. ([59153](https://github.com/WordPress/gutenberg/pull/59153)) -- Clean up link control CSS. ([58934](https://github.com/WordPress/gutenberg/pull/58934)) -- HeadingLevelDropdown: Remove unnecessary isPressed prop. ([56636](https://github.com/WordPress/gutenberg/pull/56636)) -- Move 'ParentSelectorMenuItem' into a separate file. ([59146](https://github.com/WordPress/gutenberg/pull/59146)) -- Remove 'BlockSettingsMenu' styles. ([59147](https://github.com/WordPress/gutenberg/pull/59147)) - -#### Components -- Add Higher Order Function to ignore Input Method Editor (IME) keydowns. ([59081](https://github.com/WordPress/gutenberg/pull/59081)) -- Add lint rules for theme color CSS var usage. ([59022](https://github.com/WordPress/gutenberg/pull/59022)) -- ColorPicker: Style without accessing InputControl internals. ([59069](https://github.com/WordPress/gutenberg/pull/59069)) -- CustomSelectControl (v1 & v2): Fix errors in unit test setup. ([59038](https://github.com/WordPress/gutenberg/pull/59038)) -- CustomSelectControl: Hard deprecate constrained width. ([58974](https://github.com/WordPress/gutenberg/pull/58974)) - -#### Post Editor -- DocumentBar: Fix browser warning error. ([59193](https://github.com/WordPress/gutenberg/pull/59193)) -- DocumentBar: Simplify component, use framer for animation. ([58656](https://github.com/WordPress/gutenberg/pull/58656)) -- Editor: Remove unused selector value from 'PostTitle'. ([59204](https://github.com/WordPress/gutenberg/pull/59204)) -- Editor: Unify Mode Switcher component between post and site editor. ([59100](https://github.com/WordPress/gutenberg/pull/59100)) - -#### Interactivity API -- Refactor to use string instead of an object on `wp-data-interactive`. ([59034](https://github.com/WordPress/gutenberg/pull/59034)) -- Remove `data-wp-interactive` object for core/router. ([59030](https://github.com/WordPress/gutenberg/pull/59030)) -- Use `data_wp_context` helper in core blocks and remove `data-wp-interactive` object. ([58943](https://github.com/WordPress/gutenberg/pull/58943)) - -#### Site Editor -- Add stylelint rule to prevent theme CSS vars outside of wp-components. ([59020](https://github.com/WordPress/gutenberg/pull/59020)) -- Don't memoize the canvas container title. ([59000](https://github.com/WordPress/gutenberg/pull/59000)) -- Remove old patterns list code and styles. ([58966](https://github.com/WordPress/gutenberg/pull/58966)) - - -### Tools - -- Remove reference to CODE_OF_CONDUCT.md in documentation. ([59206](https://github.com/WordPress/gutenberg/pull/59206)) -- Remove repository specific Code of Conduct. ([59027](https://github.com/WordPress/gutenberg/pull/59027)) -- env: Fix mariadb version to LTS. ([59237](https://github.com/WordPress/gutenberg/pull/59237)) - -#### Testing -- Components: Add sleep() before all Tab() to fix flaky tests. ([59012](https://github.com/WordPress/gutenberg/pull/59012)) -- Components: Try fixing some flaky `Composite` and `Tabs` tests. ([58968](https://github.com/WordPress/gutenberg/pull/58968)) -- Migrate `change-detection` to Playwright. ([58767](https://github.com/WordPress/gutenberg/pull/58767)) -- Tabs: Fix flaky unit tests. ([58629](https://github.com/WordPress/gutenberg/pull/58629)) -- Update test environment default theme versions to latest. ([58955](https://github.com/WordPress/gutenberg/pull/58955)) - -#### Build Tooling -- Add test:e2e:playwright:debug command to debug Playwright tests. ([58808](https://github.com/WordPress/gutenberg/pull/58808)) -- Updating Storybook to v7.6.15 (latest). ([59074](https://github.com/WordPress/gutenberg/pull/59074)) - - - - -## Contributors - -The following contributors merged PRs in this release: - -@aaronrobertshaw @afercia @ajlende @alexstine @andrewhayward @andrewserong @brookewp @c4rl0sbr4v0 @chad1008 @ciampo @DAreRodz @derekblank @desrosj @draganescu @ellatrix @fabiankaegy @gaambo @glendaviesnz @jameskoster @janboddez @jasmussen @jeryj @jorgefilipecosta @jsnajdr @juanfra @kevin940726 @Mamaduka @MarieComet @matiasbenedetto @mirka @noisysocks @ntsekouras @oandregal @ockham @pbking @ramonjd @SantosGuillamot @scruffian @shreyash3087 @t-hamano @talldan @tellthemachines @tyxla @youknowriad = 17.7.0 = From df5dc24f607c8c14ed0e8b003430351f98e0ffc1 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 28 Feb 2024 17:25:52 -0300 Subject: [PATCH 10/45] Font Library: fix infinite loop when calling wp_get_upload_dir in a function that's used to filter font_dir (#58839) * fix infinite loop when calling wp_get_upload_dir in a function thats used to filter font_dir * remove not needed parameter * remove filter after use * removing not needed parameter * Add a comment explaining the reasons behind the implementation Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> * fix spaces Co-authored-by: Colin Stewart <79332690+costdev@users.noreply.github.com> --------- Co-authored-by: matiasbenedetto Co-authored-by: costdev Co-authored-by: creativecoder Co-authored-by: youknowriad Co-authored-by: dd32 Co-authored-by: jazzsequence --- .../class-wp-rest-font-faces-controller.php | 19 ++++++++++--- lib/compat/wordpress-6.5/fonts/fonts.php | 27 +++++++------------ 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php index 8a4040e3397e0c..1d65e0f63aab9f 100644 --- a/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php +++ b/lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php @@ -858,7 +858,21 @@ protected function sanitize_src( $value ) { */ protected function handle_font_file_upload( $file ) { add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); - add_filter( 'upload_dir', 'wp_get_font_dir' ); + + /* + * Set the upload directory to the fonts directory. + * + * wp_get_font_dir() contains the 'font_dir' hook, whose callbacks are + * likely to call wp_get_upload_dir(). + * + * To avoid an infinite loop, don't hook wp_get_font_dir() to 'upload_dir'. + * Instead, just pass its return value to the 'upload_dir' callback. + */ + $font_dir = wp_get_font_dir(); + $set_upload_dir = function () use ( $font_dir ) { + return $font_dir; + }; + add_filter( 'upload_dir', $set_upload_dir ); $overrides = array( 'upload_error_handler' => array( $this, 'handle_font_file_upload_error' ), @@ -875,8 +889,7 @@ protected function handle_font_file_upload( $file ) { ); $uploaded_file = wp_handle_upload( $file, $overrides ); - - remove_filter( 'upload_dir', 'wp_get_font_dir' ); + remove_filter( 'upload_dir', $set_upload_dir ); remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); return $uploaded_file; diff --git a/lib/compat/wordpress-6.5/fonts/fonts.php b/lib/compat/wordpress-6.5/fonts/fonts.php index 3911f61ceaec4b..55dc2d3429af92 100644 --- a/lib/compat/wordpress-6.5/fonts/fonts.php +++ b/lib/compat/wordpress-6.5/fonts/fonts.php @@ -201,16 +201,6 @@ function gutenberg_register_font_collections() { * * @since 6.5.0 * - * @param array $defaults { - * Array of information about the upload directory. - * - * @type string $path Base directory and subdirectory or full path to the fonts upload directory. - * @type string $url Base URL and subdirectory or absolute URL to the fonts upload directory. - * @type string $subdir Subdirectory - * @type string $basedir Path without subdir. - * @type string $baseurl URL path without subdir. - * @type string|false $error False or error message. - * } * @return array $defaults { * Array of information about the upload directory. * @@ -222,19 +212,20 @@ function gutenberg_register_font_collections() { * @type string|false $error False or error message. * } */ - function wp_get_font_dir( $defaults = array() ) { + function wp_get_font_dir() { $site_path = ''; if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { $site_path = '/sites/' . get_current_blog_id(); } - // Sets the defaults. - $defaults['path'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; - $defaults['url'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; - $defaults['subdir'] = ''; - $defaults['basedir'] = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; - $defaults['baseurl'] = untrailingslashit( content_url( 'fonts' ) ) . $site_path; - $defaults['error'] = false; + $defaults = array( + 'path' => path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path, + 'url' => untrailingslashit( content_url( 'fonts' ) ) . $site_path, + 'subdir' => '', + 'basedir' => path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path, + 'baseurl' => untrailingslashit( content_url( 'fonts' ) ) . $site_path, + 'error' => false, + ); /** * Filters the fonts directory data. From ab2123d24607849b49606fd1d800d8df8a3b96e8 Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Wed, 28 Feb 2024 18:00:46 -0300 Subject: [PATCH 11/45] Avoid creating font families without font faces. (#59436) If we ae trying to create a new font family with font faces but the font family was created in the database but the font faces failed because of the file system permissions, the font family is removed from the database. --- Co-authored-by: matiasbenedetto Co-authored-by: mikachan --- .../components/global-styles/font-library-modal/context.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 8da33192859f14..9609da1780d3b6 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -202,6 +202,7 @@ function FontLibraryProvider( { children } ) { async function installFont( fontFamilyToInstall ) { setIsInstalling( true ); + let isANewFontFamily = false; try { // Get the font family if it already exists. let installedFontFamily = await fetchGetFontFamilyBySlug( @@ -210,6 +211,7 @@ function FontLibraryProvider( { children } ) { // Otherwise create it. if ( ! installedFontFamily ) { + isANewFontFamily = true; // Prepare font family form data to install. installedFontFamily = await fetchInstallFontFamily( makeFontFamilyFormData( fontFamilyToInstall ) @@ -268,6 +270,11 @@ function FontLibraryProvider( { children } ) { sucessfullyInstalledFontFaces.length === 0 && alreadyInstalledFontFaces.length === 0 ) { + if ( isANewFontFamily ) { + // If the font family is new, delete it to avoid having font families without font faces. + await fetchUninstallFontFamily( installedFontFamily.id ); + } + throw new Error( sprintf( /* translators: %s: Specific error message returned from server. */ From cca1165ca463a1ab436be391a84dfd7369a9ae08 Mon Sep 17 00:00:00 2001 From: annezazu Date: Wed, 28 Feb 2024 13:01:48 -0800 Subject: [PATCH 12/45] Update versions in WP for 6.5 (#59446) --- docs/contributors/versions-in-wordpress.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/contributors/versions-in-wordpress.md b/docs/contributors/versions-in-wordpress.md index a385c1ced9e43c..b287d574e56b45 100644 --- a/docs/contributors/versions-in-wordpress.md +++ b/docs/contributors/versions-in-wordpress.md @@ -6,6 +6,7 @@ If anything looks incorrect here, please bring it up in #core-editor in [WordPre | Gutenberg Versions | WordPress Version | | ------------------ | ----------------- | +| 16.8-17.7 | 6.5 | | 16.2-16.7 | 6.4.3 | | 16.2-16.7 | 6.4.2 | | 16.2-16.7 | 6.4.1 | From 226b2ba87f8a02dad3c92e22505c1a56003c6fcf Mon Sep 17 00:00:00 2001 From: Jerry Jones Date: Wed, 28 Feb 2024 22:56:03 +0100 Subject: [PATCH 13/45] Refactor: useBlockTools hook (#58979) There are times where a block is selected but no tools are available. If we don't have any tools, we don't want to show a toolbar. This commit helps unify the logic so there is a source of truth for if it will be showing or not. * Adds a useHasBlockToolbar hook that has the logic for if the block-toolbar will render or not. * Adds a useBlockTools hook that privately exports which block tools will be showing (block toolbar popover, fixed block toolbar, block inserter, block breadcrumb). * Checks if any block tools are available before adding the is-collapsed class to the central toolbar area * Removes selected block tools wrapper from DOM if no block tools available. I'm not sure if this was intentional to leave it in the DOM before, or if it was because there wasn't a reliable way of checking for it. --------- Co-authored-by: jeryj Co-authored-by: draganescu Co-authored-by: Mamaduka Co-authored-by: MaggieCabrera --- .../src/components/block-toolbar/index.js | 14 +--- .../block-toolbar/use-has-block-toolbar.js | 49 +++++++++++++ .../src/components/block-tools/index.js | 49 ++++--------- .../block-tools/use-show-block-tools.js | 73 +++++++++++++++++++ packages/block-editor/src/private-apis.js | 4 +- .../utils/use-can-block-toolbar-be-focused.js | 48 ------------ .../edit-post/src/components/header/index.js | 43 ++++++----- .../src/components/header-edit-mode/index.js | 51 ++++++------- .../components/header/document-tools/index.js | 6 +- .../src/components/document-tools/index.js | 6 +- 10 files changed, 188 insertions(+), 155 deletions(-) create mode 100644 packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js create mode 100644 packages/block-editor/src/components/block-tools/use-show-block-tools.js delete mode 100644 packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 140532de15b768..a641df0f7e5cf4 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -34,9 +34,8 @@ import { useShowHoveredOrFocusedGestures } from './utils'; import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; import NavigableToolbar from '../navigable-toolbar'; -import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; import Shuffle from './shuffle'; - +import { useHasBlockToolbar } from './use-has-block-toolbar'; /** * Renders the block toolbar. * @@ -122,15 +121,8 @@ export function PrivateBlockToolbar( { const isLargeViewport = ! useViewportMatch( 'medium', '<' ); - const isToolbarEnabled = - blockType && - hasBlockSupport( blockType, '__experimentalToolbar', true ); - const hasAnyBlockControls = useHasAnyBlockControls(); - - if ( - ! isToolbarEnabled || - ( ! isDefaultEditingMode && ! hasAnyBlockControls ) - ) { + const hasBlockToolbar = useHasBlockToolbar(); + if ( ! hasBlockToolbar ) { return null; } diff --git a/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js b/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js new file mode 100644 index 00000000000000..adfdd59263194a --- /dev/null +++ b/packages/block-editor/src/components/block-toolbar/use-has-block-toolbar.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; + +/** + * Returns true if the block toolbar should be shown. + * + * @return {boolean} Whether the block toolbar component will be rendered. + */ +export function useHasBlockToolbar() { + const hasAnyBlockControls = useHasAnyBlockControls(); + return useSelect( + ( select ) => { + const { + getBlockEditingMode, + getBlockName, + getSelectedBlockClientIds, + } = select( blockEditorStore ); + + const selectedBlockClientIds = getSelectedBlockClientIds(); + const selectedBlockClientId = selectedBlockClientIds[ 0 ]; + const isDefaultEditingMode = + getBlockEditingMode( selectedBlockClientId ) === 'default'; + const blockType = + selectedBlockClientId && + getBlockType( getBlockName( selectedBlockClientId ) ); + const isToolbarEnabled = + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ); + + if ( + ! isToolbarEnabled || + ( ! isDefaultEditingMode && ! hasAnyBlockControls ) + ) { + return false; + } + + return true; + }, + [ hasAnyBlockControls ] + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index e00058eb905329..3959257ecf4e86 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -5,7 +5,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -20,14 +19,13 @@ import BlockToolbarBreadcrumb from './block-toolbar-breadcrumb'; import { store as blockEditorStore } from '../../store'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; +import { useShowBlockTools } from './use-show-block-tools'; function selector( select ) { const { getSelectedBlockClientId, getFirstMultiSelectedBlockClientId, - getBlock, getSettings, - hasMultiSelection, __unstableGetEditorMode, isTyping, } = select( blockEditorStore ); @@ -35,36 +33,13 @@ function selector( select ) { const clientId = getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); - const { name = '', attributes = {} } = getBlock( clientId ) || {}; const editorMode = __unstableGetEditorMode(); - const hasSelectedBlock = clientId && name; - const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( { - name, - attributes, - } ); - const _showEmptyBlockSideInserter = - clientId && - ! isTyping() && - editorMode === 'edit' && - isUnmodifiedDefaultBlock( { name, attributes } ); - const maybeShowBreadcrumb = - hasSelectedBlock && - ! hasMultiSelection() && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ); return { clientId, hasFixedToolbar: getSettings().hasFixedToolbar, isTyping: isTyping(), isZoomOutMode: editorMode === 'zoom-out', - showEmptyBlockSideInserter: _showEmptyBlockSideInserter, - showBreadcrumb: ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, - showBlockToolbar: - ! getSettings().hasFixedToolbar && - ! _showEmptyBlockSideInserter && - hasSelectedBlock && - ! isEmptyDefaultBlock && - ! maybeShowBreadcrumb, }; } @@ -82,18 +57,20 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const { - clientId, - hasFixedToolbar, - isTyping, - isZoomOutMode, - showEmptyBlockSideInserter, - showBreadcrumb, - showBlockToolbar, - } = useSelect( selector, [] ); + const { clientId, hasFixedToolbar, isTyping, isZoomOutMode } = useSelect( + selector, + [] + ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = useSelect( blockEditorStore ); + + const { + showEmptyBlockSideInserter, + showBreadcrumb, + showBlockToolbarPopover, + } = useShowBlockTools(); + const { duplicateBlocks, removeBlocks, @@ -186,7 +163,7 @@ export default function BlockTools( { /> ) } - { showBlockToolbar && ( + { showBlockToolbarPopover && ( { + const { + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + getBlock, + getSettings, + hasMultiSelection, + __unstableGetEditorMode, + isTyping, + } = select( blockEditorStore ); + + const clientId = + getSelectedBlockClientId() || + getFirstMultiSelectedBlockClientId(); + + const { name = '', attributes = {} } = getBlock( clientId ) || {}; + const editorMode = __unstableGetEditorMode(); + const hasSelectedBlock = clientId && name; + const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( { + name, + attributes, + } ); + const _showEmptyBlockSideInserter = + clientId && + ! isTyping() && + editorMode === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ); + const maybeShowBreadcrumb = + hasSelectedBlock && + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ); + + return { + showEmptyBlockSideInserter: _showEmptyBlockSideInserter, + showBreadcrumb: + ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, + showBlockToolbarPopover: + hasBlockToolbar && + ! getSettings().hasFixedToolbar && + ! _showEmptyBlockSideInserter && + hasSelectedBlock && + ! isEmptyDefaultBlock && + ! maybeShowBreadcrumb, + showFixedToolbar: + editorMode !== 'zoom-out' && + hasBlockToolbar && + getSettings().hasFixedToolbar, + }; + }, + [ hasBlockToolbar ] + ); +} diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index ec6843ead24895..c78815a810247a 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -10,7 +10,7 @@ import { ComposedPrivateInserter as PrivateInserter } from './components/inserte import { default as PrivateQuickInserter } from './components/inserter/quick-inserter'; import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; -import { useCanBlockToolbarBeFocused } from './utils/use-can-block-toolbar-be-focused'; +import { useShowBlockTools } from './components/block-tools/use-show-block-tools'; import { cleanEmptyObject, useStyleOverride } from './hooks/utils'; import BlockQuickNavigation from './components/block-quick-navigation'; import { LayoutStyle } from './components/block-list/layout'; @@ -44,7 +44,7 @@ lock( privateApis, { PrivateListView, ResizableBoxPopover, BlockInfo, - useCanBlockToolbarBeFocused, + useShowBlockTools, cleanEmptyObject, useStyleOverride, BlockQuickNavigation, diff --git a/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js b/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js deleted file mode 100644 index f118c88dc2b1d4..00000000000000 --- a/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../store'; -import { unlock } from '../lock-unlock'; - -/** - * Returns true if the block toolbar should be able to receive focus. - * - * @return {boolean} Whether the block toolbar should be able to receive focus - */ -export function useCanBlockToolbarBeFocused() { - return useSelect( ( select ) => { - const { - __unstableGetEditorMode, - getBlock, - getSettings, - getSelectedBlockClientId, - getFirstMultiSelectedBlockClientId, - } = unlock( select( blockEditorStore ) ); - - const selectedBlockId = - getFirstMultiSelectedBlockClientId() || getSelectedBlockClientId(); - const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( - getBlock( selectedBlockId ) || {} - ); - - // Fixed Toolbar can be focused when: - // - a block is selected - // - fixed toolbar is on - // Block Toolbar Popover can be focused when: - // - a block is selected - // - we are in edit mode - // - it is not an empty default block - return ( - !! selectedBlockId && - ( getSettings().hasFixedToolbar || - ( __unstableGetEditorMode() === 'edit' && - ! isEmptyDefaultBlock ) ) - ); - }, [] ); -} diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 62a6a462d8dd18..187a03b763a13f 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { BlockToolbar, + privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; import { @@ -40,6 +41,7 @@ import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; +const { useShowBlockTools } = unlock( blockEditorPrivateApis ); const { DocumentTools, PostViewLink, PreviewDropdown } = unlock( editorPrivateApis ); @@ -63,7 +65,6 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { isTextEditor, blockSelectionStart, hasActiveMetaboxes, - hasFixedToolbar, isPublishSidebarOpened, showIconLabels, hasHistory, @@ -81,11 +82,13 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { .onNavigateToPreviousEntityRecord, isPublishSidebarOpened: select( editPostStore ).isPublishSidebarOpened(), - hasFixedToolbar: getPreference( 'core', 'fixedToolbar' ), showIconLabels: getPreference( 'core', 'showIconLabels' ), }; }, [] ); + const { showFixedToolbar } = useShowBlockTools(); + const showTopToolbar = isLargeViewport && showFixedToolbar; + const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = useState( true ); const hasBlockSelection = !! blockSelectionStart; @@ -116,7 +119,7 @@ function Header( { setEntitiesSavedStatesCallback, initialPost } ) { className="edit-post-header__toolbar" > - { hasFixedToolbar && isLargeViewport && ( + { showTopToolbar && ( <>
- { hasBlockSelection && ( -
); diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 9737aa3d462835..c77323d3c796d3 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -14,6 +14,7 @@ import { Icon, privateApis as componentsPrivateApis, CheckboxControl, + Spinner, } from '@wordpress/components'; import { forwardRef, @@ -464,7 +465,7 @@ function ViewTable( { id={ tableNoticeId } > { ! hasData && ( -

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

+

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

) }
From f3676dcb33ac2b4a57ef2b64ca6fcf96b91d9683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:19:13 +0100 Subject: [PATCH 24/45] DataViews: set color for primary field/`a` element when focused (#58814) Co-authored-by: oandregal Co-authored-by: jasmussen Co-authored-by: jameskoster Co-authored-by: t-hamano --- packages/base-styles/_mixins.scss | 8 ++++++++ packages/dataviews/src/style.scss | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index 109926ebf411a9..a1de82c2081cc5 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -367,6 +367,14 @@ } } +@mixin link-reset { + &:focus { + color: var(--wp-admin-theme-color--rgb); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color, #007cba); + border-radius: $radius-block-ui; + } +} + // The editor input reset with increased specificity to avoid theme styles bleeding in. @mixin editor-input-reset() { font-family: $editor-html-font !important; diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 290a615e56ee18..7c8216b095cb43 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -222,7 +222,6 @@ color: $gray-900; text-overflow: ellipsis; white-space: nowrap; - overflow: hidden; display: block; width: 100%; @@ -238,6 +237,7 @@ &:hover { color: $gray-900; } + @include link-reset(); } button.components-button.is-link { From 0491d24b3c5e6a6ba4bd9b7d5ab5d8d469d869c1 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 29 Feb 2024 18:45:26 +0200 Subject: [PATCH 25/45] List: copy wrapper when multi selecting items (#59460) Co-authored-by: ellatrix Co-authored-by: t-hamano Co-authored-by: annezazu --- .../list-view/use-clipboard-handler.js | 5 +- .../writing-flow/use-clipboard-handler.js | 5 +- .../src/components/writing-flow/utils.js | 47 ++++++++++++------- packages/block-editor/src/private-apis.js | 2 + packages/block-library/src/list-item/edit.js | 3 +- .../src/list-item/hooks/index.js | 1 - .../src/list-item/hooks/use-copy.js | 38 --------------- packages/block-library/src/list-item/index.js | 3 ++ .../src/page-utils/press-keys.ts | 34 +++++++------- test/e2e/specs/editor/blocks/list.spec.js | 30 ++++++++++++ 10 files changed, 91 insertions(+), 77 deletions(-) delete mode 100644 packages/block-library/src/list-item/hooks/use-copy.js diff --git a/packages/block-editor/src/components/list-view/use-clipboard-handler.js b/packages/block-editor/src/components/list-view/use-clipboard-handler.js index cd25c71e9bf7c4..dd3ac65ac79d24 100644 --- a/packages/block-editor/src/components/list-view/use-clipboard-handler.js +++ b/packages/block-editor/src/components/list-view/use-clipboard-handler.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; /** @@ -15,6 +15,7 @@ import { getPasteBlocks, setClipboardBlocks } from '../writing-flow/utils'; // This hook borrows from useClipboardHandler in ../writing-flow/use-clipboard-handler.js // and adds behaviour for the list view, while skipping partial selection. export default function useClipboardHandler( { selectBlock } ) { + const registry = useRegistry(); const { getBlockOrder, getBlockRootClientId, @@ -106,7 +107,7 @@ export default function useClipboardHandler( { selectBlock } ) { notifyCopy( event.type, selectedBlockClientIds ); const blocks = getBlocksByClientId( selectedBlockClientIds ); - setClipboardBlocks( event, blocks ); + setClipboardBlocks( event, blocks, registry ); } if ( event.type === 'cut' ) { diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js index 8528655c1dcc9e..43e887888dbd13 100644 --- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js +++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js @@ -5,7 +5,7 @@ import { documentHasSelection, documentHasUncollapsedSelection, } from '@wordpress/dom'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; /** @@ -16,6 +16,7 @@ import { useNotifyCopy } from '../../utils/use-notify-copy'; import { getPasteBlocks, setClipboardBlocks } from './utils'; export default function useClipboardHandler() { + const registry = useRegistry(); const { getBlocksByClientId, getSelectedBlockClientIds, @@ -104,7 +105,7 @@ export default function useClipboardHandler() { blocks = [ head, ...inBetweenBlocks, tail ]; } - setClipboardBlocks( event, blocks ); + setClipboardBlocks( event, blocks, registry ); } } diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js index ef1827077ccbf1..2a2010854ed205 100644 --- a/packages/block-editor/src/components/writing-flow/utils.js +++ b/packages/block-editor/src/components/writing-flow/utils.js @@ -8,36 +8,51 @@ import { pasteHandler, findTransform, getBlockTransforms, + store as blocksStore, } from '@wordpress/blocks'; /** * Internal dependencies */ import { getPasteEventData } from '../../utils/pasting'; +import { store as blockEditorStore } from '../../store'; + +export const requiresWrapperOnCopy = Symbol( 'requiresWrapperOnCopy' ); /** * Sets the clipboard data for the provided blocks, with both HTML and plain * text representations. * - * @param {ClipboardEvent} event Clipboard event. - * @param {WPBlock[]} blocks Blocks to set as clipboard data. + * @param {ClipboardEvent} event Clipboard event. + * @param {WPBlock[]} blocks Blocks to set as clipboard data. + * @param {Object} registry The registry to select from. */ -export function setClipboardBlocks( event, blocks ) { +export function setClipboardBlocks( event, blocks, registry ) { let _blocks = blocks; - const wrapperBlockName = event.clipboardData.getData( - '__unstableWrapperBlockName' - ); - if ( wrapperBlockName ) { - _blocks = createBlock( - wrapperBlockName, - JSON.parse( - event.clipboardData.getData( - '__unstableWrapperBlockAttributes' - ) - ), - _blocks - ); + const [ firstBlock ] = blocks; + + if ( firstBlock ) { + const firstBlockType = registry + .select( blocksStore ) + .getBlockType( firstBlock.name ); + + if ( firstBlockType[ requiresWrapperOnCopy ] ) { + const { getBlockRootClientId, getBlockName, getBlockAttributes } = + registry.select( blockEditorStore ); + const wrapperBlockClientId = getBlockRootClientId( + firstBlock.clientId + ); + const wrapperBlockName = getBlockName( wrapperBlockClientId ); + + if ( wrapperBlockName ) { + _blocks = createBlock( + wrapperBlockName, + getBlockAttributes( wrapperBlockClientId ), + _blocks + ); + } + } } const serialized = serialize( _blocks ); diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index c78815a810247a..92da4b87196325 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -27,6 +27,7 @@ import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; import { selectBlockPatternsKey } from './store/private-keys'; +import { requiresWrapperOnCopy } from './components/writing-flow/utils'; import { PrivateRichText } from './components/rich-text/'; /** @@ -59,5 +60,6 @@ lock( privateApis, { usesContextKey, useFlashEditableBlocks, selectBlockPatternsKey, + requiresWrapperOnCopy, PrivateRichText, } ); diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 46cbd3a94831d5..467154f76992e1 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -29,7 +29,6 @@ import { useOutdentListItem, useSplit, useMerge, - useCopy, } from './hooks'; import { convertToListItems } from './utils'; @@ -79,7 +78,7 @@ export default function ListItemEdit( { mergeBlocks, } ) { const { placeholder, content } = attributes; - const blockProps = useBlockProps( { ref: useCopy( clientId ) } ); + const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( blockProps, { renderAppender: false, __unstableDisableDropZone: true, diff --git a/packages/block-library/src/list-item/hooks/index.js b/packages/block-library/src/list-item/hooks/index.js index 3bbc3167abed32..1687adbe740d0a 100644 --- a/packages/block-library/src/list-item/hooks/index.js +++ b/packages/block-library/src/list-item/hooks/index.js @@ -4,4 +4,3 @@ export { default as useEnter } from './use-enter'; export { default as useSpace } from './use-space'; export { default as useSplit } from './use-split'; export { default as useMerge } from './use-merge'; -export { default as useCopy } from './use-copy'; diff --git a/packages/block-library/src/list-item/hooks/use-copy.js b/packages/block-library/src/list-item/hooks/use-copy.js deleted file mode 100644 index 7a76019ad11a4b..00000000000000 --- a/packages/block-library/src/list-item/hooks/use-copy.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * WordPress dependencies - */ -import { useRefEffect } from '@wordpress/compose'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; - -export default function useCopy( clientId ) { - const { getBlockRootClientId, getBlockName, getBlockAttributes } = - useSelect( blockEditorStore ); - - return useRefEffect( ( node ) => { - function onCopy( event ) { - // The event propagates through all nested lists, so don't override - // when copying nested list items. - if ( event.clipboardData.getData( '__unstableWrapperBlockName' ) ) { - return; - } - - const rootClientId = getBlockRootClientId( clientId ); - event.clipboardData.setData( - '__unstableWrapperBlockName', - getBlockName( rootClientId ) - ); - event.clipboardData.setData( - '__unstableWrapperBlockAttributes', - JSON.stringify( getBlockAttributes( rootClientId ) ) - ); - } - - node.addEventListener( 'copy', onCopy ); - node.addEventListener( 'cut', onCopy ); - return () => { - node.removeEventListener( 'copy', onCopy ); - node.removeEventListener( 'cut', onCopy ); - }; - }, [] ); -} diff --git a/packages/block-library/src/list-item/index.js b/packages/block-library/src/list-item/index.js index 00adc1c2c40266..07c5bb7fda9015 100644 --- a/packages/block-library/src/list-item/index.js +++ b/packages/block-library/src/list-item/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { listItem as icon } from '@wordpress/icons'; +import { privateApis } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,6 +12,7 @@ import metadata from './block.json'; import edit from './edit'; import save from './save'; import transforms from './transforms'; +import { unlock } from '../lock-unlock'; const { name } = metadata; @@ -27,6 +29,7 @@ export const settings = { }; }, transforms, + [ unlock( privateApis ).requiresWrapperOnCopy ]: true, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts index 3b187625fd47cf..11bc11c43f603c 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts @@ -55,18 +55,26 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { const canvasDoc = // @ts-ignore document.activeElement?.contentDocument ?? document; - const clipboardDataTransfer = new DataTransfer(); + const event = new ClipboardEvent( _type, { + bubbles: true, + cancelable: true, + clipboardData: new DataTransfer(), + } ); + + if ( ! event.clipboardData ) { + throw new Error( 'ClipboardEvent.clipboardData is null' ); + } if ( _type === 'paste' ) { - clipboardDataTransfer.setData( + event.clipboardData.setData( 'text/plain', _clipboardData[ 'text/plain' ] ); - clipboardDataTransfer.setData( + event.clipboardData.setData( 'text/html', _clipboardData[ 'text/html' ] ); - clipboardDataTransfer.setData( + event.clipboardData.setData( 'rich-text', _clipboardData[ 'rich-text' ] ); @@ -85,22 +93,16 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { ) .join( '' ); } - clipboardDataTransfer.setData( 'text/plain', plainText ); - clipboardDataTransfer.setData( 'text/html', html ); + event.clipboardData.setData( 'text/plain', plainText ); + event.clipboardData.setData( 'text/html', html ); } - canvasDoc.activeElement?.dispatchEvent( - new ClipboardEvent( _type, { - bubbles: true, - cancelable: true, - clipboardData: clipboardDataTransfer, - } ) - ); + canvasDoc.activeElement.dispatchEvent( event ); return { - 'text/plain': clipboardDataTransfer.getData( 'text/plain' ), - 'text/html': clipboardDataTransfer.getData( 'text/html' ), - 'rich-text': clipboardDataTransfer.getData( 'rich-text' ), + 'text/plain': event.clipboardData.getData( 'text/plain' ), + 'text/html': event.clipboardData.getData( 'text/html' ), + 'rich-text': event.clipboardData.getData( 'rich-text' ), }; }, [ type, clipboardDataHolder ] as const diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js index 48d50a862a50b4..10f25d6b3609f0 100644 --- a/test/e2e/specs/editor/blocks/list.spec.js +++ b/test/e2e/specs/editor/blocks/list.spec.js @@ -8,6 +8,36 @@ test.describe( 'List (@firefox)', () => { await admin.createNewPost(); } ); + test( 'can be copied from multi selection', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { name: 'core/list' } ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'two' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+c' ); + await editor.insertBlock( { name: 'core/paragraph' } ); + await pageUtils.pressKeys( 'primary+v' ); + + const copied = ` +
    +
  • one
  • + + + +
  • two
  • +
+`; + + expect( await editor.getEditedPostContent() ).toBe( + copied + '\n\n' + copied + ); + } ); + test( 'can be created by using an asterisk at the start of a paragraph block', async ( { editor, page, From 28295ee43b04104bbde8cf3014b2daaecade61df Mon Sep 17 00:00:00 2001 From: Siobhan Bamber Date: Thu, 29 Feb 2024 17:01:08 +0000 Subject: [PATCH 26/45] Mobile Release v1.114.0 (#59461) Co-authored-by: SiobhyB Co-authored-by: geriux Co-authored-by: fluiddot --- package-lock.json | 6 +++--- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 2 ++ packages/react-native-editor/ios/Podfile.lock | 8 ++++---- packages/react-native-editor/package.json | 2 +- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 324736a01403c0..de9f4a0e90def8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56142,7 +56142,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.112.0", + "version": "1.114.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56155,7 +56155,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.112.0", + "version": "1.114.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56166,7 +56166,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.112.0", + "version": "1.114.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index c2372e1cb6a24c..9e9bdbc899cfdd 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.112.0", + "version": "1.114.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 5317b625c04491..445c3726d04551 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.112.0", + "version": "1.114.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 029e216aa962a9..ea60a7d531b2fd 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased + +## 1.114.0 - [*] Prevent crash when autoscrolling to blocks [#59110] - [*] Remove opacity change when images are being uploaded [#59264] - [*] Media & Text blocks correctly show an error message when the attached video upload fails [#59288] diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 3075abf049066e..43f67ed9a026f1 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.15) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.112.0): + - Gutenberg (1.114.0): - React-Core (= 0.71.15) - React-CoreModules (= 0.71.15) - React-RCTImage (= 0.71.15) @@ -429,7 +429,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.112.0): + - RNTAztecView (1.114.0): - React-Core - WordPress-Aztec-iOS (= 1.19.9) - SDWebImage (5.11.1): @@ -617,7 +617,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 8f5ee005451bf28f6a3ae995914b2f04b3584122 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: fad00864ab916e9f7778e5493c1a7f3bd9316eb4 + Gutenberg: 3f546b663d6776141e069f4fb505428009e4e59a hermes-engine: 04437e4291ede4af0c76c25e7efd0eacb8fd25e5 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -662,7 +662,7 @@ SPEC CHECKSUMS: RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: 74c676c87e4a9db79104fcdad9624c1e5c02fe15 + RNTAztecView: 68c970e04c9f7ae81ba64810667a07a66e6ba0b7 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index d143d214ce0e09..8f98cd17619b4a 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.112.0", + "version": "1.114.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 1c4418b93ae8979823f3ad7ff7be870c966b6b29 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 29 Feb 2024 19:32:49 +0000 Subject: [PATCH 27/45] Add visual indicator if a block is connected to block binding source (#59185) * Add BlockControlsFirst slot to block controls groups * Add connection icon to BlockControls toolbar button * Add block binding toolbar button if block is connected to a source * Add i18n support for block toolbar button label * Add BlockBindingsButton component and remove BlockControlsFirst group * Refactor BlockBindingsButton to check for block connections * Change the ToolbarButton label * Update block-bindings-button import to block-bindings-indicator * Block Bindings: Add connection icon to list view (#59331) * Add connection icon to list view * Remove extraneous string * Move bindings style to useBlockProps * Remove extraneous comment * Move bindings selector logic to toolbar * Rename indicator file * Move purple stroke style from SVG markup to CSS * Check if block can be bound before adding styles * Simplify the SVG icon: - get rid of 2 unnecessary `` elements - move the stroke styles to CSS - add the `evenodd` rule * Update the CSS namespacing to include the `__` * Fix issues with block binding indicator color --------- Co-authored-by: michalczaplinski Co-authored-by: artemiomorales Co-authored-by: Mamaduka Co-authored-by: gziolo Co-authored-by: SantosGuillamot Co-authored-by: jasmussen Co-authored-by: SaxonF Co-authored-by: afercia --- .../_default-custom-properties.scss | 2 +- .../block-bindings-toolbar-indicator/index.js | 20 +++++++++++++++ .../style.scss | 14 +++++++++++ .../block-list/use-block-props/index.js | 14 +++++++++-- .../src/components/block-toolbar/index.js | 18 ++++++++++--- .../list-view/block-select-button.js | 18 +++++++++++-- .../src/components/list-view/style.scss | 8 ++++++ packages/block-editor/src/style.scss | 1 + packages/icons/src/index.js | 1 + packages/icons/src/library/connection.js | 25 +++++++++++++++++++ 10 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js create mode 100644 packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss create mode 100644 packages/icons/src/library/connection.js diff --git a/packages/base-styles/_default-custom-properties.scss b/packages/base-styles/_default-custom-properties.scss index 52dfeb3899d772..5760753c48ce85 100644 --- a/packages/base-styles/_default-custom-properties.scss +++ b/packages/base-styles/_default-custom-properties.scss @@ -1,4 +1,3 @@ - // It is important to include these styles in all built stylesheets. // This allows to CSS variables post CSS plugin to generate fallbacks. // It also provides default CSS variables for npm package consumers. @@ -6,4 +5,5 @@ @include admin-scheme(#007cba); --wp-block-synced-color: #7a00df; --wp-block-synced-color--rgb: #{hex-to-rgb(#7a00df)}; + --wp-bound-block-color: #9747ff; } diff --git a/packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js b/packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js new file mode 100644 index 00000000000000..4b2d3df725a66b --- /dev/null +++ b/packages/block-editor/src/components/block-bindings-toolbar-indicator/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { ToolbarItem, ToolbarGroup, Icon } from '@wordpress/components'; +import { connection } from '@wordpress/icons'; +import { _x } from '@wordpress/i18n'; + +export default function BlockBindingsToolbarIndicator() { + return ( + + + + + + ); +} diff --git a/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss new file mode 100644 index 00000000000000..4aeabdf8acf6e8 --- /dev/null +++ b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss @@ -0,0 +1,14 @@ +.block-editor-block-bindings-toolbar-indicator { + display: inline-flex; + align-items: center; + height: 48px; + padding: 6px; + + svg g { + stroke: var(--wp-bound-block-color); + fill: transparent; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; + } +} diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 08b43fa46257e4..c929c1014dc030 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -19,13 +19,17 @@ import useMovingAnimation from '../../use-moving-animation'; import { PrivateBlockContext } from '../private-block-context'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; -import { useBlockEditContext } from '../../block-edit/context'; +import { + blockBindingsKey, + useBlockEditContext, +} from '../../block-edit/context'; import { useFocusHandler } from './use-focus-handler'; import { useEventHandlers } from './use-selected-block-event-handlers'; import { useNavModeExit } from './use-nav-mode-exit'; import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; +import { canBindBlock } from '../../../hooks/use-bindings-attributes'; /** * This hook is used to lightly mark an element as a block element. The element @@ -123,6 +127,12 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { ] ); const blockEditContext = useBlockEditContext(); + const hasBlockBindings = !! blockEditContext[ blockBindingsKey ]; + const bindingsStyle = + hasBlockBindings && canBindBlock( name ) + ? { '--wp-admin-theme-color': 'var(--wp-bound-block-color)' } + : {}; + // Ensures it warns only inside the `edit` implementation for the block. if ( blockApiVersion < 2 && clientId === blockEditContext.clientId ) { warning( @@ -168,7 +178,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { wrapperProps.className, defaultClassName ), - style: { ...wrapperProps.style, ...props.style }, + style: { ...wrapperProps.style, ...props.style, ...bindingsStyle }, }; } diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index a641df0f7e5cf4..e566096d54f269 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -35,7 +35,9 @@ import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; import NavigableToolbar from '../navigable-toolbar'; import Shuffle from './shuffle'; +import BlockBindingsIndicator from '../block-bindings-toolbar-indicator'; import { useHasBlockToolbar } from './use-has-block-toolbar'; +import { canBindBlock } from '../../hooks/use-bindings-attributes'; /** * Renders the block toolbar. * @@ -60,8 +62,10 @@ export function PrivateBlockToolbar( { blockClientIds, isDefaultEditingMode, blockType, + blockName, shouldShowVisualToolbar, showParentSelector, + isUsingBindings, } = useSelect( ( select ) => { const { getBlockName, @@ -71,6 +75,7 @@ export function PrivateBlockToolbar( { isBlockValid, getBlockRootClientId, getBlockEditingMode, + getBlockAttributes, } = select( blockEditorStore ); const selectedBlockClientIds = getSelectedBlockClientIds(); const selectedBlockClientId = selectedBlockClientIds[ 0 ]; @@ -81,20 +86,21 @@ export function PrivateBlockToolbar( { const parentBlockType = getBlockType( parentBlockName ); const _isDefaultEditingMode = getBlockEditingMode( selectedBlockClientId ) === 'default'; + const _blockName = getBlockName( selectedBlockClientId ); const isValid = selectedBlockClientIds.every( ( id ) => isBlockValid( id ) ); const isVisual = selectedBlockClientIds.every( ( id ) => getBlockMode( id ) === 'visual' ); + const _isUsingBindings = !! getBlockAttributes( selectedBlockClientId ) + ?.metadata?.bindings; return { blockClientId: selectedBlockClientId, blockClientIds: selectedBlockClientIds, isDefaultEditingMode: _isDefaultEditingMode, - blockType: - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ), - + blockName: _blockName, + blockType: selectedBlockClientId && getBlockType( _blockName ), shouldShowVisualToolbar: isValid && isVisual, rootClientId: blockRootClientId, showParentSelector: @@ -107,6 +113,7 @@ export function PrivateBlockToolbar( { ) && selectedBlockClientIds.length === 1 && _isDefaultEditingMode, + isUsingBindings: _isUsingBindings, }; }, [] ); @@ -158,6 +165,9 @@ export function PrivateBlockToolbar( { { ! isMultiToolbar && isLargeViewport && isDefaultEditingMode && } + { isUsingBindings && canBindBlock( blockName ) && ( + + ) } { ( shouldShowVisualToolbar || isMultiToolbar ) && isDefaultEditingMode && (
) } + { isConnected && canBindBlock( blockName ) && ( + + + + ) } { positionLabel && isSticky && ( diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index 11cf1fafa0e14b..1245bfbabcb7a7 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -557,3 +557,11 @@ $block-navigation-max-indent: 8; .list-view-appender__description { display: none; } + +.block-editor-list-view-block-select-button__bindings svg g { + stroke: var(--wp-bound-block-color); + fill: transparent; + stroke-width: 1.5; + stroke-linecap: round; + stroke-linejoin: round; +} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 1cbc49f58551e5..015cffde42a239 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -1,5 +1,6 @@ @import "./autocompleters/style.scss"; @import "./components/block-alignment-control/style.scss"; +@import "./components/block-bindings-toolbar-indicator/style.scss"; @import "./components/block-canvas/style.scss"; @import "./components/block-icon/style.scss"; @import "./components/block-inspector/style.scss"; diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 1d3c8c24c5cfd9..788fc0152ba1de 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -59,6 +59,7 @@ export { default as commentAuthorName } from './library/comment-author-name'; export { default as commentContent } from './library/comment-content'; export { default as commentReplyLink } from './library/comment-reply-link'; export { default as commentEditLink } from './library/comment-edit-link'; +export { default as connection } from './library/connection'; export { default as cover } from './library/cover'; export { default as create } from './library/create'; export { default as crop } from './library/crop'; diff --git a/packages/icons/src/library/connection.js b/packages/icons/src/library/connection.js new file mode 100644 index 00000000000000..47cee6c66a3ef8 --- /dev/null +++ b/packages/icons/src/library/connection.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { SVG, Path, G } from '@wordpress/primitives'; + +const connection = ( + + + + + + + + + + +); + +export default connection; From 9957b858fb0657f234b8b392a91996f654256fcf Mon Sep 17 00:00:00 2001 From: Ramon Date: Fri, 1 Mar 2024 09:17:54 +1100 Subject: [PATCH 28/45] Global styles: add background to top-level theme.json styles (#59354) * First commit. Define theme.json schema Updating constants and docs Fix props * Adding background to global styles output in the site editor * Testing generic block support function to calculate block support for background and global styles * Pass the entire background object * testing out theme source * Comments and some checks * Added image fixture for theme source Added tests Supporting string and object for backgroundImage * Use the JS style engine to generate styles * Default background size is always 'cover' - this matches current behaviour Now assuming that a string value will have it's own `url()` * The linter of doom * Whoops, reverting bad setting * Reverting source:theme support * Update theme.json schema * Tests * LINTY! * We don't need assets in block theme since the "theme" source functionality was reverted * Something about yoda conditions Co-authored-by: ramonjd Co-authored-by: andrewserong --- .../theme-json-reference/theme-json-living.md | 13 +++ lib/block-supports/background.php | 53 ++++++------- lib/class-wp-theme-json-gutenberg.php | 18 +++++ .../global-styles/use-global-styles-output.js | 1 + packages/blocks/src/api/constants.js | 5 ++ .../src/styles/background/index.ts | 18 ++++- packages/style-engine/src/test/index.js | 27 +++++++ packages/style-engine/src/types.ts | 7 +- phpunit/block-supports/background-test.php | 79 +++++++++++++++++++ phpunit/class-wp-theme-json-test.php | 46 +++++++++++ schemas/json/theme.json | 68 ++++++++++++++++ 11 files changed, 302 insertions(+), 33 deletions(-) diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 0b800757b4ecd0..cec4374388284e 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -203,6 +203,19 @@ Generate custom CSS custom properties of the form `--wp--custom--{key}--{nested- ## Styles +### background + +Background styles + +| Property | Type | Props | +| --- | --- |--- | +| backgroundImage | string, object | | +| backgroundPosition | string, object | | +| backgroundRepeat | string, object | | +| backgroundSize | string, object | | + +--- + ### border Border styles. diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index 4b5f5614d64c9f..64a9dbaf7f2774 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -30,6 +30,28 @@ function gutenberg_register_background_support( $block_type ) { } } +/** + * Given a theme.json or block background styles, returns the background styles for a block. + * + * @since 6.6.0 + * + * @param array $background_styles Background style properties. + * @return array Style engine array of CSS string and style declarations. + */ +function gutenberg_get_background_support_styles( $background_styles = array() ) { + $background_image_source = isset( $background_styles['backgroundImage']['source'] ) ? $background_styles['backgroundImage']['source'] : null; + $background_styles['backgroundSize'] = ! empty( $background_styles['backgroundSize'] ) ? $background_styles['backgroundSize'] : 'cover'; + + if ( 'file' === $background_image_source && ! empty( $background_styles['backgroundImage']['url'] ) ) { + // If the background size is set to `contain` and no position is set, set the position to `center`. + if ( 'contain' === $background_styles['backgroundSize'] && ! isset( $background_styles['backgroundPosition'] ) ) { + $background_styles['backgroundPosition'] = 'center'; + } + } + + return gutenberg_style_engine_get_styles( array( 'background' => $background_styles ) ); +} + /** * Renders the background styles to the block wrapper. * This block support uses the `render_block` hook to ensure that @@ -46,38 +68,13 @@ function gutenberg_render_background_support( $block_content, $block ) { if ( ! $has_background_image_support || - wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ) + wp_should_skip_block_supports_serialization( $block_type, 'background', 'backgroundImage' ) || + ! isset( $block_attributes['style']['background'] ) ) { return $block_content; } - $background_image_source = $block_attributes['style']['background']['backgroundImage']['source'] ?? null; - $background_image_url = $block_attributes['style']['background']['backgroundImage']['url'] ?? null; - $background_size = $block_attributes['style']['background']['backgroundSize'] ?? 'cover'; - $background_position = $block_attributes['style']['background']['backgroundPosition'] ?? null; - $background_repeat = $block_attributes['style']['background']['backgroundRepeat'] ?? null; - - $background_block_styles = array(); - - if ( - 'file' === $background_image_source && - $background_image_url - ) { - // Set file based background URL. - // TODO: In a follow-up, similar logic could be added to inject a featured image url. - $background_block_styles['backgroundImage']['url'] = $background_image_url; - // Only output the background size and repeat when an image url is set. - $background_block_styles['backgroundSize'] = $background_size; - $background_block_styles['backgroundRepeat'] = $background_repeat; - $background_block_styles['backgroundPosition'] = $background_position; - - // If the background size is set to `contain` and no position is set, set the position to `center`. - if ( 'contain' === $background_size && ! isset( $background_position ) ) { - $background_block_styles['backgroundPosition'] = 'center'; - } - } - - $styles = gutenberg_style_engine_get_styles( array( 'background' => $background_block_styles ) ); + $styles = gutenberg_get_background_support_styles( $block_attributes['style']['background'] ); if ( ! empty( $styles['css'] ) ) { // Inject background styles to the first element, presuming it's the wrapper, if it exists. diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 2a3fd1f873c531..f98adfac67a017 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -203,6 +203,7 @@ class WP_Theme_JSON_Gutenberg { * removed the `--wp--style--block-gap` property. * @since 6.2.0 Added `outline-*`, and `min-height` properties. * @since 6.3.0 Added `writing-mode` property. + * @since 6.6.0 Added `background-[image|position|repeat|size]` properties. * * @var array */ @@ -210,6 +211,10 @@ class WP_Theme_JSON_Gutenberg { 'aspect-ratio' => array( 'dimensions', 'aspectRatio' ), 'background' => array( 'color', 'gradient' ), 'background-color' => array( 'color', 'background' ), + 'background-image' => array( 'background', 'backgroundImage' ), + 'background-position' => array( 'background', 'backgroundPosition' ), + 'background-repeat' => array( 'background', 'backgroundRepeat' ), + 'background-size' => array( 'background', 'backgroundSize' ), 'border-radius' => array( 'border', 'radius' ), 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), @@ -461,10 +466,17 @@ class WP_Theme_JSON_Gutenberg { * added new property `shadow`, * updated `blockGap` to be allowed at any level. * @since 6.2.0 Added `outline`, and `minHeight` properties. + * @since 6.6.0 Added `background` sub properties to top-level only. * * @var array */ const VALID_STYLES = array( + 'background' => array( + 'backgroundImage' => 'top', + 'backgroundPosition' => 'top', + 'backgroundRepeat' => 'top', + 'backgroundSize' => 'top', + ), 'border' => array( 'color' => null, 'radius' => null, @@ -2119,6 +2131,12 @@ protected static function compute_style_properties( $styles, $settings = array() } } + // Processes background styles. + if ( 'background' === $value_path[0] && isset( $styles['background'] ) ) { + $background_styles = gutenberg_get_background_support_styles( $styles['background'] ); + $value = $background_styles['declarations'][ $css_property ] ?? $value; + } + // Skip if empty and not "0" or value represents array of longhand values. $has_missing_value = empty( $value ) && ! is_numeric( $value ); if ( $has_missing_value || is_array( $value ) ) { diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 4e8ea6dc0ff95b..f5cb968924459a 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -600,6 +600,7 @@ const STYLE_KEYS = [ 'filter', 'outline', 'shadow', + 'background', ]; function pickStyleKeys( treeToPickFrom ) { diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 7062c98404a2a6..62933d69d764f4 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -36,6 +36,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { requiresOptOut: true, useEngine: true, }, + backgroundImage: { + value: [ 'background', 'backgroundImage' ], + support: [ 'background', 'backgroundImage' ], + useEngine: true, + }, backgroundRepeat: { value: [ 'background', 'backgroundRepeat' ], support: [ 'background', 'backgroundRepeat' ], diff --git a/packages/style-engine/src/styles/background/index.ts b/packages/style-engine/src/styles/background/index.ts index 8ce8c7d577fb28..f738cefd5e83a4 100644 --- a/packages/style-engine/src/styles/background/index.ts +++ b/packages/style-engine/src/styles/background/index.ts @@ -16,7 +16,23 @@ const backgroundImage = { return styleRules; } - if ( _backgroundImage?.source === 'file' && _backgroundImage?.url ) { + /* + * If the background image is a string, it could already contain a url() function, + * or have a linear-gradient value. + */ + if ( typeof _backgroundImage === 'string' ) { + styleRules.push( { + selector: options.selector, + key: 'backgroundImage', + value: _backgroundImage, + } ); + } + + if ( + typeof _backgroundImage === 'object' && + _backgroundImage?.source === 'file' && + _backgroundImage?.url + ) { styleRules.push( { selector: options.selector, key: 'backgroundImage', diff --git a/packages/style-engine/src/test/index.js b/packages/style-engine/src/test/index.js index b679775d3f37f4..bc76278de9791c 100644 --- a/packages/style-engine/src/test/index.js +++ b/packages/style-engine/src/test/index.js @@ -459,6 +459,33 @@ describe( 'getCSSRules', () => { ] ); } ); + it( 'should output background image value when that value is a string', () => { + expect( + getCSSRules( + { + background: { + backgroundImage: + "linear-gradient(to bottom,rgb(255 255 0 / 50%),rgb(0 0 255 / 50%), url('https://example.com/image.jpg')", + }, + }, + { + selector: '.some-selector', + } + ) + ).toEqual( [ + { + selector: '.some-selector', + key: 'backgroundImage', + value: "linear-gradient(to bottom,rgb(255 255 0 / 50%),rgb(0 0 255 / 50%), url('https://example.com/image.jpg')", + }, + { + selector: '.some-selector', + key: 'backgroundSize', + value: 'cover', + }, + ] ); + } ); + it( 'should output fallback center position for contain background size', () => { expect( getCSSRules( diff --git a/packages/style-engine/src/types.ts b/packages/style-engine/src/types.ts index b5085ce53f1363..5b361836a8e375 100644 --- a/packages/style-engine/src/types.ts +++ b/packages/style-engine/src/types.ts @@ -22,10 +22,9 @@ export interface BorderIndividualStyles< T extends BoxEdge > { export interface Style { background?: { - backgroundImage: { - url?: CSSProperties[ 'backgroundImage' ]; - source?: string; - }; + backgroundImage?: + | { url?: CSSProperties[ 'backgroundImage' ]; source?: string } + | CSSProperties[ 'backgroundImage' ]; backgroundPosition?: CSSProperties[ 'backgroundPosition' ]; backgroundRepeat?: CSSProperties[ 'backgroundRepeat' ]; backgroundSize?: CSSProperties[ 'backgroundSize' ]; diff --git a/phpunit/block-supports/background-test.php b/phpunit/block-supports/background-test.php index 92e1d2fc345a0e..a4ddb7da3f74af 100644 --- a/phpunit/block-supports/background-test.php +++ b/phpunit/block-supports/background-test.php @@ -137,6 +137,18 @@ public function data_background_block_support() { 'expected_wrapper' => '
Content
', 'wrapper' => '
Content
', ), + 'background image style is applied when backgroundImage is a string' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-rules-are-output', + 'background_settings' => array( + 'backgroundImage' => true, + ), + 'background_style' => array( + 'backgroundImage' => "url('https://example.com/image.jpg')", + ), + 'expected_wrapper' => '
Content
', + 'wrapper' => '
Content
', + ), 'background image style with contain, position, and repeat is applied' => array( 'theme_name' => 'block-theme-child-with-fluid-typography', 'block_name' => 'test/background-rules-are-output', @@ -201,4 +213,71 @@ public function data_background_block_support() { ), ); } + + /** + * Tests generating background styles. + * + * @covers ::gutenberg_get_background_support_styles + * + * @dataProvider data_get_background_support_styles + * + * @param mixed $background_style The background styles within the block attributes. + * @param string $expected_css Expected markup for the block wrapper. + */ + public function test_get_background_support_styles( $background_style, $expected_css ) { + $actual = gutenberg_get_background_support_styles( $background_style )['css']; + + $this->assertEquals( + $expected_css, + $actual, + 'Background CSS should be correct.' + ); + } + public function data_get_background_support_styles() { + return array( + 'css generated with file source' => array( + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + 'source' => 'file', + ), + ), + 'expected_css' => "background-image:url('https://example.com/image.jpg');background-size:cover;", + ), + 'css generated where backgroundImage is a string' => array( + 'background_style' => array( + 'backgroundImage' => "url('https://example.com/image.jpg')", + ), + 'expected_css' => "background-image:url('https://example.com/image.jpg');background-size:cover;", + ), + 'css generated with escaped URL' => array( + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg?q=pom-poms+%3Cscript%3Eevil_script()%3C/script%3E', + ), + 'backgroundSize' => 'cover', + ), + 'expected_css' => 'background-size:cover;', + ), + 'css generated with expected properties' => array( + 'background_style' => array( + 'backgroundImage' => "url('https://example.com/image.jpg')", + 'backgroundSize' => '6px, auto, contain', + 'backgroundPosition' => 'bottom 10px right 20px', + 'backgroundRepeat' => 'repeat space', + ), + 'expected_css' => "background-image:url('https://example.com/image.jpg');background-position:bottom 10px right 20px;background-repeat:repeat space;background-size:6px, auto, contain;", + ), + 'css generated for file source with contain size should add center position' => array( + 'background_style' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + 'source' => 'file', + ), + 'backgroundSize' => 'contain', + ), + 'expected_css' => "background-image:url('https://example.com/image.jpg');background-position:center;background-size:contain;", + ), + ); + } } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 376eaaff4a2051..4220288b720362 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -4745,6 +4745,52 @@ public function test_get_shadow_styles_for_blocks() { $this->assertSame( $expected_styles, $theme_json->get_stylesheet() ); } + public function test_get_top_level_background_image_styles() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/image.png', + ), + 'backgroundSize' => 'cover', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + ), + 'blocks' => array( + 'core/paragraph' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/image.png', + 'source' => 'file', + ), + 'backgroundSize' => 'cover', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + ), + ), + ), + 'elements' => array( + 'button' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/image.png', + ), + 'backgroundSize' => 'cover', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + ), + ), + ), + ), + ) + ); + + $expected_styles = "body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}body{background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: cover;}"; + $this->assertSame( $expected_styles, $theme_json->get_stylesheet() ); + } + public function test_get_custom_css_handles_global_custom_css() { $theme_json = new WP_Theme_JSON_Gutenberg( array( diff --git a/schemas/json/theme.json b/schemas/json/theme.json index a9823f6cc6534b..549cf5cd2207a1 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -1110,6 +1110,74 @@ "stylesProperties": { "type": "object", "properties": { + "background": { + "description": "Background styles", + "type": "object", + "properties": { + "backgroundImage": { + "description": "Sets the `background-image` CSS property.", + "oneOf": [ + { + "description": "A valid CSS value for the background-image property.", + "type": "string" + }, + { + "type": "object", + "properties": { + "source": { + "description": "The origin of the image. 'file' denotes that the 'url' is a path to an image or other file.", + "type": "string", + "enum": [ "file" ], + "default": "file" + }, + "url": { + "description": "A URL to an image file.", + "type": "string" + } + }, + "additionalProperties": false + }, + { + "$ref": "#/definitions/refComplete" + } + ] + }, + "backgroundPosition": { + "description": "Sets the `background-position` CSS property.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/refComplete" + } + ] + }, + "backgroundRepeat": { + "description": "Sets the `background-repeat` CSS property.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/refComplete" + } + ] + }, + "backgroundSize": { + "description": "Sets the `background-size` CSS property.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/refComplete" + } + ] + } + }, + "additionalProperties": false + }, "border": { "description": "Border styles.", "type": "object", From c53b018c99b8167db53110f2f7f8b651caaa95bf Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 1 Mar 2024 16:03:16 +0800 Subject: [PATCH 29/45] Use block naming for marking blocks as overridable in patterns (#59268) Co-authored-by: talldan Co-authored-by: aaronrobertshaw Co-authored-by: kevin940726 Co-authored-by: michalczaplinski Co-authored-by: youknowriad Co-authored-by: gziolo --- .../block-bindings/pattern-overrides.php | 25 +++- package-lock.json | 6 +- .../block-library/src/block/deprecated.js | 87 ++++++++++++-- packages/block-library/src/block/edit.js | 83 +++++++++++--- packages/block-library/src/block/index.php | 33 ++++-- packages/editor/src/hooks/index.js | 2 +- ...artial-syncing.js => pattern-overrides.js} | 26 +++-- packages/patterns/package.json | 3 +- .../components/partial-syncing-controls.js | 108 ------------------ .../src/components/reset-overrides-control.js | 16 +-- .../components/use-set-pattern-bindings.js | 106 +++++++++++++++++ packages/patterns/src/private-apis.js | 4 +- .../editor/various/pattern-overrides.spec.js | 58 ++++------ .../core__block__overrides__deprecated-1.html | 1 + .../core__block__overrides__deprecated-1.json | 15 +++ ...block__overrides__deprecated-1.parsed.json | 16 +++ ...k__overrides__deprecated-1.serialized.html | 1 + .../core__block__overrides__deprecated-2.html | 1 + .../core__block__overrides__deprecated-2.json | 15 +++ ...block__overrides__deprecated-2.parsed.json | 18 +++ ...k__overrides__deprecated-2.serialized.html | 1 + 21 files changed, 412 insertions(+), 213 deletions(-) rename packages/editor/src/hooks/{pattern-partial-syncing.js => pattern-overrides.js} (81%) delete mode 100644 packages/patterns/src/components/partial-syncing-controls.js create mode 100644 packages/patterns/src/components/use-set-pattern-bindings.js create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-1.html create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-1.json create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-1.parsed.json create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-1.serialized.html create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-2.html create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-2.json create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-2.parsed.json create mode 100644 test/integration/fixtures/blocks/core__block__overrides__deprecated-2.serialized.html diff --git a/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php index 76c3d49ca8085f..e5f9891f04c471 100644 --- a/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php +++ b/lib/compat/wordpress-6.5/block-bindings/pattern-overrides.php @@ -15,11 +15,30 @@ * @return mixed The value computed for the source. */ function gutenberg_block_bindings_pattern_overrides_callback( $source_attrs, $block_instance, $attribute_name ) { - if ( empty( $block_instance->attributes['metadata']['id'] ) ) { + if ( ! isset( $block_instance->context['pattern/overrides'] ) ) { return null; } - $block_id = $block_instance->attributes['metadata']['id']; - return _wp_array_get( $block_instance->context, array( 'pattern/overrides', $block_id, 'values', $attribute_name ), null ); + + $override_content = $block_instance->context['pattern/overrides']; + + // Back compat. Pattern overrides previously used a metadata `id` instead of `name`. + // We check first for the name, and if it exists, use that value. + if ( isset( $block_instance->attributes['metadata']['name'] ) ) { + $metadata_name = $block_instance->attributes['metadata']['name']; + if ( array_key_exists( $metadata_name, $override_content ) ) { + return _wp_array_get( $override_content, array( $metadata_name, $attribute_name ), null ); + } + } + + // Next check for the `id`. + if ( isset( $block_instance->attributes['metadata']['id'] ) ) { + $metadata_id = $block_instance->attributes['metadata']['id']; + if ( array_key_exists( $metadata_id, $override_content ) ) { + return _wp_array_get( $override_content, array( $metadata_id, $attribute_name ), null ); + } + } + + return null; } /** diff --git a/package-lock.json b/package-lock.json index de9f4a0e90def8..8c6b476dc0e53a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55967,8 +55967,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url", - "nanoid": "^3.3.4" + "@wordpress/url": "file:../url" }, "engines": { "node": ">=16.0.0" @@ -70828,8 +70827,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url", - "nanoid": "^3.3.4" + "@wordpress/url": "file:../url" } }, "@wordpress/plugins": { diff --git a/packages/block-library/src/block/deprecated.js b/packages/block-library/src/block/deprecated.js index 7bc243bbf4ce98..f820867fff6271 100644 --- a/packages/block-library/src/block/deprecated.js +++ b/packages/block-library/src/block/deprecated.js @@ -1,4 +1,75 @@ -// v1: Migrate and rename the `overrides` attribute to the `content` attribute. +const isObject = ( obj ) => + typeof obj === 'object' && ! Array.isArray( obj ) && obj !== null; + +// v2: Migrate to a more condensed version of the 'content' attribute attribute. +const v2 = { + attributes: { + ref: { + type: 'number', + }, + content: { + type: 'object', + }, + }, + supports: { + customClassName: false, + html: false, + inserter: false, + renaming: false, + }, + // Force this deprecation to run whenever there's a values sub-property that's an object. + // + // This could fail in the future if a block ever has binding to a `values` attribute. + // Some extra protection is added to ensure `values` is an object, but this only reduces + // the likelihood, it doesn't solve it completely. + isEligible( { content } ) { + return ( + !! content && + Object.keys( content ).every( + ( contentKey ) => + content[ contentKey ].values && + isObject( content[ contentKey ].values ) + ) + ); + }, + /* + * Old attribute format: + * content: { + * "V98q_x": { + * // The attribute values are now stored as a 'values' sub-property. + * values: { content: 'My content value' }, + * // ... additional metadata, like the block name can be stored here. + * } + * } + * + * New attribute format: + * content: { + * "V98q_x": { + * content: 'My content value', + * } + * } + */ + migrate( attributes ) { + const { content, ...retainedAttributes } = attributes; + + if ( content && Object.keys( content ).length ) { + const updatedContent = { ...content }; + + for ( const contentKey in content ) { + updatedContent[ contentKey ] = content[ contentKey ].values; + } + + return { + ...retainedAttributes, + content: updatedContent, + }; + } + + return attributes; + }, +}; + +// v1: Rename the `overrides` attribute to the `content` attribute. const v1 = { attributes: { ref: { @@ -23,16 +94,12 @@ const v1 = { * overrides: { * // An key is an id that represents a block. * // The values are the attribute values of the block. - * "V98q_x": { content: 'dwefwefwefwe' } + * "V98q_x": { content: 'My content value' } * } * * New attribute format: * content: { - * "V98q_x": { - * // The attribute values are now stored as a 'values' sub-property. - * values: { content: 'dwefwefwefwe' }, - * // ... additional metadata, like the block name can be stored here. - * } + * "V98q_x": { content: 'My content value' } * } * */ @@ -42,9 +109,7 @@ const v1 = { const content = {}; Object.keys( overrides ).forEach( ( id ) => { - content[ id ] = { - values: overrides[ id ], - }; + content[ id ] = overrides[ id ]; } ); return { @@ -54,4 +119,4 @@ const v1 = { }, }; -export default [ v1 ]; +export default [ v2, v1 ]; diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 5efe245c935fc8..ddacc47dbd0391 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -42,6 +42,25 @@ const { PARTIAL_SYNCING_SUPPORTED_BLOCKS } = unlock( patternsPrivateApis ); const fullAlignments = [ 'full', 'wide', 'left', 'right' ]; +function getLegacyIdMap( blocks, content, nameCount = {} ) { + let idToClientIdMap = {}; + for ( const block of blocks ) { + if ( block?.innerBlocks?.length ) { + idToClientIdMap = { + ...idToClientIdMap, + ...getLegacyIdMap( block.innerBlocks, content, nameCount ), + }; + } + + const id = block.attributes.metadata?.id; + const clientId = block.clientId; + if ( id && content?.[ id ] ) { + idToClientIdMap[ clientId ] = id; + } + } + return idToClientIdMap; +} + const useInferredLayout = ( blocks, parentLayout ) => { const initialInferredAlignmentRef = useRef(); @@ -101,25 +120,31 @@ function getOverridableAttributes( block ) { function applyInitialContentValuesToInnerBlocks( blocks, content = {}, - defaultValues + defaultValues, + legacyIdMap ) { return blocks.map( ( block ) => { const innerBlocks = applyInitialContentValuesToInnerBlocks( block.innerBlocks, content, - defaultValues + defaultValues, + legacyIdMap ); - const blockId = block.attributes.metadata?.id; - if ( ! hasOverridableAttributes( block ) || ! blockId ) + const metadataName = + legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name; + + if ( ! metadataName || ! hasOverridableAttributes( block ) ) { return { ...block, innerBlocks }; + } + const attributes = getOverridableAttributes( block ); const newAttributes = { ...block.attributes }; for ( const attributeKey of attributes ) { - defaultValues[ blockId ] ??= {}; - defaultValues[ blockId ][ attributeKey ] = + defaultValues[ metadataName ] ??= {}; + defaultValues[ metadataName ][ attributeKey ] = block.attributes[ attributeKey ]; - const contentValues = content[ blockId ]?.values; + const contentValues = content[ metadataName ]; if ( contentValues?.[ attributeKey ] !== undefined ) { newAttributes[ attributeKey ] = contentValues[ attributeKey ]; } @@ -142,29 +167,40 @@ function isAttributeEqual( attribute1, attribute2 ) { return attribute1 === attribute2; } -function getContentValuesFromInnerBlocks( blocks, defaultValues ) { +function getContentValuesFromInnerBlocks( blocks, defaultValues, legacyIdMap ) { /** @type {Record}>} */ const content = {}; for ( const block of blocks ) { if ( block.name === patternBlockName ) continue; - Object.assign( - content, - getContentValuesFromInnerBlocks( block.innerBlocks, defaultValues ) - ); - const blockId = block.attributes.metadata?.id; - if ( ! hasOverridableAttributes( block ) || ! blockId ) continue; + if ( block.innerBlocks.length ) { + Object.assign( + content, + getContentValuesFromInnerBlocks( + block.innerBlocks, + defaultValues, + legacyIdMap + ) + ); + } + const metadataName = + legacyIdMap?.[ block.clientId ] ?? block.attributes.metadata?.name; + if ( ! metadataName || ! hasOverridableAttributes( block ) ) { + continue; + } + const attributes = getOverridableAttributes( block ); + for ( const attributeKey of attributes ) { if ( ! isAttributeEqual( block.attributes[ attributeKey ], - defaultValues[ blockId ][ attributeKey ] + defaultValues?.[ metadataName ]?.[ attributeKey ] ) ) { - content[ blockId ] ??= { values: {}, blockName: block.name }; + content[ metadataName ] ??= {}; // TODO: We need a way to represent `undefined` in the serialized overrides. // Also see: https://github.com/WordPress/gutenberg/pull/57249#discussion_r1452987871 - content[ blockId ].values[ attributeKey ] = + content[ metadataName ][ attributeKey ] = block.attributes[ attributeKey ] === undefined ? // TODO: We use an empty string to represent undefined for now until // we support a richer format for overrides and the block binding API. @@ -278,8 +314,15 @@ export default function ReusableBlockEdit( { [ editedRecord.blocks, editedRecord.content ] ); + const legacyIdMap = useRef( {} ); + // Apply the initial overrides from the pattern block to the inner blocks. useEffect( () => { + // Build a map of clientIds to the old nano id system to provide back compat. + legacyIdMap.current = getLegacyIdMap( + initialBlocks, + initialContent.current + ); defaultContent.current = {}; const originalEditingMode = getBlockEditingMode( patternClientId ); // Replace the contents of the blocks with the overrides. @@ -291,7 +334,8 @@ export default function ReusableBlockEdit( { applyInitialContentValuesToInnerBlocks( initialBlocks, initialContent.current, - defaultContent.current + defaultContent.current, + legacyIdMap.current ) ); } ); @@ -343,7 +387,8 @@ export default function ReusableBlockEdit( { setAttributes( { content: getContentValuesFromInnerBlocks( blocks, - defaultContent.current + defaultContent.current, + legacyIdMap.current ), } ); } ); diff --git a/packages/block-library/src/block/index.php b/packages/block-library/src/block/index.php index 4aabe98ffa4650..49b5786eacc79f 100644 --- a/packages/block-library/src/block/index.php +++ b/packages/block-library/src/block/index.php @@ -48,26 +48,35 @@ function render_block_core_block( $attributes ) { $content = $wp_embed->run_shortcode( $reusable_block->post_content ); $content = $wp_embed->autoembed( $content ); - // Back compat, the content attribute was previously named overrides and - // had a slightly different format. For blocks that have not been migrated, - // also convert the format here so that the provided `pattern/overrides` - // context is correct. - if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) { - $migrated_content = array(); - foreach ( $attributes['overrides'] as $id => $values ) { - $migrated_content[ $id ] = array( - 'values' => $values, - ); + // Back compat. + // For blocks that have not been migrated in the editor, add some back compat + // so that front-end rendering continues to work. + + // This matches the `v2` deprecation. Removes the inner `values` property + // from every item. + if ( isset( $attributes['content'] ) ) { + foreach ( $attributes['content'] as &$content_data ) { + if ( isset( $content_data['values'] ) ) { + $is_assoc_array = is_array( $content_data['values'] ) && ! wp_is_numeric_array( $content_data['values'] ); + + if ( $is_assoc_array ) { + $content_data = $content_data['values']; + } + } } - $attributes['content'] = $migrated_content; } - $has_pattern_overrides = isset( $attributes['content'] ); + + // This matches the `v1` deprecation. Rename `overrides` to `content`. + if ( isset( $attributes['overrides'] ) && ! isset( $attributes['content'] ) ) { + $attributes['content'] = $attributes['overrides']; + } /** * We set the `pattern/overrides` context through the `render_block_context` * filter so that it is available when a pattern's inner blocks are * rendering via do_blocks given it only receives the inner content. */ + $has_pattern_overrides = isset( $attributes['content'] ); if ( $has_pattern_overrides ) { $filter_block_context = static function ( $context ) use ( $attributes ) { $context['pattern/overrides'] = $attributes['content']; diff --git a/packages/editor/src/hooks/index.js b/packages/editor/src/hooks/index.js index 5a48ec1bf49566..75bb34abf6cfa3 100644 --- a/packages/editor/src/hooks/index.js +++ b/packages/editor/src/hooks/index.js @@ -3,4 +3,4 @@ */ import './custom-sources-backwards-compatibility'; import './default-autocompleters'; -import './pattern-partial-syncing'; +import './pattern-overrides'; diff --git a/packages/editor/src/hooks/pattern-partial-syncing.js b/packages/editor/src/hooks/pattern-overrides.js similarity index 81% rename from packages/editor/src/hooks/pattern-partial-syncing.js rename to packages/editor/src/hooks/pattern-overrides.js index f86268cb495463..442ce70a2bf71c 100644 --- a/packages/editor/src/hooks/pattern-partial-syncing.js +++ b/packages/editor/src/hooks/pattern-overrides.js @@ -14,7 +14,7 @@ import { store as editorStore } from '../store'; import { unlock } from '../lock-unlock'; const { - PartialSyncingControls, + useSetPatternBindings, ResetOverridesControl, PATTERN_TYPES, PARTIAL_SYNCING_SUPPORTED_BLOCKS, @@ -29,7 +29,7 @@ const { * * @return {Component} Wrapped component. */ -const withPartialSyncingControls = createHigherOrderComponent( +const withPatternOverrideControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const isSupportedBlock = Object.keys( PARTIAL_SYNCING_SUPPORTED_BLOCKS @@ -38,6 +38,7 @@ const withPartialSyncingControls = createHigherOrderComponent( return ( <> + { isSupportedBlock && } { props.isSelected && isSupportedBlock && ( ) } @@ -46,6 +47,15 @@ const withPartialSyncingControls = createHigherOrderComponent( } ); +function BindingUpdater( props ) { + const postType = useSelect( + ( select ) => select( editorStore ).getCurrentPostType(), + [] + ); + useSetPatternBindings( props, postType ); + return null; +} + // Split into a separate component to avoid a store subscription // on every block. function ControlsWithStoreSubscription( props ) { @@ -55,6 +65,7 @@ function ControlsWithStoreSubscription( props ) { select( editorStore ).getCurrentPostType() === PATTERN_TYPES.user, [] ); + const bindings = props.attributes.metadata?.bindings; const hasPatternBindings = !! bindings && @@ -62,19 +73,14 @@ function ControlsWithStoreSubscription( props ) { ( binding ) => binding.source === 'core/pattern-overrides' ); - const shouldShowPartialSyncingControls = - isEditingPattern && blockEditingMode === 'default'; const shouldShowResetOverridesControl = ! isEditingPattern && - !! props.attributes.metadata?.id && + !! props.attributes.metadata?.name && blockEditingMode !== 'disabled' && hasPatternBindings; return ( <> - { shouldShowPartialSyncingControls && ( - - ) } { shouldShowResetOverridesControl && ( ) } @@ -84,6 +90,6 @@ function ControlsWithStoreSubscription( props ) { addFilter( 'editor.BlockEdit', - 'core/editor/with-partial-syncing-controls', - withPartialSyncingControls + 'core/editor/with-pattern-override-controls', + withPatternOverrideControls ); diff --git a/packages/patterns/package.json b/packages/patterns/package.json index eaef15fe2a9327..f35a629508cb08 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -44,8 +44,7 @@ "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", "@wordpress/private-apis": "file:../private-apis", - "@wordpress/url": "file:../url", - "nanoid": "^3.3.4" + "@wordpress/url": "file:../url" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js deleted file mode 100644 index 7b3e5cb312e82e..00000000000000 --- a/packages/patterns/src/components/partial-syncing-controls.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * External dependencies - */ -import { nanoid } from 'nanoid'; - -/** - * WordPress dependencies - */ -import { InspectorControls } from '@wordpress/block-editor'; -import { BaseControl, CheckboxControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; - -function PartialSyncingControls( { name, attributes, setAttributes } ) { - const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; - const attributeSources = syncedAttributes.map( - ( attributeName ) => - attributes.metadata?.bindings?.[ attributeName ]?.source - ); - const isConnectedToOtherSources = attributeSources.every( - ( source ) => source && source !== 'core/pattern-overrides' - ); - - // Render nothing if all supported attributes are connected to other sources. - if ( isConnectedToOtherSources ) { - return null; - } - - function updateBindings( isChecked ) { - let updatedBindings = { - ...attributes?.metadata?.bindings, - }; - - if ( ! isChecked ) { - for ( const attributeName of syncedAttributes ) { - if ( - updatedBindings[ attributeName ]?.source === - 'core/pattern-overrides' - ) { - delete updatedBindings[ attributeName ]; - } - } - if ( ! Object.keys( updatedBindings ).length ) { - updatedBindings = undefined; - } - setAttributes( { - metadata: { - ...attributes.metadata, - bindings: updatedBindings, - }, - } ); - return; - } - - for ( const attributeName of syncedAttributes ) { - if ( ! updatedBindings[ attributeName ] ) { - updatedBindings[ attributeName ] = { - source: 'core/pattern-overrides', - }; - } - } - - if ( typeof attributes.metadata?.id === 'string' ) { - setAttributes( { - metadata: { - ...attributes.metadata, - bindings: updatedBindings, - }, - } ); - return; - } - - const id = nanoid( 6 ); - setAttributes( { - metadata: { - ...attributes.metadata, - id, - bindings: updatedBindings, - }, - } ); - } - - return ( - - - - { __( 'Pattern overrides' ) } - - source === 'core/pattern-overrides' - ) } - onChange={ ( isChecked ) => { - updateBindings( isChecked ); - } } - /> - - - ); -} - -export default PartialSyncingControls; diff --git a/packages/patterns/src/components/reset-overrides-control.js b/packages/patterns/src/components/reset-overrides-control.js index 586f4608352340..1d3ae013addd3d 100644 --- a/packages/patterns/src/components/reset-overrides-control.js +++ b/packages/patterns/src/components/reset-overrides-control.js @@ -11,13 +11,13 @@ import { store as coreStore } from '@wordpress/core-data'; import { parse } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; -function recursivelyFindBlockWithId( blocks, id ) { +function recursivelyFindBlockWithName( blocks, name ) { for ( const block of blocks ) { - if ( block.attributes.metadata?.id === id ) { + if ( block.attributes.metadata?.name === name ) { return block; } - const found = recursivelyFindBlockWithId( block.innerBlocks, id ); + const found = recursivelyFindBlockWithName( block.innerBlocks, name ); if ( found ) { return found; } @@ -26,10 +26,10 @@ function recursivelyFindBlockWithId( blocks, id ) { export default function ResetOverridesControl( props ) { const registry = useRegistry(); - const id = props.attributes.metadata?.id; + const name = props.attributes.metadata?.name; const patternWithOverrides = useSelect( ( select ) => { - if ( ! id ) { + if ( ! name ) { return undefined; } @@ -39,13 +39,13 @@ export default function ResetOverridesControl( props ) { getBlockParentsByBlockName( props.clientId, 'core/block' ) )[ 0 ]; - if ( ! patternBlock?.attributes.content?.[ id ] ) { + if ( ! patternBlock?.attributes.content?.[ name ] ) { return undefined; } return patternBlock; }, - [ props.clientId, id ] + [ props.clientId, name ] ); const resetOverrides = async () => { @@ -57,7 +57,7 @@ export default function ResetOverridesControl( props ) { patternWithOverrides.attributes.ref ); const blocks = editedRecord.blocks ?? parse( editedRecord.content ); - const block = recursivelyFindBlockWithId( blocks, id ); + const block = recursivelyFindBlockWithName( blocks, name ); const newAttributes = Object.assign( // Reset every existing attribute to undefined. diff --git a/packages/patterns/src/components/use-set-pattern-bindings.js b/packages/patterns/src/components/use-set-pattern-bindings.js new file mode 100644 index 00000000000000..df16d2b2b05916 --- /dev/null +++ b/packages/patterns/src/components/use-set-pattern-bindings.js @@ -0,0 +1,106 @@ +/** + * WordPress dependencies + */ +import { usePrevious } from '@wordpress/compose'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; + +function removeBindings( bindings, syncedAttributes ) { + let updatedBindings = {}; + for ( const attributeName of syncedAttributes ) { + // Omit any pattern override bindings from the `updatedBindings` object. + if ( + bindings?.[ attributeName ]?.source !== 'core/pattern-overrides' && + bindings?.[ attributeName ]?.source !== undefined + ) { + updatedBindings[ attributeName ] = bindings[ attributeName ]; + } + } + if ( ! Object.keys( updatedBindings ).length ) { + updatedBindings = undefined; + } + return updatedBindings; +} + +function addBindings( bindings, syncedAttributes ) { + const updatedBindings = { ...bindings }; + for ( const attributeName of syncedAttributes ) { + if ( ! bindings?.[ attributeName ] ) { + updatedBindings[ attributeName ] = { + source: 'core/pattern-overrides', + }; + } + } + return updatedBindings; +} + +export default function useSetPatternBindings( + { name, attributes, setAttributes }, + currentPostType +) { + const metadataName = attributes?.metadata?.name ?? ''; + const prevMetadataName = usePrevious( metadataName ) ?? ''; + const bindings = attributes?.metadata?.bindings; + + useEffect( () => { + // Bindings should only be created when editing a wp_block post type, + // and also when there's a change to the user-given name for the block. + if ( + currentPostType !== 'wp_block' || + metadataName === prevMetadataName + ) { + return; + } + + const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; + const attributeSources = syncedAttributes.map( + ( attributeName ) => + attributes.metadata?.bindings?.[ attributeName ]?.source + ); + const isConnectedToOtherSources = attributeSources.every( + ( source ) => source && source !== 'core/pattern-overrides' + ); + + // Avoid overwriting other (e.g. meta) bindings. + if ( isConnectedToOtherSources ) { + return; + } + + // The user-given name for the block was deleted, remove the bindings. + if ( ! metadataName?.length && prevMetadataName?.length ) { + const updatedBindings = removeBindings( + bindings, + syncedAttributes + ); + setAttributes( { + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, + } ); + } + + // The user-given name for the block was set, set the bindings. + if ( ! prevMetadataName?.length && metadataName.length ) { + const updatedBindings = addBindings( bindings, syncedAttributes ); + setAttributes( { + metadata: { + ...attributes.metadata, + bindings: updatedBindings, + }, + } ); + } + }, [ + bindings, + prevMetadataName, + metadataName, + currentPostType, + name, + attributes.metadata, + setAttributes, + ] ); +} diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index a5fbddb62fd62c..54ad5a4aa47d1b 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -13,7 +13,7 @@ import { import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; -import PartialSyncingControls from './components/partial-syncing-controls'; +import useSetPatternBindings from './components/use-set-pattern-bindings'; import ResetOverridesControl from './components/reset-overrides-control'; import { useAddPatternCategory } from './private-hooks'; import { @@ -34,7 +34,7 @@ lock( privateApis, { RenamePatternModal, PatternsMenuItems, RenamePatternCategoryModal, - PartialSyncingControls, + useSetPatternBindings, ResetOverridesControl, useAddPatternCategory, PATTERN_TYPES, diff --git a/test/e2e/specs/editor/various/pattern-overrides.spec.js b/test/e2e/specs/editor/various/pattern-overrides.spec.js index 86d7b9117bef53..4542e8c789ad15 100644 --- a/test/e2e/specs/editor/various/pattern-overrides.spec.js +++ b/test/e2e/specs/editor/various/pattern-overrides.spec.js @@ -32,7 +32,7 @@ test.describe( 'Pattern Overrides', () => { editor, } ) => { let patternId; - let editableParagraphId; + const editableParagraphName = 'Editable Paragraph'; await test.step( 'Create a synced pattern and assign blocks to allow overrides', async () => { await admin.visitSiteEditor( { path: '/patterns' } ); @@ -85,8 +85,8 @@ test.describe( 'Pattern Overrides', () => { await advancedPanel.click(); } await editorSettings - .getByRole( 'checkbox', { name: 'Allow instance overrides' } ) - .setChecked( true ); + .getByRole( 'textbox', { name: 'Block Name' } ) + .fill( editableParagraphName ); await expect.poll( editor.getBlocks ).toMatchObject( [ { @@ -94,7 +94,7 @@ test.describe( 'Pattern Overrides', () => { attributes: { content: 'This paragraph can be edited', metadata: { - id: expect.any( String ), + name: editableParagraphName, bindings: { content: { source: 'core/pattern-overrides', @@ -123,8 +123,6 @@ test.describe( 'Pattern Overrides', () => { ).toBeVisible(); patternId = new URL( page.url() ).searchParams.get( 'postId' ); - const blocks = await editor.getBlocks(); - editableParagraphId = blocks[ 0 ].attributes.metadata.id; } ); await test.step( 'Create a post and insert the pattern with overrides', async () => { @@ -176,10 +174,8 @@ test.describe( 'Pattern Overrides', () => { attributes: { ref: patternId, content: { - [ editableParagraphId ]: { - values: { - content: 'I would word it this way', - }, + [ editableParagraphName ]: { + content: 'I would word it this way', }, }, }, @@ -189,10 +185,8 @@ test.describe( 'Pattern Overrides', () => { attributes: { ref: patternId, content: { - [ editableParagraphId ]: { - values: { - content: 'This one is different', - }, + [ editableParagraphName ]: { + content: 'This one is different', }, }, }, @@ -276,11 +270,11 @@ test.describe( 'Pattern Overrides', () => { editor, context, } ) => { - const buttonId = 'button-id'; + const buttonName = 'Editable button'; const { id } = await requestUtils.createBlock( { title: 'Button with target', content: ` -
+ `, @@ -386,21 +380,21 @@ test.describe( 'Pattern Overrides', () => { requestUtils, editor, } ) => { - const paragraphId = 'paragraph-id'; - const headingId = 'heading-id'; + const paragraphName = 'Editable paragraph'; + const headingName = 'Editable heading'; const innerPattern = await requestUtils.createBlock( { title: 'Inner Pattern', - content: ` + content: `

Inner paragraph

`, status: 'publish', } ); const outerPattern = await requestUtils.createBlock( { title: 'Outer Pattern', - content: ` + content: `

Outer heading

-`, +`, status: 'publish', } ); @@ -425,8 +419,8 @@ test.describe( 'Pattern Overrides', () => { attributes: { ref: outerPattern.id, content: { - [ headingId ]: { - values: { content: 'Outer heading (edited)' }, + [ headingName ]: { + content: 'Outer heading (edited)', }, }, }, @@ -440,10 +434,8 @@ test.describe( 'Pattern Overrides', () => { attributes: { ref: innerPattern.id, content: { - [ paragraphId ]: { - values: { - content: 'Inner paragraph (edited)', - }, + [ paragraphName ]: { + content: 'Inner paragraph (edited)', }, }, }, @@ -505,14 +497,14 @@ test.describe( 'Pattern Overrides', () => { requestUtils, editor, } ) => { - const headingId = 'heading-id'; - const paragraphId = 'paragraph-id'; + const headingName = 'Editable heading'; + const paragraphName = 'Editable paragraph'; const { id } = await requestUtils.createBlock( { title: 'Pattern', - content: ` + content: `

Heading

- +

Paragraph

`, status: 'publish', @@ -597,14 +589,14 @@ test.describe( 'Pattern Overrides', () => { requestUtils, editor, } ) => { - const imageId = 'image-id'; + const imageName = 'Editable image'; const TEST_IMAGE_FILE_PATH = path.resolve( __dirname, '../../../assets/10x10_e2e_test_image_z9T8jK.png' ); const { id } = await requestUtils.createBlock( { title: 'Pattern', - content: ` + content: `
`, status: 'publish', diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.html b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.html new file mode 100644 index 00000000000000..5b7b2cdf95cfb8 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.json b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.json new file mode 100644 index 00000000000000..3f0292c83ac35e --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.json @@ -0,0 +1,15 @@ +[ + { + "name": "core/block", + "isValid": true, + "attributes": { + "ref": 123, + "content": { + "V98q_x": { + "content": "Some value" + } + } + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.parsed.json b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.parsed.json new file mode 100644 index 00000000000000..2cf2ac37974450 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.parsed.json @@ -0,0 +1,16 @@ +[ + { + "blockName": "core/block", + "attrs": { + "ref": 123, + "overrides": { + "V98q_x": { + "content": "Some value" + } + } + }, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.serialized.html b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.serialized.html new file mode 100644 index 00000000000000..3d91f47859d018 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-1.serialized.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.html b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.html new file mode 100644 index 00000000000000..d75c773c630efd --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.json b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.json new file mode 100644 index 00000000000000..3f0292c83ac35e --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.json @@ -0,0 +1,15 @@ +[ + { + "name": "core/block", + "isValid": true, + "attributes": { + "ref": 123, + "content": { + "V98q_x": { + "content": "Some value" + } + } + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.parsed.json b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.parsed.json new file mode 100644 index 00000000000000..41cd1dc9afd443 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/block", + "attrs": { + "ref": 123, + "content": { + "V98q_x": { + "values": { + "content": "Some value" + } + } + } + }, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.serialized.html b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.serialized.html new file mode 100644 index 00000000000000..3d91f47859d018 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides__deprecated-2.serialized.html @@ -0,0 +1 @@ + From 1b5360cb2afa95ce54f66ca8c540dd60b22a9a11 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Fri, 1 Mar 2024 19:55:23 +1100 Subject: [PATCH 30/45] Site Logo: Update url for site icon settings with fallback for WP core versions earlier than 6.5 (#59485) * Site Logo: Update url for site icon settings with fallback for WP core versions earlier than 6.5 * Move PHP changes to compat.php file Co-authored-by: andrewserong Co-authored-by: ramonjd Co-authored-by: tellthemachines Co-authored-by: t-hamano Co-authored-by: Mamaduka Co-authored-by: carolinan Co-authored-by: richtabor Co-authored-by: aaronjorbin --- lib/compat/wordpress-6.5/compat.php | 15 +++++++++++++++ packages/block-library/src/site-logo/edit.js | 13 +++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/compat/wordpress-6.5/compat.php b/lib/compat/wordpress-6.5/compat.php index 78447927125894..39edaef83e5cc8 100644 --- a/lib/compat/wordpress-6.5/compat.php +++ b/lib/compat/wordpress-6.5/compat.php @@ -36,3 +36,18 @@ function array_is_list( $arr ) { return true; } } + +/** + * Sets a global JS variable used to flag whether to direct the Site Logo block's admin urls + * to the Customizer. This allows Gutenberg running on versions of WordPress < 6.5.0 to + * support the previous location for the Site Icon settings. This function should not be + * backported to core, and should be removed when the required WP core version for Gutenberg + * is >= 6.5.0. + */ +function gutenberg_add_use_customizer_site_logo_url_flag() { + if ( ! is_wp_version_compatible( '6.5' ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalUseCustomizerSiteLogoUrl = true', 'before' ); + } +} + +add_action( 'admin_init', 'gutenberg_add_use_customizer_site_logo_url_flag' ); diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index fe4146406ddf60..079811f0aae958 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -268,6 +268,14 @@ const SiteLogo = ( { ); + // Support the previous location for the Site Icon settings. To be removed + // when the required WP core version for Gutenberg is >= 6.5.0. + const shouldUseNewUrl = ! window?.__experimentalUseCustomizerSiteLogoUrl; + + const siteIconSettingsUrl = shouldUseNewUrl + ? siteUrl + '/wp-admin/options-general.php' + : siteUrl + '/wp-admin/customize.php?autofocus[section]=title_tagline'; + const syncSiteIconHelpText = createInterpolateElement( __( 'Site Icons are what you see in browser tabs, bookmark bars, and within the WordPress mobile apps. To use a custom icon that is different from your site logo, use the Site Icon settings.' @@ -276,10 +284,7 @@ const SiteLogo = ( { a: ( // eslint-disable-next-line jsx-a11y/anchor-has-content From 44b5cd5eb5974fdb5b9ad6e3212ac86d2f81e825 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Fri, 1 Mar 2024 16:59:04 +0800 Subject: [PATCH 31/45] Add a fixture for the wp/block pattern block current version with overrides (#59492) Co-authored-by: aaronrobertshaw Co-authored-by: talldan --- .../fixtures/blocks/core__block__overrides.html | 1 + .../fixtures/blocks/core__block__overrides.json | 15 +++++++++++++++ .../blocks/core__block__overrides.parsed.json | 16 ++++++++++++++++ .../core__block__overrides.serialized.html | 1 + 4 files changed, 33 insertions(+) create mode 100644 test/integration/fixtures/blocks/core__block__overrides.html create mode 100644 test/integration/fixtures/blocks/core__block__overrides.json create mode 100644 test/integration/fixtures/blocks/core__block__overrides.parsed.json create mode 100644 test/integration/fixtures/blocks/core__block__overrides.serialized.html diff --git a/test/integration/fixtures/blocks/core__block__overrides.html b/test/integration/fixtures/blocks/core__block__overrides.html new file mode 100644 index 00000000000000..0fd835b5013a82 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__block__overrides.json b/test/integration/fixtures/blocks/core__block__overrides.json new file mode 100644 index 00000000000000..7d72f03e00072b --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides.json @@ -0,0 +1,15 @@ +[ + { + "name": "core/block", + "isValid": true, + "attributes": { + "ref": 123, + "content": { + "Nice paragraph": { + "content": "Some value" + } + } + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__block__overrides.parsed.json b/test/integration/fixtures/blocks/core__block__overrides.parsed.json new file mode 100644 index 00000000000000..ce5a0ab732c7f1 --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides.parsed.json @@ -0,0 +1,16 @@ +[ + { + "blockName": "core/block", + "attrs": { + "ref": 123, + "content": { + "Nice paragraph": { + "content": "Some value" + } + } + }, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__block__overrides.serialized.html b/test/integration/fixtures/blocks/core__block__overrides.serialized.html new file mode 100644 index 00000000000000..5c8716afd6a2ad --- /dev/null +++ b/test/integration/fixtures/blocks/core__block__overrides.serialized.html @@ -0,0 +1 @@ + From 19792094dc8befa7bdc4226126b04ac34aec6e6a Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:01:46 +0200 Subject: [PATCH 32/45] Rich text: fix typing into empty flex element (#59473) Co-authored-by: ellatrix Co-authored-by: fabiankaegy Co-authored-by: annezazu Co-authored-by: colorful-tones Co-authored-by: youknowriad Co-authored-by: t-hamano Co-authored-by: Mgrmjp --- packages/rich-text/src/create.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index fce6d5a55b2027..3629ff8c60a749 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -472,11 +472,9 @@ function createFromElement( { element, range, isEditableTree } ) { if ( isEditableTree && - // Ignore any placeholders. - ( node.getAttribute( 'data-rich-text-placeholder' ) || - // Ignore any line breaks that are not inserted by us. - ( tagName === 'br' && - ! node.getAttribute( 'data-rich-text-line-break' ) ) ) + // Ignore any line breaks that are not inserted by us. + tagName === 'br' && + ! node.getAttribute( 'data-rich-text-line-break' ) ) { accumulateSelection( accumulator, node, range, createEmptyValue() ); continue; @@ -541,7 +539,9 @@ function createFromElement( { element, range, isEditableTree } ) { accumulateSelection( accumulator, node, range, value ); - if ( ! format ) { + // Ignore any placeholders, but keep their content since the browser + // might insert text inside them when the editable element is flex. + if ( ! format || node.getAttribute( 'data-rich-text-placeholder' ) ) { mergePair( accumulator, value ); } else if ( value.text.length === 0 ) { if ( format.attributes ) { From 2e074d927f6460c30df60f137fbab16b7f8e37a9 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Fri, 1 Mar 2024 06:23:05 -0500 Subject: [PATCH 33/45] docs: Fix inaccurate release group (#59475) The relocated entry was not included in the 1.114.0 release. Presumably it was misplaced during an erroneous automatic Git merge resolution. --- packages/react-native-editor/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index ea60a7d531b2fd..8fa342bb1b311c 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,12 +10,12 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [*] Improve consistency of the block outline indicating the currently selected block [#59415] ## 1.114.0 - [*] Prevent crash when autoscrolling to blocks [#59110] - [*] Remove opacity change when images are being uploaded [#59264] - [*] Media & Text blocks correctly show an error message when the attached video upload fails [#59288] -- [*] Improve consistency of the block outline indicating the currently selected block [#59415] ## 1.112.0 - [*] [internal] Upgrade React Native to version 0.71.15 [#57667] From 8c77df8c1052015f26b57d65fe5f043cd4f774ac Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 1 Mar 2024 20:42:50 +0900 Subject: [PATCH 34/45] Tweak: Sidebar categories panel (#59495) Co-authored-by: t-hamano Co-authored-by: jameskoster --- .../components/post-taxonomies/hierarchical-term-selector.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js index c93458ed3cfef2..79039d0a5d0d25 100644 --- a/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js +++ b/packages/editor/src/components/post-taxonomies/hierarchical-term-selector.js @@ -439,6 +439,7 @@ export function HierarchicalTermSelector( { slug } ) {
{ !! availableTerms.length && (