Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<tr>`/`<td>` 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).
Expand Down
311 changes: 163 additions & 148 deletions packages/block-editor/src/components/list-view/branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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';

Expand Down Expand Up @@ -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
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still exactly the same counting logic, the placeholder collapsing doesn't need to modify it, it's just deduplicating the code that was in both countBlocks and countReducer. Is that right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. Just a minor cleanup.

}
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 = () => {};

Expand Down Expand Up @@ -123,8 +104,6 @@ function ListViewBranch( props ) {
draggedClientIds,
} = useListViewContext();

const nextPositionRef = useRef();

if ( ! canParentExpand ) {
return null;
}
Expand All @@ -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(
<tr key={ `placeholder-${ placeholderKey }` }>
<td
className="block-editor-list-view-placeholder"
style={ {
height: placeholderRows * BLOCK_LIST_ITEM_HEIGHT,
} }
/>
</tr>
);
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 (
<AsyncModeProvider key={ clientId } value={ ! isSelected }>
{ showBlock && (
<ListViewBlock
block={ block }
selectBlock={ selectBlock }
isSelected={ isSelected }
isBranchSelected={ isSelectedBranch }
isDragged={ isDragged }
level={ level }
position={ position }
rowCount={ rowCount }
siblingBlockCount={ blockCount }
showBlockMovers={ showBlockMovers }
path={ updatedPath }
isExpanded={ isDragged ? false : shouldExpand }
listPosition={ nextPositionRef.current }
selectedClientIds={ selectedClientIds }
isSyncedBranch={ syncedBranch }
displacement={ displacement }
isAfterDraggedBlocks={ isAfterDraggedBlocks }
isNesting={ isNesting }
/>
) }
{ ! showBlock && (
<tr>
<td className="block-editor-list-view-placeholder" />
</tr>
) }
{ hasNestedBlocks && shouldExpand && ! isDragged && (
<ListViewBranch
parentId={ clientId }
blocks={ innerBlocks }
selectBlock={ selectBlock }
showBlockMovers={ showBlockMovers }
level={ level + 1 }
path={ updatedPath }
listPosition={ nextPositionRef.current + 1 }
fixedListWindow={ fixedListWindow }
isBranchSelected={ isSelectedBranch }
selectedClientIds={ selectedClientIds }
isExpanded={ isExpanded }
isSyncedBranch={ syncedBranch }
/>
) }
</AsyncModeProvider>
);
} ) }
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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition means that when showNestedBlocks is true, then we still display individual non-merged placeholders?

Because in this case, we immediately call pushPlaceholderRow after placeholderRows was increased from 0 to 1.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent height is merged into the placeholder rows above it because the parent row precedes its child rows.

Every hidden-but-expanded parent stops the current group and starts a new one. So a deeply nested tree that's fully expanded and entirely off-screen gets one placeholder per level instead of one for the whole thing.

I think this can be improved, but it requires more changes/refactoring. We can try that in the follow-up.

}

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(
<AsyncModeProvider key={ clientId } value={ ! isSelected }>
{ showBlock && (
<ListViewBlock
block={ block }
selectBlock={ selectBlock }
isSelected={ isSelected }
isBranchSelected={ isSelectedBranch }
isDragged={ isDragged }
level={ level }
position={ position }
rowCount={ rowCount }
siblingBlockCount={ blockCount }
showBlockMovers={ showBlockMovers }
path={ updatedPath }
isExpanded={ isDragged ? false : shouldExpand }
listPosition={ blockListPosition }
selectedClientIds={ selectedClientIds }
isSyncedBranch={ syncedBranch }
displacement={ displacement }
isAfterDraggedBlocks={ isAfterDraggedBlocks }
isNesting={ isNesting }
/>
) }
{ showNestedBlocks && (
<ListViewBranch
parentId={ clientId }
blocks={ innerBlocks }
selectBlock={ selectBlock }
showBlockMovers={ showBlockMovers }
level={ level + 1 }
path={ updatedPath }
listPosition={ blockListPosition + 1 }
fixedListWindow={ fixedListWindow }
isBranchSelected={ isSelectedBranch }
selectedClientIds={ selectedClientIds }
isExpanded={ isExpanded }
isSyncedBranch={ syncedBranch }
/>
) }
</AsyncModeProvider>
);
} );

pushPlaceholderRow();

return (
<>
{ rows }
{ showAppender && (
<TreeGridRow
level={ level }
Expand Down
7 changes: 1 addition & 6 deletions packages/block-editor/src/components/list-view/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import useListViewDropZone from './use-list-view-drop-zone';
import useListViewExpandSelectedItem from './use-list-view-expand-selected-item';
import { store as blockEditorStore } from '../../store';
import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown';
import { focusListItem } from './utils';
import { BLOCK_LIST_ITEM_HEIGHT, focusListItem } from './utils';
import useClipboardHandler from './use-clipboard-handler';

const expanded = ( state, action ) => {
Expand All @@ -61,8 +61,6 @@ const expanded = ( state, action ) => {
return state;
};

export const BLOCK_LIST_ITEM_HEIGHT = 32;

/** @typedef {React.ComponentType} ComponentType */
/** @typedef {React.Ref<HTMLElement>} Ref */

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading