From 8e563a7e681e124690173ef25c03cd3a74a3499f Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 13 May 2024 13:50:47 +0200 Subject: [PATCH 01/17] wip: parse rtl query param --- pages/app/page.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pages/app/page.tsx b/pages/app/page.tsx index bcb399ab..14afec90 100644 --- a/pages/app/page.tsx +++ b/pages/app/page.tsx @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import { Suspense } from "react"; +import { useSearchParams } from "react-router-dom"; import { pagesMap } from "../pages"; export interface PageProps { @@ -8,6 +10,10 @@ export interface PageProps { } export default function Page({ pageId }: PageProps) { + const [searchParams] = useSearchParams(); + const direction = searchParams.get("direction") ?? "ltr"; + document.documentElement.setAttribute("dir", direction); + const Component = pagesMap[pageId]; return ( From 99e666539b06f7199a15d31bc6da78d9032be7b2 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 13 May 2024 14:19:27 +0200 Subject: [PATCH 02/17] rtl support for keyboard commands --- src/board/transition.ts | 6 ++++-- src/internal/grid/grid.tsx | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/board/transition.ts b/src/board/transition.ts index d5af9959..86d18c17 100644 --- a/src/board/transition.ts +++ b/src/board/transition.ts @@ -297,11 +297,13 @@ function updateTransitionWithKeyboardEvent( } }; + const isRtl = document.documentElement.dir === "rtl"; + switch (direction) { case "left": - return updateManualItemTransition(transition, "left"); + return updateManualItemTransition(transition, !isRtl ? "left" : "right"); case "right": - return updateManualItemTransition(transition, "right"); + return updateManualItemTransition(transition, !isRtl ? "right" : "left"); case "up": return updateManualItemTransition(transition, "up"); case "down": diff --git a/src/internal/grid/grid.tsx b/src/internal/grid/grid.tsx index 5d08da23..80d46715 100644 --- a/src/internal/grid/grid.tsx +++ b/src/internal/grid/grid.tsx @@ -38,7 +38,11 @@ export default function Grid({ layout, children: render, columns }: GridProps) { return colspan * cellWidth + (colspan - 1) * gridGap; }; const getHeight = (rowspan: number) => rowspan * rowspanHeight + (rowspan - 1) * gridGap; - const getColOffset = (x: number) => getWidth(x) + gridGap; + const getColOffset = (x: number) => { + const offset = getWidth(x) + gridGap; + const isRtl = document.documentElement.dir === "rtl"; + return !isRtl ? offset : -offset; + }; const getRowOffset = (y: number) => getHeight(y) + gridGap; const gridContext = { getWidth, getHeight, getColOffset, getRowOffset }; From 66f6d1d3cfa46a89ea133947a7cdfd18c20368b4 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 13 May 2024 14:22:55 +0200 Subject: [PATCH 03/17] fix resize handle position --- src/board-item/styles.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/board-item/styles.scss b/src/board-item/styles.scss index bfbe84ad..af73e683 100644 --- a/src/board-item/styles.scss +++ b/src/board-item/styles.scss @@ -50,6 +50,6 @@ .resizer { position: absolute; // offset for inner paddings in the handle - bottom: calc(#{cs.$space-static-xs} - #{cs.$space-static-xxxs}); - right: calc(#{cs.$space-static-xs} - #{cs.$space-static-xxxs}); + inset-block-end: calc(#{cs.$space-static-xs} - #{cs.$space-static-xxxs}); + inset-inline-end: calc(#{cs.$space-static-xs} - #{cs.$space-static-xxxs}); } From ced3ec7a144d88300e6a88a93d7c1e359b7f9485 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 13 May 2024 14:58:24 +0200 Subject: [PATCH 04/17] rtl support for pointer events --- src/board/transition.ts | 8 ++- src/internal/item-container/index.tsx | 23 ++++++--- src/internal/utils/coordinates.ts | 5 +- src/internal/utils/screen.ts | 70 +++++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/src/board/transition.ts b/src/board/transition.ts index 86d18c17..b5e97a97 100644 --- a/src/board/transition.ts +++ b/src/board/transition.ts @@ -7,6 +7,7 @@ import { LayoutEngine } from "../internal/layout-engine/engine"; import { Coordinates } from "../internal/utils/coordinates"; import { getDefaultColumnSpan, getDefaultRowSpan, getMinColumnSpan, getMinRowSpan } from "../internal/utils/layout"; import { Position } from "../internal/utils/position"; +import { getLogicalBoundingClientRect } from "../internal/utils/screen"; import { BoardProps, RemoveTransition, Transition, TransitionAnnouncement } from "./interfaces"; import { createOperationAnnouncement } from "./utils/announcements"; import { getHoveredRect } from "./utils/get-hovered-rect"; @@ -323,9 +324,12 @@ function acquireTransitionItem( const { columns } = transition.itemsLayout; - const layoutRect = layoutElement.getBoundingClientRect(); + const layoutRect = getLogicalBoundingClientRect(layoutElement); const itemRect = transition.draggableRect; - const offset = new Coordinates({ x: itemRect.left - layoutRect.x, y: itemRect.top - layoutRect.y }); + const isRtl = document.documentElement.dir === "rtl"; + // TODO: update + const coordinatesX = !isRtl ? itemRect.left - layoutRect.left : itemRect.left - layoutRect.left; + const offset = new Coordinates({ x: coordinatesX, y: itemRect.top - layoutRect.top }); const insertionDirection = getInsertionDirection(offset); // Update original insertion position if the item can't fit into the layout by width. diff --git a/src/internal/item-container/index.tsx b/src/internal/item-container/index.tsx index e217f09d..92f87242 100644 --- a/src/internal/item-container/index.tsx +++ b/src/internal/item-container/index.tsx @@ -28,7 +28,7 @@ import { } from "../dnd-controller/controller"; import { BoardItemDefinitionBase, Direction, ItemId, Transform } from "../interfaces"; import { Coordinates } from "../utils/coordinates"; -import { getNormalizedElementRect } from "../utils/screen"; +import { getLogicalBoundingClientRect, getLogicalClientX, getNormalizedElementRect } from "../utils/screen"; import { throttle } from "../utils/throttle"; import { getCollisionRect } from "./get-collision-rect"; import { getNextDroppable } from "./get-next-droppable"; @@ -308,8 +308,10 @@ function ItemContainerComponent( function onDragHandlePointerDown(event: ReactPointerEvent) { // Calculate the offset between item's top-left corner and the pointer landing position. - const rect = itemRef.current!.getBoundingClientRect(); - pointerOffsetRef.current = new Coordinates({ x: event.clientX - rect.left, y: event.clientY - rect.top }); + const rect = getLogicalBoundingClientRect(itemRef.current!); + const clientX = getLogicalClientX(event); + const clientY = event.clientY; + pointerOffsetRef.current = new Coordinates({ x: clientX - rect.left, y: clientY - rect.top }); originalSizeRef.current = { width: rect.width, height: rect.height }; pointerBoundariesRef.current = null; @@ -322,16 +324,18 @@ function ItemContainerComponent( function onResizeHandlePointerDown(event: ReactPointerEvent) { // Calculate the offset between item's bottom-right corner and the pointer landing position. - const rect = itemRef.current!.getBoundingClientRect(); - pointerOffsetRef.current = new Coordinates({ x: event.clientX - rect.right, y: event.clientY - rect.bottom }); + const rect = getLogicalBoundingClientRect(itemRef.current!); + const clientX = getLogicalClientX(event); + const clientY = event.clientY; + pointerOffsetRef.current = new Coordinates({ x: clientX - rect.right, y: clientY - rect.bottom }); originalSizeRef.current = { width: rect.width, height: rect.height }; // Calculate boundaries below which the cursor cannot move. const minWidth = getItemSize(null).minWidth; const minHeight = getItemSize(null).minHeight; pointerBoundariesRef.current = new Coordinates({ - x: event.clientX - rect.width + minWidth, - y: event.clientY - rect.height + minHeight, + x: clientX - rect.width + minWidth, + y: clientY - rect.height + minHeight, }); draggableApi.start("resize", "pointer", Coordinates.fromEvent(event)); @@ -349,9 +353,12 @@ function ItemContainerComponent( } if (transition && transition.interactionType === "pointer") { + const isRtl = document.documentElement.dir === "rtl"; + const property = !isRtl ? "left" : "right"; + // Adjust the dragged/resized item to the pointer's location. itemTransitionClassNames.push(transition.operation === "resize" ? styles.resized : styles.dragged); - itemTransitionStyle.left = transition.positionTransform?.x; + itemTransitionStyle[property] = transition.positionTransform?.x; itemTransitionStyle.top = transition.positionTransform?.y; itemTransitionStyle.width = transition.sizeTransform?.width; itemTransitionStyle.height = transition.sizeTransform?.height; diff --git a/src/internal/utils/coordinates.ts b/src/internal/utils/coordinates.ts index d496e591..32ccc75d 100644 --- a/src/internal/utils/coordinates.ts +++ b/src/internal/utils/coordinates.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { PointerEvent as ReactPointerEvent } from "react"; +import { getLogicalClientX } from "./screen"; export class Coordinates { readonly __type = "Coordinates"; @@ -11,7 +12,9 @@ export class Coordinates { readonly scrollY = window.scrollY; static fromEvent(event: PointerEvent | ReactPointerEvent): Coordinates { - return new Coordinates({ x: event.clientX, y: event.clientY }); + const clientX = getLogicalClientX(event); + const clientY = event.clientY; + return new Coordinates({ x: clientX, y: clientY }); } static cursorOffset(current: Coordinates, start: Coordinates): Coordinates { diff --git a/src/internal/utils/screen.ts b/src/internal/utils/screen.ts index 306a5cc0..496326c3 100644 --- a/src/internal/utils/screen.ts +++ b/src/internal/utils/screen.ts @@ -1,18 +1,78 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -export function getNormalizedElementRect(element: HTMLElement): DOMRect { - const { x, y, left, right, top, bottom, width, height } = element.getBoundingClientRect(); +import { PointerEvent as ReactPointerEvent } from "react"; + +export function getNormalizedElementRect(element: HTMLElement): { + left: number; + right: number; + top: number; + bottom: number; + width: number; + height: number; +} { + const { left, right, top, bottom, width, height } = getLogicalBoundingClientRect(element); const xOffset = element.ownerDocument.defaultView!.pageXOffset - window.scrollX; const yOffset = element.ownerDocument.defaultView!.pageYOffset - window.scrollY; return { - x: x + xOffset, left: left + xOffset, right: right + xOffset, - y: y + yOffset, top: top + yOffset, bottom: bottom + yOffset, width: width, height: height, - } as DOMRect; + }; +} + +// The below code is copied from components/src/internal/direction.ts + +export function getLogicalBoundingClientRect(element: HTMLElement | SVGElement) { + const boundingClientRect = element.getBoundingClientRect(); + + const blockSize = boundingClientRect.height; + const inlineSize = boundingClientRect.width; + const insetBlockStart = boundingClientRect.top; + const insetBlockEnd = boundingClientRect.bottom; + const insetInlineStart = getIsRtl(element) + ? document.documentElement.clientWidth - boundingClientRect.right + : boundingClientRect.left; + const insetInlineEnd = insetInlineStart + inlineSize; + + return { + height: blockSize, + width: inlineSize, + top: insetBlockStart, + bottom: insetBlockEnd, + left: insetInlineStart, + right: insetInlineEnd, + }; +} + +export function getIsRtl(element: HTMLElement | SVGElement) { + return getComputedStyle(element).direction === "rtl"; +} + +export function getOffsetInlineStart(element: HTMLElement) { + const offsetParentWidth = element.offsetParent?.clientWidth ?? 0; + return getIsRtl(element) ? offsetParentWidth - element.offsetWidth - element.offsetLeft : element.offsetLeft; +} + +/** + * The scrollLeft value will be a negative number if the direction is RTL and + * needs to be converted to a positive value for direction independent scroll + * computations. Additionally, the scrollLeft value can be a decimal value on + * systems using display scaling requiring the floor and ceiling calls. + */ +export function getScrollInlineStart(element: HTMLElement) { + return getIsRtl(element) ? Math.floor(element.scrollLeft) * -1 : Math.ceil(element.scrollLeft); +} + +/** + * The clientX position needs to be converted so it is relative to the right of + * the document in order for computations to yield the same result in both + * element directions. + */ +export function getLogicalClientX(event: PointerEvent | ReactPointerEvent) { + const isRtl = document.documentElement.dir === "rtl"; + return isRtl ? document.documentElement.clientWidth - event.clientX : event.clientX; } From b360772a32cf8d4a3f34dc9a96a433866d7a3bb9 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 13 May 2024 15:02:27 +0200 Subject: [PATCH 05/17] fix pages button dropdown --- pages/dnd/engine-page-template.tsx | 1 + pages/widget-container/permutations.page.tsx | 3 +++ pages/with-app-layout/widgets-board.tsx | 1 + 3 files changed, 5 insertions(+) diff --git a/pages/dnd/engine-page-template.tsx b/pages/dnd/engine-page-template.tsx index 5b5564db..dc917047 100644 --- a/pages/dnd/engine-page-template.tsx +++ b/pages/dnd/engine-page-template.tsx @@ -41,6 +41,7 @@ export function EnginePageTemplate({ ariaLabel="Widget settings" variant="icon" onItemClick={() => actions.removeItem()} + expandToViewport={true} /> } i18nStrings={boardItemI18nStrings} diff --git a/pages/widget-container/permutations.page.tsx b/pages/widget-container/permutations.page.tsx index c88bccff..a5820112 100644 --- a/pages/widget-container/permutations.page.tsx +++ b/pages/widget-container/permutations.page.tsx @@ -56,6 +56,7 @@ export default function WidgetContainerPermutations() { { id: "two", text: "Two" }, ]} variant="icon" + expandToViewport={true} /> } i18nStrings={i18nStrings.boardItemI18nStrings} @@ -91,6 +92,7 @@ export default function WidgetContainerPermutations() { { id: "two", text: "Two" }, ]} variant="icon" + expandToViewport={true} /> } i18nStrings={i18nStrings.boardItemI18nStrings} @@ -119,6 +121,7 @@ export default function WidgetContainerPermutations() { { id: "two", text: "Two" }, ]} variant="icon" + expandToViewport={true} /> } i18nStrings={i18nStrings.boardItemI18nStrings} diff --git a/pages/with-app-layout/widgets-board.tsx b/pages/with-app-layout/widgets-board.tsx index 79fac5be..e0083067 100644 --- a/pages/with-app-layout/widgets-board.tsx +++ b/pages/with-app-layout/widgets-board.tsx @@ -44,6 +44,7 @@ export function WidgetsBoard({ loading, widgets, onWidgetsChange }: WidgetsBoard ariaLabel={clientI18nStrings.widgetsBoard.widgetSettings} variant="icon" onItemClick={() => setDeleteConfirmation(item.id)} + expandToViewport={true} /> } i18nStrings={boardItemI18nStrings} From 9e2103c034ed4effe72694d23f5e53c14d7e5057 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Mon, 13 May 2024 15:13:29 +0200 Subject: [PATCH 06/17] fix keyboard insertion direction --- src/board/transition.ts | 4 +--- src/internal/utils/rects.ts | 7 +++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/board/transition.ts b/src/board/transition.ts index b5e97a97..f58680fb 100644 --- a/src/board/transition.ts +++ b/src/board/transition.ts @@ -326,9 +326,7 @@ function acquireTransitionItem( const layoutRect = getLogicalBoundingClientRect(layoutElement); const itemRect = transition.draggableRect; - const isRtl = document.documentElement.dir === "rtl"; - // TODO: update - const coordinatesX = !isRtl ? itemRect.left - layoutRect.left : itemRect.left - layoutRect.left; + const coordinatesX = itemRect.left - layoutRect.left; const offset = new Coordinates({ x: coordinatesX, y: itemRect.top - layoutRect.top }); const insertionDirection = getInsertionDirection(offset); diff --git a/src/internal/utils/rects.ts b/src/internal/utils/rects.ts index 3c56cc6e..bc2abda2 100644 --- a/src/internal/utils/rects.ts +++ b/src/internal/utils/rects.ts @@ -72,6 +72,13 @@ export function getClosestNeighbor(target: Rect, sources: readonly Rect[], direc const verticalDiff = (r1: Rect, r2: Rect) => Math.abs(r1.top - target.top) - Math.abs(r2.top - target.top); const horizontalDiff = (r1: Rect, r2: Rect) => Math.abs(r1.left - target.left) - Math.abs(r2.left - target.left); + const isRtl = document.documentElement.dir === "rtl"; + if (isRtl && direction === "left") { + direction = "right"; + } else if (isRtl && direction === "right") { + direction = "left"; + } + switch (direction) { case "left": return getFirst( From b09eae1a718f850491db9c6f558e1066d15262b8 Mon Sep 17 00:00:00 2001 From: Scott O'Brien Date: Mon, 20 May 2024 13:13:02 -0400 Subject: [PATCH 07/17] Adjust cursor in RTL. --- src/internal/global-drag-state-styles/styles.scss | 4 ++++ src/internal/resize-handle/styles.scss | 4 ++++ src/internal/shared.scss | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/src/internal/global-drag-state-styles/styles.scss b/src/internal/global-drag-state-styles/styles.scss index 72b41648..e565ea16 100644 --- a/src/internal/global-drag-state-styles/styles.scss +++ b/src/internal/global-drag-state-styles/styles.scss @@ -4,6 +4,10 @@ .show-resize-cursor * { cursor: nwse-resize; + + @include shared.with-direction('rtl') { + cursor: nesw-resize; + } } .disable-selection * { diff --git a/src/internal/resize-handle/styles.scss b/src/internal/resize-handle/styles.scss index f10f1ea9..6c852392 100644 --- a/src/internal/resize-handle/styles.scss +++ b/src/internal/resize-handle/styles.scss @@ -2,6 +2,10 @@ .handle { cursor: nwse-resize; + + @include shared.with-direction('rtl') { + cursor: nesw-resize; + } } .handle:not(.active):focus-visible { diff --git a/src/internal/shared.scss b/src/internal/shared.scss index eab76e15..2a37e426 100644 --- a/src/internal/shared.scss +++ b/src/internal/shared.scss @@ -19,3 +19,9 @@ border: 2px solid cs.$color-border-item-focused; } } + +@mixin with-direction($direction) { + &:dir(#{$direction}) { + @content; + } +} From c9803b3f3ef0c302bbb6d07678c1bbacf2e28d20 Mon Sep 17 00:00:00 2001 From: Scott O'Brien Date: Mon, 20 May 2024 13:15:09 -0400 Subject: [PATCH 08/17] Migrate to logical properties. --- src/board-item/styles.scss | 15 ++++++++------- src/board/styles.scss | 8 +++++--- src/internal/handle/styles.scss | 3 ++- src/internal/item-container/styles.scss | 2 +- .../screenreader-grid-navigation/styles.scss | 3 ++- src/internal/screenreader-only/styles.scss | 4 ++-- src/internal/shared.scss | 8 ++++---- 7 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/board-item/styles.scss b/src/board-item/styles.scss index af73e683..92f9a012 100644 --- a/src/board-item/styles.scss +++ b/src/board-item/styles.scss @@ -17,7 +17,8 @@ .header { display: flex; justify-items: center; - padding: cs.$space-scaled-s calc(#{cs.$space-container-horizontal} - #{cs.$space-scaled-xs}); + padding-block: cs.$space-scaled-s; + padding-inline: calc(#{cs.$space-container-horizontal} - #{cs.$space-scaled-xs}); } .flexible { @@ -25,21 +26,21 @@ } .handle { - margin-top: calc(cs.$space-scaled-xxs + 1px); + margin-block-start: calc(cs.$space-scaled-xxs + 1px); .refresh > & { - margin-top: calc(cs.$space-static-xxxs + 1px); + margin-block-start: calc(cs.$space-static-xxxs + 1px); } } .header-content { - margin-left: cs.$space-scaled-xxs; + margin-inline-start: cs.$space-scaled-xxs; } .settings { - margin-top: calc(cs.$space-scaled-xxxs + 1px); - margin-left: cs.$space-static-xs; + margin-block-start: calc(cs.$space-scaled-xxxs + 1px); + margin-inline-start: cs.$space-static-xs; .refresh > & { - margin-top: 0px; + margin-block-start: 0px; } } diff --git a/src/board/styles.scss b/src/board/styles.scss index 8b080722..a02676f3 100644 --- a/src/board/styles.scss +++ b/src/board/styles.scss @@ -2,7 +2,7 @@ .placeholder { border-radius: cs.$border-radius-container; - height: 100%; + block-size: 100%; &--active { background-color: cs.$color-board-placeholder-active; } @@ -17,8 +17,10 @@ .empty { box-sizing: border-box; - width: 100%; - padding: cs.$space-scaled-m cs.$space-scaled-l cs.$space-scaled-l; + inline-size: 100%; + padding-block-start: cs.$space-scaled-m; + padding-block-end: cs.$space-scaled-l; + padding-inline: cs.$space-scaled-l; color: cs.$color-text-empty; display: flex; justify-content: center; diff --git a/src/internal/handle/styles.scss b/src/internal/handle/styles.scss index e237ffff..53b7c437 100644 --- a/src/internal/handle/styles.scss +++ b/src/internal/handle/styles.scss @@ -4,7 +4,8 @@ appearance: none; background: transparent; border: none; - padding: cs.$space-scaled-xxs; + padding-block: cs.$space-scaled-xxs; + padding-inline: cs.$space-scaled-xxs; color: cs.$color-text-interactive-default; diff --git a/src/internal/item-container/styles.scss b/src/internal/item-container/styles.scss index 0fdf328d..3fe6503d 100644 --- a/src/internal/item-container/styles.scss +++ b/src/internal/item-container/styles.scss @@ -1,7 +1,7 @@ .root { touch-action: none; position: relative; - height: 100%; + block-size: 100%; } .inTransition { diff --git a/src/internal/screenreader-grid-navigation/styles.scss b/src/internal/screenreader-grid-navigation/styles.scss index 12c6678a..3d9d280d 100644 --- a/src/internal/screenreader-grid-navigation/styles.scss +++ b/src/internal/screenreader-grid-navigation/styles.scss @@ -5,7 +5,8 @@ .screen-reader-navigation-visible { position: fixed; background: white; - padding: 8px; + padding-block: 8px; + padding-inline: 8px; border: 1px solid black; z-index: 10001; } diff --git a/src/internal/screenreader-only/styles.scss b/src/internal/screenreader-only/styles.scss index 7e75bce8..5f6bd960 100644 --- a/src/internal/screenreader-only/styles.scss +++ b/src/internal/screenreader-only/styles.scss @@ -5,6 +5,6 @@ .root { position: absolute !important; - top: -9999px !important; - left: -9999px !important; + inset-block-start: -9999px !important; + inset-inline-start: -9999px !important; } diff --git a/src/internal/shared.scss b/src/internal/shared.scss index 2a37e426..349f0cc7 100644 --- a/src/internal/shared.scss +++ b/src/internal/shared.scss @@ -11,10 +11,10 @@ display: block; position: absolute; box-sizing: border-box; - left: calc(-1 * #{$gutter}); - top: calc(-1 * #{$gutter}); - width: calc(100% + 2 * #{$gutter}); - height: calc(100% + 2 * #{$gutter}); + inset-inline-start: calc(-1 * #{$gutter}); + inset-block-start: calc(-1 * #{$gutter}); + inline-size: calc(100% + 2 * #{$gutter}); + block-size: calc(100% + 2 * #{$gutter}); border-radius: $border-radius; border: 2px solid cs.$color-border-item-focused; } From d11b203bc5a9a33bb9e190501a6662be747ae9a4 Mon Sep 17 00:00:00 2001 From: Scott O'Brien Date: Mon, 20 May 2024 13:19:00 -0400 Subject: [PATCH 09/17] Add missing import. --- src/internal/global-drag-state-styles/styles.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/internal/global-drag-state-styles/styles.scss b/src/internal/global-drag-state-styles/styles.scss index e565ea16..2a67ee04 100644 --- a/src/internal/global-drag-state-styles/styles.scss +++ b/src/internal/global-drag-state-styles/styles.scss @@ -1,3 +1,5 @@ +@use "../shared.scss" as shared; + .show-grab-cursor * { cursor: grabbing; } From 955c1999283f9e3cfa894b65803e0c8992c35956 Mon Sep 17 00:00:00 2001 From: Scott O'Brien Date: Mon, 20 May 2024 14:05:08 -0400 Subject: [PATCH 10/17] Miscellaneous PR updates. --- src/board/transition.ts | 8 +-- src/internal/grid/grid.tsx | 3 +- src/internal/item-container/index.tsx | 33 ++++++------ src/internal/utils/coordinates.ts | 5 +- src/internal/utils/rects.ts | 3 +- src/internal/utils/screen.ts | 76 +++++++++++---------------- 6 files changed, 60 insertions(+), 68 deletions(-) diff --git a/src/board/transition.ts b/src/board/transition.ts index f58680fb..1d7d985a 100644 --- a/src/board/transition.ts +++ b/src/board/transition.ts @@ -7,7 +7,7 @@ import { LayoutEngine } from "../internal/layout-engine/engine"; import { Coordinates } from "../internal/utils/coordinates"; import { getDefaultColumnSpan, getDefaultRowSpan, getMinColumnSpan, getMinRowSpan } from "../internal/utils/layout"; import { Position } from "../internal/utils/position"; -import { getLogicalBoundingClientRect } from "../internal/utils/screen"; +import { getIsRtl, getLogicalBoundingClientRect } from "../internal/utils/screen"; import { BoardProps, RemoveTransition, Transition, TransitionAnnouncement } from "./interfaces"; import { createOperationAnnouncement } from "./utils/announcements"; import { getHoveredRect } from "./utils/get-hovered-rect"; @@ -298,7 +298,7 @@ function updateTransitionWithKeyboardEvent( } }; - const isRtl = document.documentElement.dir === "rtl"; + const isRtl = getIsRtl(document.documentElement); switch (direction) { case "left": @@ -326,8 +326,8 @@ function acquireTransitionItem( const layoutRect = getLogicalBoundingClientRect(layoutElement); const itemRect = transition.draggableRect; - const coordinatesX = itemRect.left - layoutRect.left; - const offset = new Coordinates({ x: coordinatesX, y: itemRect.top - layoutRect.top }); + const coordinatesX = itemRect.left - layoutRect.insetInlineStart; + const offset = new Coordinates({ x: coordinatesX, y: itemRect.top - layoutRect.insetBlockStart }); const insertionDirection = getInsertionDirection(offset); // Update original insertion position if the item can't fit into the layout by width. diff --git a/src/internal/grid/grid.tsx b/src/internal/grid/grid.tsx index 80d46715..113179fb 100644 --- a/src/internal/grid/grid.tsx +++ b/src/internal/grid/grid.tsx @@ -5,6 +5,7 @@ import { useContainerQuery } from "@cloudscape-design/component-toolkit"; import { useDensityMode } from "@cloudscape-design/component-toolkit/internal"; import clsx from "clsx"; import { Children, useRef } from "react"; +import { getIsRtl } from "../../internal/utils/screen"; import { useMergeRefs } from "../utils/use-merge-refs"; import { zipTwoArrays } from "../utils/zip-arrays"; @@ -30,6 +31,7 @@ export default function Grid({ layout, children: render, columns }: GridProps) { const densityMode = useDensityMode(gridRef); const gridGap = GRID_GAP[densityMode]; const rowspanHeight = ROWSPAN_HEIGHT[densityMode]; + const isRtl = gridRef.current ? getIsRtl(gridRef.current) : false; // The below getters translate relative grid units into size/offset values in pixels. const getWidth = (colspan: number) => { @@ -40,7 +42,6 @@ export default function Grid({ layout, children: render, columns }: GridProps) { const getHeight = (rowspan: number) => rowspan * rowspanHeight + (rowspan - 1) * gridGap; const getColOffset = (x: number) => { const offset = getWidth(x) + gridGap; - const isRtl = document.documentElement.dir === "rtl"; return !isRtl ? offset : -offset; }; const getRowOffset = (y: number) => getHeight(y) + gridGap; diff --git a/src/internal/item-container/index.tsx b/src/internal/item-container/index.tsx index 92f87242..eb85f5ee 100644 --- a/src/internal/item-container/index.tsx +++ b/src/internal/item-container/index.tsx @@ -28,7 +28,7 @@ import { } from "../dnd-controller/controller"; import { BoardItemDefinitionBase, Direction, ItemId, Transform } from "../interfaces"; import { Coordinates } from "../utils/coordinates"; -import { getLogicalBoundingClientRect, getLogicalClientX, getNormalizedElementRect } from "../utils/screen"; +import { getIsRtl, getLogicalBoundingClientRect, getLogicalClientX, getNormalizedElementRect } from "../utils/screen"; import { throttle } from "../utils/throttle"; import { getCollisionRect } from "./get-collision-rect"; import { getNextDroppable } from "./get-next-droppable"; @@ -114,6 +114,7 @@ function ItemContainerComponent( const [isHidden, setIsHidden] = useState(false); const muteEventsRef = useRef(false); const itemRef = useRef(null); + const isRtl = itemRef.current ? getIsRtl(itemRef.current) : false; const draggableApi = useDraggable({ draggableItem: item, getCollisionRect: (operation, coordinates, dropTarget) => { @@ -309,10 +310,13 @@ function ItemContainerComponent( function onDragHandlePointerDown(event: ReactPointerEvent) { // Calculate the offset between item's top-left corner and the pointer landing position. const rect = getLogicalBoundingClientRect(itemRef.current!); - const clientX = getLogicalClientX(event); + const clientX = getLogicalClientX(event, isRtl); const clientY = event.clientY; - pointerOffsetRef.current = new Coordinates({ x: clientX - rect.left, y: clientY - rect.top }); - originalSizeRef.current = { width: rect.width, height: rect.height }; + pointerOffsetRef.current = new Coordinates({ + x: clientX - rect.insetInlineStart, + y: clientY - rect.insetBlockStart, + }); + originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; pointerBoundariesRef.current = null; draggableApi.start(!placed ? "insert" : "reorder", "pointer", Coordinates.fromEvent(event)); @@ -325,17 +329,17 @@ function ItemContainerComponent( function onResizeHandlePointerDown(event: ReactPointerEvent) { // Calculate the offset between item's bottom-right corner and the pointer landing position. const rect = getLogicalBoundingClientRect(itemRef.current!); - const clientX = getLogicalClientX(event); + const clientX = getLogicalClientX(event, isRtl); const clientY = event.clientY; - pointerOffsetRef.current = new Coordinates({ x: clientX - rect.right, y: clientY - rect.bottom }); - originalSizeRef.current = { width: rect.width, height: rect.height }; + pointerOffsetRef.current = new Coordinates({ x: clientX - rect.insetInlineEnd, y: clientY - rect.insetBlockEnd }); + originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; // Calculate boundaries below which the cursor cannot move. const minWidth = getItemSize(null).minWidth; const minHeight = getItemSize(null).minHeight; pointerBoundariesRef.current = new Coordinates({ - x: clientX - rect.width + minWidth, - y: clientY - rect.height + minHeight, + x: clientX - rect.inlineSize + minWidth, + y: clientY - rect.blockSize + minHeight, }); draggableApi.start("resize", "pointer", Coordinates.fromEvent(event)); @@ -353,15 +357,12 @@ function ItemContainerComponent( } if (transition && transition.interactionType === "pointer") { - const isRtl = document.documentElement.dir === "rtl"; - const property = !isRtl ? "left" : "right"; - // Adjust the dragged/resized item to the pointer's location. itemTransitionClassNames.push(transition.operation === "resize" ? styles.resized : styles.dragged); - itemTransitionStyle[property] = transition.positionTransform?.x; - itemTransitionStyle.top = transition.positionTransform?.y; - itemTransitionStyle.width = transition.sizeTransform?.width; - itemTransitionStyle.height = transition.sizeTransform?.height; + itemTransitionStyle.insetInlineStart = transition.positionTransform?.x; + itemTransitionStyle.insetBlockStart = transition.positionTransform?.y; + itemTransitionStyle.inlineSize = transition.sizeTransform?.width; + itemTransitionStyle.blockSize = transition.sizeTransform?.height; itemTransitionStyle.pointerEvents = "none"; } diff --git a/src/internal/utils/coordinates.ts b/src/internal/utils/coordinates.ts index 32ccc75d..107754fa 100644 --- a/src/internal/utils/coordinates.ts +++ b/src/internal/utils/coordinates.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { PointerEvent as ReactPointerEvent } from "react"; -import { getLogicalClientX } from "./screen"; +import { getIsRtl, getLogicalClientX } from "./screen"; export class Coordinates { readonly __type = "Coordinates"; @@ -12,7 +12,8 @@ export class Coordinates { readonly scrollY = window.scrollY; static fromEvent(event: PointerEvent | ReactPointerEvent): Coordinates { - const clientX = getLogicalClientX(event); + const isRtl = getIsRtl(document.documentElement); + const clientX = getLogicalClientX(event, isRtl); const clientY = event.clientY; return new Coordinates({ x: clientX, y: clientY }); } diff --git a/src/internal/utils/rects.ts b/src/internal/utils/rects.ts index bc2abda2..5ff6e1b3 100644 --- a/src/internal/utils/rects.ts +++ b/src/internal/utils/rects.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Direction, Rect } from "../interfaces"; +import { getIsRtl } from "./screen"; export function isInside(rect: Rect, bounds: Rect) { return ( @@ -68,11 +69,11 @@ export function getGridPlacement(target: Rect, grid: readonly Rect[]): Rect { } export function getClosestNeighbor(target: Rect, sources: readonly Rect[], direction: Direction): null | Rect { + const isRtl = getIsRtl(document.documentElement); const getFirst = (rects: Rect[]) => rects[0] ?? null; const verticalDiff = (r1: Rect, r2: Rect) => Math.abs(r1.top - target.top) - Math.abs(r2.top - target.top); const horizontalDiff = (r1: Rect, r2: Rect) => Math.abs(r1.left - target.left) - Math.abs(r2.left - target.left); - const isRtl = document.documentElement.dir === "rtl"; if (isRtl && direction === "left") { direction = "right"; } else if (isRtl && direction === "right") { diff --git a/src/internal/utils/screen.ts b/src/internal/utils/screen.ts index 496326c3..b9494e5c 100644 --- a/src/internal/utils/screen.ts +++ b/src/internal/utils/screen.ts @@ -1,6 +1,5 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 - import { PointerEvent as ReactPointerEvent } from "react"; export function getNormalizedElementRect(element: HTMLElement): { @@ -11,21 +10,39 @@ export function getNormalizedElementRect(element: HTMLElement): { width: number; height: number; } { - const { left, right, top, bottom, width, height } = getLogicalBoundingClientRect(element); + const { insetInlineStart, insetInlineEnd, insetBlockStart, insetBlockEnd, inlineSize, blockSize } = + getLogicalBoundingClientRect(element); const xOffset = element.ownerDocument.defaultView!.pageXOffset - window.scrollX; const yOffset = element.ownerDocument.defaultView!.pageYOffset - window.scrollY; return { - left: left + xOffset, - right: right + xOffset, - top: top + yOffset, - bottom: bottom + yOffset, - width: width, - height: height, + left: insetInlineStart + xOffset, + right: insetInlineEnd + xOffset, + top: insetBlockStart + yOffset, + bottom: insetBlockEnd + yOffset, + width: inlineSize, + height: blockSize, }; } -// The below code is copied from components/src/internal/direction.ts +export function getIsRtl(element: HTMLElement | SVGElement) { + return getComputedStyle(element).direction === "rtl"; +} +/** + * The clientX position needs to be converted so it is relative to the right of + * the document in order for computations to yield the same result in both + * element directions. + */ +export function getLogicalClientX(event: PointerEvent | ReactPointerEvent, IsRtl: boolean) { + return IsRtl ? document.documentElement.clientWidth - event.clientX : event.clientX; +} + +/** + * The getBoundingClientRect() function returns values relative to the top left + * corner of the document regardless of document direction. The left/right position + * will be transformed to insetInlineStart based on element direction in order to + * support direction agnostic position computation. + */ export function getLogicalBoundingClientRect(element: HTMLElement | SVGElement) { const boundingClientRect = element.getBoundingClientRect(); @@ -39,40 +56,11 @@ export function getLogicalBoundingClientRect(element: HTMLElement | SVGElement) const insetInlineEnd = insetInlineStart + inlineSize; return { - height: blockSize, - width: inlineSize, - top: insetBlockStart, - bottom: insetBlockEnd, - left: insetInlineStart, - right: insetInlineEnd, + blockSize, + inlineSize, + insetBlockStart, + insetBlockEnd, + insetInlineStart, + insetInlineEnd, }; } - -export function getIsRtl(element: HTMLElement | SVGElement) { - return getComputedStyle(element).direction === "rtl"; -} - -export function getOffsetInlineStart(element: HTMLElement) { - const offsetParentWidth = element.offsetParent?.clientWidth ?? 0; - return getIsRtl(element) ? offsetParentWidth - element.offsetWidth - element.offsetLeft : element.offsetLeft; -} - -/** - * The scrollLeft value will be a negative number if the direction is RTL and - * needs to be converted to a positive value for direction independent scroll - * computations. Additionally, the scrollLeft value can be a decimal value on - * systems using display scaling requiring the floor and ceiling calls. - */ -export function getScrollInlineStart(element: HTMLElement) { - return getIsRtl(element) ? Math.floor(element.scrollLeft) * -1 : Math.ceil(element.scrollLeft); -} - -/** - * The clientX position needs to be converted so it is relative to the right of - * the document in order for computations to yield the same result in both - * element directions. - */ -export function getLogicalClientX(event: PointerEvent | ReactPointerEvent) { - const isRtl = document.documentElement.dir === "rtl"; - return isRtl ? document.documentElement.clientWidth - event.clientX : event.clientX; -} From 5fd1bc32ab0e9234def9437dd278cba8442f301b Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Tue, 21 May 2024 10:28:43 +0200 Subject: [PATCH 11/17] fix in-board check --- src/board/internal.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/board/internal.tsx b/src/board/internal.tsx index aca6e09c..1b2483fa 100644 --- a/src/board/internal.tsx +++ b/src/board/internal.tsx @@ -22,6 +22,7 @@ import { interpretItems, } from "../internal/utils/layout"; import { Position } from "../internal/utils/position"; +import { getIsRtl } from "../internal/utils/screen"; import { useAutoScroll } from "../internal/utils/use-auto-scroll"; import { useMergeRefs } from "../internal/utils/use-merge-refs"; @@ -112,11 +113,15 @@ export function InternalBoard({ function isElementOverBoard(rect: Rect) { const board = containerAccessRef.current!; const boardContains = (target: null | Element) => board === target || board.contains(target); + const isRtl = getIsRtl(document.documentElement); + const left = !isRtl ? rect.left : document.documentElement.clientWidth - rect.left; + const right = !isRtl ? rect.right : document.documentElement.clientWidth - rect.right; + const { top, bottom } = rect; return ( - boardContains(document.elementFromPoint(rect.left, rect.top)) || - boardContains(document.elementFromPoint(rect.right, rect.top)) || - boardContains(document.elementFromPoint(rect.right, rect.bottom)) || - boardContains(document.elementFromPoint(rect.left, rect.bottom)) + boardContains(document.elementFromPoint(left, top)) || + boardContains(document.elementFromPoint(right, top)) || + boardContains(document.elementFromPoint(right, bottom)) || + boardContains(document.elementFromPoint(left, bottom)) ); } From 70711ebb7947caa04a0fde83a5d5591c4a6d08ae Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Wed, 22 May 2024 11:30:16 +0200 Subject: [PATCH 12/17] refactor get-is-rtl uses --- src/board/internal.tsx | 12 +++- src/board/transition.ts | 49 +++++++------ src/internal/grid/grid.tsx | 4 +- src/internal/grid/interfaces.ts | 1 + .../__tests__/get-next-droppable.test.ts | 15 ++-- .../__tests__/item-container.test.tsx | 1 + .../item-container/get-next-droppable.ts | 23 ++++-- src/internal/item-container/index.tsx | 14 ++-- src/internal/utils/__tests__/rects.test.ts | 72 ++++++++++++++++--- src/internal/utils/coordinates.ts | 5 +- src/internal/utils/rects.ts | 14 +++- src/internal/utils/screen.ts | 9 ++- src/items-palette/internal.tsx | 4 ++ 13 files changed, 159 insertions(+), 64 deletions(-) diff --git a/src/board/internal.tsx b/src/board/internal.tsx index 1b2483fa..24b74c6d 100644 --- a/src/board/internal.tsx +++ b/src/board/internal.tsx @@ -48,11 +48,13 @@ export function InternalBoard({ const containerRef = useMergeRefs(containerAccessRef, containerQueryRef); const itemContainerRef = useRef<{ [id: ItemId]: ItemContainerRef }>({}); + const isRtl = getIsRtl(containerAccessRef.current); + useGlobalDragStateStyles(); const autoScrollHandlers = useAutoScroll(); - const [transitionState, dispatch] = useTransition(); + const [transitionState, dispatch] = useTransition({ isRtl }); const transition = transitionState.transition; const removeTransition = transitionState.removeTransition; const transitionAnnouncement = transitionState.announcement; @@ -113,7 +115,6 @@ export function InternalBoard({ function isElementOverBoard(rect: Rect) { const board = containerAccessRef.current!; const boardContains = (target: null | Element) => board === target || board.contains(target); - const isRtl = getIsRtl(document.documentElement); const left = !isRtl ? rect.left : document.documentElement.clientWidth - rect.left; const right = !isRtl ? rect.right : document.documentElement.clientWidth - rect.right; const { top, bottom } = rect; @@ -233,7 +234,11 @@ export function InternalBoard({
{rows > 0 ? ( - + {(gridContext) => { const layoutShift = transition?.layoutShift ?? removeTransition?.layoutShift; const transforms = layoutShift ? createTransforms(itemsLayout, layoutShift.moves, gridContext) : {}; @@ -294,6 +299,7 @@ export function InternalBoard({ maxHeight: gridContext.getHeight(itemMaxSize.height), })} onKeyMove={onItemMove} + isRtl={isRtl} > {item.id === acquiredItem?.id && acquiredItemElement ? () => acquiredItemElement diff --git a/src/board/transition.ts b/src/board/transition.ts index 1d7d985a..be96bcef 100644 --- a/src/board/transition.ts +++ b/src/board/transition.ts @@ -7,7 +7,7 @@ import { LayoutEngine } from "../internal/layout-engine/engine"; import { Coordinates } from "../internal/utils/coordinates"; import { getDefaultColumnSpan, getDefaultRowSpan, getMinColumnSpan, getMinRowSpan } from "../internal/utils/layout"; import { Position } from "../internal/utils/position"; -import { getIsRtl, getLogicalBoundingClientRect } from "../internal/utils/screen"; +import { getLogicalBoundingClientRect } from "../internal/utils/screen"; import { BoardProps, RemoveTransition, Transition, TransitionAnnouncement } from "./interfaces"; import { createOperationAnnouncement } from "./utils/announcements"; import { getHoveredRect } from "./utils/get-hovered-rect"; @@ -67,31 +67,37 @@ interface AcquireItemAction { acquiredItemElement?: ReactNode; } -export function useTransition(): [TransitionState, Dispatch>] { - return useReducer(transitionReducer, { transition: null, removeTransition: null, announcement: null }); +export function useTransition({ isRtl }: { isRtl: boolean }): [TransitionState, Dispatch>] { + return useReducer(createTransitionReducer({ isRtl }), { + transition: null, + removeTransition: null, + announcement: null, + }); } export function selectTransitionRows(state: TransitionState) { return state.transition ? getLayoutRows(state.transition) : 0; } -function transitionReducer(state: TransitionState, action: Action): TransitionState { - switch (action.type) { - case "init": - return initTransition(action); - case "init-remove": - return initRemoveTransition(action); - case "submit": - return submitTransition(state); - case "discard": - return discardTransition(state); - case "update-with-pointer": - return updateTransitionWithPointerEvent(state, action); - case "update-with-keyboard": - return updateTransitionWithKeyboardEvent(state, action); - case "acquire-item": - return acquireTransitionItem(state, action); - } +function createTransitionReducer({ isRtl }: { isRtl: boolean }) { + return function transitionReducer(state: TransitionState, action: Action): TransitionState { + switch (action.type) { + case "init": + return initTransition(action); + case "init-remove": + return initRemoveTransition(action); + case "submit": + return submitTransition(state); + case "discard": + return discardTransition(state); + case "update-with-pointer": + return updateTransitionWithPointerEvent(state, action); + case "update-with-keyboard": + return updateTransitionWithKeyboardEvent(state, action, { isRtl }); + case "acquire-item": + return acquireTransitionItem(state, action); + } + }; } function initTransition({ @@ -257,6 +263,7 @@ function updateTransitionWithPointerEvent( function updateTransitionWithKeyboardEvent( state: TransitionState, { direction }: UpdateWithKeyboardAction, + { isRtl }: { isRtl: boolean }, ): TransitionState { const { transition } = state; @@ -298,8 +305,6 @@ function updateTransitionWithKeyboardEvent( } }; - const isRtl = getIsRtl(document.documentElement); - switch (direction) { case "left": return updateManualItemTransition(transition, !isRtl ? "left" : "right"); diff --git a/src/internal/grid/grid.tsx b/src/internal/grid/grid.tsx index 113179fb..a8589d30 100644 --- a/src/internal/grid/grid.tsx +++ b/src/internal/grid/grid.tsx @@ -5,7 +5,6 @@ import { useContainerQuery } from "@cloudscape-design/component-toolkit"; import { useDensityMode } from "@cloudscape-design/component-toolkit/internal"; import clsx from "clsx"; import { Children, useRef } from "react"; -import { getIsRtl } from "../../internal/utils/screen"; import { useMergeRefs } from "../utils/use-merge-refs"; import { zipTwoArrays } from "../utils/zip-arrays"; @@ -25,13 +24,12 @@ const ROWSPAN_HEIGHT = { compact: 76, }; -export default function Grid({ layout, children: render, columns }: GridProps) { +export default function Grid({ layout, children: render, columns, isRtl }: GridProps) { const gridRef = useRef(null); const [gridWidth, containerQueryRef] = useContainerQuery((entry) => entry.contentBoxWidth, []); const densityMode = useDensityMode(gridRef); const gridGap = GRID_GAP[densityMode]; const rowspanHeight = ROWSPAN_HEIGHT[densityMode]; - const isRtl = gridRef.current ? getIsRtl(gridRef.current) : false; // The below getters translate relative grid units into size/offset values in pixels. const getWidth = (colspan: number) => { diff --git a/src/internal/grid/interfaces.ts b/src/internal/grid/interfaces.ts index 712ecd0a..a5eed13f 100644 --- a/src/internal/grid/interfaces.ts +++ b/src/internal/grid/interfaces.ts @@ -8,6 +8,7 @@ export interface GridProps { layout: GridLayoutItem[]; columns: number; children?: (context: GridContext) => ReactNode; + isRtl?: boolean; } export interface GridContext { diff --git a/src/internal/item-container/__tests__/get-next-droppable.test.ts b/src/internal/item-container/__tests__/get-next-droppable.test.ts index e1be84a4..99d2f195 100644 --- a/src/internal/item-container/__tests__/get-next-droppable.test.ts +++ b/src/internal/item-container/__tests__/get-next-droppable.test.ts @@ -19,19 +19,22 @@ function getMockElement({ left, right, top, bottom }: Rect) { test("returns null if there are no droppables", () => { const elementMock = getMockElement({ left: 0, right: 0, top: 0, bottom: 0 }); - expect(getNextDroppable(elementMock, [], "left")).toBe(null); + expect(getNextDroppable({ draggableElement: elementMock, droppables: [], direction: "left", isRtl: false })).toBe( + null, + ); }); test("returns next droppable matching the direction", () => { const elementMock = getMockElement({ left: 6, right: 4, top: 0, bottom: 0 }); - const next = getNextDroppable( - elementMock, - [ + const next = getNextDroppable({ + draggableElement: elementMock, + droppables: [ ["1", { element: getMockElement({ left: 0, right: 10, top: 0, bottom: 0 }) } as Droppable], ["2", { element: getMockElement({ left: 5, right: 5, top: 0, bottom: 0 }) } as Droppable], ["3", { element: getMockElement({ left: 10, right: 0, top: 0, bottom: 0 }) } as Droppable], ], - "right", - ); + direction: "right", + isRtl: false, + }); expect(next).toBe("2"); }); diff --git a/src/internal/item-container/__tests__/item-container.test.tsx b/src/internal/item-container/__tests__/item-container.test.tsx index 3ac47089..89a092ca 100644 --- a/src/internal/item-container/__tests__/item-container.test.tsx +++ b/src/internal/item-container/__tests__/item-container.test.tsx @@ -22,6 +22,7 @@ const defaultProps: ItemContainerProps = { inTransition: false, getItemSize: () => ({ width: 1, minWidth: 1, maxWidth: 1, height: 1, minHeight: 1, maxHeight: 1 }), children: () => , + isRtl: false, }; function Item() { diff --git a/src/internal/item-container/get-next-droppable.ts b/src/internal/item-container/get-next-droppable.ts index 952b089e..911689e1 100644 --- a/src/internal/item-container/get-next-droppable.ts +++ b/src/internal/item-container/get-next-droppable.ts @@ -10,13 +10,24 @@ import { getNormalizedElementRect } from "../utils/screen"; * Finds closest droppable to provided draggable element and direction. * Returns null if there is no droppable in the given direction. */ -export function getNextDroppable( - draggableElement: HTMLElement, - droppables: readonly [ItemId, Droppable][], - direction: Direction, -): null | ItemId { +export function getNextDroppable({ + draggableElement, + droppables, + direction, + isRtl, +}: { + draggableElement: HTMLElement; + droppables: readonly [ItemId, Droppable][]; + direction: Direction; + isRtl: boolean; +}): null | ItemId { const draggableRect = getNormalizedElementRect(draggableElement); const sources = new Map(droppables.map(([id, d]) => [getNormalizedElementRect(d.element), id])); - const closest = getClosestNeighbor(draggableRect, [...sources.keys()], direction); + const closest = getClosestNeighbor({ + target: draggableRect, + sources: [...sources.keys()], + direction, + isRtl, + }); return sources.get(closest as DOMRect) ?? null; } diff --git a/src/internal/item-container/index.tsx b/src/internal/item-container/index.tsx index eb85f5ee..e8da19bc 100644 --- a/src/internal/item-container/index.tsx +++ b/src/internal/item-container/index.tsx @@ -28,7 +28,7 @@ import { } from "../dnd-controller/controller"; import { BoardItemDefinitionBase, Direction, ItemId, Transform } from "../interfaces"; import { Coordinates } from "../utils/coordinates"; -import { getIsRtl, getLogicalBoundingClientRect, getLogicalClientX, getNormalizedElementRect } from "../utils/screen"; +import { getLogicalBoundingClientRect, getLogicalClientX, getNormalizedElementRect } from "../utils/screen"; import { throttle } from "../utils/throttle"; import { getCollisionRect } from "./get-collision-rect"; import { getNextDroppable } from "./get-next-droppable"; @@ -99,12 +99,13 @@ export interface ItemContainerProps { }; onKeyMove?(direction: Direction): void; children: (hasDropTarget: boolean) => ReactNode; + isRtl: boolean; } export const ItemContainer = forwardRef(ItemContainerComponent); function ItemContainerComponent( - { item, placed, acquired, inTransition, transform, getItemSize, onKeyMove, children }: ItemContainerProps, + { item, placed, acquired, inTransition, transform, getItemSize, onKeyMove, children, isRtl }: ItemContainerProps, ref: Ref, ) { const originalSizeRef = useRef({ width: 0, height: 0 }); @@ -114,7 +115,6 @@ function ItemContainerComponent( const [isHidden, setIsHidden] = useState(false); const muteEventsRef = useRef(false); const itemRef = useRef(null); - const isRtl = itemRef.current ? getIsRtl(itemRef.current) : false; const draggableApi = useDraggable({ draggableItem: item, getCollisionRect: (operation, coordinates, dropTarget) => { @@ -177,7 +177,7 @@ function ItemContainerComponent( const transitionItemId = transition?.itemId ?? null; useEffect(() => { const onPointerMove = throttle((event: PointerEvent) => { - const coordinates = Coordinates.fromEvent(event); + const coordinates = Coordinates.fromEvent(event, { isRtl }); draggableApi.updateTransition( new Coordinates({ x: Math.max(coordinates.x, pointerBoundariesRef.current?.x ?? Number.NEGATIVE_INFINITY), @@ -246,7 +246,7 @@ function ItemContainerComponent( function handleInsert(direction: Direction) { // Find the closest droppable (in the direction) to the item. const droppables = draggableApi.getDroppables(); - const nextDroppable = getNextDroppable(itemRef.current!, droppables, direction); + const nextDroppable = getNextDroppable({ draggableElement: itemRef.current!, droppables, direction, isRtl }); if (!nextDroppable) { // TODO: add announcement @@ -319,7 +319,7 @@ function ItemContainerComponent( originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; pointerBoundariesRef.current = null; - draggableApi.start(!placed ? "insert" : "reorder", "pointer", Coordinates.fromEvent(event)); + draggableApi.start(!placed ? "insert" : "reorder", "pointer", Coordinates.fromEvent(event, { isRtl })); } function onDragHandleKeyDown(event: KeyboardEvent) { @@ -342,7 +342,7 @@ function ItemContainerComponent( y: clientY - rect.blockSize + minHeight, }); - draggableApi.start("resize", "pointer", Coordinates.fromEvent(event)); + draggableApi.start("resize", "pointer", Coordinates.fromEvent(event, { isRtl })); } function onResizeHandleKeyDown(event: KeyboardEvent) { diff --git a/src/internal/utils/__tests__/rects.test.ts b/src/internal/utils/__tests__/rects.test.ts index a84482a5..85d2a21e 100644 --- a/src/internal/utils/__tests__/rects.test.ts +++ b/src/internal/utils/__tests__/rects.test.ts @@ -95,16 +95,72 @@ describe("getGridPlacement", () => { describe("getClosestNeighbor", () => { test("returns null if can't find a neighbor in the given direction", () => { - expect(getClosestNeighbor({ left: -2, right: -1, top: 0, bottom: 1 }, grid, "left")).toBe(null); - expect(getClosestNeighbor({ left: 12, right: 13, top: 0, bottom: 1 }, grid, "right")).toBe(null); - expect(getClosestNeighbor({ left: 0, right: 1, top: -2, bottom: -1 }, grid, "up")).toBe(null); - expect(getClosestNeighbor({ left: 0, right: 1, top: 8, bottom: 9 }, grid, "down")).toBe(null); + expect( + getClosestNeighbor({ + target: { left: -2, right: -1, top: 0, bottom: 1 }, + sources: grid, + direction: "left", + isRtl: false, + }), + ).toBe(null); + expect( + getClosestNeighbor({ + target: { left: 12, right: 13, top: 0, bottom: 1 }, + sources: grid, + direction: "right", + isRtl: false, + }), + ).toBe(null); + expect( + getClosestNeighbor({ + target: { left: 0, right: 1, top: -2, bottom: -1 }, + sources: grid, + direction: "up", + isRtl: false, + }), + ).toBe(null); + expect( + getClosestNeighbor({ + target: { left: 0, right: 1, top: 8, bottom: 9 }, + sources: grid, + direction: "down", + isRtl: false, + }), + ).toBe(null); }); test("returns closest grid cell in the given direction", () => { - expect(getClosestNeighbor({ left: -2, right: -1, top: 3, bottom: 4 }, grid, "right")).toBe(grid[3]); - expect(getClosestNeighbor({ left: 12, right: 13, top: 3, bottom: 4 }, grid, "left")).toBe(grid[5]); - expect(getClosestNeighbor({ left: 5, right: 6, top: -2, bottom: -1 }, grid, "down")).toBe(grid[1]); - expect(getClosestNeighbor({ left: 5, right: 6, top: 8, bottom: 9 }, grid, "up")).toBe(grid[4]); + expect( + getClosestNeighbor({ + target: { left: -2, right: -1, top: 3, bottom: 4 }, + sources: grid, + direction: "right", + isRtl: false, + }), + ).toBe(grid[3]); + expect( + getClosestNeighbor({ + target: { left: 12, right: 13, top: 3, bottom: 4 }, + sources: grid, + direction: "left", + isRtl: false, + }), + ).toBe(grid[5]); + expect( + getClosestNeighbor({ + target: { left: 5, right: 6, top: -2, bottom: -1 }, + sources: grid, + direction: "down", + isRtl: false, + }), + ).toBe(grid[1]); + expect( + getClosestNeighbor({ + target: { left: 5, right: 6, top: 8, bottom: 9 }, + sources: grid, + direction: "up", + isRtl: false, + }), + ).toBe(grid[4]); }); }); diff --git a/src/internal/utils/coordinates.ts b/src/internal/utils/coordinates.ts index 107754fa..80b0ca67 100644 --- a/src/internal/utils/coordinates.ts +++ b/src/internal/utils/coordinates.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { PointerEvent as ReactPointerEvent } from "react"; -import { getIsRtl, getLogicalClientX } from "./screen"; +import { getLogicalClientX } from "./screen"; export class Coordinates { readonly __type = "Coordinates"; @@ -11,8 +11,7 @@ export class Coordinates { readonly scrollX = window.scrollX; readonly scrollY = window.scrollY; - static fromEvent(event: PointerEvent | ReactPointerEvent): Coordinates { - const isRtl = getIsRtl(document.documentElement); + static fromEvent(event: PointerEvent | ReactPointerEvent, { isRtl }: { isRtl: boolean }): Coordinates { const clientX = getLogicalClientX(event, isRtl); const clientY = event.clientY; return new Coordinates({ x: clientX, y: clientY }); diff --git a/src/internal/utils/rects.ts b/src/internal/utils/rects.ts index 5ff6e1b3..03f9e94b 100644 --- a/src/internal/utils/rects.ts +++ b/src/internal/utils/rects.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { Direction, Rect } from "../interfaces"; -import { getIsRtl } from "./screen"; export function isInside(rect: Rect, bounds: Rect) { return ( @@ -68,8 +67,17 @@ export function getGridPlacement(target: Rect, grid: readonly Rect[]): Rect { return placement; } -export function getClosestNeighbor(target: Rect, sources: readonly Rect[], direction: Direction): null | Rect { - const isRtl = getIsRtl(document.documentElement); +export function getClosestNeighbor({ + target, + sources, + direction, + isRtl, +}: { + target: Rect; + sources: readonly Rect[]; + direction: Direction; + isRtl: boolean; +}): null | Rect { const getFirst = (rects: Rect[]) => rects[0] ?? null; const verticalDiff = (r1: Rect, r2: Rect) => Math.abs(r1.top - target.top) - Math.abs(r2.top - target.top); const horizontalDiff = (r1: Rect, r2: Rect) => Math.abs(r1.left - target.left) - Math.abs(r2.left - target.left); diff --git a/src/internal/utils/screen.ts b/src/internal/utils/screen.ts index b9494e5c..8c7d262c 100644 --- a/src/internal/utils/screen.ts +++ b/src/internal/utils/screen.ts @@ -24,7 +24,10 @@ export function getNormalizedElementRect(element: HTMLElement): { }; } -export function getIsRtl(element: HTMLElement | SVGElement) { +export function getIsRtl(element: null | HTMLElement | SVGElement) { + if (!element || !(element instanceof HTMLElement) || !(element instanceof SVGElement)) { + return false; + } return getComputedStyle(element).direction === "rtl"; } @@ -33,8 +36,8 @@ export function getIsRtl(element: HTMLElement | SVGElement) { * the document in order for computations to yield the same result in both * element directions. */ -export function getLogicalClientX(event: PointerEvent | ReactPointerEvent, IsRtl: boolean) { - return IsRtl ? document.documentElement.clientWidth - event.clientX : event.clientX; +export function getLogicalClientX(event: PointerEvent | ReactPointerEvent, isRtl: boolean) { + return isRtl ? document.documentElement.clientWidth - event.clientX : event.clientX; } /** diff --git a/src/items-palette/internal.tsx b/src/items-palette/internal.tsx index 08f1cbed..a74fcd63 100644 --- a/src/items-palette/internal.tsx +++ b/src/items-palette/internal.tsx @@ -9,6 +9,7 @@ import { ItemId } from "../internal/interfaces"; import { ItemContainer, ItemContainerRef } from "../internal/item-container"; import LiveRegion from "../internal/live-region"; import { ScreenReaderGridNavigation } from "../internal/screenreader-grid-navigation"; +import { getIsRtl } from "../internal/utils/screen"; import { ItemsPaletteProps } from "./interfaces"; import styles from "./styles.css.js"; @@ -24,6 +25,8 @@ export function InternalItemsPalette({ const [dropState, setDropState] = useState<{ id: string }>(); const [announcement, setAnnouncement] = useState(""); + const isRtl = getIsRtl(paletteRef.current); + function focusItem(itemId: ItemId) { itemContainerRef.current[itemId].focusDragHandle(); } @@ -109,6 +112,7 @@ export function InternalItemsPalette({ const { width, height } = dropContext.scale(item); return { width, minWidth: width, maxWidth: width, height, minHeight: height, maxHeight: height }; }} + isRtl={isRtl} > {(hasDropTarget) => renderItem(item, { showPreview: hasDropTarget })} From 1d8e4fa1bdc63d1e84a6d56acf48fc25196799cb Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Wed, 22 May 2024 11:36:54 +0200 Subject: [PATCH 13/17] fix test mock --- .../item-container/__tests__/get-next-droppable.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal/item-container/__tests__/get-next-droppable.test.ts b/src/internal/item-container/__tests__/get-next-droppable.test.ts index 99d2f195..caae61e2 100644 --- a/src/internal/item-container/__tests__/get-next-droppable.test.ts +++ b/src/internal/item-container/__tests__/get-next-droppable.test.ts @@ -7,7 +7,7 @@ import { getNextDroppable } from "../../../../lib/components/internal/item-conta function getMockElement({ left, right, top, bottom }: Rect) { return { - getBoundingClientRect: () => ({ left, right, top, bottom }), + getBoundingClientRect: () => ({ left, right, top, bottom, width: right - left, height: bottom - top }), ownerDocument: { defaultView: { pageXOffset: 0, From b3169e01b5e279dd6399d52169405b63b34981c8 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Wed, 22 May 2024 18:13:00 +0200 Subject: [PATCH 14/17] set direction inside effect --- pages/app/page.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pages/app/page.tsx b/pages/app/page.tsx index 14afec90..aff8d1b3 100644 --- a/pages/app/page.tsx +++ b/pages/app/page.tsx @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Suspense } from "react"; +import { Suspense, useEffect } from "react"; import { useSearchParams } from "react-router-dom"; import { pagesMap } from "../pages"; @@ -12,7 +12,10 @@ export interface PageProps { export default function Page({ pageId }: PageProps) { const [searchParams] = useSearchParams(); const direction = searchParams.get("direction") ?? "ltr"; - document.documentElement.setAttribute("dir", direction); + + useEffect(() => { + document.documentElement.setAttribute("dir", direction); + }, [direction]); const Component = pagesMap[pageId]; From 3e294d0cf138349e1b0c6022f2e2586f9d4cc587 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Thu, 23 May 2024 09:07:32 +0200 Subject: [PATCH 15/17] fix is-rtl getter --- src/internal/utils/screen.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/internal/utils/screen.ts b/src/internal/utils/screen.ts index 8c7d262c..7b82af71 100644 --- a/src/internal/utils/screen.ts +++ b/src/internal/utils/screen.ts @@ -24,11 +24,14 @@ export function getNormalizedElementRect(element: HTMLElement): { }; } -export function getIsRtl(element: null | HTMLElement | SVGElement) { - if (!element || !(element instanceof HTMLElement) || !(element instanceof SVGElement)) { +export function getIsRtl(element: null | Element | SVGElement) { + if (!element) { return false; } - return getComputedStyle(element).direction === "rtl"; + if (element instanceof Element) { + return getComputedStyle(element).direction === "rtl"; + } + return false; } /** From 40ba92466e0a8b9524cc1881df42eae778a91966 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Thu, 23 May 2024 09:24:44 +0200 Subject: [PATCH 16/17] fix is-rtl race condition --- src/board/internal.tsx | 8 +++---- src/board/transition.ts | 10 ++++---- src/internal/grid/grid.tsx | 2 +- src/internal/grid/interfaces.ts | 2 +- .../__tests__/item-container.test.tsx | 2 +- src/internal/item-container/index.tsx | 19 +++++++++------ src/internal/utils/screen.ts | 24 +++++++++++-------- src/items-palette/internal.tsx | 4 ++-- 8 files changed, 40 insertions(+), 31 deletions(-) diff --git a/src/board/internal.tsx b/src/board/internal.tsx index 24b74c6d..d2b25f27 100644 --- a/src/board/internal.tsx +++ b/src/board/internal.tsx @@ -22,7 +22,7 @@ import { interpretItems, } from "../internal/utils/layout"; import { Position } from "../internal/utils/position"; -import { getIsRtl } from "../internal/utils/screen"; +import { useIsRtl } from "../internal/utils/screen"; import { useAutoScroll } from "../internal/utils/use-auto-scroll"; import { useMergeRefs } from "../internal/utils/use-merge-refs"; @@ -48,7 +48,7 @@ export function InternalBoard({ const containerRef = useMergeRefs(containerAccessRef, containerQueryRef); const itemContainerRef = useRef<{ [id: ItemId]: ItemContainerRef }>({}); - const isRtl = getIsRtl(containerAccessRef.current); + const isRtl = useIsRtl(containerAccessRef); useGlobalDragStateStyles(); @@ -115,8 +115,8 @@ export function InternalBoard({ function isElementOverBoard(rect: Rect) { const board = containerAccessRef.current!; const boardContains = (target: null | Element) => board === target || board.contains(target); - const left = !isRtl ? rect.left : document.documentElement.clientWidth - rect.left; - const right = !isRtl ? rect.right : document.documentElement.clientWidth - rect.right; + const left = !isRtl() ? rect.left : document.documentElement.clientWidth - rect.left; + const right = !isRtl() ? rect.right : document.documentElement.clientWidth - rect.right; const { top, bottom } = rect; return ( boardContains(document.elementFromPoint(left, top)) || diff --git a/src/board/transition.ts b/src/board/transition.ts index be96bcef..c31a2453 100644 --- a/src/board/transition.ts +++ b/src/board/transition.ts @@ -67,7 +67,7 @@ interface AcquireItemAction { acquiredItemElement?: ReactNode; } -export function useTransition({ isRtl }: { isRtl: boolean }): [TransitionState, Dispatch>] { +export function useTransition({ isRtl }: { isRtl: () => boolean }): [TransitionState, Dispatch>] { return useReducer(createTransitionReducer({ isRtl }), { transition: null, removeTransition: null, @@ -79,7 +79,7 @@ export function selectTransitionRows(state: TransitionState) { return state.transition ? getLayoutRows(state.transition) : 0; } -function createTransitionReducer({ isRtl }: { isRtl: boolean }) { +function createTransitionReducer({ isRtl }: { isRtl: () => boolean }) { return function transitionReducer(state: TransitionState, action: Action): TransitionState { switch (action.type) { case "init": @@ -263,7 +263,7 @@ function updateTransitionWithPointerEvent( function updateTransitionWithKeyboardEvent( state: TransitionState, { direction }: UpdateWithKeyboardAction, - { isRtl }: { isRtl: boolean }, + { isRtl }: { isRtl: () => boolean }, ): TransitionState { const { transition } = state; @@ -307,9 +307,9 @@ function updateTransitionWithKeyboardEvent( switch (direction) { case "left": - return updateManualItemTransition(transition, !isRtl ? "left" : "right"); + return updateManualItemTransition(transition, !isRtl() ? "left" : "right"); case "right": - return updateManualItemTransition(transition, !isRtl ? "right" : "left"); + return updateManualItemTransition(transition, !isRtl() ? "right" : "left"); case "up": return updateManualItemTransition(transition, "up"); case "down": diff --git a/src/internal/grid/grid.tsx b/src/internal/grid/grid.tsx index a8589d30..d27cdf04 100644 --- a/src/internal/grid/grid.tsx +++ b/src/internal/grid/grid.tsx @@ -40,7 +40,7 @@ export default function Grid({ layout, children: render, columns, isRtl }: GridP const getHeight = (rowspan: number) => rowspan * rowspanHeight + (rowspan - 1) * gridGap; const getColOffset = (x: number) => { const offset = getWidth(x) + gridGap; - return !isRtl ? offset : -offset; + return !isRtl?.() ? offset : -offset; }; const getRowOffset = (y: number) => getHeight(y) + gridGap; diff --git a/src/internal/grid/interfaces.ts b/src/internal/grid/interfaces.ts index a5eed13f..212d8d84 100644 --- a/src/internal/grid/interfaces.ts +++ b/src/internal/grid/interfaces.ts @@ -8,7 +8,7 @@ export interface GridProps { layout: GridLayoutItem[]; columns: number; children?: (context: GridContext) => ReactNode; - isRtl?: boolean; + isRtl?: () => boolean; } export interface GridContext { diff --git a/src/internal/item-container/__tests__/item-container.test.tsx b/src/internal/item-container/__tests__/item-container.test.tsx index 89a092ca..033ded52 100644 --- a/src/internal/item-container/__tests__/item-container.test.tsx +++ b/src/internal/item-container/__tests__/item-container.test.tsx @@ -22,7 +22,7 @@ const defaultProps: ItemContainerProps = { inTransition: false, getItemSize: () => ({ width: 1, minWidth: 1, maxWidth: 1, height: 1, minHeight: 1, maxHeight: 1 }), children: () => , - isRtl: false, + isRtl: () => false, }; function Item() { diff --git a/src/internal/item-container/index.tsx b/src/internal/item-container/index.tsx index e8da19bc..f8f88844 100644 --- a/src/internal/item-container/index.tsx +++ b/src/internal/item-container/index.tsx @@ -99,7 +99,7 @@ export interface ItemContainerProps { }; onKeyMove?(direction: Direction): void; children: (hasDropTarget: boolean) => ReactNode; - isRtl: boolean; + isRtl: () => boolean; } export const ItemContainer = forwardRef(ItemContainerComponent); @@ -177,7 +177,7 @@ function ItemContainerComponent( const transitionItemId = transition?.itemId ?? null; useEffect(() => { const onPointerMove = throttle((event: PointerEvent) => { - const coordinates = Coordinates.fromEvent(event, { isRtl }); + const coordinates = Coordinates.fromEvent(event, { isRtl: isRtl() }); draggableApi.updateTransition( new Coordinates({ x: Math.max(coordinates.x, pointerBoundariesRef.current?.x ?? Number.NEGATIVE_INFINITY), @@ -246,7 +246,12 @@ function ItemContainerComponent( function handleInsert(direction: Direction) { // Find the closest droppable (in the direction) to the item. const droppables = draggableApi.getDroppables(); - const nextDroppable = getNextDroppable({ draggableElement: itemRef.current!, droppables, direction, isRtl }); + const nextDroppable = getNextDroppable({ + draggableElement: itemRef.current!, + droppables, + direction, + isRtl: isRtl(), + }); if (!nextDroppable) { // TODO: add announcement @@ -310,7 +315,7 @@ function ItemContainerComponent( function onDragHandlePointerDown(event: ReactPointerEvent) { // Calculate the offset between item's top-left corner and the pointer landing position. const rect = getLogicalBoundingClientRect(itemRef.current!); - const clientX = getLogicalClientX(event, isRtl); + const clientX = getLogicalClientX(event, isRtl()); const clientY = event.clientY; pointerOffsetRef.current = new Coordinates({ x: clientX - rect.insetInlineStart, @@ -319,7 +324,7 @@ function ItemContainerComponent( originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; pointerBoundariesRef.current = null; - draggableApi.start(!placed ? "insert" : "reorder", "pointer", Coordinates.fromEvent(event, { isRtl })); + draggableApi.start(!placed ? "insert" : "reorder", "pointer", Coordinates.fromEvent(event, { isRtl: isRtl() })); } function onDragHandleKeyDown(event: KeyboardEvent) { @@ -329,7 +334,7 @@ function ItemContainerComponent( function onResizeHandlePointerDown(event: ReactPointerEvent) { // Calculate the offset between item's bottom-right corner and the pointer landing position. const rect = getLogicalBoundingClientRect(itemRef.current!); - const clientX = getLogicalClientX(event, isRtl); + const clientX = getLogicalClientX(event, isRtl()); const clientY = event.clientY; pointerOffsetRef.current = new Coordinates({ x: clientX - rect.insetInlineEnd, y: clientY - rect.insetBlockEnd }); originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; @@ -342,7 +347,7 @@ function ItemContainerComponent( y: clientY - rect.blockSize + minHeight, }); - draggableApi.start("resize", "pointer", Coordinates.fromEvent(event, { isRtl })); + draggableApi.start("resize", "pointer", Coordinates.fromEvent(event, { isRtl: isRtl() })); } function onResizeHandleKeyDown(event: KeyboardEvent) { diff --git a/src/internal/utils/screen.ts b/src/internal/utils/screen.ts index 7b82af71..7de00ce0 100644 --- a/src/internal/utils/screen.ts +++ b/src/internal/utils/screen.ts @@ -24,14 +24,17 @@ export function getNormalizedElementRect(element: HTMLElement): { }; } -export function getIsRtl(element: null | Element | SVGElement) { - if (!element) { +export function useIsRtl(elementRef: React.RefObject) { + const getIsRtl = (): boolean => { + if (!elementRef.current) { + return false; + } + if (elementRef.current instanceof Element) { + return getComputedStyle(elementRef.current).direction === "rtl"; + } return false; - } - if (element instanceof Element) { - return getComputedStyle(element).direction === "rtl"; - } - return false; + }; + return getIsRtl; } /** @@ -56,9 +59,10 @@ export function getLogicalBoundingClientRect(element: HTMLElement | SVGElement) const inlineSize = boundingClientRect.width; const insetBlockStart = boundingClientRect.top; const insetBlockEnd = boundingClientRect.bottom; - const insetInlineStart = getIsRtl(element) - ? document.documentElement.clientWidth - boundingClientRect.right - : boundingClientRect.left; + const insetInlineStart = + element instanceof Element && getComputedStyle(element).direction === "rtl" + ? document.documentElement.clientWidth - boundingClientRect.right + : boundingClientRect.left; const insetInlineEnd = insetInlineStart + inlineSize; return { diff --git a/src/items-palette/internal.tsx b/src/items-palette/internal.tsx index a74fcd63..7237e0d8 100644 --- a/src/items-palette/internal.tsx +++ b/src/items-palette/internal.tsx @@ -9,7 +9,7 @@ import { ItemId } from "../internal/interfaces"; import { ItemContainer, ItemContainerRef } from "../internal/item-container"; import LiveRegion from "../internal/live-region"; import { ScreenReaderGridNavigation } from "../internal/screenreader-grid-navigation"; -import { getIsRtl } from "../internal/utils/screen"; +import { useIsRtl } from "../internal/utils/screen"; import { ItemsPaletteProps } from "./interfaces"; import styles from "./styles.css.js"; @@ -25,7 +25,7 @@ export function InternalItemsPalette({ const [dropState, setDropState] = useState<{ id: string }>(); const [announcement, setAnnouncement] = useState(""); - const isRtl = getIsRtl(paletteRef.current); + const isRtl = useIsRtl(paletteRef); function focusItem(itemId: ItemId) { itemContainerRef.current[itemId].focusDragHandle(); From 6f122362caf30047e675bb47492e4448b74e7be6 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Thu, 23 May 2024 09:38:31 +0200 Subject: [PATCH 17/17] test coverage --- src/internal/utils/screen.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/internal/utils/screen.ts b/src/internal/utils/screen.ts index 7de00ce0..5587cfef 100644 --- a/src/internal/utils/screen.ts +++ b/src/internal/utils/screen.ts @@ -25,15 +25,10 @@ export function getNormalizedElementRect(element: HTMLElement): { } export function useIsRtl(elementRef: React.RefObject) { - const getIsRtl = (): boolean => { - if (!elementRef.current) { - return false; - } - if (elementRef.current instanceof Element) { - return getComputedStyle(elementRef.current).direction === "rtl"; - } - return false; - }; + const getIsRtl = (): boolean => + elementRef.current && elementRef.current instanceof Element + ? getComputedStyle(elementRef.current).direction === "rtl" + : false; return getIsRtl; }