Skip to content

Commit aa1f984

Browse files
johannes-weberJohannes Weber
andauthored
feat: Add UAP directional controls and tooltips to drag/resize handles (#356)
Co-authored-by: Johannes Weber <[email protected]>
1 parent 86b06de commit aa1f984

File tree

24 files changed

+1436
-273
lines changed

24 files changed

+1436
-273
lines changed

pages/shared/i18n.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export const boardItemI18nStrings: BoardItemProps.I18nStrings = {
6161
resizeHandleAriaLabel: "Resize handle",
6262
resizeHandleAriaDescription:
6363
"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.",
64+
dragHandleTooltipText: "Drag or select to move",
65+
resizeHandleTooltipText: "Drag or select to resize",
6466
};
6567

6668
export const itemsPaletteI18nStrings: ItemsPaletteProps.I18nStrings<ItemData> = {

src/__tests__/__snapshots__/documenter.test.ts.snap

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,9 @@ ARIA labels:
201201
* \`dragHandleAriaLabel\` (string) - the ARIA label for the drag handle.
202202
* \`dragHandleAriaDescription\` (string, optional) - the ARIA description for the drag handle.
203203
* \`resizeHandleAriaLabel\` (string) - the ARIA label for the resize handle.
204-
* \`resizeHandleAriaDescription\` (string, optional) - the ARIA description for the resize handle.",
204+
* \`resizeHandleAriaDescription\` (string, optional) - the ARIA description for the resize handle.
205+
* \`dragHandleTooltipText\` (string, optional) - the ARIA description for the resize handle.
206+
* \`resizeHandleTooltipText\` (string, optional) - the Text for the resize handle Tooltip.",
205207
"inlineType": {
206208
"name": "BoardItemProps.I18nStrings",
207209
"properties": [
@@ -215,6 +217,11 @@ ARIA labels:
215217
"optional": false,
216218
"type": "string",
217219
},
220+
{
221+
"name": "dragHandleTooltipText",
222+
"optional": true,
223+
"type": "string",
224+
},
218225
{
219226
"name": "resizeHandleAriaDescription",
220227
"optional": true,
@@ -225,6 +232,11 @@ ARIA labels:
225232
"optional": false,
226233
"type": "string",
227234
},
235+
{
236+
"name": "resizeHandleTooltipText",
237+
"optional": true,
238+
"type": "string",
239+
},
228240
],
229241
"type": "object",
230242
},

src/board-item/__tests__/board-item-wrapper.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ export function ItemContextWrapper({ children }: { children: ReactNode }) {
99
<ItemContext.Provider
1010
value={{
1111
isActive: false,
12+
isHidden: false,
1213
dragHandle: {
1314
ref: { current: null },
1415
onPointerDown: () => {},
1516
onKeyDown: () => {},
16-
isActive: false,
17+
activeState: null,
18+
onDirectionClick: () => {},
1719
},
1820
resizeHandle: {
1921
onPointerDown: () => {},
2022
onKeyDown: () => {},
21-
isActive: false,
23+
activeState: null,
24+
onDirectionClick: () => {},
2225
},
2326
}}
2427
>

src/board-item/__tests__/board-item.test.tsx

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { ReactElement } from "react";
5-
import { cleanup, render as libRender } from "@testing-library/react";
5+
import { cleanup, fireEvent, render as libRender } from "@testing-library/react";
66
import { afterEach, describe, expect, test } from "vitest";
77

88
import Button from "@cloudscape-design/components/button";
99
import Container from "@cloudscape-design/components/container";
1010
import ExpandableSection from "@cloudscape-design/components/expandable-section";
1111
import Header from "@cloudscape-design/components/header";
12+
import DragHandleWrapper from "@cloudscape-design/components/test-utils/dom/internal/drag-handle";
13+
import TooltipWrapper from "@cloudscape-design/components/test-utils/dom/internal/tooltip";
1214

1315
import "@cloudscape-design/components/test-utils/dom";
1416
import BoardItem from "../../../lib/components/board-item";
@@ -18,6 +20,8 @@ import { ItemContextWrapper } from "./board-item-wrapper";
1820
const i18nStrings = {
1921
dragHandleAriaLabel: "Drag handle",
2022
resizeHandleAriaLabel: "Resize handle",
23+
dragHandleTooltipText: "Drag or select to move",
24+
resizeHandleTooltipText: "Drag or select to resize",
2125
};
2226

2327
function render(jsx: ReactElement) {
@@ -61,4 +65,72 @@ describe("WidgetContainer", () => {
6165
expect(getByLabelText("Drag handle")).toBeDefined();
6266
expect(getByLabelText("Resize handle")).toBeDefined();
6367
});
68+
69+
test("renders drag handle tooltip text if provided", () => {
70+
render(<BoardItem i18nStrings={i18nStrings} />);
71+
const wrapper = createWrapper();
72+
const dragHandleEl = wrapper.findBoardItem()!.findDragHandle().getElement();
73+
74+
fireEvent(dragHandleEl, new MouseEvent("pointerover", { bubbles: true }));
75+
const tooltipEl = wrapper.findByClassName(TooltipWrapper.rootSelector)!.getElement();
76+
expect(tooltipEl.textContent).toBe("Drag or select to move");
77+
});
78+
79+
test("does not render drag handle tooltip text if not provided", () => {
80+
render(<BoardItem i18nStrings={{ ...i18nStrings, dragHandleTooltipText: undefined }} />);
81+
const wrapper = createWrapper();
82+
const dragHandleEl = wrapper.findBoardItem()!.findDragHandle().getElement();
83+
84+
fireEvent(dragHandleEl, new MouseEvent("pointerover", { bubbles: true }));
85+
expect(wrapper.findByClassName(TooltipWrapper.rootSelector)).toBeNull();
86+
});
87+
88+
test("renders drag handle UAP actions on handle click", () => {
89+
render(<BoardItem i18nStrings={i18nStrings} />);
90+
const dragHandleEl = createWrapper().findBoardItem()!.findDragHandle()!.getElement();
91+
92+
fireEvent(dragHandleEl, new MouseEvent("pointerdown", { bubbles: true }));
93+
fireEvent(dragHandleEl, new MouseEvent("pointerup", { bubbles: true }));
94+
95+
const dragHandleWrapper = new DragHandleWrapper(document.body);
96+
expect(dragHandleWrapper.findAllVisibleDirectionButtons()).toHaveLength(4);
97+
expect(dragHandleWrapper.findVisibleDirectionButtonBlockStart()).toBeDefined();
98+
expect(dragHandleWrapper.findVisibleDirectionButtonBlockEnd()).toBeDefined();
99+
expect(dragHandleWrapper.findVisibleDirectionButtonInlineStart()).toBeDefined();
100+
expect(dragHandleWrapper.findVisibleDirectionButtonInlineEnd()).toBeDefined();
101+
});
102+
103+
test("renders resize handle tooltip text", () => {
104+
render(<BoardItem i18nStrings={i18nStrings} />);
105+
const wrapper = createWrapper();
106+
const resizeHandleEl = wrapper.findBoardItem()!.findResizeHandle()!.getElement();
107+
108+
fireEvent(resizeHandleEl, new MouseEvent("pointerover", { bubbles: true }));
109+
const tooltipEl = wrapper.findByClassName(TooltipWrapper.rootSelector)!.getElement();
110+
expect(tooltipEl.textContent).toBe("Drag or select to resize");
111+
});
112+
113+
test("does not render resize handle tooltip text if not provided", () => {
114+
render(<BoardItem i18nStrings={{ ...i18nStrings, resizeHandleTooltipText: undefined }} />);
115+
const wrapper = createWrapper();
116+
const resizeHandleEl = wrapper.findBoardItem()!.findResizeHandle()!.getElement();
117+
118+
fireEvent(resizeHandleEl, new MouseEvent("pointerover", { bubbles: true }));
119+
expect(wrapper.findByClassName(TooltipWrapper.rootSelector)).toBeNull();
120+
});
121+
122+
test("renders resize handle UAP actions on handle click", () => {
123+
render(<BoardItem i18nStrings={i18nStrings} />);
124+
const resizeHandleEl = createWrapper().findBoardItem()!.findResizeHandle()!.getElement();
125+
126+
fireEvent(resizeHandleEl, new MouseEvent("pointerdown", { bubbles: true }));
127+
fireEvent(resizeHandleEl, new MouseEvent("pointerup", { bubbles: true }));
128+
129+
const dragHandleWrapper = new DragHandleWrapper(document.body);
130+
expect(dragHandleWrapper.findAllVisibleDirectionButtons()).toHaveLength(4);
131+
expect(dragHandleWrapper.findVisibleDirectionButtonBlockStart()).toBeDefined();
132+
expect(dragHandleWrapper.findVisibleDirectionButtonBlockEnd()).toBeDefined();
133+
expect(dragHandleWrapper.findVisibleDirectionButtonInlineStart()).toBeDefined();
134+
expect(dragHandleWrapper.findVisibleDirectionButtonInlineEnd()).toBeDefined();
135+
});
64136
});

src/board-item/interfaces.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export interface BoardItemProps {
3737
* * `dragHandleAriaDescription` (string, optional) - the ARIA description for the drag handle.
3838
* * `resizeHandleAriaLabel` (string) - the ARIA label for the resize handle.
3939
* * `resizeHandleAriaDescription` (string, optional) - the ARIA description for the resize handle.
40+
* * `dragHandleTooltipText` (string, optional) - the ARIA description for the resize handle.
41+
* * `resizeHandleTooltipText` (string, optional) - the Text for the resize handle Tooltip.
4042
*/
4143
i18nStrings: BoardItemProps.I18nStrings;
4244
}
@@ -47,5 +49,7 @@ export namespace BoardItemProps {
4749
dragHandleAriaDescription?: string;
4850
resizeHandleAriaLabel: string;
4951
resizeHandleAriaDescription?: string;
52+
dragHandleTooltipText?: string;
53+
resizeHandleTooltipText?: string;
5054
}
5155
}

src/board-item/internal.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { useId } from "react";
44
import clsx from "clsx";
55

66
import Container from "@cloudscape-design/components/container";
7+
import { InternalDragHandleProps } from "@cloudscape-design/components/internal/do-not-use/drag-handle";
78

89
import { getDataAttributes } from "../internal/base-component/get-data-attributes";
910
import { InternalBaseComponentProps } from "../internal/base-component/use-base-component";
1011
import DragHandle from "../internal/drag-handle";
12+
import { Direction } from "../internal/interfaces";
1113
import { useItemContext } from "../internal/item-container";
1214
import ResizeHandle from "../internal/resize-handle";
1315
import ScreenreaderOnly from "../internal/screenreader-only";
@@ -16,6 +18,16 @@ import type { BoardItemProps } from "./interfaces";
1618

1719
import styles from "./styles.css.js";
1820

21+
const mapToKeyboardDirection = (direction: InternalDragHandleProps.Direction) => {
22+
const directionMap: Record<InternalDragHandleProps.Direction, Direction> = {
23+
"inline-start": "left",
24+
"inline-end": "right",
25+
"block-start": "up",
26+
"block-end": "down",
27+
};
28+
return directionMap[direction];
29+
};
30+
1931
export function InternalBoardItem({
2032
children,
2133
header,
@@ -26,14 +38,20 @@ export function InternalBoardItem({
2638
__internalRootRef,
2739
...rest
2840
}: BoardItemProps & InternalBaseComponentProps) {
29-
const { dragHandle, resizeHandle, isActive } = useItemContext();
41+
const { dragHandle, resizeHandle, isActive, isHidden } = useItemContext();
3042

3143
const dragHandleAriaLabelledBy = useId();
3244
const dragHandleAriaDescribedBy = useId();
3345

3446
const resizeHandleAriaLabelledBy = useId();
3547
const resizeHandleAriaDescribedBy = useId();
3648

49+
// A board item is hidden while moving a board item from the palette to the board via keyboard or UAP.
50+
// The wrapping container is set to invisible, so we don't need to render anything.
51+
if (isHidden) {
52+
return null;
53+
}
54+
3755
return (
3856
<div ref={__internalRootRef} className={styles.root} {...getDataAttributes(rest)}>
3957
<Container
@@ -48,7 +66,10 @@ export function InternalBoardItem({
4866
ariaDescribedBy={dragHandleAriaDescribedBy}
4967
onPointerDown={dragHandle.onPointerDown}
5068
onKeyDown={dragHandle.onKeyDown}
51-
isActive={dragHandle.isActive}
69+
activeState={dragHandle.activeState}
70+
initialShowButtons={dragHandle.initialShowButtons}
71+
onDirectionClick={(direction) => dragHandle.onDirectionClick(mapToKeyboardDirection(direction), "drag")}
72+
dragHandleTooltipText={i18nStrings.dragHandleTooltipText}
5273
/>
5374
}
5475
settings={settings}
@@ -69,7 +90,11 @@ export function InternalBoardItem({
6990
ariaDescribedBy={resizeHandleAriaDescribedBy}
7091
onPointerDown={resizeHandle.onPointerDown}
7192
onKeyDown={resizeHandle.onKeyDown}
72-
isActive={resizeHandle.isActive}
93+
activeState={resizeHandle.activeState}
94+
onDirectionClick={(direction) => {
95+
resizeHandle.onDirectionClick(mapToKeyboardDirection(direction), "resize");
96+
}}
97+
resizeHandleTooltipText={i18nStrings.resizeHandleTooltipText}
7398
/>
7499
</div>
75100
)}

src/board-item/styles.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
.container-override.active {
1414
box-shadow: cs.$shadow-container-active;
1515

16-
:global([data-awsui-focus-visible]) & {
17-
@include shared.focus-highlight(0px, cs.$border-radius-container);
18-
}
16+
@include shared.focus-highlight(0px, cs.$border-radius-container);
1917
}
2018

2119
.header {

0 commit comments

Comments
 (0)