Repository navigation
List View: Collapse off-window placeholder rows into a single spacer row #80953
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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( | ||
| <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; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This condition means that when Because in this case, we immediately call
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } | ||
|
|
||
There was a problem hiding this comment.
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
countBlocksandcountReducer. Is that right?There was a problem hiding this comment.
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.