Skip to content

Commit

Permalink
useToolsPanel: calculate menuItems in layout effect to avoid painting…
Browse files Browse the repository at this point in the history
… intermediate state (#65494)

* useToolsPanel: calculate menuItems in layout effect to avoid painting intermediate state

* useToolsPanel: remove setState deps, calculate derived values in layout effects

* ToolsPanelItem: also use layout effect to prevent loops

* Changelog entry

Co-authored-by: jsnajdr <jsnajdr@git.wordpress.org>
Co-authored-by: ciampo <mciampini@git.wordpress.org>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent 1bbe4c4 commit 5e37a13
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 80 deletions.
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Bug Fixes

- `ToolsPanel`: avoid paining intermediate unfinished states ([#65494](https://github.com/WordPress/gutenberg/pull/65494)).

## 28.8.0 (2024-09-19)

### Bug Fixes
Expand Down
13 changes: 4 additions & 9 deletions packages/components/src/tools-panel/tools-panel-item/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@
* WordPress dependencies
*/
import { usePrevious } from '@wordpress/compose';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
} from '@wordpress/element';
import { useCallback, useLayoutEffect, useMemo } from '@wordpress/element';

/**
* Internal dependencies
Expand Down Expand Up @@ -101,7 +96,7 @@ export function useToolsPanelItem(
deregisterPanelItem,
] );

useEffect( () => {
useLayoutEffect( () => {
if ( hasMatchingPanel ) {
registerResetAllFilter( resetAllFilterCallback );
}
Expand All @@ -127,7 +122,7 @@ export function useToolsPanelItem(
const isValueSet = hasValue();
// Notify the panel when an item's value has changed except for optional
// items without value because the item should not cause itself to hide.
useEffect( () => {
useLayoutEffect( () => {
if ( ! isShownByDefault && ! isValueSet ) {
return;
}
Expand All @@ -143,7 +138,7 @@ export function useToolsPanelItem(

// Determine if the panel item's corresponding menu is being toggled and
// trigger appropriate callback if it is.
useEffect( () => {
useLayoutEffect( () => {
// We check whether this item is currently registered as items rendered
// via fills can persist through the parent panel being remounted.
// See: https://github.com/WordPress/gutenberg/pull/45673
Expand Down
131 changes: 60 additions & 71 deletions packages/components/src/tools-panel/tools-panel/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
Expand Down Expand Up @@ -101,7 +101,7 @@ export function useToolsPanel(
// the resetAll task. Without this, the flag is cleared after the first
// control updates and forces a rerender with subsequent controls then
// believing they need to reset, unfortunately using stale data.
useEffect( () => {
useLayoutEffect( () => {
if ( wasResetting ) {
isResettingRef.current = false;
}
Expand All @@ -114,76 +114,66 @@ export function useToolsPanel(
ResetAllFilter[]
>( [] );

const registerPanelItem = useCallback(
( item: ToolsPanelItem ) => {
// Add item to panel items.
setPanelItems( ( items ) => {
const newItems = [ ...items ];
// If an item with this label has already been registered, remove it
// first. This can happen when an item is moved between the default
// and optional groups.
const existingIndex = newItems.findIndex(
( oldItem ) => oldItem.label === item.label
);
if ( existingIndex !== -1 ) {
newItems.splice( existingIndex, 1 );
}
return [ ...newItems, item ];
} );
const registerPanelItem = useCallback( ( item: ToolsPanelItem ) => {
// Add item to panel items.
setPanelItems( ( items ) => {
const newItems = [ ...items ];
// If an item with this label has already been registered, remove it
// first. This can happen when an item is moved between the default
// and optional groups.
const existingIndex = newItems.findIndex(
( oldItem ) => oldItem.label === item.label
);
if ( existingIndex !== -1 ) {
newItems.splice( existingIndex, 1 );
}
return [ ...newItems, item ];
} );

// Track the initial order of item registration. This is used for
// maintaining menu item order later.
setMenuItemOrder( ( items ) => {
if ( items.includes( item.label ) ) {
return items;
}
// Track the initial order of item registration. This is used for
// maintaining menu item order later.
setMenuItemOrder( ( items ) => {
if ( items.includes( item.label ) ) {
return items;
}

return [ ...items, item.label ];
} );
},
[ setPanelItems, setMenuItemOrder ]
);
return [ ...items, item.label ];
} );
}, [] );

// Panels need to deregister on unmount to avoid orphans in menu state.
// This is an issue when panel items are being injected via SlotFills.
const deregisterPanelItem = useCallback(
( label: string ) => {
// When switching selections between components injecting matching
// controls, e.g. both panels have a "padding" control, the
// deregistration of the first panel doesn't occur until after the
// registration of the next.
setPanelItems( ( items ) => {
const newItems = [ ...items ];
const index = newItems.findIndex(
( item ) => item.label === label
);
if ( index !== -1 ) {
newItems.splice( index, 1 );
}
return newItems;
} );
},
[ setPanelItems ]
);
const deregisterPanelItem = useCallback( ( label: string ) => {
// When switching selections between components injecting matching
// controls, e.g. both panels have a "padding" control, the
// deregistration of the first panel doesn't occur until after the
// registration of the next.
setPanelItems( ( items ) => {
const newItems = [ ...items ];
const index = newItems.findIndex(
( item ) => item.label === label
);
if ( index !== -1 ) {
newItems.splice( index, 1 );
}
return newItems;
} );
}, [] );

const registerResetAllFilter = useCallback(
( newFilter: ResetAllFilter ) => {
setResetAllFilters( ( filters ) => {
return [ ...filters, newFilter ];
} );
setResetAllFilters( ( filters ) => [ ...filters, newFilter ] );
},
[ setResetAllFilters ]
[]
);

const deregisterResetAllFilter = useCallback(
( filterToRemove: ResetAllFilter ) => {
setResetAllFilters( ( filters ) => {
return filters.filter(
( filter ) => filter !== filterToRemove
);
} );
setResetAllFilters( ( filters ) =>
filters.filter( ( filter ) => filter !== filterToRemove )
);
},
[ setResetAllFilters ]
[]
);

// Manage and share display state of menu items representing child controls.
Expand All @@ -193,17 +183,16 @@ export function useToolsPanel(
} );

// Setup menuItems state as panel items register themselves.
useEffect( () => {
setMenuItems( ( prevState ) => {
const items = generateMenuItems( {
useLayoutEffect( () => {
setMenuItems( ( currentMenuItems ) =>
generateMenuItems( {
panelItems,
shouldReset: false,
currentMenuItems: prevState,
currentMenuItems,
menuItemOrder,
} );
return items;
} );
}, [ panelItems, setMenuItems, menuItemOrder ] );
} )
);
}, [ panelItems, menuItemOrder ] );

// Updates the status of the panel’s menu items. For default items the
// value represents whether it differs from the default and for optional
Expand All @@ -225,7 +214,7 @@ export function useToolsPanel(
return newState;
} );
},
[ setMenuItems ]
[]
);

// Whether all optional menu items are hidden or not must be tracked
Expand All @@ -235,7 +224,7 @@ export function useToolsPanel(
const [ areAllOptionalControlsHidden, setAreAllOptionalControlsHidden ] =
useState( false );

useEffect( () => {
useLayoutEffect( () => {
if (
isMenuItemTypeEmpty( menuItems?.default ) &&
! isMenuItemTypeEmpty( menuItems?.optional )
Expand All @@ -245,7 +234,7 @@ export function useToolsPanel(
).some( ( [ , isSelected ] ) => isSelected );
setAreAllOptionalControlsHidden( allControlsHidden );
}
}, [ menuItems, setAreAllOptionalControlsHidden ] );
}, [ menuItems ] );

const cx = useCx();
const classes = useMemo( () => {
Expand Down Expand Up @@ -297,7 +286,7 @@ export function useToolsPanel(

setMenuItems( newMenuItems );
},
[ menuItems, panelItems, setMenuItems ]
[ menuItems, panelItems ]
);

// Resets display of children and executes resetAll callback if available.
Expand All @@ -314,7 +303,7 @@ export function useToolsPanel(
shouldReset: true,
} );
setMenuItems( resetMenuItems );
}, [ panelItems, resetAllFilters, resetAll, setMenuItems, menuItemOrder ] );
}, [ panelItems, resetAllFilters, resetAll, menuItemOrder ] );

// Assist ItemGroup styling when there are potentially hidden placeholder
// items by identifying first & last items that are toggled on for display.
Expand Down

0 comments on commit 5e37a13

Please sign in to comment.