diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 15ebf70da93a82..9a928e30227f8f 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -7,6 +7,7 @@ import { __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -92,8 +93,27 @@ function useHasTextDecorationControl( settings ) { return settings?.typography?.textDecoration; } -function TypographyToolsPanel( { ...props } ) { - return ; +function TypographyToolsPanel( { + resetAllFilter, + onChange, + value, + panelId, + children, +} ) { + const resetAll = () => { + const updatedValue = resetAllFilter( value ); + onChange( updatedValue ); + }; + + return ( + + { children } + + ); } const DEFAULT_CONTROLS = { @@ -260,15 +280,20 @@ export default function TypographyPanel( { const hasTextDecoration = () => !! value?.typography?.textDecoration; const resetTextDecoration = () => setTextDecoration( undefined ); - const resetAll = () => { - onChange( { - ...value, + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, typography: {}, - } ); - }; + }; + }, [] ); return ( - + { hasFontFamilyEnabled && ( { ( fillProps ) => { - // Children passed to InspectorControlsFill will not have - // access to any React Context whose Provider is part of - // the InspectorControlsSlot tree. So we re-create the - // Provider in this subtree. - const value = ! isEmpty( fillProps ) ? fillProps : null; return ( - - { children } - + ); } } ); } + +function ToolsPanelInspectorControl( { children, resetAllFilter, fillProps } ) { + const { registerResetAllFilter, deregisterResetAllFilter } = fillProps; + useEffect( () => { + if ( resetAllFilter && registerResetAllFilter ) { + registerResetAllFilter( resetAllFilter ); + } + return () => { + if ( resetAllFilter && deregisterResetAllFilter ) { + deregisterResetAllFilter( resetAllFilter ); + } + }; + }, [ resetAllFilter, registerResetAllFilter, deregisterResetAllFilter ] ); + + // Children passed to InspectorControlsFill will not have + // access to any React Context whose Provider is part of + // the InspectorControlsSlot tree. So we re-create the + // Provider in this subtree. + const value = ! isEmpty( fillProps ) ? fillProps : null; + return ( + + { children } + + ); +} diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index a5aac0cb2d3127..3cb949b6471152 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; -import { useMemo } from '@wordpress/element'; +import { useMemo, useCallback } from '@wordpress/element'; /** * Internal dependencies @@ -46,9 +46,64 @@ export const TYPOGRAPHY_SUPPORT_KEYS = [ LETTER_SPACING_SUPPORT_KEY, ]; -function TypographyInspectorControl( { children } ) { +function styleToAttributes( style ) { + const updatedStyle = { ...omit( style, [ 'fontFamily' ] ) }; + const fontSizeValue = style?.typography?.fontSize; + const fontFamilyValue = style?.typography?.fontFamily; + const fontSizeSlug = fontSizeValue?.startsWith( 'var:preset|font-size|' ) + ? fontSizeValue.substring( 'var:preset|font-size|'.length ) + : undefined; + const fontFamilySlug = fontFamilyValue?.startsWith( + 'var:preset|font-family|' + ) + ? fontFamilyValue.substring( 'var:preset|font-family|'.length ) + : undefined; + updatedStyle.typography = { + ...omit( updatedStyle.typography, [ 'fontFamily' ] ), + fontSize: fontSizeSlug ? undefined : fontSizeValue, + }; + return { + style: cleanEmptyObject( updatedStyle ), + fontFamily: fontFamilySlug, + fontSize: fontSizeSlug, + }; +} + +function attributesToStyle( attributes ) { + return { + ...attributes.style, + typography: { + ...attributes.style?.typography, + fontFamily: attributes.fontFamily + ? 'var:preset|font-family|' + attributes.fontFamily + : undefined, + fontSize: attributes.fontSize + ? 'var:preset|font-size|' + attributes.fontSize + : attributes.style?.typography?.fontSize, + }, + }; +} + +function TypographyInspectorControl( { children, resetAllFilter } ) { + const attributesResetAllFilter = useCallback( + ( attributes ) => { + const existingStyle = attributesToStyle( attributes ); + const updatedStyle = resetAllFilter( existingStyle ); + return { + ...attributes, + ...styleToAttributes( updatedStyle ), + }; + }, + [ resetAllFilter ] + ); + return ( - { children } + + { children } + ); } @@ -106,43 +161,15 @@ export function TypographyPanel( { const settings = useBlockSettings( name ); const isEnabled = useHasTypographyPanel( settings ); const value = useMemo( () => { - return { - ...attributes.style, - typography: { - ...attributes.style?.typography, - fontFamily: attributes.fontFamily - ? 'var:preset|font-family|' + attributes.fontFamily - : undefined, - fontSize: attributes.fontSize - ? 'var:preset|font-size|' + attributes.fontSize - : attributes.style?.typography?.fontSize, - }, - }; + return attributesToStyle( { + style: attributes.style, + fontFamily: attributes.fontFamily, + fontSize: attributes.fontSize, + } ); }, [ attributes.style, attributes.fontSize, attributes.fontFamily ] ); const onChange = ( newStyle ) => { - const updatedStyle = { ...omit( newStyle, [ 'fontFamily' ] ) }; - const fontSizeValue = newStyle?.typography?.fontSize; - const fontFamilyValue = newStyle?.typography?.fontFamily; - const fontSizeSlug = fontSizeValue?.startsWith( - 'var:preset|font-size|' - ) - ? fontSizeValue.substring( 'var:preset|font-size|'.length ) - : undefined; - const fontFamilySlug = fontFamilyValue?.startsWith( - 'var:preset|font-family|' - ) - ? fontFamilyValue.substring( 'var:preset|font-family|'.length ) - : undefined; - updatedStyle.typography = { - ...omit( updatedStyle.typography, [ 'fontFamily' ] ), - fontSize: fontSizeSlug ? undefined : fontSizeValue, - }; - setAttributes( { - style: cleanEmptyObject( updatedStyle ), - fontFamily: fontFamilySlug, - fontSize: fontSizeSlug, - } ); + setAttributes( styleToAttributes( newStyle ) ); }; if ( ! isEnabled ) { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index a180b283a78ff2..450c381a83c521 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- `ToolsPanel`: Separate reset all filter registration from items registration and support global resets ([#48123](https://github.com/WordPress/gutenberg/pull/48123#pullrequestreview-1308386926)). + ## 23.4.0 (2023-02-15) ### Bug Fix diff --git a/packages/components/src/tools-panel/context.ts b/packages/components/src/tools-panel/context.ts index 7b13e59a92acd9..d39c49c04ef203 100644 --- a/packages/components/src/tools-panel/context.ts +++ b/packages/components/src/tools-panel/context.ts @@ -18,6 +18,8 @@ export const ToolsPanelContext = createContext< ToolsPanelContextType >( { registerPanelItem: noop, deregisterPanelItem: noop, flagItemCustomization: noop, + registerResetAllFilter: noop, + deregisterResetAllFilter: noop, areAllOptionalControlsHidden: true, } ); diff --git a/packages/components/src/tools-panel/test/index.tsx b/packages/components/src/tools-panel/test/index.tsx index bc91691f9c7d87..2a4fdbdf0aa96e 100644 --- a/packages/components/src/tools-panel/test/index.tsx +++ b/packages/components/src/tools-panel/test/index.tsx @@ -9,7 +9,10 @@ import userEvent from '@testing-library/user-event'; */ import { ToolsPanel, ToolsPanelContext, ToolsPanelItem } from '../'; import { createSlotFill, Provider as SlotFillProvider } from '../../slot-fill'; -import type { ToolsPanelContext as ToolsPanelContextType } from '../types'; +import type { + ToolsPanelContext as ToolsPanelContextType, + ResetAllFilter, +} from '../types'; const { Fill: ToolsPanelItems, Slot } = createSlotFill( 'ToolsPanelSlot' ); const resetAll = jest.fn(); @@ -104,6 +107,8 @@ const panelContext: ToolsPanelContextType = { shouldRenderPlaceholderItems: false, registerPanelItem: jest.fn(), deregisterPanelItem: jest.fn(), + registerResetAllFilter: jest.fn(), + deregisterResetAllFilter: jest.fn(), flagItemCustomization: noop, areAllOptionalControlsHidden: true, }; @@ -971,6 +976,8 @@ describe( 'ToolsPanel', () => { shouldRenderPlaceholderItems: false, registerPanelItem: noop, deregisterPanelItem: noop, + registerResetAllFilter: noop, + deregisterResetAllFilter: noop, flagItemCustomization: noop, areAllOptionalControlsHidden: true, }; @@ -1142,6 +1149,69 @@ describe( 'ToolsPanel', () => { // The dropdown toggle no longer has a description. expect( optionsDisplayedIcon ).not.toHaveAccessibleDescription(); } ); + + it( 'should not call reset all for different panelIds', async () => { + const resetItem = jest.fn(); + const resetItemB = jest.fn(); + + const children = ( + <> + true } + panelId="a" + resetAllFilter={ resetItem } + isShownByDefault + > +
Example control
+
+ true } + panelId="b" + resetAllFilter={ resetItemB } + isShownByDefault + > +
Alt control
+
+ + ); + + const resetAllCallback = ( + filters: ResetAllFilter[] | undefined + ) => filters?.forEach( ( f ) => f() ); + + const { rerender } = render( + + { children } + + ); + + await openDropdownMenu(); + await selectMenuItem( 'Reset all' ); + expect( resetItem ).toHaveBeenCalled(); + expect( resetItemB ).not.toHaveBeenCalled(); + + resetItem.mockClear(); + + rerender( + + { children } + + ); + + await selectMenuItem( 'Reset all' ); + expect( resetItem ).not.toHaveBeenCalled(); + expect( resetItemB ).toHaveBeenCalled(); + } ); } ); describe( 'reset all button', () => { diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.ts b/packages/components/src/tools-panel/tools-panel-item/hook.ts index 84572fba451a5e..9acee8ee1d52c0 100644 --- a/packages/components/src/tools-panel/tools-panel-item/hook.ts +++ b/packages/components/src/tools-panel/tools-panel-item/hook.ts @@ -33,6 +33,8 @@ export function useToolsPanelItem( const { panelId: currentPanelId, menuItems, + registerResetAllFilter, + deregisterResetAllFilter, registerPanelItem, deregisterPanelItem, flagItemCustomization, @@ -62,7 +64,6 @@ export function useToolsPanelItem( hasValue: hasValueCallback, isShownByDefault, label, - resetAllFilter: resetAllFilterCallback, panelId, } ); } @@ -83,11 +84,26 @@ export function useToolsPanelItem( hasValueCallback, panelId, previousPanelId, - resetAllFilterCallback, registerPanelItem, deregisterPanelItem, ] ); + useEffect( () => { + if ( hasMatchingPanel ) { + registerResetAllFilter( resetAllFilterCallback ); + } + return () => { + if ( hasMatchingPanel ) { + deregisterResetAllFilter( resetAllFilterCallback ); + } + }; + }, [ + registerResetAllFilter, + deregisterResetAllFilter, + resetAllFilterCallback, + hasMatchingPanel, + ] ); + // Note: `label` is used as a key when building menu item state in // `ToolsPanel`. const menuGroup = isShownByDefault ? 'default' : 'optional'; diff --git a/packages/components/src/tools-panel/tools-panel/hook.ts b/packages/components/src/tools-panel/tools-panel/hook.ts index 66b91f0ee671f2..65e1e7bd761b45 100644 --- a/packages/components/src/tools-panel/tools-panel/hook.ts +++ b/packages/components/src/tools-panel/tools-panel/hook.ts @@ -21,6 +21,7 @@ import type { ToolsPanelMenuItems, ToolsPanelMenuItemsConfig, ToolsPanelProps, + ResetAllFilter, } from '../types'; const DEFAULT_COLUMNS = 2; @@ -81,6 +82,9 @@ export function useToolsPanel( // Allow panel items to register themselves. const [ panelItems, setPanelItems ] = useState< ToolsPanelItem[] >( [] ); + const [ resetAllFilters, setResetAllFilters ] = useState< + ResetAllFilter[] + >( [] ); const registerPanelItem = useCallback( ( item: ToolsPanelItem ) => { @@ -123,6 +127,26 @@ export function useToolsPanel( [ setPanelItems ] ); + const registerResetAllFilter = useCallback( + ( newFilter: ResetAllFilter ) => { + setResetAllFilters( ( filters ) => { + return [ ...filters, newFilter ]; + } ); + }, + [ setResetAllFilters ] + ); + + const deregisterResetAllFilter = useCallback( + ( filterToRemove: ResetAllFilter ) => { + setResetAllFilters( ( filters ) => { + return filters.filter( + ( filter ) => filter !== filterToRemove + ); + } ); + }, + [ setResetAllFilters ] + ); + // Manage and share display state of menu items representing child controls. const [ menuItems, setMenuItems ] = useState< ToolsPanelMenuItems >( { default: {}, @@ -237,16 +261,7 @@ export function useToolsPanel( const resetAllItems = useCallback( () => { if ( typeof resetAll === 'function' ) { isResetting.current = true; - - // Collect available reset filters from panel items. - const filters: Array< () => void > = []; - panelItems.forEach( ( item ) => { - if ( item.resetAllFilter ) { - filters.push( item.resetAllFilter ); - } - } ); - - resetAll( filters ); + resetAll( resetAllFilters ); } // Turn off display of all non-default items. @@ -255,7 +270,7 @@ export function useToolsPanel( shouldReset: true, } ); setMenuItems( resetMenuItems ); - }, [ panelItems, resetAll, setMenuItems ] ); + }, [ panelItems, resetAllFilters, resetAll, setMenuItems ] ); // Assist ItemGroup styling when there are potentially hidden placeholder // items by identifying first & last items that are toggled on for display. @@ -277,6 +292,7 @@ export function useToolsPanel( () => ( { areAllOptionalControlsHidden, deregisterPanelItem, + deregisterResetAllFilter, firstDisplayedItem, flagItemCustomization, hasMenuItems: !! panelItems.length, @@ -285,6 +301,7 @@ export function useToolsPanel( menuItems, panelId, registerPanelItem, + registerResetAllFilter, shouldRenderPlaceholderItems, __experimentalFirstVisibleItemClass, __experimentalLastVisibleItemClass, @@ -292,12 +309,14 @@ export function useToolsPanel( [ areAllOptionalControlsHidden, deregisterPanelItem, + deregisterResetAllFilter, firstDisplayedItem, flagItemCustomization, lastDisplayedItem, menuItems, panelId, panelItems, + registerResetAllFilter, registerPanelItem, shouldRenderPlaceholderItems, __experimentalFirstVisibleItemClass, diff --git a/packages/components/src/tools-panel/types.ts b/packages/components/src/tools-panel/types.ts index 86a8c1579b9b55..2657175caad7a5 100644 --- a/packages/components/src/tools-panel/types.ts +++ b/packages/components/src/tools-panel/types.ts @@ -8,7 +8,7 @@ import type { ReactNode } from 'react'; */ import type { HeadingSize } from '../heading/types'; -type ResetAllFilter = ( attributes?: any ) => any; +export type ResetAllFilter = ( attributes?: any ) => any; type ResetAll = ( filters?: ResetAllFilter[] ) => void; export type ToolsPanelProps = { @@ -122,14 +122,6 @@ export type ToolsPanelItem = { * from a shared source. */ panelId?: string | null; - /** - * A `ToolsPanel` will collect each item's `resetAllFilter` and pass an - * array of these functions through to the panel's `resetAll` callback. They - * can then be iterated over to perform additional tasks. - * - * @default noop - */ - resetAllFilter?: ResetAllFilter; }; export type ToolsPanelItemProps = ToolsPanelItem & { @@ -147,6 +139,15 @@ export type ToolsPanelItemProps = ToolsPanelItem & { * menu. */ onSelect?: () => void; + + /** + * A `ToolsPanel` will collect each item's `resetAllFilter` and pass an + * array of these functions through to the panel's `resetAll` callback. They + * can then be iterated over to perform additional tasks. + * + * @default noop + */ + resetAllFilter?: ResetAllFilter; }; export type ToolsPanelMenuItemKey = 'default' | 'optional'; @@ -161,6 +162,8 @@ export type ToolsPanelContext = { hasMenuItems: boolean; registerPanelItem: ( item: ToolsPanelItem ) => void; deregisterPanelItem: ( label: string ) => void; + registerResetAllFilter: ( filter: ResetAllFilter ) => void; + deregisterResetAllFilter: ( filter: ResetAllFilter ) => void; flagItemCustomization: ( label: string, group?: ToolsPanelMenuItemKey