diff --git a/pages/shared/i18n.ts b/pages/shared/i18n.ts index 2f8f067c..8a5c8232 100644 --- a/pages/shared/i18n.ts +++ b/pages/shared/i18n.ts @@ -61,8 +61,6 @@ export const boardItemI18nStrings: BoardItemProps.I18nStrings = { resizeHandleAriaLabel: "Resize handle", resizeHandleAriaDescription: "Use Space or Enter to activate resize, arrow keys to move, Space or Enter to submit, or Escape to discard. Be sure to temporarily disable any screen reader navigation feature that may interfere with the functionality of the arrow keys.", - dragHandleTooltipText: "Drag or select to move", - resizeHandleTooltipText: "Drag or select to resize", }; export const itemsPaletteI18nStrings: ItemsPaletteProps.I18nStrings = { diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 18e93b1e..ca8abe3f 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -201,9 +201,7 @@ ARIA labels: * \`dragHandleAriaLabel\` (string) - the ARIA label for the drag handle. * \`dragHandleAriaDescription\` (string, optional) - the ARIA description for the drag handle. * \`resizeHandleAriaLabel\` (string) - the ARIA label for the resize handle. -* \`resizeHandleAriaDescription\` (string, optional) - the ARIA description for the resize handle. -* \`dragHandleTooltipText\` (string, optional) - the ARIA description for the resize handle. -* \`resizeHandleTooltipText\` (string, optional) - the Text for the resize handle Tooltip.", +* \`resizeHandleAriaDescription\` (string, optional) - the ARIA description for the resize handle.", "inlineType": { "name": "BoardItemProps.I18nStrings", "properties": [ @@ -217,11 +215,6 @@ ARIA labels: "optional": false, "type": "string", }, - { - "name": "dragHandleTooltipText", - "optional": true, - "type": "string", - }, { "name": "resizeHandleAriaDescription", "optional": true, @@ -232,11 +225,6 @@ ARIA labels: "optional": false, "type": "string", }, - { - "name": "resizeHandleTooltipText", - "optional": true, - "type": "string", - }, ], "type": "object", }, diff --git a/src/board-item/__tests__/board-item-wrapper.tsx b/src/board-item/__tests__/board-item-wrapper.tsx index b002eccd..6d579ecd 100644 --- a/src/board-item/__tests__/board-item-wrapper.tsx +++ b/src/board-item/__tests__/board-item-wrapper.tsx @@ -9,19 +9,16 @@ export function ItemContextWrapper({ children }: { children: ReactNode }) { {}, onKeyDown: () => {}, - activeState: null, - onDirectionClick: () => {}, + isActive: false, }, resizeHandle: { onPointerDown: () => {}, onKeyDown: () => {}, - activeState: null, - onDirectionClick: () => {}, + isActive: false, }, }} > diff --git a/src/board-item/__tests__/board-item.test.tsx b/src/board-item/__tests__/board-item.test.tsx index cb1e7169..4e617155 100644 --- a/src/board-item/__tests__/board-item.test.tsx +++ b/src/board-item/__tests__/board-item.test.tsx @@ -2,15 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 import { ReactElement } from "react"; -import { cleanup, fireEvent, render as libRender } from "@testing-library/react"; +import { cleanup, render as libRender } from "@testing-library/react"; import { afterEach, describe, expect, test } from "vitest"; import Button from "@cloudscape-design/components/button"; import Container from "@cloudscape-design/components/container"; import ExpandableSection from "@cloudscape-design/components/expandable-section"; import Header from "@cloudscape-design/components/header"; -import DragHandleWrapper from "@cloudscape-design/components/test-utils/dom/internal/drag-handle"; -import TooltipWrapper from "@cloudscape-design/components/test-utils/dom/internal/tooltip"; import "@cloudscape-design/components/test-utils/dom"; import BoardItem from "../../../lib/components/board-item"; @@ -20,8 +18,6 @@ import { ItemContextWrapper } from "./board-item-wrapper"; const i18nStrings = { dragHandleAriaLabel: "Drag handle", resizeHandleAriaLabel: "Resize handle", - dragHandleTooltipText: "Drag or select to move", - resizeHandleTooltipText: "Drag or select to resize", }; function render(jsx: ReactElement) { @@ -65,72 +61,4 @@ describe("WidgetContainer", () => { expect(getByLabelText("Drag handle")).toBeDefined(); expect(getByLabelText("Resize handle")).toBeDefined(); }); - - test("renders drag handle tooltip text if provided", () => { - render(); - const wrapper = createWrapper(); - const dragHandleEl = wrapper.findBoardItem()!.findDragHandle().getElement(); - - fireEvent(dragHandleEl, new MouseEvent("pointerover", { bubbles: true })); - const tooltipEl = wrapper.findByClassName(TooltipWrapper.rootSelector)!.getElement(); - expect(tooltipEl.textContent).toBe("Drag or select to move"); - }); - - test("does not render drag handle tooltip text if not provided", () => { - render(); - const wrapper = createWrapper(); - const dragHandleEl = wrapper.findBoardItem()!.findDragHandle().getElement(); - - fireEvent(dragHandleEl, new MouseEvent("pointerover", { bubbles: true })); - expect(wrapper.findByClassName(TooltipWrapper.rootSelector)).toBeNull(); - }); - - test("renders drag handle UAP actions on handle click", () => { - render(); - const dragHandleEl = createWrapper().findBoardItem()!.findDragHandle()!.getElement(); - - fireEvent(dragHandleEl, new MouseEvent("pointerdown", { bubbles: true })); - fireEvent(dragHandleEl, new MouseEvent("pointerup", { bubbles: true })); - - const dragHandleWrapper = new DragHandleWrapper(document.body); - expect(dragHandleWrapper.findAllVisibleDirectionButtons()).toHaveLength(4); - expect(dragHandleWrapper.findVisibleDirectionButtonBlockStart()).toBeDefined(); - expect(dragHandleWrapper.findVisibleDirectionButtonBlockEnd()).toBeDefined(); - expect(dragHandleWrapper.findVisibleDirectionButtonInlineStart()).toBeDefined(); - expect(dragHandleWrapper.findVisibleDirectionButtonInlineEnd()).toBeDefined(); - }); - - test("renders resize handle tooltip text", () => { - render(); - const wrapper = createWrapper(); - const resizeHandleEl = wrapper.findBoardItem()!.findResizeHandle()!.getElement(); - - fireEvent(resizeHandleEl, new MouseEvent("pointerover", { bubbles: true })); - const tooltipEl = wrapper.findByClassName(TooltipWrapper.rootSelector)!.getElement(); - expect(tooltipEl.textContent).toBe("Drag or select to resize"); - }); - - test("does not render resize handle tooltip text if not provided", () => { - render(); - const wrapper = createWrapper(); - const resizeHandleEl = wrapper.findBoardItem()!.findResizeHandle()!.getElement(); - - fireEvent(resizeHandleEl, new MouseEvent("pointerover", { bubbles: true })); - expect(wrapper.findByClassName(TooltipWrapper.rootSelector)).toBeNull(); - }); - - test("renders resize handle UAP actions on handle click", () => { - render(); - const resizeHandleEl = createWrapper().findBoardItem()!.findResizeHandle()!.getElement(); - - fireEvent(resizeHandleEl, new MouseEvent("pointerdown", { bubbles: true })); - fireEvent(resizeHandleEl, new MouseEvent("pointerup", { bubbles: true })); - - const dragHandleWrapper = new DragHandleWrapper(document.body); - expect(dragHandleWrapper.findAllVisibleDirectionButtons()).toHaveLength(4); - expect(dragHandleWrapper.findVisibleDirectionButtonBlockStart()).toBeDefined(); - expect(dragHandleWrapper.findVisibleDirectionButtonBlockEnd()).toBeDefined(); - expect(dragHandleWrapper.findVisibleDirectionButtonInlineStart()).toBeDefined(); - expect(dragHandleWrapper.findVisibleDirectionButtonInlineEnd()).toBeDefined(); - }); }); diff --git a/src/board-item/interfaces.ts b/src/board-item/interfaces.ts index e5cb9b2a..26957a4d 100644 --- a/src/board-item/interfaces.ts +++ b/src/board-item/interfaces.ts @@ -37,8 +37,6 @@ export interface BoardItemProps { * * `dragHandleAriaDescription` (string, optional) - the ARIA description for the drag handle. * * `resizeHandleAriaLabel` (string) - the ARIA label for the resize handle. * * `resizeHandleAriaDescription` (string, optional) - the ARIA description for the resize handle. - * * `dragHandleTooltipText` (string, optional) - the ARIA description for the resize handle. - * * `resizeHandleTooltipText` (string, optional) - the Text for the resize handle Tooltip. */ i18nStrings: BoardItemProps.I18nStrings; } @@ -49,7 +47,5 @@ export namespace BoardItemProps { dragHandleAriaDescription?: string; resizeHandleAriaLabel: string; resizeHandleAriaDescription?: string; - dragHandleTooltipText?: string; - resizeHandleTooltipText?: string; } } diff --git a/src/board-item/internal.tsx b/src/board-item/internal.tsx index 8bb8bf52..88d6fde9 100644 --- a/src/board-item/internal.tsx +++ b/src/board-item/internal.tsx @@ -4,12 +4,10 @@ import { useId } from "react"; import clsx from "clsx"; import Container from "@cloudscape-design/components/container"; -import { InternalDragHandleProps } from "@cloudscape-design/components/internal/do-not-use/drag-handle"; import { getDataAttributes } from "../internal/base-component/get-data-attributes"; import { InternalBaseComponentProps } from "../internal/base-component/use-base-component"; import DragHandle from "../internal/drag-handle"; -import { Direction } from "../internal/interfaces"; import { useItemContext } from "../internal/item-container"; import ResizeHandle from "../internal/resize-handle"; import ScreenreaderOnly from "../internal/screenreader-only"; @@ -18,16 +16,6 @@ import type { BoardItemProps } from "./interfaces"; import styles from "./styles.css.js"; -const mapToKeyboardDirection = (direction: InternalDragHandleProps.Direction) => { - const directionMap: Record = { - "inline-start": "left", - "inline-end": "right", - "block-start": "up", - "block-end": "down", - }; - return directionMap[direction]; -}; - export function InternalBoardItem({ children, header, @@ -38,7 +26,7 @@ export function InternalBoardItem({ __internalRootRef, ...rest }: BoardItemProps & InternalBaseComponentProps) { - const { dragHandle, resizeHandle, isActive, isHidden } = useItemContext(); + const { dragHandle, resizeHandle, isActive } = useItemContext(); const dragHandleAriaLabelledBy = useId(); const dragHandleAriaDescribedBy = useId(); @@ -46,12 +34,6 @@ export function InternalBoardItem({ const resizeHandleAriaLabelledBy = useId(); const resizeHandleAriaDescribedBy = useId(); - // A board item is hidden while moving a board item from the palette to the board via keyboard or UAP. - // The wrapping container is set to invisible, so we don't need to render anything. - if (isHidden) { - return null; - } - return (
dragHandle.onDirectionClick(mapToKeyboardDirection(direction), "drag")} - dragHandleTooltipText={i18nStrings.dragHandleTooltipText} + isActive={dragHandle.isActive} /> } settings={settings} @@ -90,11 +69,7 @@ export function InternalBoardItem({ ariaDescribedBy={resizeHandleAriaDescribedBy} onPointerDown={resizeHandle.onPointerDown} onKeyDown={resizeHandle.onKeyDown} - activeState={resizeHandle.activeState} - onDirectionClick={(direction) => { - resizeHandle.onDirectionClick(mapToKeyboardDirection(direction), "resize"); - }} - resizeHandleTooltipText={i18nStrings.resizeHandleTooltipText} + isActive={resizeHandle.isActive} />
)} diff --git a/src/board-item/styles.scss b/src/board-item/styles.scss index c069d725..3ab73386 100644 --- a/src/board-item/styles.scss +++ b/src/board-item/styles.scss @@ -13,7 +13,9 @@ .container-override.active { box-shadow: cs.$shadow-container-active; - @include shared.focus-highlight(0px, cs.$border-radius-container); + :global([data-awsui-focus-visible]) & { + @include shared.focus-highlight(0px, cs.$border-radius-container); + } } .header { diff --git a/src/board/__tests__/board.test.tsx b/src/board/__tests__/board.test.tsx index c0484e0b..31b078b2 100644 --- a/src/board/__tests__/board.test.tsx +++ b/src/board/__tests__/board.test.tsx @@ -2,9 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { cleanup, fireEvent, render, waitFor } from "@testing-library/react"; import { vi } from "vitest"; -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeAll, describe, expect, test } from "vitest"; -import DragHandleWrapper from "@cloudscape-design/components/test-utils/dom/internal/drag-handle"; import { KeyCode } from "@cloudscape-design/test-utils-core/utils"; import Board from "../../../lib/components/board"; @@ -12,10 +11,8 @@ import createWrapper from "../../../lib/components/test-utils/dom"; import { defaultProps } from "./utils"; import dragHandleStyles from "../../../lib/components/internal/drag-handle/styles.css.js"; -import dragHandleTestUtilsStyles from "../../../lib/components/internal/drag-handle/test-classes/styles.css.js"; import globalStateStyles from "../../../lib/components/internal/global-drag-state-styles/styles.css.js"; import resizeHandleStyles from "../../../lib/components/internal/resize-handle/styles.css.js"; -import resizeHandleTestUtilsStyles from "../../../lib/components/internal/resize-handle/test-classes/styles.css.js"; describe("Board", () => { beforeAll(() => { @@ -49,7 +46,7 @@ describe("Board", () => { itemDragHandle.keydown(KeyCode.escape); }); - test("applies reorder operation classname on pointer interaction", () => { + test("applies reorder operation classname", () => { const { container } = render(); const reorderClass = globalStateStyles["show-grab-cursor"]; @@ -66,7 +63,7 @@ describe("Board", () => { expect(container.ownerDocument.body).not.toHaveClass(reorderClass); }); - test("applies resize operation classname on pointer interaction", () => { + test("applies resize operation classname", () => { const { container } = render(); const resizeClass = globalStateStyles["show-resize-cursor"]; @@ -113,117 +110,54 @@ describe("Board", () => { expect(container.ownerDocument.body).not.toHaveClass(disableSelectionClass); }); - describe("sets pointer active states for drag and resize handles", () => { - let dragHandle: HTMLElement; - let resizeHandle: HTMLElement; - let dragHandlePointerActiveClassName: string; - let dragHandleUapActiveClassName: string; - let resizeHandlePointerActiveClassName: string; - let resizeHandleUapActiveClassName: string; - - beforeEach(() => { - render(); - - dragHandle = createWrapper().findBoardItem()!.findDragHandle()!.getElement(); - resizeHandle = createWrapper().findBoardItem()!.findResizeHandle()!.getElement(); - dragHandlePointerActiveClassName = dragHandleStyles.active; - dragHandleUapActiveClassName = dragHandleTestUtilsStyles["active-uap"]; - resizeHandlePointerActiveClassName = resizeHandleStyles.active; - resizeHandleUapActiveClassName = resizeHandleTestUtilsStyles["active-uap"]; - }); - test("drag handle", () => { - expect(dragHandle).not.toHaveClass(dragHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandleUapActiveClassName); - - // Pointerdown - activates pointer class - fireEvent(dragHandle, new MouseEvent("pointerdown", { bubbles: true })); - expect(dragHandle).toHaveClass(dragHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandleUapActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandleUapActiveClassName); - - // Pointerup - removes pointer class - adds UAP active class - fireEvent(window, new MouseEvent("pointerup", { bubbles: true })); - expect(dragHandle).toHaveClass(dragHandleUapActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandleUapActiveClassName); - - // Blur handle - removes UAP active class - fireEvent.blur(dragHandle); - expect(dragHandle).not.toHaveClass(dragHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandleUapActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandleUapActiveClassName); - }); - - test("resize handle", () => { - expect(resizeHandle).not.toHaveClass(resizeHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandleUapActiveClassName); - - // Pointerdown - activates pointer class - fireEvent(resizeHandle, new MouseEvent("pointerdown", { bubbles: true })); - expect(resizeHandle).toHaveClass(resizeHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandleUapActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandleUapActiveClassName); - - // Pointerup - removes pointer class - adds UAP active class - fireEvent(window, new MouseEvent("pointerup", { bubbles: true })); - expect(resizeHandle).toHaveClass(resizeHandleUapActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandleUapActiveClassName); - - // Blur handle - removes UAP active class - fireEvent.blur(resizeHandle); - expect(resizeHandle).not.toHaveClass(resizeHandlePointerActiveClassName); - expect(resizeHandle).not.toHaveClass(resizeHandleUapActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandlePointerActiveClassName); - expect(dragHandle).not.toHaveClass(dragHandleUapActiveClassName); - }); + test("sets active state for drag handle", () => { + render(); + + const findBoardItem = () => createWrapper().findBoard()!.findItemById("1")!; + const findDragHandle = () => findBoardItem().findDragHandle().getElement(); + const findResizeHandle = () => findBoardItem().findResizeHandle().getElement(); + + expect(findDragHandle()).not.toHaveClass(dragHandleStyles.active); + + // Start operation + fireEvent(findDragHandle(), new MouseEvent("pointerdown", { bubbles: true })); + expect(findDragHandle()).toHaveClass(dragHandleStyles.active); + expect(findResizeHandle()).not.toHaveClass(dragHandleStyles.active); + + // End operation + fireEvent(window, new MouseEvent("pointerup", { bubbles: true })); + expect(findDragHandle()).not.toHaveClass(dragHandleStyles.active); }); - test("triggers onItemsChange on drag via keyboard", () => { - const onItemsChange = vi.fn(); - render(); + test("sets active state for resize handle", () => { + render(); - const dragHandle = createWrapper().findBoardItem()!.findDragHandle()!; + const dragHandle = createWrapper().findBoardItem()!.findDragHandle()!.getElement(); + const resizeHandle = createWrapper().findBoardItem()!.findResizeHandle()!.getElement(); - dragHandle.keydown(KeyCode.enter); - dragHandle.keydown(KeyCode.down); - dragHandle.keydown(KeyCode.down); - dragHandle.keydown(KeyCode.enter); + expect(resizeHandle).not.toHaveClass(resizeHandleStyles.active); - expect(onItemsChange).toBeCalledWith( - expect.objectContaining({ - detail: { - movedItem: expect.objectContaining({ id: "1" }), - items: [ - { id: "2", data: { title: "Item 2" }, columnOffset: { 1: 0 } }, - { id: "1", data: { title: "Item 1" }, columnOffset: { 1: 0 } }, - ], - }, - }), - ); + // Start operation + fireEvent(resizeHandle, new MouseEvent("pointerdown", { bubbles: true })); + expect(resizeHandle).toHaveClass(resizeHandleStyles.active); + expect(dragHandle).not.toHaveClass(resizeHandleStyles.active); + + // End operation + fireEvent(window, new MouseEvent("pointerup", { bubbles: true })); + expect(resizeHandle).not.toHaveClass(resizeHandleStyles.active); }); - test("triggers onItemsChange on drag via UAP action button click", () => { + test("triggers onItemsChange on drag", () => { const onItemsChange = vi.fn(); render(); const dragHandle = createWrapper().findBoardItem()!.findDragHandle()!; - const dragHandleEl = dragHandle.getElement(); - fireEvent(dragHandleEl, new MouseEvent("pointerdown", { bubbles: true })); - fireEvent(dragHandleEl, new MouseEvent("pointerup", { bubbles: true })); - const dragHandleWrapper = new DragHandleWrapper(document.body); - const blockEndUapAction = dragHandleWrapper.findVisibleDirectionButtonBlockEnd()!.getElement(); - expect(dragHandleWrapper.findAllVisibleDirectionButtons()).toHaveLength(4); + dragHandle.keydown(KeyCode.enter); + dragHandle.keydown(KeyCode.down); + dragHandle.keydown(KeyCode.down); + dragHandle.keydown(KeyCode.enter); - fireEvent.click(blockEndUapAction); - fireEvent.click(blockEndUapAction); - fireEvent.blur(blockEndUapAction); expect(onItemsChange).toBeCalledWith( expect.objectContaining({ detail: { @@ -237,7 +171,7 @@ describe("Board", () => { ); }); - test("triggers onItemsChange on resize via keyboard", () => { + test("triggers onItemsChange on resize", () => { const onItemsChange = vi.fn(); render(); @@ -261,35 +195,6 @@ describe("Board", () => { ); }); - test("triggers onItemsChange on resize via UAP action button click", () => { - const onItemsChange = vi.fn(); - render(); - - const resizeHandle = createWrapper().findBoardItem()!.findResizeHandle()!; - const resizeHandleEl = resizeHandle.getElement(); - - fireEvent(resizeHandleEl, new MouseEvent("pointerdown", { bubbles: true })); - fireEvent(resizeHandleEl, new MouseEvent("pointerup", { bubbles: true })); - const resizeHandleWrapper = new DragHandleWrapper(document.body); - const blockEndUapAction = resizeHandleWrapper.findVisibleDirectionButtonBlockEnd()!.getElement(); - expect(resizeHandleWrapper.findAllVisibleDirectionButtons()).toHaveLength(4); - - fireEvent.click(blockEndUapAction); - fireEvent.click(blockEndUapAction); - fireEvent.blur(blockEndUapAction); - expect(onItemsChange).toBeCalledWith( - expect.objectContaining({ - detail: { - resizedItem: expect.objectContaining({ id: "1" }), - items: [ - { id: "1", data: { title: "Item 1" }, columnOffset: { 1: 0 }, columnSpan: 1, rowSpan: 4 }, - { id: "2", data: { title: "Item 2" }, columnOffset: { 1: 0 } }, - ], - }, - }), - ); - }); - test("triggers onItemsChange on remove", async () => { const onItemsChange = vi.fn(); render(); diff --git a/src/internal/drag-handle/index.tsx b/src/internal/drag-handle/index.tsx index d3b04449..b334ce14 100644 --- a/src/internal/drag-handle/index.tsx +++ b/src/internal/drag-handle/index.tsx @@ -3,66 +3,35 @@ import { ForwardedRef, forwardRef, KeyboardEvent, PointerEvent } from "react"; import clsx from "clsx"; -import { - InternalDragHandle, - InternalDragHandleProps, -} from "@cloudscape-design/components/internal/do-not-use/drag-handle"; +import Icon from "@cloudscape-design/components/icon"; -import { CLICK_DRAG_THRESHOLD, HandleActiveState } from "../item-container"; +import Handle from "../handle"; import styles from "./styles.css.js"; -import testUtilsStyles from "./test-classes/styles.css.js"; export interface DragHandleProps { ariaLabelledBy: string; ariaDescribedBy: string; onPointerDown: (event: PointerEvent) => void; onKeyDown: (event: KeyboardEvent) => void; - activeState: HandleActiveState; - initialShowButtons?: boolean; - onDirectionClick: InternalDragHandleProps["onDirectionClick"]; - dragHandleTooltipText?: string; + isActive: boolean; } function DragHandle( - { - ariaLabelledBy, - ariaDescribedBy, - onPointerDown, - onKeyDown, - activeState, - initialShowButtons, - onDirectionClick, - dragHandleTooltipText, - }: DragHandleProps, - ref: ForwardedRef, + { ariaLabelledBy, ariaDescribedBy, onPointerDown, onKeyDown, isActive }: DragHandleProps, + ref: ForwardedRef, ) { return ( - + onKeyDown={onKeyDown} + > + + ); } diff --git a/src/internal/drag-handle/styles.scss b/src/internal/drag-handle/styles.scss index 6321947c..fa65c8b1 100644 --- a/src/internal/drag-handle/styles.scss +++ b/src/internal/drag-handle/styles.scss @@ -7,13 +7,16 @@ .handle { cursor: grab; - @include shared.handle(); } .handle:active { cursor: grabbing; } +.handle:not(.active):focus-visible { + @include shared.focus-highlight(); +} + .active { outline: none; } diff --git a/src/internal/drag-handle/test-classes/styles.scss b/src/internal/drag-handle/test-classes/styles.scss deleted file mode 100644 index 29629953..00000000 --- a/src/internal/drag-handle/test-classes/styles.scss +++ /dev/null @@ -1,8 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -.active-uap { - /* used in test-utils */ -} diff --git a/src/internal/global-drag-state-styles/index.ts b/src/internal/global-drag-state-styles/index.ts index 1f332b60..d9d2f03a 100644 --- a/src/internal/global-drag-state-styles/index.ts +++ b/src/internal/global-drag-state-styles/index.ts @@ -9,25 +9,20 @@ function assertNever(value: never) { } function setup({ operation, interactionType }: DragAndDropData) { - const isPointerInteraction = interactionType === "pointer"; switch (operation) { case "insert": case "reorder": - if (isPointerInteraction) { - document.body.classList.add(styles["show-grab-cursor"]); - } + document.body.classList.add(styles["show-grab-cursor"]); break; case "resize": - if (isPointerInteraction) { - document.body.classList.add(styles["show-resize-cursor"]); - } + document.body.classList.add(styles["show-resize-cursor"]); break; default: // there will be a type error if not all operation types are handled assertNever(operation); } - if (isPointerInteraction) { + if (interactionType === "pointer") { document.body.classList.add(styles["disable-selection"]); } } diff --git a/src/internal/handle/index.tsx b/src/internal/handle/index.tsx new file mode 100644 index 00000000..c8501cd5 --- /dev/null +++ b/src/internal/handle/index.tsx @@ -0,0 +1,21 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { ButtonHTMLAttributes, ForwardedRef, forwardRef, PointerEvent } from "react"; +import clsx from "clsx"; + +import styles from "./styles.css.js"; + +function Handle(props: ButtonHTMLAttributes, ref: ForwardedRef) { + function handlePointerDown(event: PointerEvent) { + if (event.button !== 0) { + return; + } + props.onPointerDown?.(event); + } + + return ( + - @@ -56,71 +46,22 @@ test("renders item container", () => { expect(getByTestId("content")).not.toBe(null); }); -describe("pointer interaction", () => { - const clickHandle = (selector: HTMLElement, pointerDownOptions?: MouseEventInit) => { - fireEvent(selector, new MouseEvent("pointerdown", { bubbles: true, button: 0, ...pointerDownOptions })); - fireEvent(selector, new MouseEvent("pointerup", { bubbles: true })); - }; - afterEach(vi.resetAllMocks); - - test.each(["drag-handle", "resize-handle"])("ignores right-click on %s", (handleSelector) => { - const { getByTestId } = render(); - clickHandle(getByTestId(handleSelector), { button: 1 }); - expect(mockDraggable.start).not.toHaveBeenCalled(); - }); - - test("starts drag transition when drag handle is clicked and item belongs to grid", () => { - const { getByTestId } = render(); - clickHandle(getByTestId("drag-handle")); - expect(mockDraggable.start).toBeCalledWith("reorder", "pointer", expect.any(Coordinates)); - }); - - test("starts insert transition when drag handle is clicked and item does not belong to grid", () => { - const { getByTestId } = render(); - clickHandle(getByTestId("drag-handle")); - expect(mockDraggable.start).toBeCalledWith("insert", "pointer", expect.any(Coordinates)); - }); - - test("starts resize transition when resize handle is clicked", () => { - const { getByTestId } = render(); - clickHandle(getByTestId("resize-handle")); - expect(mockDraggable.start).toBeCalledWith("resize", "pointer", expect.any(Coordinates)); - }); - - test("does not call updateTransition on pointer down and a mouse movement within the CLICK_DRAG_THRESHOLD", () => { - const { getByTestId } = render(); - clickHandle(getByTestId("drag-handle")); - expect(mockDraggable.updateTransition).not.toBeCalledWith(); - }); - - test("call updateTransition on pointer down and a mouse movement outside the CLICK_DRAG_THRESHOLD", () => { - const { getByTestId } = render(); - const dragHandleEl = getByTestId("drag-handle"); - fireEvent(dragHandleEl, new MouseEvent("pointerdown", { clientX: 10, clientY: 20, bubbles: true, button: 0 })); - fireEvent(dragHandleEl, new MouseEvent("pointermove", { clientX: 15, clientY: 20, bubbles: true })); - expect(mockDraggable.updateTransition).toBeCalledWith(expect.any(Coordinates)); - }); +test("starts drag transition when drag handle is clicked and item belongs to grid", () => { + const { getByTestId } = render(); + getByTestId("drag-handle").click(); + expect(mockDraggable.start).toBeCalledWith("reorder", "pointer", expect.any(Coordinates)); }); -describe("keyboard interaction", () => { - describe.each(["drag", "resize"])("%s handle", (handle: string) => { - test(`starts keyboard transition when ${handle} handle receives enter and item belongs to grid`, () => { - const { getByTestId } = render(); - fireEvent.keyDown(getByTestId(`${handle}-handle`), { key: "Enter" }); - expect(mockDraggable.start).toBeCalledWith("reorder", "keyboard", expect.any(Coordinates)); - }); +test("starts insert transition when drag handle is clicked and item does not belong to grid", () => { + const { getByTestId } = render(); + getByTestId("drag-handle").click(); + expect(mockDraggable.start).toBeCalledWith("insert", "pointer", expect.any(Coordinates)); +}); - test.each([ - { key: "ArrowUp", direction: "up" }, - { key: "ArrowDown", direction: "down" }, - { key: "ArrowLeft", direction: "left" }, - { key: "ArrowRight", direction: "right" }, - ])(`calls onKeyMove($direction) when ${handle} handle receives $key keyDown event`, () => { - const { getByTestId } = render(); - fireEvent.keyDown(getByTestId(`${handle}-handle`), { key: "ArrowUp" }); - expect(onKeyMoveMock).toBeCalledWith("up"); - }); - }); +test("starts resize transition when resize handle is clicked", () => { + const { getByTestId } = render(); + getByTestId("resize-handle").click(); + expect(mockDraggable.start).toBeCalledWith("resize", "pointer", expect.any(Coordinates)); }); test("does not renders in portal when item in reorder state by a pointer", () => { diff --git a/src/internal/item-container/__tests__/utils.test.ts b/src/internal/item-container/__tests__/utils.test.ts deleted file mode 100644 index e146f7f5..00000000 --- a/src/internal/item-container/__tests__/utils.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { beforeEach, describe, expect, Mock, test, vi } from "vitest"; - -import { getLogicalClientX as originalGetLogicalClientX } from "@cloudscape-design/component-toolkit/internal"; - -import type { Operation } from "../../../../lib/components/internal/dnd-controller/controller"; -import type { Transition } from "../../../../lib/components/internal/item-container"; -import { determineHandleActiveState } from "../../../../lib/components/internal/item-container/utils"; -import { - calculateInitialPointerData, - DetermineHandleActiveStateArgs, - getDndOperationType, - hasPointerMovedBeyondThreshold, -} from "../../../../lib/components/internal/item-container/utils"; -import { Coordinates } from "../../../../lib/components/internal/utils/coordinates"; - -const mockRect = { - insetInlineStart: 10, - insetBlockStart: 20, - insetInlineEnd: 110, // 10 + 100 (width) - insetBlockEnd: 120, // 20 + 100 (height) - inlineSize: 100, - blockSize: 100, - left: 10, - right: 110, - top: 20, - bottom: 120, - width: 100, - height: 100, - x: 10, - y: 20, -}; - -const mockPointerEvent = (clientX: number, clientY: number): Partial => ({ - clientX, - clientY, -}); - -vi.mock("@cloudscape-design/component-toolkit/internal", async (importOriginal) => { - const actual = (await importOriginal()) as any; - return { - ...actual, - getLogicalClientX: vi.fn(), - }; -}); -const mockGetLogicalClientX = originalGetLogicalClientX as Mock; - -describe("getDndOperationType", () => { - interface TestCases { - operation: "drag" | "resize"; - isPlaced: boolean; - expected: Operation; - description: string; - } - - const testCases: Array = [ - { operation: "resize", isPlaced: true, expected: "resize", description: "resize when placed" }, - { - operation: "resize", - isPlaced: false, - expected: "resize", - description: "resize when not placed (should still be resize)", - }, - { operation: "drag", isPlaced: true, expected: "reorder", description: "reorder when drag and placed" }, - { operation: "drag", isPlaced: false, expected: "insert", description: "insert when drag and not placed" }, - ]; - - test.each(testCases)('should return "$expected" for $description', ({ operation, isPlaced, expected }) => { - expect(getDndOperationType(operation, isPlaced)).toBe(expected); - }); -}); - -describe("calculateInitialPointerData", () => { - const getMinSizeMock = vi.fn(); - const MOCK_DOCUMENT_CLIENT_WIDTH = 1000; // For RTL simulation - - beforeEach(() => { - getMinSizeMock.mockReset(); - mockGetLogicalClientX.mockReset(); - }); - - describe('when operation is "drag"', () => { - test("should calculate pointerOffset from top-left and null boundaries for LTR", () => { - mockGetLogicalClientX.mockImplementation((event: PointerEvent) => event.clientX); - const event = mockPointerEvent(50, 60) as PointerEvent; - const result = calculateInitialPointerData({ - event, - operation: "drag", - rect: mockRect, - getMinSize: getMinSizeMock, - isRtl: false, - }); - expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, false); - - const expectedPointerOffsetX = event.clientX - mockRect.insetInlineStart; // 50 - 10 = 40 - const expectedPointerOffsetY = event.clientY - mockRect.insetBlockStart; // 60 - 20 = 40 - expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); - expect(result.pointerBoundaries).toBeNull(); - expect(getMinSizeMock).not.toHaveBeenCalled(); - }); - - test("should calculate pointerOffset from top-left and null boundaries for RTL", () => { - mockGetLogicalClientX.mockImplementation((event: PointerEvent) => MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX); - const event = mockPointerEvent(950, 60) as PointerEvent; - const result = calculateInitialPointerData({ - event, - operation: "drag", - rect: mockRect, - getMinSize: getMinSizeMock, - isRtl: true, - }); - expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, true); - - const logicalClientX = MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX; // 1000 - 950 = 50 - const expectedPointerOffsetX = logicalClientX - mockRect.insetInlineStart; // 50 - 10 = 40 - const expectedPointerOffsetY = event.clientY - mockRect.insetBlockStart; // 60 - 20 = 40 - expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); - expect(result.pointerBoundaries).toBeNull(); - expect(getMinSizeMock).not.toHaveBeenCalled(); - }); - }); - - describe('when operation is "resize"', () => { - const minWidth = 50; - const minHeight = 50; - - beforeEach(() => { - getMinSizeMock.mockReturnValue({ minWidth, minHeight }); - }); - - test("should calculate pointerOffset from bottom-right and boundaries for LTR", () => { - mockGetLogicalClientX.mockImplementation((event: PointerEvent) => event.clientX); - const event = mockPointerEvent(150, 160) as PointerEvent; // Pointer beyond item - const result = calculateInitialPointerData({ - event, - operation: "resize", - rect: mockRect, - getMinSize: getMinSizeMock, - isRtl: false, - }); - - expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, false); - expect(getMinSizeMock).toHaveBeenCalledTimes(1); - - const expectedPointerOffsetX = event.clientX - mockRect.insetInlineEnd; // 150 - 110 = 40 - const expectedPointerOffsetY = event.clientY - mockRect.insetBlockEnd; // 160 - 120 = 40 - expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); - - const expectedBoundaryX = event.clientX - mockRect.inlineSize + minWidth; // 150 - 100 + 50 = 100 - const expectedBoundaryY = event.clientY - mockRect.blockSize + minHeight; // 160 - 100 + 50 = 110 - expect(result.pointerBoundaries).toEqual(new Coordinates({ x: expectedBoundaryX, y: expectedBoundaryY })); - }); - - test("should calculate pointerOffset from bottom-right and boundaries for RTL", () => { - mockGetLogicalClientX.mockImplementation((event: PointerEvent) => MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX); - const event = mockPointerEvent(850, 160) as PointerEvent; - const result = calculateInitialPointerData({ - event, - operation: "resize", - rect: mockRect, - getMinSize: getMinSizeMock, - isRtl: true, - }); - - expect(mockGetLogicalClientX).toHaveBeenCalledWith(event, true); - expect(getMinSizeMock).toHaveBeenCalledTimes(1); - - const logicalClientX = MOCK_DOCUMENT_CLIENT_WIDTH - event.clientX; // 1000 - 850 = 150 - const expectedPointerOffsetX = logicalClientX - mockRect.insetInlineEnd; // 150 - 110 = 40 - const expectedPointerOffsetY = event.clientY - mockRect.insetBlockEnd; // 160 - 120 = 40 - expect(result.pointerOffset).toEqual(new Coordinates({ x: expectedPointerOffsetX, y: expectedPointerOffsetY })); - - const expectedBoundaryX = logicalClientX - mockRect.inlineSize + minWidth; // 150 - 100 + 50 = 100 - const expectedBoundaryY = event.clientY - mockRect.blockSize + minHeight; // 160 - 100 + 50 = 110 - expect(result.pointerBoundaries).toEqual(new Coordinates({ x: expectedBoundaryX, y: expectedBoundaryY })); - }); - }); -}); - -describe("hasPointerMovedBeyondThreshold", () => { - const threshold = 3; - - test("should return false when initialPosition is undefined", () => { - const event = mockPointerEvent(50, 60) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, undefined)).toBe(false); - }); - - test("should return false when pointer hasn't moved beyond threshold", () => { - const initialPosition = { x: 50, y: 60 }; - const event = mockPointerEvent(51, 61) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, initialPosition, threshold)).toBe(false); - }); - - test("should return true when pointer moved beyond threshold in positive X direction", () => { - const initialPosition = { x: 50, y: 60 }; - const event = mockPointerEvent(54, 60) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, initialPosition, threshold)).toBe(true); - }); - - test("should return true when pointer moved beyond threshold in negative X direction", () => { - const initialPosition = { x: 50, y: 60 }; - const event = mockPointerEvent(46, 60) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, initialPosition, threshold)).toBe(true); - }); - - test("should return true when pointer moved beyond threshold in positive Y direction", () => { - const initialPosition = { x: 50, y: 60 }; - const event = mockPointerEvent(50, 64) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, initialPosition, threshold)).toBe(true); - }); - - test("should return true when pointer moved beyond threshold in negative Y direction", () => { - const initialPosition = { x: 50, y: 60 }; - const event = mockPointerEvent(50, 56) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, initialPosition, threshold)).toBe(true); - }); - - test("should use default threshold when not provided", () => { - const initialPosition = { x: 50, y: 60 }; - const event = mockPointerEvent(54, 60) as PointerEvent; - expect(hasPointerMovedBeyondThreshold(event, initialPosition)).toBe(true); - }); -}); - -describe("determineHandleActiveState", () => { - const mockTransition = (operation: Operation): Transition => ({ - itemId: "test-item", - operation: operation, - interactionType: "pointer", // Default value while testing, doesn't affect function's logic - sizeTransform: null, - positionTransform: null, - }); - - type InteractionHookValue = DetermineHandleActiveStateArgs["interactionHookValue"]; - - const activeStateTestCases: Array<{ - description: string; - args: Partial; - expected: "pointer" | "uap" | null; - targetOperation?: Operation; - }> = [ - // "pointer" states - { - description: 'return "pointer" if globally active, resize transition, dnd-start', - args: { - isHandleActive: true, - currentTransition: mockTransition("resize"), - interactionHookValue: "dnd-start", - targetOperation: "resize", - }, - expected: "pointer", - }, - { - description: 'return "pointer" if globally active, reorder transition, dnd-start', - args: { - isHandleActive: true, - currentTransition: mockTransition("reorder"), - interactionHookValue: "dnd-start", - targetOperation: "reorder", - }, - expected: "pointer", - }, - // "uap" states - { - description: 'return "uap" if globally active, resize transition, uap-action-start', - args: { - isHandleActive: true, - currentTransition: mockTransition("resize"), - interactionHookValue: "uap-action-start", - targetOperation: "resize", - }, - expected: "uap", - }, - { - description: 'return "uap" if globally active, reorder transition, uap-action-start', - args: { - isHandleActive: true, - currentTransition: mockTransition("reorder"), - interactionHookValue: "uap-action-start", - targetOperation: "reorder", - }, - expected: "uap", - }, - // Null states - { - description: "return null if not globally active", - args: { - isHandleActive: false, - currentTransition: mockTransition("resize"), - interactionHookValue: "dnd-start", - targetOperation: "resize", - }, - expected: null, - }, - { - description: "return null if no current transition", - args: { - isHandleActive: true, - currentTransition: null, - interactionHookValue: "dnd-start", - targetOperation: "resize", - }, - expected: null, - }, - { - description: "return null if current transition operation mismatches target", - args: { - isHandleActive: true, - currentTransition: mockTransition("reorder"), - interactionHookValue: "dnd-start", - targetOperation: "resize", - }, - expected: null, - }, - { - description: 'return null if interaction hook is not "dnd-start" or "uap-action-start" (e.g., "dnd-active")', - args: { - isHandleActive: true, - currentTransition: mockTransition("resize"), - interactionHookValue: "dnd-active", - targetOperation: "resize", - }, - expected: null, - }, - { - description: "return null if interaction hook is null", - args: { - isHandleActive: true, - currentTransition: mockTransition("resize"), - interactionHookValue: null, - targetOperation: "resize", - }, - expected: null, - }, - { - description: "return null if interaction hook is undefined", - args: { - isHandleActive: true, - currentTransition: mockTransition("resize"), - interactionHookValue: undefined as unknown as InteractionHookValue, - targetOperation: "resize", - }, - expected: null, - }, - { - description: "return null if interaction hook is an arbitrary string", - args: { - isHandleActive: true, - currentTransition: mockTransition("resize"), - interactionHookValue: "some-other-state" as InteractionHookValue, - targetOperation: "resize", - }, - expected: null, - }, - // Combined null conditions - { - description: 'return null if not globally active, even if other conditions match for "pointer"', - args: { - isHandleActive: false, - currentTransition: mockTransition("resize"), - interactionHookValue: "dnd-start", - targetOperation: "resize", - }, - expected: null, - }, - { - description: 'return null if not globally active, even if other conditions match for "uap"', - args: { - isHandleActive: false, - currentTransition: mockTransition("resize"), - interactionHookValue: "uap-action-start", - targetOperation: "resize", - }, - expected: null, - }, - ]; - - test.each(activeStateTestCases)("should $description", ({ args, expected }) => { - expect(determineHandleActiveState(args as DetermineHandleActiveStateArgs)).toBe(expected); - }); -}); diff --git a/src/internal/item-container/index.tsx b/src/internal/item-container/index.tsx index 9ce02aa1..08bb745b 100644 --- a/src/internal/item-container/index.tsx +++ b/src/internal/item-container/index.tsx @@ -10,6 +10,7 @@ import { Ref, RefObject, useContext, + useEffect, useImperativeHandle, useRef, useState, @@ -18,11 +19,7 @@ import { createPortal } from "react-dom"; import { CSS as CSSUtil } from "@dnd-kit/utilities"; import clsx from "clsx"; -import { getLogicalBoundingClientRect } from "@cloudscape-design/component-toolkit/internal"; -import { - useInternalDragHandleInteractionState, - UseInternalDragHandleInteractionStateProps, -} from "@cloudscape-design/components/internal/do-not-use/drag-handle"; +import { getLogicalBoundingClientRect, getLogicalClientX } from "@cloudscape-design/component-toolkit/internal"; import { DragAndDropData, @@ -38,12 +35,6 @@ import { getNormalizedElementRect } from "../utils/screen"; import { throttle } from "../utils/throttle"; import { getCollisionRect } from "./get-collision-rect"; import { getNextDroppable } from "./get-next-droppable"; -import { - calculateInitialPointerData, - determineHandleActiveState, - getDndOperationType, - hasPointerMovedBeyondThreshold, -} from "./utils"; import styles from "./styles.css.js"; @@ -51,79 +42,21 @@ export interface ItemContainerRef { focusDragHandle(): void; } -export type HandleActiveState = null | "pointer" | "uap"; - interface ItemContextType { - /** - * Flag indicating if a drag or resize interaction is currently active. - */ isActive: boolean; - /** - * Flag indicating if the item is currently hidden. - * (When a board item is moved from the palette to the board and the transition is not submitted) - */ - isHidden: boolean; dragHandle: { - /** - * Ref to the drag button. Used to focus the drag handle when moving an item - * from the palette to the board via keyboard or UAP actions. - */ - ref: RefObject; - /** - * Listen to pointerDown events on the drag handle. - * Used to start a transition and attach global event handlers. - */ + ref: RefObject; onPointerDown(event: ReactPointerEvent): void; - /** - * Listen to keyDown events on the drag handle. - */ onKeyDown(event: KeyboardEvent): void; - /** - * Indicating if drag handle is active. - */ - activeState: HandleActiveState; - /** - * Listen to UAP direction button clicks. - */ - onDirectionClick(direction: KeyboardEvent["key"], operation: HandleOperation): void; - /** - * Flag indicating if the UAP buttons should be shown. E.g. when a item is moved from - * the palette via keyboard or UAP to the board. - */ - initialShowButtons?: boolean; + isActive: boolean; }; resizeHandle: null | { - /** - * Listen to pointerDown events on the drag handle. - * Used to start a transition and attach global event handlers. - */ onPointerDown(event: ReactPointerEvent): void; - /** - * Listen to keyDown events on the drag handle. - */ onKeyDown(event: KeyboardEvent): void; - /** - * Indicating if resize handle is active. - */ - activeState: HandleActiveState; - /** - * Listen to UAP direction button clicks. - */ - onDirectionClick(direction: KeyboardEvent["key"], operation: HandleOperation): void; + isActive: boolean; }; } -export interface Transition { - itemId: ItemId; - operation: Operation; - interactionType: InteractionType; - sizeTransform: null | { width: number; height: number }; - positionTransform: null | { x: number; y: number }; - hasDropTarget?: boolean; -} - -export type HandleOperation = "drag" | "resize"; - export const ItemContext = createContext(null); export function useItemContext() { @@ -134,6 +67,15 @@ export function useItemContext() { return ctx; } +interface Transition { + itemId: ItemId; + operation: Operation; + interactionType: InteractionType; + sizeTransform: null | { width: number; height: number }; + positionTransform: null | { x: number; y: number }; + hasDropTarget?: boolean; +} + /** * Defines item's parameters and its relation with the layout. * @@ -159,21 +101,13 @@ export interface ItemContainerProps { minHeight: number; maxHeight: number; }; - onKeyMove?(direction: Direction): void; - children: (hasDropTarget: boolean) => ReactNode; isRtl: () => boolean; } export const ItemContainer = forwardRef(ItemContainerComponent); -// The amount of distance after pointer down that the cursor is allowed to -// jitter for a subsequent mouseup to still register as a "press" instead of -// a drag. A little allowance is needed for usability reasons, but this number -// isn't set in stone. -export const CLICK_DRAG_THRESHOLD = 3; - function ItemContainerComponent( { item, placed, acquired, inTransition, transform, getItemSize, onKeyMove, children, isRtl }: ItemContainerProps, ref: Ref, @@ -185,7 +119,6 @@ function ItemContainerComponent( const [isHidden, setIsHidden] = useState(false); const muteEventsRef = useRef(false); const itemRef = useRef(null); - const initialPointerDownPosition = useRef<{ x: number; y: number } | undefined>(); const draggableApi = useDraggable({ draggableItem: item, getCollisionRect: (operation, coordinates, dropTarget) => { @@ -243,31 +176,74 @@ function ItemContainerComponent( muteEventsRef.current = false; }); - // Handles incremental transition logic shared between different keyboard and UAP interactions. - function handleIncrementalTransition(operation: HandleOperation, submitExisting = false) { + // During the transition listen to pointer move and pointer up events to update/submit transition. + const transitionInteractionType = transition?.interactionType ?? null; + const transitionItemId = transition?.itemId ?? null; + useEffect(() => { + const onPointerMove = throttle((event: PointerEvent) => { + const coordinates = Coordinates.fromEvent(event, { isRtl: isRtl() }); + draggableApi.updateTransition( + new Coordinates({ + x: Math.max(coordinates.x, pointerBoundariesRef.current?.x ?? Number.NEGATIVE_INFINITY), + y: Math.max(coordinates.y, pointerBoundariesRef.current?.y ?? Number.NEGATIVE_INFINITY), + }), + ); + }, 10); + const onPointerUp = () => { + onPointerMove.cancel(); + draggableApi.submitTransition(); + }; + + if (transitionInteractionType === "pointer" && transitionItemId === item.id) { + window.addEventListener("pointermove", onPointerMove); + window.addEventListener("pointerup", onPointerUp); + } + return () => { + window.removeEventListener("pointermove", onPointerMove); + window.removeEventListener("pointerup", onPointerUp); + }; + // draggableApi is not expected to change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [item.id, transitionInteractionType, transitionItemId]); + + useEffect(() => { + if (transitionInteractionType === "keyboard" && transitionItemId === item.id) { + const onPointerDown = () => draggableApi.submitTransition(); + window.addEventListener("pointerdown", onPointerDown, true); + return () => { + window.removeEventListener("pointerdown", onPointerDown, true); + }; + } + // draggableApi is not expected to change + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [item.id, transitionInteractionType, transitionItemId]); + + function onKeyboardTransitionToggle(operation: "drag" | "resize") { // The acquired item is a copy and does not have the transition state. // However, pressing "Space" or "Enter" on the acquired item must submit the active transition. if (acquired) { return draggableApi.submitTransition(); } - // Submit existing transition if requested and one exists - if (submitExisting && transition) { - return draggableApi.submitTransition(); - } - - const rect = getNormalizedElementRect(itemRef.current!); - const coordinates = new Coordinates({ - x: operation === "drag" ? rect.left : rect.right, - y: operation === "drag" ? rect.top : rect.bottom, - }); + // Create new transition if missing. + if (!transition) { + const rect = getNormalizedElementRect(itemRef.current!); + const coordinates = new Coordinates({ + x: operation === "drag" ? rect.left : rect.right, + y: operation === "drag" ? rect.top : rect.bottom, + }); - if (operation === "drag" && !placed) { - draggableApi.start("insert", "keyboard", coordinates); - } else if (operation === "drag") { - draggableApi.start("reorder", "keyboard", coordinates); - } else { - draggableApi.start("resize", "keyboard", coordinates); + if (operation === "drag" && !placed) { + draggableApi.start("insert", "keyboard", coordinates); + } else if (operation === "drag") { + draggableApi.start("reorder", "keyboard", coordinates); + } else { + draggableApi.start("resize", "keyboard", coordinates); + } + } + // Submit a transition if existing. + else { + draggableApi.submitTransition(); } } @@ -295,17 +271,19 @@ function ItemContainerComponent( muteEventsRef.current = true; } - function handleDirectionalMovement(direction: Direction, operation: HandleOperation) { + function onHandleKeyDown(operation: "drag" | "resize", event: KeyboardEvent) { const canInsert = transition && operation === "drag" && !placed; const canNavigate = transition || operation === "drag"; - if (canInsert) { - handleInsert(direction); - } else if (canNavigate) { - onKeyMove?.(direction); - } - } - function onHandleKeyDown(operation: HandleOperation, event: KeyboardEvent) { + // The insert is handled by the item and the navigation is delegated to the containing layout. + const move = (direction: Direction) => { + if (canInsert) { + handleInsert(direction); + } else if (canNavigate) { + onKeyMove?.(direction); + } + }; + const discard = () => { if (transition || acquired) { draggableApi.discardTransition(); @@ -314,16 +292,16 @@ function ItemContainerComponent( switch (event.key) { case "ArrowUp": - return handleDirectionalMovement("up", operation); + return move("up"); case "ArrowDown": - return handleDirectionalMovement("down", operation); + return move("down"); case "ArrowLeft": - return handleDirectionalMovement("left", operation); + return move("left"); case "ArrowRight": - return handleDirectionalMovement("right", operation); + return move("right"); case " ": case "Enter": - return handleIncrementalTransition(operation, true); + return onKeyboardTransitionToggle(operation); case "Escape": return discard(); } @@ -333,82 +311,52 @@ function ItemContainerComponent( // When drag- or resize handle on palette or board item loses focus the transition must be submitted with two exceptions: // 1. If the last interaction is not "keyboard" (the user clicked on another handle issuing a new transition); // 2. If the item is acquired by the board (in that case the focus moves to the board item which is expected, palette item is hidden and all events handlers must be muted). - selectedHook.current.processBlur(); - initialPointerDownPosition.current = undefined; - if (acquired || (transition && transition.interactionType === "keyboard" && !muteEventsRef.current)) { + if (transition && transition.interactionType === "keyboard" && !muteEventsRef.current) { draggableApi.submitTransition(); } } - function handleGlobalPointerMove(event: PointerEvent) { - if (hasPointerMovedBeyondThreshold(event, initialPointerDownPosition.current)) { - selectedHook.current.processPointerMove(event); - } - } + 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 clientY = event.clientY; + pointerOffsetRef.current = new Coordinates({ + x: clientX - rect.insetInlineStart, + y: clientY - rect.insetBlockStart, + }); + originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; + pointerBoundariesRef.current = null; - function handleGlobalPointerUp(event: PointerEvent) { - selectedHook.current.processPointerUp(event); - initialPointerDownPosition.current = undefined; - // Clean up global listeners after interaction ends - window.removeEventListener("pointermove", handleGlobalPointerMove); - window.removeEventListener("pointerup", handleGlobalPointerUp); + draggableApi.start(!placed ? "insert" : "reorder", "pointer", Coordinates.fromEvent(event, { isRtl: isRtl() })); } - function onDragHandlePointerDown(event: ReactPointerEvent, operation: HandleOperation) { - // Prevent UI issues when right-clicking: in such a case the OS context menu will be shown and - // the board while the board-item is active. Because of the context menu under the pointer, - // onPointerUp is not called anymore. In such a case the board item would stuck in onPointerUp. - if (event.button !== 0) { - return; - } - initialPointerDownPosition.current = { x: event.clientX, y: event.clientY }; - - if (operation === "drag") { - selectedHook.current = dragInteractionHook; - } else { - selectedHook.current = resizeInteractionHook; - } - selectedHook.current.processPointerDown(event.nativeEvent); - - // If pointerdown is on our button, start listening for global move and up - window.addEventListener("pointermove", handleGlobalPointerMove); - window.addEventListener("pointerup", handleGlobalPointerUp); + function onDragHandleKeyDown(event: KeyboardEvent) { + onHandleKeyDown("drag", event); } - function handlePointerInteractionStart(event: PointerEvent, operation: "drag" | "resize") { - const currentItemElement = itemRef.current; - if (!currentItemElement) { - console.warn("ItemContainer: itemRef.current is not available on interaction start."); - return; - } - - const rect = getLogicalBoundingClientRect(currentItemElement); + 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 clientY = event.clientY; + pointerOffsetRef.current = new Coordinates({ x: clientX - rect.insetInlineEnd, y: clientY - rect.insetBlockEnd }); originalSizeRef.current = { width: rect.inlineSize, height: rect.blockSize }; - const { pointerOffset, pointerBoundaries } = calculateInitialPointerData({ - event, - operation, - rect, - getMinSize: () => getItemSize(null), - isRtl: isRtl(), + // 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.inlineSize + minWidth, + y: clientY - rect.blockSize + minHeight, }); - pointerOffsetRef.current = pointerOffset; - pointerBoundariesRef.current = pointerBoundaries; - const dndOperation = getDndOperationType(operation, placed); - const startCoordinates = Coordinates.fromEvent(event, { isRtl: isRtl() }); - draggableApi.start(dndOperation, "pointer", startCoordinates); + draggableApi.start("resize", "pointer", Coordinates.fromEvent(event, { isRtl: isRtl() })); } - const onHandleDndTransitionActive = throttle((event: PointerEvent) => { - const coordinates = Coordinates.fromEvent(event, { isRtl: isRtl() }); - draggableApi.updateTransition( - new Coordinates({ - x: Math.max(coordinates.x, pointerBoundariesRef.current?.x ?? Number.NEGATIVE_INFINITY), - y: Math.max(coordinates.y, pointerBoundariesRef.current?.y ?? Number.NEGATIVE_INFINITY), - }), - ); - }, 10) as (event: PointerEvent) => void; + function onResizeHandleKeyDown(event: KeyboardEvent) { + onHandleKeyDown("resize", event); + } const itemTransitionStyle: CSSProperties = {}; const itemTransitionClassNames: string[] = []; @@ -451,35 +399,13 @@ function ItemContainerComponent( } } - const dragHandleRef = useRef(null); + const dragHandleRef = useRef(null); useImperativeHandle(ref, () => ({ - focusDragHandle: () => { - return dragHandleRef.current?.focus(); - }, + focusDragHandle: () => dragHandleRef.current?.focus(), })); - const dragHookProps: UseInternalDragHandleInteractionStateProps = { - onDndStartAction: (event) => handlePointerInteractionStart(event, "drag"), - onDndActiveAction: onHandleDndTransitionActive, - onDndEndAction: () => transition && draggableApi.submitTransition(), - onUapActionStartAction: () => handleIncrementalTransition("drag"), - }; - const resizeHookProps: UseInternalDragHandleInteractionStateProps = { - onDndStartAction: (event) => handlePointerInteractionStart(event, "resize"), - onDndActiveAction: onHandleDndTransitionActive, - onDndEndAction: () => transition && draggableApi.submitTransition(), - onUapActionStartAction: () => handleIncrementalTransition("resize"), - }; - const dragInteractionHook = useInternalDragHandleInteractionState(dragHookProps); - const resizeInteractionHook = useInternalDragHandleInteractionState(resizeHookProps); - // We use a ref to the hook for the handle which is currently active. Distinguishment is managed in the handle button's onPointerDown callback. - const selectedHook = useRef(dragInteractionHook); - const isActive = (!!transition && !isHidden) || !!acquired; - const shouldUsePortal = - transition?.operation === "insert" && - transition?.interactionType === "pointer" && - selectedHook.current.interaction.value === "dnd-active"; + const shouldUsePortal = transition?.operation === "insert" && transition?.interactionType === "pointer"; const childrenRef = useRef(null); if (!inTransition || isActive) { childrenRef.current = children(!!transition?.hasDropTarget); @@ -496,32 +422,17 @@ function ItemContainerComponent( onDragHandlePointerDown(e, "drag"), - onKeyDown: (event: KeyboardEvent) => onHandleKeyDown("drag", event), - activeState: determineHandleActiveState({ - isHandleActive: isActive, - currentTransition: transition, - interactionHookValue: dragInteractionHook.interaction.value, - targetOperation: "reorder", - }), - onDirectionClick: handleDirectionalMovement, - initialShowButtons: - dragInteractionHook.interaction.value === "uap-action-start" || (inTransition && acquired), + onPointerDown: onDragHandlePointerDown, + onKeyDown: onDragHandleKeyDown, + isActive: isActive && transition?.operation === "reorder", }, resizeHandle: placed ? { - onPointerDown: (e) => onDragHandlePointerDown(e, "resize"), - onKeyDown: (event: KeyboardEvent) => onHandleKeyDown("resize", event), - activeState: determineHandleActiveState({ - isHandleActive: isActive, - currentTransition: transition, - interactionHookValue: resizeInteractionHook.interaction.value, - targetOperation: "resize", - }), - onDirectionClick: handleDirectionalMovement, + onPointerDown: onResizeHandlePointerDown, + onKeyDown: onResizeHandleKeyDown, + isActive: isActive && transition?.operation === "resize", } : null, }} diff --git a/src/internal/item-container/utils.ts b/src/internal/item-container/utils.ts deleted file mode 100644 index c43d9760..00000000 --- a/src/internal/item-container/utils.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { getLogicalBoundingClientRect, getLogicalClientX } from "@cloudscape-design/component-toolkit/internal"; -import { InteractionState } from "@cloudscape-design/components/internal/components/drag-handle/hooks/interfaces"; - -import { Operation } from "../dnd-controller/controller"; // Adjust this path -import { Coordinates } from "../utils/coordinates"; // Adjust this path based on your project structure -import { CLICK_DRAG_THRESHOLD, Transition } from "."; - -export type HandleActiveState = null | "pointer" | "uap"; - -export interface DetermineHandleActiveStateArgs { - isHandleActive: boolean; - currentTransition: Transition | null; - interactionHookValue: InteractionState; - targetOperation: Operation; -} - -export function getDndOperationType(uiOperation: "drag" | "resize", isItemPlaced: boolean): Operation { - if (uiOperation === "resize") { - return "resize"; - } - return isItemPlaced ? "reorder" : "insert"; -} - -interface CalculateInitialPointerDataArgs { - event: PointerEvent; - operation: "drag" | "resize"; - rect: ReturnType; - getMinSize: () => { minWidth: number; minHeight: number }; - isRtl: boolean; -} - -/** - * Calculates the initial pointer offset and boundaries for a drag or resize interaction - * to help determine how the item's movement or resizing behaves relative to the pointer. - */ -export function calculateInitialPointerData({ - event, - operation, - rect, - getMinSize, - isRtl, -}: CalculateInitialPointerDataArgs): { - pointerOffset: Coordinates; - pointerBoundaries: Coordinates | null; -} { - const clientX = getLogicalClientX(event, isRtl); - const clientY = event.clientY; - - let pointerOffset: Coordinates; - let pointerBoundaries: Coordinates | null = null; - - if (operation === "resize") { - // For resize, offset is calculated from the bottom-right corner. - pointerOffset = new Coordinates({ - x: clientX - rect.insetInlineEnd, - y: clientY - rect.insetBlockEnd, - }); - - // Boundaries to ensure resize doesn't go below minimum dimensions. - const { minWidth, minHeight } = getMinSize(); - pointerBoundaries = new Coordinates({ - x: clientX - rect.inlineSize + minWidth, - y: clientY - rect.blockSize + minHeight, - }); - } else { - // For drag, offset is calculated from the top-left corner. - pointerOffset = new Coordinates({ - x: clientX - rect.insetInlineStart, - y: clientY - rect.insetBlockStart, - }); - } - - return { pointerOffset, pointerBoundaries }; -} - -export function hasPointerMovedBeyondThreshold( - event: PointerEvent, - initialPosition: { x: number; y: number } | undefined, - threshold: number = CLICK_DRAG_THRESHOLD, -): boolean { - if (!initialPosition) { - return false; - } - return ( - event.clientX > initialPosition.x + threshold || - event.clientX < initialPosition.x - threshold || - event.clientY > initialPosition.y + threshold || - event.clientY < initialPosition.y - threshold - ); -} - -export function determineHandleActiveState({ - isHandleActive, - currentTransition, - interactionHookValue, - targetOperation, -}: DetermineHandleActiveStateArgs): HandleActiveState { - if (isHandleActive && currentTransition?.operation === targetOperation && interactionHookValue === "dnd-start") { - return "pointer"; - } else if ( - isHandleActive && - currentTransition?.operation === targetOperation && - interactionHookValue === "uap-action-start" - ) { - return "uap"; - } - return null; -} diff --git a/src/internal/resize-handle/index.tsx b/src/internal/resize-handle/index.tsx index c9712566..16634aa4 100644 --- a/src/internal/resize-handle/index.tsx +++ b/src/internal/resize-handle/index.tsx @@ -3,24 +3,18 @@ import { KeyboardEvent, PointerEvent } from "react"; import clsx from "clsx"; -import { - InternalDragHandle, - InternalDragHandleProps, -} from "@cloudscape-design/components/internal/do-not-use/drag-handle"; +import Icon from "@cloudscape-design/components/icon"; -import { CLICK_DRAG_THRESHOLD, HandleActiveState } from "../item-container"; +import Handle from "../handle"; import styles from "./styles.css.js"; -import testUtilsStyles from "./test-classes/styles.css.js"; export interface ResizeHandleProps { ariaLabelledBy: string; ariaDescribedBy: string; onPointerDown: (event: PointerEvent) => void; onKeyDown: (event: KeyboardEvent) => void; - activeState: HandleActiveState; - onDirectionClick: InternalDragHandleProps["onDirectionClick"]; - resizeHandleTooltipText?: string; + isActive: boolean; } export default function ResizeHandle({ @@ -28,33 +22,17 @@ export default function ResizeHandle({ ariaDescribedBy, onPointerDown, onKeyDown, - activeState, - onDirectionClick, - resizeHandleTooltipText, + isActive, }: ResizeHandleProps) { return ( - + onKeyDown={onKeyDown} + > + + ); } diff --git a/src/internal/resize-handle/styles.scss b/src/internal/resize-handle/styles.scss index ce50a882..881e0b0e 100644 --- a/src/internal/resize-handle/styles.scss +++ b/src/internal/resize-handle/styles.scss @@ -7,13 +7,16 @@ .handle { cursor: nwse-resize; - @include shared.handle(); @include shared.with-direction("rtl") { cursor: nesw-resize; } } +.handle:not(.active):focus-visible { + @include shared.focus-highlight(); +} + .active { outline: none; } diff --git a/src/internal/resize-handle/test-classes/styles.scss b/src/internal/resize-handle/test-classes/styles.scss deleted file mode 100644 index 29629953..00000000 --- a/src/internal/resize-handle/test-classes/styles.scss +++ /dev/null @@ -1,8 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -.active-uap { - /* used in test-utils */ -} diff --git a/src/internal/shared.scss b/src/internal/shared.scss index 6d71eac0..f204a39b 100644 --- a/src/internal/shared.scss +++ b/src/internal/shared.scss @@ -30,18 +30,3 @@ @content; } } - -@mixin handle { - appearance: none; - background: transparent; - border: none; - padding-block: cs.$space-scaled-xxs; - padding-inline: cs.$space-scaled-xxs; - block-size: auto; - - color: cs.$color-text-interactive-default; - - &:hover { - color: cs.$color-text-interactive-hover; - } -} diff --git a/test/functional/board-layout/dnd-page-object.ts b/test/functional/board-layout/dnd-page-object.ts index b95fce3b..5341527f 100644 --- a/test/functional/board-layout/dnd-page-object.ts +++ b/test/functional/board-layout/dnd-page-object.ts @@ -174,11 +174,4 @@ export class DndPageObject extends BasePageObject { const liveAnnouncements = await this.browser.execute(() => window.__liveAnnouncements ?? []); return liveAnnouncements; } - - // Clicking on a drag/resize handle performs an animation (show the UAP actions, moving the board item). - // The pause after the click makes the functional test stable. - async handleClick(selector: string) { - await this.click(selector); - await this.pause(100); - } } diff --git a/test/functional/board-layout/uap-action-button-interactions.test.ts b/test/functional/board-layout/uap-action-button-interactions.test.ts deleted file mode 100644 index 642699df..00000000 --- a/test/functional/board-layout/uap-action-button-interactions.test.ts +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import { describe, expect, test } from "vitest"; - -import DragHandleWrapper from "@cloudscape-design/components/test-utils/selectors/internal/drag-handle"; - -import createWrapper from "../../../lib/components/test-utils/selectors"; -import { makeQueryUrl, setupTest } from "../../utils"; -import { DndPageObject } from "./dnd-page-object"; - -const boardWrapper = createWrapper().findBoard(); -const itemsPaletteWrapper = createWrapper().findItemsPalette(); -const boardItemDragHandle = (id: string) => boardWrapper.findItemById(id).findDragHandle().toSelector(); -const boardItemResizeHandle = (id: string) => boardWrapper.findItemById(id).findResizeHandle().toSelector(); -const paletteItemDragHandle = (id: string) => itemsPaletteWrapper.findItemById(id).findDragHandle().toSelector(); -const dragHandleWrapper = new DragHandleWrapper("body"); -const directionButtonUp = () => dragHandleWrapper.findVisibleDirectionButtonBlockStart().toSelector(); -const directionButtonDown = () => dragHandleWrapper.findVisibleDirectionButtonBlockEnd().toSelector(); -const directionButtonLeft = () => dragHandleWrapper.findVisibleDirectionButtonInlineStart().toSelector(); -const directionButtonRight = () => dragHandleWrapper.findVisibleDirectionButtonInlineEnd().toSelector(); - -describe("items reordered with UAP actions", () => { - test( - "item move can be submitted", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemDragHandle("A")); - await page.handleClick(directionButtonRight()); - await page.handleClick(directionButtonRight()); - await page.handleClick(directionButtonDown()); - await page.handleClick(directionButtonLeft()); - await page.handleClick(directionButtonUp()); - await page.keys(["Enter"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["B", "A", "C", "D"], - ["B", "A", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - ]); - }), - ); - - test( - "item move via UAP actions and keyboard can be submitted", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemDragHandle("A")); - await page.handleClick(directionButtonRight()); - await page.keys(["ArrowRight"]); - await page.keys(["ArrowDown"]); - await page.handleClick(directionButtonLeft()); - await page.keys(["ArrowUp"]); - await page.keys(["Enter"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["B", "A", "C", "D"], - ["B", "A", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - ]); - }), - ); - - test( - "item move can be discarded", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemDragHandle("A")); - await page.handleClick(directionButtonRight()); - await page.keys(["Escape"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "B", "C", "D"], - ["A", "B", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - ]); - }), - ); - - test( - "item keyboard move automatically submits after leaving focus", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemDragHandle("A")); - await page.handleClick(directionButtonRight()); - await page.keys(["Tab"]); - await expect(page.getGrid()).resolves.toEqual([ - ["B", "A", "C", "D"], - ["B", "A", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - ]); - }), - ); -}); - -describe("items resized with keyboard", () => { - test( - "item resize can be submitted", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemResizeHandle("A")); - await page.handleClick(directionButtonRight()); - await page.handleClick(directionButtonDown()); - await page.keys(["Enter"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "A", "C", "D"], - ["A", "A", "C", "D"], - ["A", "A", "G", "H"], - ["E", "B", "G", "H"], - ["E", "B", " ", " "], - [" ", "F", " ", " "], - [" ", "F", " ", " "], - ]); - }), - ); - - test( - "item resize via UAP actions and keyboard can be submitted", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemResizeHandle("A")); - await page.handleClick(directionButtonRight()); - await page.keys(["ArrowDown"]); - await page.keys(["Enter"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "A", "C", "D"], - ["A", "A", "C", "D"], - ["A", "A", "G", "H"], - ["E", "B", "G", "H"], - ["E", "B", " ", " "], - [" ", "F", " ", " "], - [" ", "F", " ", " "], - ]); - }), - ); - - test( - "item resize can be discarded", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(boardItemResizeHandle("A")); - await page.handleClick(directionButtonRight()); - await page.handleClick(directionButtonDown()); - await page.keys(["Escape"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "B", "C", "D"], - ["A", "B", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - ]); - }), - ); - - test( - "can't resize below min row/col span", - setupTest( - makeQueryUrl( - [ - ["X", "X", " ", " "], - ["X", "X", " ", " "], - ["X", "X", " ", " "], - ["X", "X", " ", " "], - ], - [], - ), - DndPageObject, - async (page) => { - await page.handleClick(boardItemResizeHandle("X")); - await page.handleClick(directionButtonLeft()); - await page.handleClick(directionButtonUp()); - await page.keys(["Enter"]); - await expect(page.getGrid()).resolves.toEqual([ - ["X", "X", " ", " "], - ["X", "X", " ", " "], - ["X", "X", " ", " "], - ["X", "X", " ", " "], - ]); - }, - ), - ); -}); - -describe("items inserted with keyboard", () => { - test( - "item insert can be submitted", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(paletteItemDragHandle("I")); - await page.handleClick(directionButtonLeft()); - await page.handleClick(directionButtonDown()); - await page.handleClick(directionButtonDown()); - await page.handleClick(directionButtonDown()); - await page.handleClick(directionButtonDown()); - await page.keys(["Enter"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "B", "C", "D"], - ["A", "B", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - [" ", " ", " ", "I"], - [" ", " ", " ", "I"], - ]); - }), - ); - - test( - "item insert with keyboard automatically submits after mouse interaction", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(paletteItemDragHandle("I")); - await page.handleClick(directionButtonLeft()); - - // click anywhere on the page to submit the current transition, for example on another item handle - await page.handleClick(boardItemResizeHandle("A")); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "B", "C", "I"], - ["A", "B", "C", "I"], - ["E", "F", "G", "D"], - ["E", "F", "G", "D"], - [" ", " ", " ", "H"], - [" ", " ", " ", "H"], - ]); - }), - ); - - test( - "item insert can be discarded", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(paletteItemDragHandle("I")); - - await page.handleClick(directionButtonDown()); - await page.handleClick(directionButtonDown()); - await page.keys(["Escape"]); - - await expect(page.getGrid()).resolves.toEqual([ - ["A", "B", "C", "D"], - ["A", "B", "C", "D"], - ["E", "F", "G", "H"], - ["E", "F", "G", "H"], - ]); - }), - ); - - test( - "item to be inserted with keyboard has preview content", - setupTest("/index.html#/dnd/engine-a2h-test", DndPageObject, async (page) => { - await page.handleClick(paletteItemDragHandle("I")); - await page.handleClick(directionButtonLeft()); - await expect(page.getText(boardWrapper.find(`[data-item-id="I"]`).toSelector())).resolves.toBe( - "Widget I\n(preview) Empty widget", - ); - await expect(page.isDisplayed(boardItemResizeHandle("I"))).resolves.toBe(false); - }), - ); - - test( - "item in palette should be hidden when it is acquired by the board", - setupTest("/index.html#/micro-frontend/integration", DndPageObject, async (page) => { - await page.handleClick(paletteItemDragHandle("M")); - await page.handleClick(directionButtonLeft()); - await expect(page.isDisplayed(paletteItemDragHandle("M"))).resolves.toBe(false); - }), - ); -});