From e4e558afc1e5d9794977ce360cf5b55bf2e37dbf Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:42:17 +1000 Subject: [PATCH] ListView: Update drop indicator line positioning to support rtl languages (#51284) * ListView: Update drop indicator line positioning to support rtl languages * Update tests to include rtl equivalents * Ensure drop indicator line doesn't break out of scroll containers in both LTR and RTL languages * Users shouldn't be able to drag below a non-empty expanded block * Fix width issue when target is only slightly wider than the scroll container --- .../components/list-view/drop-indicator.js | 103 ++++++- .../list-view/test/use-list-view-drop-zone.js | 281 ++++++++++++------ .../list-view/use-list-view-drop-zone.js | 145 +++++---- 3 files changed, 376 insertions(+), 153 deletions(-) diff --git a/packages/block-editor/src/components/list-view/drop-indicator.js b/packages/block-editor/src/components/list-view/drop-indicator.js index b1256afe5fbe1..8178bc92453bc 100644 --- a/packages/block-editor/src/components/list-view/drop-indicator.js +++ b/packages/block-editor/src/components/list-view/drop-indicator.js @@ -4,6 +4,7 @@ import { Popover } from '@wordpress/components'; import { getScrollContainer } from '@wordpress/dom'; import { useCallback, useMemo } from '@wordpress/element'; +import { isRTL } from '@wordpress/i18n'; export default function ListViewDropIndicator( { listViewRef, @@ -41,6 +42,8 @@ export default function ListViewDropIndicator( { // is undefined, so the indicator will appear after the rootBlockElement. const targetElement = blockElement || rootBlockElement; + const rtl = isRTL(); + const getDropIndicatorIndent = useCallback( ( targetElementRect ) => { if ( ! rootBlockElement ) { @@ -55,9 +58,11 @@ export default function ListViewDropIndicator( { ); const rootBlockIconRect = rootBlockIconElement.getBoundingClientRect(); - return rootBlockIconRect.right - targetElementRect.left; + return rtl + ? targetElementRect.right - rootBlockIconRect.left + : rootBlockIconRect.right - targetElementRect.left; }, - [ rootBlockElement ] + [ rootBlockElement, rtl ] ); const getDropIndicatorWidth = useCallback( @@ -80,21 +85,56 @@ export default function ListViewDropIndicator( { 'horizontal' ); - if ( scrollContainer ) { + const ownerDocument = targetElement.ownerDocument; + const windowScroll = + scrollContainer === ownerDocument.body || + scrollContainer === ownerDocument.documentElement; + + if ( scrollContainer && ! windowScroll ) { const scrollContainerRect = scrollContainer.getBoundingClientRect(); - if ( scrollContainer.clientWidth < width ) { + const distanceBetweenContainerAndTarget = isRTL() + ? scrollContainerRect.right - targetElementRect.right + : targetElementRect.left - scrollContainerRect.left; + + const scrollContainerWidth = scrollContainer.clientWidth; + + if ( + scrollContainerWidth < + width + distanceBetweenContainerAndTarget + ) { width = - scrollContainer.clientWidth - - ( targetElementRect.left - scrollContainerRect.left ); + scrollContainerWidth - + distanceBetweenContainerAndTarget; + } + + // LTR logic for ensuring the drop indicator does not extend + // beyond the right edge of the scroll container. + if ( + ! rtl && + targetElementRect.left + indent < scrollContainerRect.left + ) { + width -= scrollContainerRect.left - targetElementRect.left; + return width; + } + + // RTL logic for ensuring the drop indicator does not extend + // beyond the right edge of the scroll container. + if ( + rtl && + targetElementRect.right - indent > scrollContainerRect.right + ) { + width -= + targetElementRect.right - scrollContainerRect.right; + return width; } } // Subtract the indent from the final width of the indicator. return width - indent; }, - [ targetElement ] + [ rtl, targetElement ] ); const style = useMemo( () => { @@ -119,15 +159,59 @@ export default function ListViewDropIndicator( { return undefined; } + const ownerDocument = targetElement.ownerDocument; + return { - ownerDocument: targetElement.ownerDocument, + ownerDocument, getBoundingClientRect() { const rect = targetElement.getBoundingClientRect(); const indent = getDropIndicatorIndent( rect ); - const left = rect.left + indent; + // In RTL languages, the drop indicator should be positioned + // to the left of the target element, with the width of the + // indicator determining the indent at the right edge of the + // target element. In LTR languages, the drop indicator should + // end at the right edge of the target element, with the indent + // added to the position of the left edge of the target element. + let left = rtl ? rect.left : rect.left + indent; let top = 0; let bottom = 0; + // In deeply nested lists, where a scrollbar is present, + // the width of the drop indicator should be the width of + // the visible area of the scroll container. Additionally, + // the left edge of the drop indicator line needs to be + // offset by the distance the left edge of the target element + // and the left edge of the scroll container. The ensures + // that the drop indicator position never breaks out of the + // visible area of the scroll container. + const scrollContainer = getScrollContainer( + targetElement, + 'horizontal' + ); + + const windowScroll = + scrollContainer === ownerDocument.body || + scrollContainer === ownerDocument.documentElement; + + // If the scroll container is not the window, offset the left position, if need be. + if ( scrollContainer && ! windowScroll ) { + const scrollContainerRect = + scrollContainer.getBoundingClientRect(); + + // In RTL languages, a vertical scrollbar is present on the + // left edge of the scroll container. The width of the + // scrollbar needs to be accounted for when positioning the + // drop indicator. + const scrollbarWidth = rtl + ? scrollContainer.offsetWidth - + scrollContainer.clientWidth + : 0; + + if ( left < scrollContainerRect.left + scrollbarWidth ) { + left = scrollContainerRect.left + scrollbarWidth; + } + } + if ( dropPosition === 'top' ) { top = rect.top; bottom = rect.top; @@ -148,6 +232,7 @@ export default function ListViewDropIndicator( { dropPosition, getDropIndicatorIndent, getDropIndicatorWidth, + rtl, ] ); if ( ! targetElement ) { diff --git a/packages/block-editor/src/components/list-view/test/use-list-view-drop-zone.js b/packages/block-editor/src/components/list-view/test/use-list-view-drop-zone.js index bf157fd972979..98c2b31132c60 100644 --- a/packages/block-editor/src/components/list-view/test/use-list-view-drop-zone.js +++ b/packages/block-editor/src/components/list-view/test/use-list-view-drop-zone.js @@ -103,78 +103,118 @@ describe( 'getListViewDropTarget', () => { ]; it( 'should return the correct target when dragging a block over the top half of the first block', () => { - const position = { x: 50, y: 70 }; - const target = getListViewDropTarget( blocksData, position ); - - expect( target ).toEqual( { - blockIndex: 0, - clientId: 'block-1', - dropPosition: 'top', - rootClientId: '', + [ + { position: { x: 50, y: 70 }, rtl: false }, + { position: { x: 250, y: 70 }, rtl: true }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( blocksData, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 0, + clientId: 'block-1', + dropPosition: 'top', + rootClientId: '', + } ); } ); } ); it( 'should nest when dragging a block over the bottom half of an expanded block', () => { - const position = { x: 50, y: 90 }; - const target = getListViewDropTarget( blocksData, position ); - - expect( target ).toEqual( { - blockIndex: 0, - dropPosition: 'inside', - rootClientId: 'block-1', + [ + { position: { x: 50, y: 90 }, rtl: false }, + { position: { x: 250, y: 90 }, rtl: true }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( blocksData, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 0, + dropPosition: 'inside', + rootClientId: 'block-1', + } ); } ); } ); it( 'should nest when dragging a block over the right side of the bottom half of a block nested to three levels', () => { - const position = { x: 250, y: 180 }; - const target = getListViewDropTarget( blocksData, position ); - - expect( target ).toEqual( { - blockIndex: 0, - dropPosition: 'inside', - rootClientId: 'block-3', + [ + { position: { x: 250, y: 180 }, rtl: false }, + { position: { x: 50, y: 180 }, rtl: true }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( blocksData, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 0, + dropPosition: 'inside', + rootClientId: 'block-3', + } ); } ); } ); it( 'should drag below when positioned at the bottom half of a block nested to three levels, and over the third level horizontally', () => { - const position = { x: 10 + NESTING_LEVEL_INDENTATION * 3, y: 180 }; - const target = getListViewDropTarget( blocksData, position ); - - expect( target ).toEqual( { - blockIndex: 1, - clientId: 'block-3', - dropPosition: 'bottom', - rootClientId: 'block-2', + [ + { + position: { x: 10 + NESTING_LEVEL_INDENTATION * 3, y: 180 }, + rtl: false, + }, + { + position: { x: 300 - NESTING_LEVEL_INDENTATION * 3, y: 180 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( blocksData, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 1, + clientId: 'block-3', + dropPosition: 'bottom', + rootClientId: 'block-2', + } ); } ); } ); - it( 'should drag one level up below when positioned at the bottom half of a block nested to three levels, and over the second level horizontally', () => { - const position = { x: 10 + NESTING_LEVEL_INDENTATION * 2, y: 180 }; - const target = getListViewDropTarget( blocksData, position ); - - expect( target ).toEqual( { - blockIndex: 1, - clientId: 'block-3', - dropPosition: 'bottom', - rootClientId: 'block-1', + it( 'should drag one level up when positioned at the bottom half of a block nested to three levels, and over the second level horizontally', () => { + [ + { + position: { x: 10 + NESTING_LEVEL_INDENTATION * 2, y: 180 }, + rtl: false, + }, + { + position: { x: 300 - NESTING_LEVEL_INDENTATION * 2, y: 180 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( blocksData, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 1, + clientId: 'block-3', + dropPosition: 'bottom', + rootClientId: 'block-1', + } ); } ); } ); it( 'should drag two levels up below when positioned at the bottom half of a block nested to three levels, and over the first level horizontally', () => { - const position = { x: 10 + NESTING_LEVEL_INDENTATION, y: 180 }; - const target = getListViewDropTarget( blocksData, position ); - - expect( target ).toEqual( { - blockIndex: 1, - clientId: 'block-3', - dropPosition: 'bottom', - rootClientId: '', + [ + { + position: { x: 10 + NESTING_LEVEL_INDENTATION, y: 180 }, + rtl: false, + }, + { + position: { x: 300 - NESTING_LEVEL_INDENTATION, y: 180 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( blocksData, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 1, + clientId: 'block-3', + dropPosition: 'bottom', + rootClientId: '', + } ); } ); } ); it( 'should nest and append to end when dragging a block over the right side and bottom half of a collapsed block with children', () => { - const position = { x: 160, y: 90 }; - const collapsedBlockData = [ ...blocksData ]; // Set the first block to be collapsed. @@ -186,38 +226,64 @@ describe( 'getListViewDropTarget', () => { // Hide the first block's children. collapsedBlockData.splice( 1, 1 ); - const target = getListViewDropTarget( collapsedBlockData, position ); - - expect( target ).toEqual( { - blockIndex: 1, - dropPosition: 'inside', - rootClientId: 'block-1', + [ + { + position: { x: 250, y: 90 }, + rtl: false, + }, + { + position: { x: 50, y: 90 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( + collapsedBlockData, + position, + rtl + ); + + expect( target ).toEqual( { + blockIndex: 1, + dropPosition: 'inside', + rootClientId: 'block-1', + } ); } ); } ); it( 'should nest and prepend when dragging a block over the right side and bottom half of an expanded block with children', () => { - const position = { x: 160, y: 90 }; - const collapsedBlockData = [ ...blocksData ]; - // Set the first block to be collapsed. + // Set the first block to be expanded. collapsedBlockData[ 0 ] = { ...collapsedBlockData[ 0 ], isExpanded: true, }; - const target = getListViewDropTarget( collapsedBlockData, position ); - - expect( target ).toEqual( { - blockIndex: 0, - dropPosition: 'inside', - rootClientId: 'block-1', + [ + { + position: { x: 250, y: 90 }, + rtl: false, + }, + { + position: { x: 50, y: 90 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( + collapsedBlockData, + position, + rtl + ); + + expect( target ).toEqual( { + blockIndex: 0, + dropPosition: 'inside', + rootClientId: 'block-1', + } ); } ); } ); it( 'should drag below when dragging a block over the left side and bottom half of a collapsed block with children', () => { - const position = { x: 30, y: 90 }; - const collapsedBlockData = [ ...blocksData ]; // Set the first block to be collapsed. @@ -229,19 +295,32 @@ describe( 'getListViewDropTarget', () => { // Hide the first block's children. collapsedBlockData.splice( 1, 1 ); - const target = getListViewDropTarget( collapsedBlockData, position ); - - expect( target ).toEqual( { - blockIndex: 1, - clientId: 'block-1', - dropPosition: 'bottom', - rootClientId: '', + [ + { + position: { x: 30, y: 90 }, + rtl: false, + }, + { + position: { x: 270, y: 90 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( + collapsedBlockData, + position, + rtl + ); + + expect( target ).toEqual( { + blockIndex: 1, + clientId: 'block-1', + dropPosition: 'bottom', + rootClientId: '', + } ); } ); } ); it( 'should drag below when attempting to nest but the dragged block is not allowed as a child', () => { - const position = { x: 70, y: 90 }; - const childNotAllowedBlockData = [ ...blocksData ]; // Set the first block to not be allowed as a child. @@ -250,16 +329,28 @@ describe( 'getListViewDropTarget', () => { canInsertDraggedBlocksAsChild: false, }; - const target = getListViewDropTarget( - childNotAllowedBlockData, - position - ); - - expect( target ).toEqual( { - blockIndex: 1, - clientId: 'block-1', - dropPosition: 'bottom', - rootClientId: '', + [ + { + position: { x: 70, y: 90 }, + rtl: false, + }, + { + position: { x: 230, y: 90 }, + rtl: true, + }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( + childNotAllowedBlockData, + position, + rtl + ); + + expect( target ).toEqual( { + blockIndex: 1, + clientId: 'block-1', + dropPosition: 'bottom', + rootClientId: '', + } ); } ); } ); @@ -288,14 +379,18 @@ describe( 'getListViewDropTarget', () => { // This position is to the right of the block, but below the bottom of the block. // This should result in the block being moved below the bottom-most block, and // not being treated as a nesting gesture. - const position = { x: 160, y: 250 }; - const target = getListViewDropTarget( singleBlock, position ); - - expect( target ).toEqual( { - blockIndex: 1, - clientId: 'block-1', - dropPosition: 'bottom', - rootClientId: '', + [ + { position: { x: 160, y: 250 }, rtl: false }, + { position: { x: 140, y: 250 }, rtl: true }, + ].forEach( ( { position, rtl } ) => { + const target = getListViewDropTarget( singleBlock, position, rtl ); + + expect( target ).toEqual( { + blockIndex: 1, + clientId: 'block-1', + dropPosition: 'bottom', + rootClientId: '', + } ); } ); } ); } ); diff --git a/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js b/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js index 20f7d68c1dd98..b94e0d603f13a 100644 --- a/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js +++ b/packages/block-editor/src/components/list-view/use-list-view-drop-zone.js @@ -7,6 +7,7 @@ import { useThrottle, __experimentalUseDropZone as useDropZone, } from '@wordpress/compose'; +import { isRTL } from '@wordpress/i18n'; /** * Internal dependencies @@ -71,14 +72,16 @@ export const NESTING_LEVEL_INDENTATION = 28; * @param {WPPoint} point The point representing the cursor position when dragging. * @param {DOMRect} rect The rectangle. * @param {number} nestingLevel The nesting level of the block. + * @param {boolean} rtl Whether the editor is in RTL mode. * @return {boolean} Whether the gesture is an upward gesture. */ -function isUpGesture( point, rect, nestingLevel = 1 ) { +function isUpGesture( point, rect, nestingLevel = 1, rtl = false ) { // If the block is nested, and the user is dragging to the bottom - // left of the block, then it is an upward gesture. - const blockIndentPosition = - rect.left + nestingLevel * NESTING_LEVEL_INDENTATION; - return point.x < blockIndentPosition; + // left of the block (or bottom right in RTL languages), then it is an upward gesture. + const blockIndentPosition = rtl + ? rect.right - nestingLevel * NESTING_LEVEL_INDENTATION + : rect.left + nestingLevel * NESTING_LEVEL_INDENTATION; + return rtl ? point.x > blockIndentPosition : point.x < blockIndentPosition; } /** @@ -96,14 +99,29 @@ function isUpGesture( point, rect, nestingLevel = 1 ) { * @param {WPPoint} point The point representing the cursor position when dragging. * @param {DOMRect} rect The rectangle. * @param {number} nestingLevel The nesting level of the block. + * @param {boolean} rtl Whether the editor is in RTL mode. * @return {number} The desired relative parent level. */ -function getDesiredRelativeParentLevel( point, rect, nestingLevel = 1 ) { - const blockIndentPosition = - rect.left + nestingLevel * NESTING_LEVEL_INDENTATION; +function getDesiredRelativeParentLevel( + point, + rect, + nestingLevel = 1, + rtl = false +) { + // In RTL languages, the block indent position is from the right edge of the block. + // In LTR languages, the block indent position is from the left edge of the block. + const blockIndentPosition = rtl + ? rect.right - nestingLevel * NESTING_LEVEL_INDENTATION + : rect.left + nestingLevel * NESTING_LEVEL_INDENTATION; + + const distanceBetweenPointAndBlockIndentPosition = rtl + ? blockIndentPosition - point.x + : point.x - blockIndentPosition; + const desiredParentLevel = Math.round( - ( point.x - blockIndentPosition ) / NESTING_LEVEL_INDENTATION + distanceBetweenPointAndBlockIndentPosition / NESTING_LEVEL_INDENTATION ); + return Math.abs( desiredParentLevel ); } @@ -158,14 +176,18 @@ function getNextNonDraggedBlock( blocksData, index ) { * @param {WPPoint} point The point representing the cursor position when dragging. * @param {DOMRect} rect The rectangle. * @param {number} nestingLevel The nesting level of the block. + * @param {boolean} rtl Whether the editor is in RTL mode. */ -function isNestingGesture( point, rect, nestingLevel = 1 ) { - const blockIndentPosition = - rect.left + nestingLevel * NESTING_LEVEL_INDENTATION; - return ( - point.x > blockIndentPosition + NESTING_LEVEL_INDENTATION && - point.y < rect.bottom - ); +function isNestingGesture( point, rect, nestingLevel = 1, rtl = false ) { + const blockIndentPosition = rtl + ? rect.right - nestingLevel * NESTING_LEVEL_INDENTATION + : rect.left + nestingLevel * NESTING_LEVEL_INDENTATION; + + const isNestingHorizontalGesture = rtl + ? point.x < blockIndentPosition - NESTING_LEVEL_INDENTATION + : point.x > blockIndentPosition + NESTING_LEVEL_INDENTATION; + + return isNestingHorizontalGesture && point.y < rect.bottom; } // Block navigation is always a vertical list, so only allow dropping @@ -177,10 +199,11 @@ const ALLOWED_DROP_EDGES = [ 'top', 'bottom' ]; * * @param {WPListViewDropZoneBlocks} blocksData Data about the blocks in list view. * @param {WPPoint} position The point representing the cursor position when dragging. + * @param {boolean} rtl Whether the editor is in RTL mode. * * @return {WPListViewDropZoneTarget | undefined} An object containing data about the drop target. */ -export function getListViewDropTarget( blocksData, position ) { +export function getListViewDropTarget( blocksData, position, rtl = false ) { let candidateEdge; let candidateBlockData; let candidateDistance; @@ -255,12 +278,48 @@ export function getListViewDropTarget( blocksData, position ) { const isDraggingBelow = candidateEdge === 'bottom'; + // If the user is dragging towards the bottom of the block check whether + // they might be trying to nest the block as a child. + // If the block already has inner blocks, and is expanded, this should be treated + // as nesting since the next block in the tree will be the first child. + // However, if the block is collapsed, dragging beneath the block should + // still be allowed, as the next visible block in the tree will be a sibling. + if ( + isDraggingBelow && + candidateBlockData.canInsertDraggedBlocksAsChild && + ( ( candidateBlockData.innerBlockCount > 0 && + candidateBlockData.isExpanded ) || + isNestingGesture( + position, + candidateRect, + candidateBlockParents.length, + rtl + ) ) + ) { + // If the block is expanded, insert the block as the first child. + // Otherwise, for collapsed blocks, insert the block as the last child. + const newBlockIndex = candidateBlockData.isExpanded + ? 0 + : candidateBlockData.innerBlockCount || 0; + + return { + rootClientId: candidateBlockData.clientId, + blockIndex: newBlockIndex, + dropPosition: 'inside', + }; + } + // If the user is dragging towards the bottom of the block check whether // they might be trying to move the block to be at a parent level. if ( isDraggingBelow && candidateBlockData.rootClientId && - isUpGesture( position, candidateRect, candidateBlockParents.length ) + isUpGesture( + position, + candidateRect, + candidateBlockParents.length, + rtl + ) ) { const nextBlock = getNextNonDraggedBlock( blocksData, @@ -274,7 +333,8 @@ export function getListViewDropTarget( blocksData, position ) { const desiredRelativeLevel = getDesiredRelativeParentLevel( position, candidateRect, - candidateBlockParents.length + candidateBlockParents.length, + rtl ); const targetParentIndex = Math.max( @@ -321,36 +381,6 @@ export function getListViewDropTarget( blocksData, position ) { } } - // If the user is dragging towards the bottom of the block check whether - // they might be trying to nest the block as a child. - // If the block already has inner blocks, and is expanded, this should be treated - // as nesting since the next block in the tree will be the first child. - // However, if the block is collapsed, dragging beneath the block should - // still be allowed, as the next visible block in the tree will be a sibling. - if ( - isDraggingBelow && - candidateBlockData.canInsertDraggedBlocksAsChild && - ( ( candidateBlockData.innerBlockCount > 0 && - candidateBlockData.isExpanded ) || - isNestingGesture( - position, - candidateRect, - candidateBlockParents.length - ) ) - ) { - // If the block is expanded, insert the block as the first child. - // Otherwise, for collapsed blocks, insert the block as the last child. - const newBlockIndex = candidateBlockData.isExpanded - ? 0 - : candidateBlockData.innerBlockCount || 0; - - return { - rootClientId: candidateBlockData.clientId, - blockIndex: newBlockIndex, - dropPosition: 'inside', - }; - } - // If dropping as a sibling, but block cannot be inserted in // this context, return early. if ( ! candidateBlockData.canInsertDraggedBlocksAsSibling ) { @@ -388,6 +418,8 @@ export default function useListViewDropZone( { dropZoneElement } ) { const onBlockDrop = useOnBlockDrop( targetRootClientId, targetBlockIndex ); + const rtl = isRTL(); + const draggedBlockClientIds = getDraggedBlockClientIds(); const throttled = useThrottle( useCallback( @@ -433,13 +465,24 @@ export default function useListViewDropZone( { dropZoneElement } ) { }; } ); - const newTarget = getListViewDropTarget( blocksData, position ); + const newTarget = getListViewDropTarget( + blocksData, + position, + rtl + ); if ( newTarget ) { setTarget( newTarget ); } }, - [ draggedBlockClientIds ] + [ + canInsertBlocks, + draggedBlockClientIds, + getBlockCount, + getBlockIndex, + getBlockRootClientId, + rtl, + ] ), 200 );