Skip to content

Commit b3ef249

Browse files
author
Johannes Weber
committed
feat: Add UAP directional controls and tooltips to drag/resize handles
This commit enhances the accessibility of board items by adding UAP (Universally Accessible Post-click/tap actions) controls and tooltips to drag and resize handles. UAPs allows drag/resize movements to be operated by a single pointer without dragging. As part of that change: Show blue border around items when they are active: Before this change, the "active" borders have been shown only when interacting with keyboard. Now they will be shown on pointer interactions (drag or resize) as well.
1 parent d636840 commit b3ef249

File tree

22 files changed

+853
-249
lines changed

22 files changed

+853
-249
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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,10 @@ 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+
* \`dragHandleTooltipText\` (string, optional) - the Text for the drag handle Tooltip.
207+
* \`resizeHandleTooltipText\` (string, optional) - the Text for the resize handle Tooltip.",
205208
"inlineType": {
206209
"name": "BoardItemProps.I18nStrings",
207210
"properties": [
@@ -215,6 +218,11 @@ ARIA labels:
215218
"optional": false,
216219
"type": "string",
217220
},
221+
{
222+
"name": "dragHandleTooltipText",
223+
"optional": true,
224+
"type": "string",
225+
},
218226
{
219227
"name": "resizeHandleAriaDescription",
220228
"optional": true,
@@ -225,6 +233,11 @@ ARIA labels:
225233
"optional": false,
226234
"type": "string",
227235
},
236+
{
237+
"name": "resizeHandleTooltipText",
238+
"optional": true,
239+
"type": "string",
240+
},
228241
],
229242
"type": "object",
230243
},

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ 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+
isActivePointer: false,
18+
isActiveUap: false,
19+
onDirectionClick: () => {},
1720
},
1821
resizeHandle: {
1922
onPointerDown: () => {},
2023
onKeyDown: () => {},
21-
isActive: false,
24+
isActivePointer: false,
25+
isActiveUap: false,
26+
onDirectionClick: () => {},
2227
},
2328
}}
2429
>

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ 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+
* * `dragHandleTooltipText` (string, optional) - the Text for the drag handle Tooltip.
42+
* * `resizeHandleTooltipText` (string, optional) - the Text for the resize handle Tooltip.
4043
*/
4144
i18nStrings: BoardItemProps.I18nStrings;
4245
}
@@ -47,5 +50,7 @@ export namespace BoardItemProps {
4750
dragHandleAriaDescription?: string;
4851
resizeHandleAriaLabel: string;
4952
resizeHandleAriaDescription?: string;
53+
dragHandleTooltipText?: string;
54+
resizeHandleTooltipText?: string;
5055
}
5156
}

src/board-item/internal.tsx

Lines changed: 38 additions & 11 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,7 +38,7 @@ 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();
@@ -42,14 +54,24 @@ export function InternalBoardItem({
4254
header={
4355
<WidgetContainerHeader
4456
handle={
45-
<DragHandle
46-
ref={dragHandle.ref}
47-
ariaLabelledBy={dragHandleAriaLabelledBy}
48-
ariaDescribedBy={dragHandleAriaDescribedBy}
49-
onPointerDown={dragHandle.onPointerDown}
50-
onKeyDown={dragHandle.onKeyDown}
51-
isActive={dragHandle.isActive}
52-
/>
57+
<>
58+
{!isHidden && (
59+
<DragHandle
60+
ref={dragHandle.ref}
61+
ariaLabelledBy={dragHandleAriaLabelledBy}
62+
ariaDescribedBy={dragHandleAriaDescribedBy}
63+
onPointerDown={dragHandle.onPointerDown}
64+
onKeyDown={dragHandle.onKeyDown}
65+
isActivePointer={dragHandle.isActivePointer}
66+
isActiveUap={dragHandle.isActiveUap}
67+
initialShowButtons={dragHandle.initialShowButtons}
68+
onDirectionClick={(direction) =>
69+
dragHandle.onDirectionClick(mapToKeyboardDirection(direction), "drag")
70+
}
71+
dragHandleTooltipText={i18nStrings.dragHandleTooltipText}
72+
/>
73+
)}
74+
</>
5375
}
5476
settings={settings}
5577
>
@@ -62,14 +84,19 @@ export function InternalBoardItem({
6284
>
6385
{children}
6486
</Container>
65-
{resizeHandle && (
87+
{resizeHandle && !isHidden && (
6688
<div className={styles.resizer}>
6789
<ResizeHandle
6890
ariaLabelledBy={resizeHandleAriaLabelledBy}
6991
ariaDescribedBy={resizeHandleAriaDescribedBy}
7092
onPointerDown={resizeHandle.onPointerDown}
7193
onKeyDown={resizeHandle.onKeyDown}
72-
isActive={resizeHandle.isActive}
94+
isActivePointer={resizeHandle.isActivePointer}
95+
isActiveUap={resizeHandle.isActiveUap}
96+
onDirectionClick={(direction) => {
97+
resizeHandle.onDirectionClick(mapToKeyboardDirection(direction), "resize");
98+
}}
99+
resizeHandleTooltipText={i18nStrings.resizeHandleTooltipText}
73100
/>
74101
</div>
75102
)}

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)