Skip to content

Commit 9914a31

Browse files
authored
[composite] Simplify keyboard bookkeeping (#5249)
1 parent c5ab168 commit 9914a31

3 files changed

Lines changed: 61 additions & 55 deletions

File tree

packages/react/src/internals/composite/composite.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,11 @@ export const END = 'End';
1919
export const PAGE_UP = 'PageUp';
2020
export const PAGE_DOWN = 'PageDown';
2121

22-
export const HORIZONTAL_KEYS = new Set([ARROW_LEFT, ARROW_RIGHT]);
23-
export const HORIZONTAL_KEYS_WITH_EXTRA_KEYS = new Set([ARROW_LEFT, ARROW_RIGHT, HOME, END]);
24-
export const VERTICAL_KEYS = new Set([ARROW_UP, ARROW_DOWN]);
25-
export const VERTICAL_KEYS_WITH_EXTRA_KEYS = new Set([ARROW_UP, ARROW_DOWN, HOME, END]);
26-
export const ARROW_KEYS = new Set([...HORIZONTAL_KEYS, ...VERTICAL_KEYS]);
27-
export const COMPOSITE_KEYS = new Set([...ARROW_KEYS, HOME, END]);
22+
export const COMPOSITE_KEYS = new Set([ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, HOME, END]);
2823

2924
export const SHIFT = 'Shift' as const;
30-
export const CONTROL = 'Control' as const;
31-
export const ALT = 'Alt' as const;
32-
export const META = 'Meta' as const;
33-
export const MODIFIER_KEYS = new Set([SHIFT, CONTROL, ALT, META] as const);
34-
export type ModifierKey = typeof MODIFIER_KEYS extends Set<infer Keys> ? Keys : never;
25+
export const MODIFIER_KEYS = [SHIFT, 'Control', 'Alt', 'Meta'] as const;
26+
export type ModifierKey = (typeof MODIFIER_KEYS)[number];
3527

3628
function isInputElement(element: EventTarget): element is HTMLInputElement {
3729
return isHTMLElement(element) && element.tagName === 'INPUT';

packages/react/src/internals/composite/root/CompositeRoot.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,33 @@ describe('Composite', () => {
186186
expect(item2).not.toHaveFocus();
187187
});
188188

189+
it.each([
190+
{ orientation: 'horizontal' as const, key: 'ArrowRight', prevented: true },
191+
{ orientation: 'horizontal' as const, key: 'ArrowDown', prevented: false },
192+
{ orientation: 'vertical' as const, key: 'ArrowDown', prevented: true },
193+
{ orientation: 'vertical' as const, key: 'ArrowRight', prevented: false },
194+
{ orientation: 'both' as const, key: 'ArrowRight', prevented: true },
195+
{ orientation: 'both' as const, key: 'ArrowDown', prevented: true },
196+
])(
197+
'sets default prevention to $prevented for $key with $orientation orientation',
198+
async ({ orientation, key, prevented }) => {
199+
await render(
200+
<CompositeRoot orientation={orientation}>
201+
<CompositeItem data-testid="1">1</CompositeItem>
202+
<CompositeItem data-testid="2">2</CompositeItem>
203+
</CompositeRoot>,
204+
);
205+
206+
const item1 = screen.getByTestId('1');
207+
act(() => item1.focus());
208+
209+
const allowed = fireEvent.keyDown(item1, { key });
210+
await flushMicrotasks();
211+
212+
expect(allowed).toBe(!prevented);
213+
},
214+
);
215+
189216
it.skipIf(isJSDOM)('updates the order of items', async () => {
190217
function App(props: { items: string[] }) {
191218
return (
@@ -464,6 +491,22 @@ describe('Composite', () => {
464491
});
465492

466493
describe('grid', () => {
494+
it('prevents default for grid navigation outside the configured orientation', async () => {
495+
await render(
496+
<CompositeRoot grid={threeColsGrid} orientation="horizontal">
497+
<TestGridItems />
498+
</CompositeRoot>,
499+
);
500+
501+
const item1 = screen.getByTestId('1');
502+
act(() => item1.focus());
503+
504+
const allowed = fireEvent.keyDown(item1, { key: 'ArrowDown' });
505+
await flushMicrotasks();
506+
507+
expect(allowed).toBe(false);
508+
});
509+
467510
it('uniform 1x1 items', async () => {
468511
function App() {
469512
return (

packages/react/src/internals/composite/root/useCompositeRoot.ts

Lines changed: 15 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,12 @@ import type { TextDirection } from '../../direction-context/DirectionContext';
88
import {
99
COMPOSITE_KEYS,
1010
ARROW_DOWN,
11-
ARROW_KEYS,
1211
ARROW_LEFT,
1312
ARROW_RIGHT,
1413
ARROW_UP,
1514
END,
1615
HOME,
17-
HORIZONTAL_KEYS,
18-
HORIZONTAL_KEYS_WITH_EXTRA_KEYS,
1916
MODIFIER_KEYS,
20-
VERTICAL_KEYS,
21-
VERTICAL_KEYS_WITH_EXTRA_KEYS,
2217
findNonDisabledListIndex,
2318
getMaxListIndex,
2419
getMinListIndex,
@@ -182,8 +177,8 @@ export function useCompositeRoot(params: UseCompositeRootParameters) {
182177
// Stable so that `relayKeyboardEvent` does not invalidate identity-sensitive
183178
// consumers (the `CompositeRootContext` value and trigger data forwarding).
184179
const onKeyDown = useStableCallback((event: React.KeyboardEvent) => {
185-
const RELEVANT_KEYS = enableHomeAndEndKeys ? COMPOSITE_KEYS : ARROW_KEYS;
186-
if (!RELEVANT_KEYS.has(event.key)) {
180+
const isHomeOrEnd = event.key === HOME || event.key === END;
181+
if (!COMPOSITE_KEYS.has(event.key) || (!enableHomeAndEndKeys && isHomeOrEnd)) {
187182
return;
188183
}
189184

@@ -199,17 +194,9 @@ export function useCompositeRoot(params: UseCompositeRootParameters) {
199194
const isRtl = direction === 'rtl';
200195

201196
const horizontalForwardKey = isRtl ? ARROW_LEFT : ARROW_RIGHT;
202-
const forwardKey = {
203-
horizontal: horizontalForwardKey,
204-
vertical: ARROW_DOWN,
205-
both: horizontalForwardKey,
206-
}[orientation];
207197
const horizontalBackwardKey = isRtl ? ARROW_RIGHT : ARROW_LEFT;
208-
const backwardKey = {
209-
horizontal: horizontalBackwardKey,
210-
vertical: ARROW_UP,
211-
both: horizontalBackwardKey,
212-
}[orientation];
198+
const forwardKey = orientation === 'vertical' ? ARROW_DOWN : horizontalForwardKey;
199+
const backwardKey = orientation === 'vertical' ? ARROW_UP : horizontalBackwardKey;
213200

214201
const target = getTarget(event.nativeEvent);
215202
if (target != null && isNativeInput(target) && !isElementDisabled(target)) {
@@ -250,25 +237,12 @@ export function useCompositeRoot(params: UseCompositeRootParameters) {
250237
});
251238
}
252239

253-
const forwardKeys = {
254-
horizontal: [horizontalForwardKey],
255-
vertical: [ARROW_DOWN],
256-
both: [horizontalForwardKey, ARROW_DOWN],
257-
}[orientation];
258-
259-
const backwardKeys = {
260-
horizontal: [horizontalBackwardKey],
261-
vertical: [ARROW_UP],
262-
both: [horizontalBackwardKey, ARROW_UP],
263-
}[orientation];
264-
265-
const preventedKeys = isGrid
266-
? RELEVANT_KEYS
267-
: {
268-
horizontal: enableHomeAndEndKeys ? HORIZONTAL_KEYS_WITH_EXTRA_KEYS : HORIZONTAL_KEYS,
269-
vertical: enableHomeAndEndKeys ? VERTICAL_KEYS_WITH_EXTRA_KEYS : VERTICAL_KEYS,
270-
both: RELEVANT_KEYS,
271-
}[orientation];
240+
const isForwardKey =
241+
(orientation !== 'vertical' && event.key === horizontalForwardKey) ||
242+
(orientation !== 'horizontal' && event.key === ARROW_DOWN);
243+
const isBackwardKey =
244+
(orientation !== 'vertical' && event.key === horizontalBackwardKey) ||
245+
(orientation !== 'horizontal' && event.key === ARROW_UP);
272246

273247
if (enableHomeAndEndKeys) {
274248
if (event.key === HOME) {
@@ -278,24 +252,21 @@ export function useCompositeRoot(params: UseCompositeRootParameters) {
278252
}
279253
}
280254

281-
if (
282-
nextIndex === highlightedIndex &&
283-
(forwardKeys.includes(event.key) || backwardKeys.includes(event.key))
284-
) {
285-
if (loopFocus && nextIndex === maxIndex && forwardKeys.includes(event.key)) {
255+
if (nextIndex === highlightedIndex && (isForwardKey || isBackwardKey)) {
256+
if (loopFocus && nextIndex === maxIndex && isForwardKey) {
286257
nextIndex = minIndex;
287258
if (onLoop) {
288259
nextIndex = onLoop(event, highlightedIndex, nextIndex, elementsRef);
289260
}
290-
} else if (loopFocus && nextIndex === minIndex && backwardKeys.includes(event.key)) {
261+
} else if (loopFocus && nextIndex === minIndex && isBackwardKey) {
291262
nextIndex = maxIndex;
292263
if (onLoop) {
293264
nextIndex = onLoop(event, highlightedIndex, nextIndex, elementsRef);
294265
}
295266
} else {
296267
nextIndex = findNonDisabledListIndex(elementsRef.current, {
297268
startingIndex: nextIndex,
298-
decrement: backwardKeys.includes(event.key),
269+
decrement: isBackwardKey,
299270
disabledIndices,
300271
});
301272
}
@@ -306,7 +277,7 @@ export function useCompositeRoot(params: UseCompositeRootParameters) {
306277
event.stopPropagation();
307278
}
308279

309-
if (preventedKeys.has(event.key)) {
280+
if (isGrid || isHomeOrEnd || isForwardKey || isBackwardKey) {
310281
event.preventDefault();
311282
}
312283
onHighlightedIndexChange(nextIndex, true);

0 commit comments

Comments
 (0)