From defdebe1f173d066da79dc355579d9805cca15be Mon Sep 17 00:00:00 2001 From: bfintal Date: Tue, 14 Jul 2026 15:34:45 +0800 Subject: [PATCH] perf: reduce Icon List item mount cost in widgets editor Use lightweight SvgIcon for inherited parent icons and unselected items, resolve block editor selectors on demand in useEnter, and sync list context attributes immediately without delayed store writes. Fixes #3062 Co-authored-by: Cursor --- src/block-components/icon/index.js | 8 ++- src/block/icon-list-item/edit.js | 67 ++++++++-------------- src/block/icon-list-item/list-item-icon.js | 59 +++++++++++++++++++ src/block/icon-list-item/util.js | 9 +-- 4 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 src/block/icon-list-item/list-item-icon.js diff --git a/src/block-components/icon/index.js b/src/block-components/icon/index.js index f968cca946..f79276fc96 100644 --- a/src/block-components/icon/index.js +++ b/src/block-components/icon/index.js @@ -282,6 +282,12 @@ export const Icon = props => { lastIconValueRef.current = newIcon } processedIconRef.current = _icon + } else if ( iconStr.includes( ' { const classNames = classnames( [ 'stk--svg-wrapper' ], { - 'stk--show-cursor': debouncedIsSelected, + 'stk--show-cursor': debouncedIsSelected || openEvenIfUnselected, 'stk--has-icon2': getAttribute( 'icon2' ), } ) diff --git a/src/block/icon-list-item/edit.js b/src/block/icon-list-item/edit.js index 4e78765fec..b8ce42e1b8 100644 --- a/src/block/icon-list-item/edit.js +++ b/src/block/icon-list-item/edit.js @@ -2,7 +2,7 @@ * Internal dependencies */ import blockStyles from './style' -import { getUseSvgDef } from '../icon-list/util' +import { ListItemIcon } from './list-item-icon' import { convertToListItems, useEnter, @@ -23,7 +23,6 @@ import { EffectsAnimations, ConditionalDisplay, Transform, - Icon, } from '~stackable/block-components' import { version as VERSION } from 'stackable' import classnames from 'classnames' @@ -40,10 +39,8 @@ import { */ import { __ } from '@wordpress/i18n' import { compose, createHigherOrderComponent } from '@wordpress/compose' -import { dispatch, useSelect } from '@wordpress/data' -import { - useEffect, useRef, memo, -} from '@wordpress/element' +import { dispatch, select } from '@wordpress/data' +import { useEffect, memo } from '@wordpress/element' const TABS = [ 'style', 'advanced' ] @@ -61,42 +58,26 @@ const Edit = props => { const { icon, text } = attributes const textClasses = getTypographyClasses( props.attributes ) const blockAlignmentClass = getAlignmentClasses( props.attributes ) - const { parentBlock } = useSelect( select => { - const { getBlockRootClientId, getBlock } = select( 'core/block-editor' ) - const parentClientId = getBlockRootClientId( clientId ) - return { - parentBlock: getBlock( parentClientId ), - } - }, [ clientId ] ) const { 'stackable/ordered': ordered, 'stackable/uniqueId': parentUniqueId, } = context - const updateOrderedTimeout = useRef() - const updateUniqueIdTimeout = useRef() - // Set the attributes so they can be used in Save. useEffect( () => { - clearTimeout( updateOrderedTimeout.current ) if ( ordered !== props.attributes.ordered ) { - updateOrderedTimeout.current = setTimeout( () => { - dispatch( 'core/block-editor' ).__unstableMarkNextChangeAsNotPersistent() - setAttributes( { ordered } ) - }, 300 ) + dispatch( 'core/block-editor' ).__unstableMarkNextChangeAsNotPersistent() + setAttributes( { ordered } ) } - }, [ ordered ] ) + }, [ ordered, props.attributes.ordered, setAttributes ] ) useEffect( () => { - clearTimeout( updateUniqueIdTimeout.current ) if ( parentUniqueId !== props.attributes.parentUniqueId ) { - updateUniqueIdTimeout.current = setTimeout( () => { - dispatch( 'core/block-editor' ).__unstableMarkNextChangeAsNotPersistent() - setAttributes( { parentUniqueId } ) - }, 300 ) + dispatch( 'core/block-editor' ).__unstableMarkNextChangeAsNotPersistent() + setAttributes( { parentUniqueId } ) } - }, [ parentUniqueId ] ) + }, [ parentUniqueId, props.attributes.parentUniqueId, setAttributes ] ) const blockClassNames = classnames( [ className, @@ -115,10 +96,14 @@ const Edit = props => { mergeBlocks( forward ) // Remove icon list item and icon list on backspace if there is no text and is the only item on the list. - if ( ! forward && - ! attributes.text && - parentBlock.innerBlocks.length === 1 ) { - dispatch( 'core/block-editor' ).removeBlocks( [ clientId, parentBlock.clientId ] ) + if ( ! forward && ! attributes.text ) { + const { getBlockRootClientId, getBlock } = select( 'core/block-editor' ) + const parentClientId = getBlockRootClientId( clientId ) + const parentBlock = getBlock( parentClientId ) + + if ( parentBlock?.innerBlocks?.length === 1 ) { + dispatch( 'core/block-editor' ).removeBlocks( [ clientId, parentClientId ] ) + } } } @@ -150,18 +135,12 @@ const Edit = props => {
- { ! ordered && icon && - } - { ! ordered && ! icon && parentUniqueId && - } + { ordered && // This will contain the numbers in ::before pseudo-element for ordered lists. // Placing the numbers here instead on li allows us to center the text vertically. diff --git a/src/block/icon-list-item/list-item-icon.js b/src/block/icon-list-item/list-item-icon.js new file mode 100644 index 0000000000..c2ba12f253 --- /dev/null +++ b/src/block/icon-list-item/list-item-icon.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { Icon } from '~stackable/block-components' + +/** + * Internal dependencies + */ +import { getUseSvgDef } from '../icon-list/util' + +/** + * Icon display for icon list items. + * + * Always mounts the full Icon picker so users can click any entry's icon + * without first selecting that list item (openEvenIfUnselected). + * + * @param {Object} props + * @param {boolean} props.ordered + * @param {string} props.icon + * @param {string} props.parentUniqueId + * @param {Function} props.setAttributes + */ +export const ListItemIcon = ( { + ordered, + icon, + parentUniqueId, + setAttributes, +} ) => { + if ( ordered ) { + return null + } + + const hasCustomIcon = !! icon + const iconValue = icon || ( + parentUniqueId + ? getUseSvgDef( `#stk-icon-list__icon-svg-def-${ parentUniqueId }` ) + : '' + ) + + if ( ! iconValue ) { + return null + } + + // Remount when switching between custom and inherited icons so the Icon + // component's internal state resets correctly (e.g. after "Clear icon"). + const iconKey = hasCustomIcon ? 'custom' : `inherit-${ parentUniqueId }` + + return ( + { + setAttributes( { icon: newIcon } ) + } } + /> + ) +} diff --git a/src/block/icon-list-item/util.js b/src/block/icon-list-item/util.js index ca48a416e9..80947669f6 100644 --- a/src/block/icon-list-item/util.js +++ b/src/block/icon-list-item/util.js @@ -5,7 +5,7 @@ import { useRefEffect } from '@wordpress/compose' import { useRef } from '@wordpress/element' import { - useSelect, + select, useDispatch, } from '@wordpress/data' import { @@ -51,9 +51,6 @@ export const useEnter = ( text, clientId ) => { const { removeBlocks, selectionChange, insertBlocks, } = useDispatch( blockEditorStore ) - const { - getBlock, getBlockRootClientId, getBlockIndex, - } = useSelect( blockEditorStore ) const textRef = useRef( text ) textRef.current = text @@ -69,6 +66,10 @@ export const useEnter = ( text, clientId ) => { } event.preventDefault() + const { + getBlock, getBlockRootClientId, getBlockIndex, + } = select( blockEditorStore ) + // Here we are in top level list so we need to split. const topParentListBlock = getBlock( getBlockRootClientId( clientId )