diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index b051f1aaae5efb..8754f7a0f7cba6 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -9,6 +9,10 @@ - Inherited Global Styles now resolve the `link` element (`styles.elements.link`) for whole-block link blocks — those that render as a link — so their inspector controls reflect inherited link styles, mirroring the `button` element treatment for `core/button`. Covers `core/read-more`, `core/loginout`, `core/post-navigation-link`, `core/query-pagination-next`, `core/query-pagination-previous`, `core/query-pagination-numbers`, `core/comments-pagination-next`, `core/comments-pagination-previous`, `core/comments-pagination-numbers`, `core/comment-edit-link`, `core/comment-reply-link`, and `core/post-comments-link` [#80607](https://github.com/WordPress/gutenberg/pull/80607)). - Add support for the `blockStatesEnabled` editor setting, which hides state controls for blocks when set to `false` ([#80956](https://github.com/WordPress/gutenberg/pull/80956)). +### Performance + +- `ListView`: Collapse the placeholder rows that stand in for blocks outside of the render window into a single spacer row per run, instead of rendering a ``/`` pair for every block. On a post with 1000 top-level blocks this removes ~1900 elements (about 60% of the List View's DOM and nearly half of the document's elements), which cuts the style recalculation and layout work done when the List View opens ([#80953](https://github.com/WordPress/gutenberg/pull/80953)). + ### Internal - `ListView`: Reimplement the Firefox description-recomputation workaround in `AriaReferencedText` by keying the element on its text, so React replaces it instead of updating the existing text node in place ([#80929](https://github.com/WordPress/gutenberg/pull/80929). diff --git a/packages/block-editor/src/components/list-view/branch.js b/packages/block-editor/src/components/list-view/branch.js index 5df7134e125cdb..b121f05367fea5 100644 --- a/packages/block-editor/src/components/list-view/branch.js +++ b/packages/block-editor/src/components/list-view/branch.js @@ -5,7 +5,7 @@ import { __experimentalTreeGridRow as TreeGridRow, __experimentalTreeGridCell as TreeGridCell, } from '@wordpress/components'; -import { memo, useRef } from '@wordpress/element'; +import { memo } from '@wordpress/element'; import { AsyncModeProvider, useSelect } from '@wordpress/data'; /** @@ -14,7 +14,11 @@ import { AsyncModeProvider, useSelect } from '@wordpress/data'; import { Appender } from './appender'; import ListViewBlock from './block'; import { useListViewContext } from './context'; -import { getDragDisplacementValues, isClientIdSelected } from './utils'; +import { + BLOCK_LIST_ITEM_HEIGHT, + getDragDisplacementValues, + isClientIdSelected, +} from './utils'; import { store as blockEditorStore } from '../../store'; import useBlockDisplayInformation from '../use-block-display-information'; @@ -42,44 +46,21 @@ function countBlocks( return 0; } const isExpanded = expandedState[ block.clientId ] ?? isExpandedByDefault; - - if ( isExpanded ) { - return ( - 1 + - block.innerBlocks.reduce( - countReducer( - expandedState, - draggedClientIds, - isExpandedByDefault - ), - 0 - ) - ); + if ( ! isExpanded ) { + return 1; } - return 1; + return block.innerBlocks.reduce( + ( count, innerBlock ) => + count + + countBlocks( + innerBlock, + expandedState, + draggedClientIds, + isExpandedByDefault + ), + 1 + ); } -const countReducer = - ( expandedState, draggedClientIds, isExpandedByDefault ) => - ( count, block ) => { - const isDragged = draggedClientIds?.includes( block.clientId ); - if ( isDragged ) { - return count; - } - const isExpanded = - expandedState[ block.clientId ] ?? isExpandedByDefault; - if ( isExpanded && block.innerBlocks.length > 0 ) { - return ( - count + - countBlocks( - block, - expandedState, - draggedClientIds, - isExpandedByDefault - ) - ); - } - return count + 1; - }; const noop = () => {}; @@ -123,8 +104,6 @@ function ListViewBranch( props ) { draggedClientIds, } = useListViewContext(); - const nextPositionRef = useRef(); - if ( ! canParentExpand ) { return null; } @@ -135,123 +114,159 @@ function ListViewBranch( props ) { const blockCount = filteredBlocks.length; // The appender means an extra row in List View, so add 1 to the row count. const rowCount = showAppender ? blockCount + 1 : blockCount; - nextPositionRef.current = listPosition; + let nextPosition = listPosition; - return ( - <> - { filteredBlocks.map( ( block, index ) => { - const { clientId, innerBlocks } = block; + const rows = []; - if ( index > 0 ) { - nextPositionRef.current += countBlocks( - filteredBlocks[ index - 1 ], - expandedState, - draggedClientIds, - isExpanded - ); - } + // A run of blocks outside of the render window becomes one spacer row that + // stands in for their height. + let placeholderRows = 0; + let placeholderKey; - const isDragged = !! draggedClientIds?.includes( clientId ); + // Adds the pending placeholder row, if there is one. Call before + // adding a real row. + const pushPlaceholderRow = () => { + if ( ! placeholderRows ) { + return; + } + rows.push( + + + + ); + placeholderRows = 0; + placeholderKey = undefined; + }; - // Determine the displacement of the block while dragging. This - // works out whether the current block should be displaced up or - // down, relative to the dragged blocks and the drop target. - const { displacement, isAfterDraggedBlocks, isNesting } = - getDragDisplacementValues( { - blockIndexes, - blockDropTargetIndex, - blockDropPosition, - clientId, - firstDraggedBlockIndex, - isDragged, - } ); + filteredBlocks.forEach( ( block, index ) => { + const { clientId, innerBlocks } = block; - const { itemInView } = fixedListWindow; - const blockInView = itemInView( nextPositionRef.current ); + // The next sibling is offset by this block's subtree. + const blockListPosition = nextPosition; + nextPosition += countBlocks( + block, + expandedState, + draggedClientIds, + isExpanded + ); - const position = index + 1; - const updatedPath = - path.length > 0 - ? `${ path }_${ position }` - : `${ position }`; - const hasNestedBlocks = !! innerBlocks?.length; + const isDragged = !! draggedClientIds?.includes( clientId ); - const shouldExpand = - hasNestedBlocks && shouldShowInnerBlocks - ? expandedState[ clientId ] ?? isExpanded - : undefined; + const { itemInView } = fixedListWindow; + const blockInView = itemInView( blockListPosition ); - // Make updates to the selected or dragged blocks synchronous, - // but asynchronous for any other block. - const isSelected = isClientIdSelected( - clientId, - selectedClientIds - ); - const isSelectedBranch = - isBranchSelected || ( isSelected && hasNestedBlocks ); + const position = index + 1; + const updatedPath = + path.length > 0 ? `${ path }_${ position }` : `${ position }`; + const hasNestedBlocks = !! innerBlocks?.length; - // To avoid performance issues, we only render blocks that are in view, - // or blocks that are selected or dragged. If a block is selected, - // it is only counted if it is the first of the block selection. - // This prevents the entire tree from being rendered when a branch is - // selected, or a user selects all blocks, while still enabling scroll - // into view behavior when selecting a block or opening the list view. - // The first and last blocks of the list are always rendered, to ensure - // that Home and End keys work as expected. - const showBlock = - isDragged || - blockInView || - ( isSelected && clientId === selectedClientIds[ 0 ] ) || - index === 0 || - index === blockCount - 1; - return ( - - { showBlock && ( - - ) } - { ! showBlock && ( - - - - ) } - { hasNestedBlocks && shouldExpand && ! isDragged && ( - - ) } - - ); - } ) } + const shouldExpand = + hasNestedBlocks && shouldShowInnerBlocks + ? expandedState[ clientId ] ?? isExpanded + : undefined; + + // Make updates to the selected or dragged blocks synchronous, + // but asynchronous for any other block. + const isSelected = isClientIdSelected( clientId, selectedClientIds ); + const isSelectedBranch = + isBranchSelected || ( isSelected && hasNestedBlocks ); + + // To avoid performance issues, we only render blocks that are in view, + // or blocks that are selected or dragged. If a block is selected, + // it is only counted if it is the first of the block selection. + // This prevents the entire tree from being rendered when a branch is + // selected, or a user selects all blocks, while still enabling scroll + // into view behavior when selecting a block or opening the list view. + // The first and last blocks of the list are always rendered, to ensure + // that Home and End keys work as expected. + const showBlock = + isDragged || + blockInView || + ( isSelected && clientId === selectedClientIds[ 0 ] ) || + index === 0 || + index === blockCount - 1; + + const showNestedBlocks = hasNestedBlocks && shouldExpand && ! isDragged; + + if ( ! showBlock ) { + placeholderRows += 1; + placeholderKey ??= clientId; + + // Inner blocks of an off-window block may still be in view. + if ( ! showNestedBlocks ) { + return; + } + } + + pushPlaceholderRow(); + + // Determine the displacement of the block while dragging. This + // works out whether the current block should be displaced up or + // down, relative to the dragged blocks and the drop target. + const { displacement, isAfterDraggedBlocks, isNesting } = + getDragDisplacementValues( { + blockIndexes, + blockDropTargetIndex, + blockDropPosition, + clientId, + firstDraggedBlockIndex, + isDragged, + } ); + + rows.push( + + { showBlock && ( + + ) } + { showNestedBlocks && ( + + ) } + + ); + } ); + + pushPlaceholderRow(); + + return ( + <> + { rows } { showAppender && ( { @@ -61,8 +61,6 @@ const expanded = ( state, action ) => { return state; }; -export const BLOCK_LIST_ITEM_HEIGHT = 32; - /** @typedef {React.ComponentType} ComponentType */ /** @typedef {React.Ref} Ref */ @@ -322,9 +320,6 @@ function ListViewComponent( ] ); - // List View renders a fixed number of items and relies on each having a fixed item height of 36px. - // If this value changes, we should also change the itemHeight value set in useFixedWindowList. - // See: https://github.com/WordPress/gutenberg/pull/35230 for additional context. const [ fixedListWindow ] = useFixedWindowList( elementRef, BLOCK_LIST_ITEM_HEIGHT, diff --git a/packages/block-editor/src/components/list-view/style.scss b/packages/block-editor/src/components/list-view/style.scss index f2f7a8183c12cb..3f096ea27cfa0b 100644 --- a/packages/block-editor/src/components/list-view/style.scss +++ b/packages/block-editor/src/components/list-view/style.scss @@ -568,6 +568,7 @@ svg { .block-editor-list-view-placeholder { padding: 0; margin: 0; + // One row. Spacers standing in for several rows set their height inline. height: 32px; } diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js index 66db52a7ab06ac..d88985af2bf289 100644 --- a/packages/block-editor/src/components/list-view/utils.js +++ b/packages/block-editor/src/components/list-view/utils.js @@ -4,6 +4,10 @@ import { __, sprintf } from '@wordpress/i18n'; import { focus } from '@wordpress/dom'; +// Must match the row height in `style.scss`; the windowing math relies on it. +// See: https://github.com/WordPress/gutenberg/pull/35230 for additional context. +export const BLOCK_LIST_ITEM_HEIGHT = 32; + export const getBlockPositionDescription = ( position, siblingCount, level ) => sprintf( /* translators: 1: The numerical position of the block. 2: The total number of blocks. 3. The level of nesting for the block. */ diff --git a/tools/eslint/suppressions.json b/tools/eslint/suppressions.json index 2b3d5c06ffad24..78424075d3017d 100644 --- a/tools/eslint/suppressions.json +++ b/tools/eslint/suppressions.json @@ -265,11 +265,6 @@ "count": 1 } }, - "packages/block-editor/src/components/list-view/branch.js": { - "react-hooks/refs": { - "count": 2 - } - }, "packages/block-editor/src/components/list-view/drop-indicator.js": { "@wordpress/use-recommended-components": { "count": 1