@astryxdesign/core 0.4.6-canary.cea9ecd → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/useListFocus.d.ts +2 -5
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +6 -12
- package/package.json +3 -3
- package/src/Popover/Popover.test.tsx +1 -27
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +3 -65
- package/src/hooks/useListFocus.ts +7 -15
|
@@ -38,9 +38,7 @@ export interface UseListFocusOptions {
|
|
|
38
38
|
*/
|
|
39
39
|
wrap?: boolean;
|
|
40
40
|
/**
|
|
41
|
-
* Callback when Escape key is pressed.
|
|
42
|
-
* consume the key (`preventDefault`); without it Escape passes through to
|
|
43
|
-
* the surrounding layer.
|
|
41
|
+
* Callback when Escape key is pressed.
|
|
44
42
|
*/
|
|
45
43
|
onEscape?: () => void;
|
|
46
44
|
/**
|
|
@@ -146,8 +144,7 @@ export interface UseListFocusReturn<T extends HTMLElement = HTMLElement> {
|
|
|
146
144
|
* - ArrowUp/ArrowLeft: Move to previous item (wraps to last)
|
|
147
145
|
* - Home: Move to first item
|
|
148
146
|
* - End: Move to last item
|
|
149
|
-
* - Escape:
|
|
150
|
-
* is left alone, so a surrounding layer can still dismiss on it.
|
|
147
|
+
* - Escape: Custom callback (e.g., close menu)
|
|
151
148
|
*
|
|
152
149
|
* By default the hook only *moves* focus and leaves `tabindex` management to
|
|
153
150
|
* the caller. Opt into {@link UseListFocusOptions.hasRovingTabIndex} for a hook
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useListFocus.d.ts","sourceRoot":"","sources":["../../src/hooks/useListFocus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useListFocus.d.ts","sourceRoot":"","sources":["../../src/hooks/useListFocus.ts"],"names":[],"mappings":"AAsBA;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;;;;;;OAgBG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,oBAAoB,CAAC;IAEnC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;;;;;;;;;OAYG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW;IACrE;;OAEG;IACH,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAEnC;;OAEG;IACH,aAAa,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;IAEhD;;;;OAIG;IACH,WAAW,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAE3C;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEnC;;OAEG;IACH,UAAU,EAAE,MAAM,OAAO,CAAC;IAE1B;;OAEG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;IAEzB;;;;;;OAMG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,OAAO,CAAC;IAE/C;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,WAAW,EAAE,CAAC;CAC/B;AA+FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAC9D,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAAC,CAAC,CAAC,CAwVvB"}
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
*
|
|
15
15
|
* SYNC: When modified, update:
|
|
16
16
|
* - /packages/core/src/hooks/index.ts
|
|
17
|
-
* - /packages/core/src/hooks/useListFocus.doc.mjs
|
|
18
|
-
* - /packages/core/src/hooks/useListFocus.test.tsx
|
|
19
17
|
*/
|
|
20
18
|
import { useCallback, useRef } from 'react';
|
|
21
19
|
import { isRtlElement } from "./isRtlElement.js";
|
|
@@ -129,8 +127,7 @@ function shouldDeferToCaret(target, key) {
|
|
|
129
127
|
* - ArrowUp/ArrowLeft: Move to previous item (wraps to last)
|
|
130
128
|
* - Home: Move to first item
|
|
131
129
|
* - End: Move to last item
|
|
132
|
-
* - Escape:
|
|
133
|
-
* is left alone, so a surrounding layer can still dismiss on it.
|
|
130
|
+
* - Escape: Custom callback (e.g., close menu)
|
|
134
131
|
*
|
|
135
132
|
* By default the hook only *moves* focus and leaves `tabindex` management to
|
|
136
133
|
* the caller. Opt into {@link UseListFocusOptions.hasRovingTabIndex} for a hook
|
|
@@ -383,15 +380,12 @@ export function useListFocus(options = {}) {
|
|
|
383
380
|
return;
|
|
384
381
|
}
|
|
385
382
|
|
|
386
|
-
// Escape is handled regardless of orientation
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
// `defaultPrevented` (see `useFocusTrap`) or to the native popover.
|
|
383
|
+
// Escape is handled regardless of orientation. Preserve the historical
|
|
384
|
+
// behavior of always consuming Escape here (preventDefault) so consumers
|
|
385
|
+
// that relied on it are unaffected.
|
|
390
386
|
if (e.key === 'Escape') {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
onEscape();
|
|
394
|
-
}
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
onEscape?.();
|
|
395
389
|
return;
|
|
396
390
|
}
|
|
397
391
|
const horizontal = orientation === 'horizontal' || orientation === 'both';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.4.6
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -656,7 +656,6 @@
|
|
|
656
656
|
"react-dom": ">=19.0.0"
|
|
657
657
|
},
|
|
658
658
|
"devDependencies": {
|
|
659
|
-
"@astryxdesign/cli": "0.4.6-canary.cea9ecd",
|
|
660
659
|
"@babel/cli": "^8.0.4",
|
|
661
660
|
"@babel/core": "^7.29.7",
|
|
662
661
|
"@babel/preset-react": "^8.0.1",
|
|
@@ -665,7 +664,8 @@
|
|
|
665
664
|
"@testing-library/dom": "^10.0.0",
|
|
666
665
|
"@testing-library/jest-dom": "^6.6.0",
|
|
667
666
|
"@testing-library/react": "^16.3.2",
|
|
668
|
-
"rimraf": "^6.0.1"
|
|
667
|
+
"rimraf": "^6.0.1",
|
|
668
|
+
"@astryxdesign/cli": "0.4.6"
|
|
669
669
|
},
|
|
670
670
|
"dependencies": {
|
|
671
671
|
"intl-messageformat": "^11.2.9"
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Popover.test.tsx
|
|
5
|
-
* @input Uses vitest, @testing-library/react, Popover
|
|
6
|
-
* SegmentedControl
|
|
5
|
+
* @input Uses vitest, @testing-library/react, Popover component
|
|
7
6
|
* @output Unit tests for Popover component behavior
|
|
8
7
|
* @position Testing; validates Popover.tsx implementation
|
|
9
8
|
*
|
|
@@ -15,7 +14,6 @@ import {render, screen, fireEvent} from '@testing-library/react';
|
|
|
15
14
|
import React, {useRef} from 'react';
|
|
16
15
|
import {Popover} from './Popover';
|
|
17
16
|
import {Dialog} from '../Dialog';
|
|
18
|
-
import {SegmentedControl, SegmentedControlItem} from '../SegmentedControl';
|
|
19
17
|
|
|
20
18
|
// Store original matches to restore later
|
|
21
19
|
const originalMatches = HTMLElement.prototype.matches;
|
|
@@ -308,30 +306,6 @@ describe('Popover', () => {
|
|
|
308
306
|
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
309
307
|
});
|
|
310
308
|
|
|
311
|
-
it('dismisses on Escape pressed inside a roving-focus list', () => {
|
|
312
|
-
render(
|
|
313
|
-
<Popover
|
|
314
|
-
content={
|
|
315
|
-
<SegmentedControl value="grid" onChange={() => {}} label="View">
|
|
316
|
-
<SegmentedControlItem value="grid" label="Grid" />
|
|
317
|
-
<SegmentedControlItem value="list" label="List" />
|
|
318
|
-
</SegmentedControl>
|
|
319
|
-
}
|
|
320
|
-
label="Test">
|
|
321
|
-
<button type="button">Open</button>
|
|
322
|
-
</Popover>,
|
|
323
|
-
);
|
|
324
|
-
const trigger = screen.getByRole('button', {name: 'Open'});
|
|
325
|
-
fireEvent.click(trigger);
|
|
326
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
327
|
-
|
|
328
|
-
// From the segment, so the list's own key handler runs first. jsdom has
|
|
329
|
-
// no popover display, so the open content still reads as hidden.
|
|
330
|
-
const segment = screen.getByRole('radio', {name: 'Grid', hidden: true});
|
|
331
|
-
fireEvent.keyDown(segment, {key: 'Escape'});
|
|
332
|
-
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
333
|
-
});
|
|
334
|
-
|
|
335
309
|
it('stays open on Escape when hasEscapeDismiss is false', () => {
|
|
336
310
|
render(
|
|
337
311
|
<Popover
|
|
@@ -36,7 +36,7 @@ export const docs = {
|
|
|
36
36
|
{
|
|
37
37
|
name: 'options.onEscape',
|
|
38
38
|
type: '() => void',
|
|
39
|
-
description: 'Callback when Escape key is pressed (e.g., close menu).
|
|
39
|
+
description: 'Callback when Escape key is pressed (e.g., close menu).',
|
|
40
40
|
required: false,
|
|
41
41
|
},
|
|
42
42
|
{
|
|
@@ -144,7 +144,7 @@ export const docsDense = {
|
|
|
144
144
|
'options.itemSelector': 'selector for focusable items in list.',
|
|
145
145
|
'options.boundarySelector': "boundary selector for lists that contain nested lists (e.g. submenu flyouts); scopes items + key handling to this level.",
|
|
146
146
|
'options.wrap': 'whether arrow navigation wraps around at ends.',
|
|
147
|
-
'options.onEscape': 'callback when Escape key pressed (e.g. close menu).
|
|
147
|
+
'options.onEscape': 'callback when Escape key pressed (e.g. close menu).',
|
|
148
148
|
'options.orientation': "navigation orientation. 'horizontal' uses ArrowLeft/ArrowRight, 'vertical' uses ArrowUp/ArrowDown, 'both' accepts all four arrows.",
|
|
149
149
|
'options.hasHomeEnd': 'whether Home/End jump to first/last enabled item.',
|
|
150
150
|
'options.isRtl': 'ArrowLeft/ArrowRight swap for horizontal nav (RTL). default: auto-detect from container computed direction; explicit boolean wins.',
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file useListFocus.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, useListFocus hook
|
|
6
|
-
* @output Unit tests for useListFocus disabled-item skipping, navigation,
|
|
7
|
-
*
|
|
6
|
+
* @output Unit tests for useListFocus disabled-item skipping, navigation, and
|
|
7
|
+
* RTL auto-detection
|
|
8
8
|
* @position Testing; validates useListFocus.ts keyboard navigation
|
|
9
9
|
*
|
|
10
10
|
* SYNC: When useListFocus.ts changes, update tests to match new behavior
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {describe, it, expect
|
|
13
|
+
import {describe, it, expect} from 'vitest';
|
|
14
14
|
import type {KeyboardEvent as ReactKeyboardEvent} from 'react';
|
|
15
15
|
import {render, screen, fireEvent} from '@testing-library/react';
|
|
16
16
|
import {useListFocus} from './useListFocus';
|
|
@@ -525,65 +525,3 @@ describe('useListFocus boundarySelector (nested lists)', () => {
|
|
|
525
525
|
expect(innerProbe).toHaveAttribute('data-owns', 'false');
|
|
526
526
|
});
|
|
527
527
|
});
|
|
528
|
-
|
|
529
|
-
// A list inside a host that dismisses on Escape. The host's guard mirrors
|
|
530
|
-
// `useFocusTrap`: it acts only on a key no inner handler has consumed.
|
|
531
|
-
function EscapeHost({
|
|
532
|
-
onEscape,
|
|
533
|
-
onHostEscape,
|
|
534
|
-
}: {
|
|
535
|
-
onEscape?: () => void;
|
|
536
|
-
onHostEscape: () => void;
|
|
537
|
-
}) {
|
|
538
|
-
const {listRef, handleKeyDown} = useListFocus<HTMLDivElement>({onEscape});
|
|
539
|
-
return (
|
|
540
|
-
<div
|
|
541
|
-
data-testid="host"
|
|
542
|
-
onKeyDown={e => {
|
|
543
|
-
if (e.key === 'Escape' && !e.defaultPrevented) {
|
|
544
|
-
onHostEscape();
|
|
545
|
-
}
|
|
546
|
-
}}>
|
|
547
|
-
<div ref={listRef} role="menu" onKeyDown={handleKeyDown}>
|
|
548
|
-
<div role="menuitem" tabIndex={-1} data-testid="One">
|
|
549
|
-
One
|
|
550
|
-
</div>
|
|
551
|
-
<div role="menuitem" tabIndex={-1} data-testid="Two">
|
|
552
|
-
Two
|
|
553
|
-
</div>
|
|
554
|
-
</div>
|
|
555
|
-
</div>
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
describe('useListFocus Escape', () => {
|
|
560
|
-
it('leaves Escape to the host when no onEscape is supplied', () => {
|
|
561
|
-
const onHostEscape = vi.fn();
|
|
562
|
-
render(<EscapeHost onHostEscape={onHostEscape} />);
|
|
563
|
-
|
|
564
|
-
fireEvent.keyDown(screen.getByRole('menu'), {key: 'Escape'});
|
|
565
|
-
expect(onHostEscape).toHaveBeenCalledTimes(1);
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
it('consumes Escape and runs onEscape when one is supplied', () => {
|
|
569
|
-
const onEscape = vi.fn();
|
|
570
|
-
const onHostEscape = vi.fn();
|
|
571
|
-
render(<EscapeHost onEscape={onEscape} onHostEscape={onHostEscape} />);
|
|
572
|
-
|
|
573
|
-
fireEvent.keyDown(screen.getByRole('menu'), {key: 'Escape'});
|
|
574
|
-
expect(onEscape).toHaveBeenCalledTimes(1);
|
|
575
|
-
expect(onHostEscape).not.toHaveBeenCalled();
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it('still consumes arrow keys with no onEscape (page-scroll suppression)', () => {
|
|
579
|
-
render(<EscapeHost onHostEscape={() => {}} />);
|
|
580
|
-
screen.getByTestId('One').focus();
|
|
581
|
-
|
|
582
|
-
// fireEvent returns false when a handler cancelled the event.
|
|
583
|
-
const wasCancelled = !fireEvent.keyDown(screen.getByRole('menu'), {
|
|
584
|
-
key: 'ArrowDown',
|
|
585
|
-
});
|
|
586
|
-
expect(wasCancelled).toBe(true);
|
|
587
|
-
expect(screen.getByTestId('Two')).toHaveFocus();
|
|
588
|
-
});
|
|
589
|
-
});
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
*
|
|
15
15
|
* SYNC: When modified, update:
|
|
16
16
|
* - /packages/core/src/hooks/index.ts
|
|
17
|
-
* - /packages/core/src/hooks/useListFocus.doc.mjs
|
|
18
|
-
* - /packages/core/src/hooks/useListFocus.test.tsx
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
import {useCallback, useRef} from 'react';
|
|
@@ -66,9 +64,7 @@ export interface UseListFocusOptions {
|
|
|
66
64
|
wrap?: boolean;
|
|
67
65
|
|
|
68
66
|
/**
|
|
69
|
-
* Callback when Escape key is pressed.
|
|
70
|
-
* consume the key (`preventDefault`); without it Escape passes through to
|
|
71
|
-
* the surrounding layer.
|
|
67
|
+
* Callback when Escape key is pressed.
|
|
72
68
|
*/
|
|
73
69
|
onEscape?: () => void;
|
|
74
70
|
|
|
@@ -281,8 +277,7 @@ function shouldDeferToCaret(target: EventTarget | null, key: string): boolean {
|
|
|
281
277
|
* - ArrowUp/ArrowLeft: Move to previous item (wraps to last)
|
|
282
278
|
* - Home: Move to first item
|
|
283
279
|
* - End: Move to last item
|
|
284
|
-
* - Escape:
|
|
285
|
-
* is left alone, so a surrounding layer can still dismiss on it.
|
|
280
|
+
* - Escape: Custom callback (e.g., close menu)
|
|
286
281
|
*
|
|
287
282
|
* By default the hook only *moves* focus and leaves `tabindex` management to
|
|
288
283
|
* the caller. Opt into {@link UseListFocusOptions.hasRovingTabIndex} for a hook
|
|
@@ -562,15 +557,12 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
562
557
|
return;
|
|
563
558
|
}
|
|
564
559
|
|
|
565
|
-
// Escape is handled regardless of orientation
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
// `defaultPrevented` (see `useFocusTrap`) or to the native popover.
|
|
560
|
+
// Escape is handled regardless of orientation. Preserve the historical
|
|
561
|
+
// behavior of always consuming Escape here (preventDefault) so consumers
|
|
562
|
+
// that relied on it are unaffected.
|
|
569
563
|
if (e.key === 'Escape') {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
onEscape();
|
|
573
|
-
}
|
|
564
|
+
e.preventDefault();
|
|
565
|
+
onEscape?.();
|
|
574
566
|
return;
|
|
575
567
|
}
|
|
576
568
|
|