Skip to content

Commit c9f14f6

Browse files
johannes-weberJohannes Weber
andauthored
feat: Add uapTriggerMode property to InternalDragHandle component (#3493)
Co-authored-by: Johannes Weber <jowejowe@amazon.com>
1 parent eca10bb commit c9f14f6

File tree

6 files changed

+140
-41
lines changed

6 files changed

+140
-41
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
{
165165
"path": "lib/components/internal/widget-exports.js",
166166
"brotli": false,
167-
"limit": "775 kB",
167+
"limit": "776 kB",
168168
"ignore": "react-dom"
169169
}
170170
],

src/internal/components/drag-handle-wrapper/__tests__/drag-handle-wrapper.test.tsx

Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ afterEach(() => {
2323
jest.restoreAllMocks();
2424
});
2525

26+
function getDirectionButton(direction: Direction) {
27+
return document.querySelector<HTMLButtonElement>(
28+
`.${styles[`direction-button-wrapper-${direction}`]} .${styles['direction-button']}`
29+
);
30+
}
31+
32+
function expectDirectionButtonHidden(direction: Direction) {
33+
// Direction buttons get hidden via transition which doesn't end in JSDOM, so we just listen
34+
// for the exiting classname instead.
35+
const motionExitingClass = styles['direction-button-wrapper-motion-exiting'];
36+
expect(getDirectionButton(direction)?.parentElement).toHaveClass(motionExitingClass);
37+
}
38+
2639
function renderDragHandle(props: Omit<DragHandleWrapperProps, 'children'>) {
2740
const { container } = render(
2841
<DragHandleWrapper {...props}>
@@ -39,11 +52,6 @@ function renderDragHandle(props: Omit<DragHandleWrapperProps, 'children'>) {
3952
container.querySelector<HTMLButtonElement>('#drag-button')!.focus();
4053
},
4154
getTooltip: () => document.querySelector(`.${tooltipStyles.root}`),
42-
getDirectionButton: (direction: Direction) => {
43-
return document.querySelector<HTMLButtonElement>(
44-
`.${styles[`direction-button-wrapper-${direction}`]} .${styles['direction-button']}`
45-
);
46-
},
4755
};
4856
}
4957

@@ -127,43 +135,125 @@ test('hides tooltip on Escape', () => {
127135
});
128136

129137
test("doesn't show direction buttons by default", () => {
130-
const { getDirectionButton } = renderDragHandle({
138+
renderDragHandle({
131139
directions: { 'block-start': 'active' },
132-
tooltipText: 'Click me!',
133140
});
134141

135142
expect(getDirectionButton('block-start')).not.toBeInTheDocument();
136143
expect(getDirectionButton('block-end')).not.toBeInTheDocument();
137144
});
138145

139-
test('shows direction buttons when focus enters the button as result of a key input', () => {
140-
const { dragHandle, getDirectionButton } = renderDragHandle({
141-
directions: { 'block-start': 'active', 'block-end': 'active' },
142-
tooltipText: 'Click me!',
146+
describe('triggerMode = focus (default)', () => {
147+
test('shows direction buttons when focus enters the button', () => {
148+
const { dragHandle } = renderDragHandle({
149+
directions: { 'block-start': 'active', 'block-end': 'active' },
150+
});
151+
152+
document.body.dataset.awsuiFocusVisible = 'true';
153+
dragHandle.focus();
154+
expect(getDirectionButton('block-start')).toBeVisible();
155+
expect(getDirectionButton('block-end')).toBeVisible();
156+
expect(getDirectionButton('inline-start')).not.toBeInTheDocument();
143157
});
144158

145-
document.body.dataset.awsuiFocusVisible = 'true';
146-
dragHandle.focus();
147-
expect(getDirectionButton('block-start')).toBeVisible();
148-
expect(getDirectionButton('block-end')).toBeVisible();
149-
expect(getDirectionButton('inline-start')).not.toBeInTheDocument();
159+
test('hides direction buttons when focus leaves the button', () => {
160+
const { dragHandle } = renderDragHandle({
161+
directions: { 'block-start': 'active', 'block-end': 'active' },
162+
tooltipText: 'Click me!',
163+
});
164+
165+
document.body.dataset.awsuiFocusVisible = 'true';
166+
dragHandle.focus();
167+
168+
expect(getDirectionButton('block-start')).toBeInTheDocument();
169+
expect(getDirectionButton('block-end')).toBeInTheDocument();
170+
171+
fireEvent.blur(dragHandle);
172+
expectDirectionButtonHidden('block-start');
173+
expectDirectionButtonHidden('block-end');
174+
});
150175
});
151176

152-
test('hides direction buttons when focus leaves the button', () => {
153-
const { dragHandle, getDirectionButton } = renderDragHandle({
154-
directions: { 'block-start': 'active', 'block-end': 'active' },
155-
tooltipText: 'Click me!',
177+
describe('triggerMode = keyboard-activate', () => {
178+
test('does not show direction buttons when focus enters the button', () => {
179+
const { dragHandle } = renderDragHandle({
180+
directions: { 'block-start': 'active', 'block-end': 'active' },
181+
triggerMode: 'keyboard-activate',
182+
});
183+
184+
document.body.dataset.awsuiFocusVisible = 'true';
185+
dragHandle.focus();
186+
expect(getDirectionButton('block-start')).toBeNull();
187+
expect(getDirectionButton('block-end')).toBeNull();
188+
expect(getDirectionButton('inline-start')).toBeNull();
189+
expect(getDirectionButton('inline-end')).toBeNull();
156190
});
157191

158-
document.body.dataset.awsuiFocusVisible = 'true';
159-
fireEvent.focusIn(dragHandle, { relatedElement: document.body });
160-
fireEvent.focusOut(dragHandle, { relatedElement: document.body });
161-
expect(getDirectionButton('block-start')).not.toBeInTheDocument();
162-
expect(getDirectionButton('block-end')).not.toBeInTheDocument();
192+
test.each(['Enter', ' '])('show direction buttons when "%s" key is pressed on the focused button', key => {
193+
const { dragHandle } = renderDragHandle({
194+
directions: { 'block-start': 'active', 'block-end': 'active' },
195+
triggerMode: 'keyboard-activate',
196+
});
197+
198+
document.body.dataset.awsuiFocusVisible = 'true';
199+
dragHandle.focus();
200+
expect(getDirectionButton('block-start')).not.toBeInTheDocument();
201+
expect(getDirectionButton('block-end')).not.toBeInTheDocument();
202+
expect(getDirectionButton('inline-start')).not.toBeInTheDocument();
203+
expect(getDirectionButton('inline-end')).not.toBeInTheDocument();
204+
205+
fireEvent.keyDown(dragHandle, { key });
206+
207+
expect(getDirectionButton('block-start')).toBeInTheDocument();
208+
expect(getDirectionButton('block-end')).toBeInTheDocument();
209+
expect(getDirectionButton('inline-start')).not.toBeInTheDocument();
210+
expect(getDirectionButton('inline-end')).not.toBeInTheDocument();
211+
});
212+
213+
test('hides direction buttons when focus leaves the button', () => {
214+
const { dragHandle } = renderDragHandle({
215+
directions: { 'block-start': 'active', 'block-end': 'active' },
216+
triggerMode: 'keyboard-activate',
217+
});
218+
219+
document.body.dataset.awsuiFocusVisible = 'true';
220+
221+
dragHandle.focus();
222+
fireEvent.keyDown(dragHandle, { key: 'Enter' });
223+
expect(getDirectionButton('block-start')).toBeInTheDocument();
224+
expect(getDirectionButton('block-end')).toBeInTheDocument();
225+
expect(getDirectionButton('inline-start')).not.toBeInTheDocument();
226+
expect(getDirectionButton('inline-end')).not.toBeInTheDocument();
227+
228+
fireEvent.blur(dragHandle);
229+
expectDirectionButtonHidden('block-start');
230+
expectDirectionButtonHidden('block-end');
231+
});
232+
233+
test.each(['Enter', ' '])('hides direction buttons when toggling "%s" key', key => {
234+
const { dragHandle } = renderDragHandle({
235+
directions: { 'block-start': 'active', 'block-end': 'active' },
236+
triggerMode: 'keyboard-activate',
237+
});
238+
239+
document.body.dataset.awsuiFocusVisible = 'true';
240+
241+
fireEvent.keyDown(dragHandle, { key });
242+
243+
expect(getDirectionButton('block-start')).toBeInTheDocument();
244+
expect(getDirectionButton('block-end')).toBeInTheDocument();
245+
expect(getDirectionButton('inline-start')).not.toBeInTheDocument();
246+
expect(getDirectionButton('inline-end')).not.toBeInTheDocument();
247+
248+
fireEvent.keyDown(dragHandle, { key });
249+
250+
expectDirectionButtonHidden('block-start');
251+
expectDirectionButtonHidden('block-end');
252+
});
163253
});
164254

165255
test('shows direction buttons when clicked', () => {
166-
const { dragHandle, getDirectionButton } = renderDragHandle({
256+
const { dragHandle } = renderDragHandle({
167257
directions: { 'block-start': 'active' },
168258
tooltipText: 'Click me!',
169259
});
@@ -174,7 +264,7 @@ test('shows direction buttons when clicked', () => {
174264
});
175265

176266
test(`doesn't show direction buttons when drag is "cancelled"`, () => {
177-
const { dragHandle, getDirectionButton } = renderDragHandle({
267+
const { dragHandle } = renderDragHandle({
178268
directions: { 'block-start': 'active' },
179269
tooltipText: 'Click me!',
180270
});
@@ -185,7 +275,7 @@ test(`doesn't show direction buttons when drag is "cancelled"`, () => {
185275
});
186276

187277
test('shows direction buttons when dragged 2 pixels', () => {
188-
const { dragHandle, getDirectionButton } = renderDragHandle({
278+
const { dragHandle } = renderDragHandle({
189279
directions: { 'block-start': 'active' },
190280
tooltipText: 'Click me!',
191281
});
@@ -197,7 +287,7 @@ test('shows direction buttons when dragged 2 pixels', () => {
197287
});
198288

199289
test("doesn't show direction buttons when dragged more than 3 pixels", () => {
200-
const { dragHandle, getDirectionButton } = renderDragHandle({
290+
const { dragHandle } = renderDragHandle({
201291
directions: { 'block-start': 'active' },
202292
tooltipText: 'Click me!',
203293
});
@@ -209,21 +299,18 @@ test("doesn't show direction buttons when dragged more than 3 pixels", () => {
209299
});
210300

211301
test('hides direction buttons on Escape keypress', () => {
212-
const { dragHandle, showButtons, getDirectionButton } = renderDragHandle({
302+
const { dragHandle, showButtons } = renderDragHandle({
213303
directions: { 'block-start': 'active' },
214304
tooltipText: 'Click me!',
215305
});
216306

217307
showButtons();
218308
fireEvent.keyDown(dragHandle, { key: 'Escape' });
219-
// This kicks off an exit transition which doesn't end in JSDOM, so we just listen
220-
// for the exiting classname instead.
221-
const transitionWrapper = getDirectionButton('block-start')?.parentElement;
222-
expect(transitionWrapper).toHaveClass(styles['direction-button-wrapper-motion-exiting']);
309+
expectDirectionButtonHidden('block-start');
223310
});
224311

225312
test('renders disabled direction buttons', () => {
226-
const { showButtons, getDirectionButton } = renderDragHandle({
313+
const { showButtons } = renderDragHandle({
227314
directions: { 'block-start': 'active' },
228315
tooltipText: 'Click me!',
229316
});
@@ -233,7 +320,7 @@ test('renders disabled direction buttons', () => {
233320
});
234321

235322
test("doesn't render direction buttons if value for direction is undefined", () => {
236-
const { showButtons, getDirectionButton } = renderDragHandle({
323+
const { showButtons } = renderDragHandle({
237324
directions: { 'block-start': 'active', 'inline-start': undefined },
238325
tooltipText: 'Click me!',
239326
});
@@ -244,7 +331,7 @@ test("doesn't render direction buttons if value for direction is undefined", ()
244331
});
245332

246333
test('focus returns to drag button after direction button is clicked', () => {
247-
const { dragHandle, showButtons, getDirectionButton } = renderDragHandle({
334+
const { dragHandle, showButtons } = renderDragHandle({
248335
directions: { 'block-start': 'active', 'inline-start': undefined },
249336
tooltipText: 'Click me!',
250337
});
@@ -257,7 +344,7 @@ test('focus returns to drag button after direction button is clicked', () => {
257344

258345
test('calls onDirectionClick when direction button is pressed', () => {
259346
const onDirectionClick = jest.fn();
260-
const { showButtons, getDirectionButton } = renderDragHandle({
347+
const { showButtons } = renderDragHandle({
261348
directions: { 'block-start': 'active', 'block-end': 'active', 'inline-start': 'active', 'inline-end': 'active' },
262349
tooltipText: 'Click me!',
263350
onDirectionClick,
@@ -282,7 +369,7 @@ test('calls onDirectionClick when direction button is pressed', () => {
282369

283370
test("doesn't call onDirectionClick when disabled direction button is pressed", () => {
284371
const onDirectionClick = jest.fn();
285-
const { showButtons, getDirectionButton } = renderDragHandle({
372+
const { showButtons } = renderDragHandle({
286373
directions: { 'block-start': 'disabled' },
287374
tooltipText: 'Click me!',
288375
onDirectionClick,

src/internal/components/drag-handle-wrapper/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export default function DragHandleWrapper({
2525
tooltipText,
2626
children,
2727
onDirectionClick,
28+
triggerMode = 'focus',
2829
}: DragHandleWrapperProps) {
2930
const wrapperRef = useRef<HTMLDivElement | null>(null);
3031
const dragHandleRef = useRef<HTMLDivElement | null>(null);
@@ -49,7 +50,9 @@ export default function DragHandleWrapper({
4950
// if the action that triggered the focus move was the result of a keypress.
5051
if (document.body.dataset.awsuiFocusVisible && !nodeContains(wrapperRef.current, event.relatedTarget)) {
5152
setShowTooltip(false);
52-
setShowButtons(true);
53+
if (triggerMode === 'focus') {
54+
setShowButtons(true);
55+
}
5356
}
5457
};
5558

@@ -151,6 +154,9 @@ export default function DragHandleWrapper({
151154
// the floating controls.
152155
if (event.key === 'Escape') {
153156
setShowButtons(false);
157+
} else if (triggerMode === 'keyboard-activate' && (event.key === 'Enter' || event.key === ' ')) {
158+
// toggle buttons when Enter or space is pressed in 'keyboard-activate' triggerMode
159+
setShowButtons(prevShowButtons => !prevShowButtons);
154160
} else if (event.key !== 'Alt' && event.key !== 'Control' && event.key !== 'Meta' && event.key !== 'Shift') {
155161
// Pressing any other key will display the focus-visible ring around the
156162
// drag handle if it's in focus, so we should also show the buttons now.

src/internal/components/drag-handle-wrapper/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
export type Direction = 'block-start' | 'block-end' | 'inline-start' | 'inline-end';
55
export type DirectionState = 'active' | 'disabled';
6+
export type TriggerMode = 'focus' | 'keyboard-activate';
67

78
export interface DragHandleWrapperProps {
89
directions: Partial<Record<Direction, DirectionState>>;
910
onDirectionClick?: (direction: Direction) => void;
1011
tooltipText?: string;
1112
children: React.ReactNode;
13+
triggerMode?: TriggerMode;
1214
}

src/internal/components/drag-handle/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const InternalDragHandle = forwardRef(
2424
onPointerDown,
2525
onKeyDown,
2626
onDirectionClick,
27+
triggerMode,
2728
...rest
2829
}: DragHandleProps,
2930
ref: React.Ref<Element>
@@ -35,6 +36,7 @@ const InternalDragHandle = forwardRef(
3536
directions={!disabled ? directions : {}}
3637
tooltipText={tooltipText}
3738
onDirectionClick={onDirectionClick}
39+
triggerMode={triggerMode}
3840
>
3941
<DragHandleButton
4042
ref={ref}

src/internal/components/drag-handle/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import {
55
Direction as WrapperDirection,
66
DirectionState as WrapperDirectionState,
7+
TriggerMode,
78
} from '../drag-handle-wrapper/interfaces';
89

910
export interface DragHandleProps {
@@ -21,6 +22,7 @@ export interface DragHandleProps {
2122
tooltipText?: string;
2223
directions?: Partial<Record<DragHandleProps.Direction, DragHandleProps.DirectionState>>;
2324
onDirectionClick?: (direction: DragHandleProps.Direction) => void;
25+
triggerMode?: TriggerMode;
2426
}
2527

2628
export namespace DragHandleProps {

0 commit comments

Comments
 (0)