@astryxdesign/core 0.4.6 → 0.4.7-canary.2ce1983
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/CHANGELOG.md +35 -0
- package/dist/BottomSheet/BottomSheet.d.ts +1 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +37 -11
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +6 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetEdgeTint.js +62 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetPanel.js +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +10 -4
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
- package/dist/BottomSheet/useSheetGestures.js +23 -5
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +5 -1
- package/dist/astryx.css +4 -0
- package/dist/hooks/useListFocus.d.ts +5 -2
- package/dist/hooks/useListFocus.d.ts.map +1 -1
- package/dist/hooks/useListFocus.js +12 -6
- package/package.json +3 -3
- package/src/BottomSheet/BottomSheet.tsx +19 -0
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +225 -0
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +82 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +66 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +19 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +13 -0
- package/src/BottomSheet/useSheetGestures.test.ts +27 -0
- package/src/BottomSheet/useSheetGestures.ts +25 -5
- package/src/Popover/Popover.test.tsx +27 -1
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.test.tsx +21 -4
- package/src/hooks/useListFocus.doc.mjs +2 -2
- package/src/hooks/useListFocus.test.tsx +65 -3
- package/src/hooks/useListFocus.ts +15 -7
package/src/TabList/Tab.tsx
CHANGED
|
@@ -260,7 +260,11 @@ export function Tab({
|
|
|
260
260
|
...(isLabelHidden ? {'aria-label': label} : {}),
|
|
261
261
|
[EDGE_COMP_ATTR]: '',
|
|
262
262
|
'data-tab-value': value,
|
|
263
|
-
|
|
263
|
+
// Generic `true` ("the current item within a set"), not `page`: the strip
|
|
264
|
+
// switches views in place at least as often as it navigates, and claiming
|
|
265
|
+
// "current page" when no page changed is a false statement to a screen
|
|
266
|
+
// reader. Stays truthful for the `href` case too, just less specific.
|
|
267
|
+
'aria-current': isSelected ? ('true' as const) : undefined,
|
|
264
268
|
// Roving tabindex: the tab strip is a single Tab stop. The selected tab is
|
|
265
269
|
// the tabbable one; the rest are reachable via arrow keys (handled by
|
|
266
270
|
// TabList's onKeyDown). When no tab is selected, TabList's repair effect
|
|
@@ -106,7 +106,7 @@ describe('TabList', () => {
|
|
|
106
106
|
);
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
it('marks selected tab with aria-current', () => {
|
|
109
|
+
it('marks selected tab with a generic aria-current, not "page"', () => {
|
|
110
110
|
render(
|
|
111
111
|
<TabList value="home" onChange={() => {}}>
|
|
112
112
|
<Tab value="home" label="Home" />
|
|
@@ -116,13 +116,30 @@ describe('TabList', () => {
|
|
|
116
116
|
|
|
117
117
|
expect(screen.getByRole('button', {name: 'Home'})).toHaveAttribute(
|
|
118
118
|
'aria-current',
|
|
119
|
-
'
|
|
119
|
+
'true',
|
|
120
120
|
);
|
|
121
121
|
expect(screen.getByRole('button', {name: 'Settings'})).not.toHaveAttribute(
|
|
122
122
|
'aria-current',
|
|
123
123
|
);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it('marks a selected link tab with the same generic aria-current', () => {
|
|
127
|
+
render(
|
|
128
|
+
<TabList value="home" onChange={() => {}}>
|
|
129
|
+
<Tab value="home" label="Home" href="/home" />
|
|
130
|
+
<Tab value="settings" label="Settings" href="/settings" />
|
|
131
|
+
</TabList>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(screen.getByRole('link', {name: 'Home'})).toHaveAttribute(
|
|
135
|
+
'aria-current',
|
|
136
|
+
'true',
|
|
137
|
+
);
|
|
138
|
+
expect(screen.getByRole('link', {name: 'Settings'})).not.toHaveAttribute(
|
|
139
|
+
'aria-current',
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
126
143
|
it('calls onChange when a tab is clicked', async () => {
|
|
127
144
|
const user = userEvent.setup();
|
|
128
145
|
const handleChange = vi.fn();
|
|
@@ -148,7 +165,7 @@ describe('TabList', () => {
|
|
|
148
165
|
|
|
149
166
|
expect(screen.getByRole('button', {name: 'Home'})).toHaveAttribute(
|
|
150
167
|
'aria-current',
|
|
151
|
-
'
|
|
168
|
+
'true',
|
|
152
169
|
);
|
|
153
170
|
|
|
154
171
|
rerender(
|
|
@@ -163,7 +180,7 @@ describe('TabList', () => {
|
|
|
163
180
|
);
|
|
164
181
|
expect(screen.getByRole('button', {name: 'Settings'})).toHaveAttribute(
|
|
165
182
|
'aria-current',
|
|
166
|
-
'
|
|
183
|
+
'true',
|
|
167
184
|
);
|
|
168
185
|
});
|
|
169
186
|
|
|
@@ -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). Supplying it also consumes the key (preventDefault); without it Escape passes through to the surrounding layer.',
|
|
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). Also consumes the key; without it Escape passes through to the surrounding layer.',
|
|
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
|
-
* RTL auto-detection
|
|
6
|
+
* @output Unit tests for useListFocus disabled-item skipping, navigation,
|
|
7
|
+
* Escape consumption, and 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} from 'vitest';
|
|
13
|
+
import {describe, it, expect, vi} 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,3 +525,65 @@ 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,6 +14,8 @@
|
|
|
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
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import {useCallback, useRef} from 'react';
|
|
@@ -64,7 +66,9 @@ export interface UseListFocusOptions {
|
|
|
64
66
|
wrap?: boolean;
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
|
-
* Callback when Escape key is pressed.
|
|
69
|
+
* Callback when Escape key is pressed. Supplying it also makes the list
|
|
70
|
+
* consume the key (`preventDefault`); without it Escape passes through to
|
|
71
|
+
* the surrounding layer.
|
|
68
72
|
*/
|
|
69
73
|
onEscape?: () => void;
|
|
70
74
|
|
|
@@ -277,7 +281,8 @@ function shouldDeferToCaret(target: EventTarget | null, key: string): boolean {
|
|
|
277
281
|
* - ArrowUp/ArrowLeft: Move to previous item (wraps to last)
|
|
278
282
|
* - Home: Move to first item
|
|
279
283
|
* - End: Move to last item
|
|
280
|
-
* - Escape:
|
|
284
|
+
* - Escape: runs `onEscape` and consumes the key. With no `onEscape` the key
|
|
285
|
+
* is left alone, so a surrounding layer can still dismiss on it.
|
|
281
286
|
*
|
|
282
287
|
* By default the hook only *moves* focus and leaves `tabindex` management to
|
|
283
288
|
* the caller. Opt into {@link UseListFocusOptions.hasRovingTabIndex} for a hook
|
|
@@ -557,12 +562,15 @@ export function useListFocus<T extends HTMLElement = HTMLElement>(
|
|
|
557
562
|
return;
|
|
558
563
|
}
|
|
559
564
|
|
|
560
|
-
// Escape is handled regardless of orientation
|
|
561
|
-
//
|
|
562
|
-
//
|
|
565
|
+
// Escape is handled regardless of orientation, but only *consumed* when
|
|
566
|
+
// a handler asked for it: a list with no dismissal to perform must leave
|
|
567
|
+
// the key to whatever host layer does have one, and those defer to
|
|
568
|
+
// `defaultPrevented` (see `useFocusTrap`) or to the native popover.
|
|
563
569
|
if (e.key === 'Escape') {
|
|
564
|
-
|
|
565
|
-
|
|
570
|
+
if (onEscape) {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
onEscape();
|
|
573
|
+
}
|
|
566
574
|
return;
|
|
567
575
|
}
|
|
568
576
|
|