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