@astryxdesign/core 0.7.3-canary.f94e962 → 0.7.3
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 +88 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +8 -4
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +7 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +15 -2
- package/dist/Calendar/Calendar.js +3 -2
- package/dist/Chat/ChatComposer.d.ts.map +1 -1
- package/dist/Chat/ChatComposer.js +25 -2
- package/dist/Chat/ChatSystemMessage.d.ts +4 -0
- package/dist/Chat/ChatSystemMessage.d.ts.map +1 -1
- package/dist/Chat/ChatSystemMessage.js +4 -2
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts +5 -5
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +27 -18
- package/dist/DateTimeInput/DateTimeInput.d.ts +5 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +25 -27
- package/dist/DateTimeInput/NativeDateSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeDateSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeDateSegment.js +5 -5
- package/dist/DateTimeInput/NativeTimeSegment.d.ts +3 -2
- package/dist/DateTimeInput/NativeTimeSegment.d.ts.map +1 -1
- package/dist/DateTimeInput/NativeTimeSegment.js +5 -5
- package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
- package/dist/DateTimeInput/TouchDateTimeField.js +12 -9
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +9 -5
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +2 -0
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +48 -15
- package/dist/MultiSelector/MultiSelector.d.ts +4 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts +2 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.d.ts.map +1 -0
- package/dist/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.js +48 -0
- package/dist/MultiSelector/MultiSelector.js +25 -18
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +5 -2
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -3
- package/dist/TimeInput/TimeInput.d.ts +5 -5
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +37 -20
- package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
- package/dist/TimeInput/TouchTimeField.js +17 -6
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +41 -8
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +8 -3
- package/dist/hooks/useGridFocus.d.ts.map +1 -1
- package/dist/hooks/useGridFocus.js +10 -1
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +24 -2
- package/src/BottomSheet/BottomSheet.spec.md +26 -53
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +45 -48
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/Button/Button.test.tsx +13 -2
- package/src/Button/Button.tsx +23 -2
- package/src/Button/__tests__/Button.a11y.states.ts +4 -3
- package/src/Calendar/Calendar.test.tsx +68 -24
- package/src/Calendar/Calendar.tsx +2 -1
- package/src/Carousel/Carousel.spec.md +146 -0
- package/src/Chat/ChatComposer.spec.md +26 -26
- package/src/Chat/ChatComposer.test.tsx +58 -2
- package/src/Chat/ChatComposer.tsx +25 -1
- package/src/Chat/ChatSystemMessage.doc.mjs +1 -1
- package/src/Chat/ChatSystemMessage.test.tsx +25 -3
- package/src/Chat/ChatSystemMessage.tsx +6 -4
- package/src/Chat/__tests__/ChatSystemMessage.a11y.chromium.spec.ts +8 -9
- package/src/CheckboxList/__tests__/CheckboxList.a11y.chromium.spec.ts +7 -6
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +6 -5
- package/src/DateRangeInput/DateRangeInput.test.tsx +82 -16
- package/src/DateRangeInput/DateRangeInput.tsx +40 -20
- package/src/DateTimeInput/DateTimeInput.doc.mjs +9 -8
- package/src/DateTimeInput/DateTimeInput.test.tsx +126 -11
- package/src/DateTimeInput/DateTimeInput.tsx +42 -26
- package/src/DateTimeInput/DateTimeInputTouch.test.tsx +24 -0
- package/src/DateTimeInput/NativeDateSegment.tsx +9 -6
- package/src/DateTimeInput/NativePickerSegments.test.tsx +31 -0
- package/src/DateTimeInput/NativeTimeSegment.tsx +9 -6
- package/src/DateTimeInput/TouchDateTimeField.tsx +22 -7
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +66 -7
- package/src/DropdownMenu/DropdownMenuItem.spec.md +209 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +62 -7
- package/src/DropdownMenu/DropdownMenuRadioGroup.spec.md +190 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -4
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +93 -0
- package/src/DropdownMenu/__tests__/DropdownMenuItemAudit.a11y.chromium.spec.ts +366 -0
- package/src/DropdownMenu/__tests__/DropdownMenuRadioGroupAudit.a11y.chromium.spec.ts +327 -0
- package/src/FieldStatus/FieldStatus.spec.md +4 -4
- package/src/FieldStatus/__tests__/StatusMessage.a11y.renders.tsx +0 -18
- package/src/FieldStatus/__tests__/StatusMessage.a11y.states.ts +3 -18
- package/src/FieldStatus/__tests__/StatusMessage.a11y.test.tsx +1 -2
- package/src/Markdown/Markdown.helpers.perf.test.ts +25 -19
- package/src/Markdown/Markdown.rerender.test.tsx +143 -0
- package/src/Markdown/Markdown.tsx +89 -35
- package/src/MultiSelector/MultiSelector.disabledFocus.a11y.chromium.spec.ts +71 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +5 -5
- package/src/MultiSelector/MultiSelector.focusableDisabled.test.tsx +121 -0
- package/src/MultiSelector/MultiSelector.test.tsx +49 -7
- package/src/MultiSelector/MultiSelector.tsx +53 -29
- package/src/Pagination/Pagination.test.tsx +5 -1
- package/src/Pagination/Pagination.tsx +3 -1
- package/src/Switch/Switch.test.tsx +56 -0
- package/src/Switch/Switch.tsx +13 -2
- package/src/Table/plugins/columnResize/useTableColumnResize.spec.md +11 -11
- package/src/Table/plugins/rowStatus/useTableRowStatus.spec.md +4 -3
- package/src/TimeInput/NativeTimeInput.test.tsx +25 -1
- package/src/TimeInput/Presentation.test.tsx +23 -0
- package/src/TimeInput/TimeInput.doc.mjs +8 -7
- package/src/TimeInput/TimeInput.test.tsx +119 -7
- package/src/TimeInput/TimeInput.tsx +51 -23
- package/src/TimeInput/TouchTimeField.tsx +27 -5
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Timestamp/Timestamp.test.tsx +168 -0
- package/src/Timestamp/Timestamp.tsx +62 -11
- package/src/TreeList/TreeList.test.tsx +28 -0
- package/src/TreeList/TreeListItem.tsx +6 -2
- package/src/__tests__/DisabledTriggerFocus.a11y.chromium.spec.ts +86 -0
- package/src/hooks/useGridFocus.ts +16 -1
|
@@ -17,7 +17,14 @@
|
|
|
17
17
|
* - /packages/cli/assets/templates/blocks/components/Timestamp/ (showcase blocks)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
lazy,
|
|
22
|
+
Suspense,
|
|
23
|
+
useEffect,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
useSyncExternalStore,
|
|
27
|
+
} from 'react';
|
|
21
28
|
import * as stylex from '@stylexjs/stylex';
|
|
22
29
|
import {Text} from '../Text';
|
|
23
30
|
import type {TextType, TextSize, TextColor, TextWeight} from '../theme/types';
|
|
@@ -215,6 +222,13 @@ function getLiveInterval(diffSeconds: number): number {
|
|
|
215
222
|
return 300_000; // every 5 minutes
|
|
216
223
|
}
|
|
217
224
|
|
|
225
|
+
// Whether this render may read the current time. Server renders (including
|
|
226
|
+
// static prerenders) and the hydration pass read `false`, so the markup never
|
|
227
|
+
// depends on when it was rendered; client renders read `true`.
|
|
228
|
+
const subscribeToNothing = () => () => {};
|
|
229
|
+
const canReadClockOnClient = () => true;
|
|
230
|
+
const canReadClockOnServer = () => false;
|
|
231
|
+
|
|
218
232
|
/** Whether a format is non-relative (i.e. shows a fixed date/time). */
|
|
219
233
|
function isAbsoluteFormat(
|
|
220
234
|
format: TimestampFormat,
|
|
@@ -279,7 +293,29 @@ export function Timestamp({
|
|
|
279
293
|
const locale = useLocale();
|
|
280
294
|
const timeRef = useRef<HTMLTimeElement>(null);
|
|
281
295
|
const mergedTimeRef = useMergedRefs(ref, timeRef);
|
|
282
|
-
|
|
296
|
+
// Only relative and auto formats depend on the current time. Absolute
|
|
297
|
+
// formats never read the clock, so they render identically on the server,
|
|
298
|
+
// in a static prerender, and on the client. A clock-dependent format reads
|
|
299
|
+
// it on the client only: server renders and the hydration pass show the
|
|
300
|
+
// full date and time in UTC, and the client switches to the relative phrase
|
|
301
|
+
// right after hydration.
|
|
302
|
+
const needsClock = !isAbsoluteFormat(format);
|
|
303
|
+
const canReadClock = useSyncExternalStore(
|
|
304
|
+
subscribeToNothing,
|
|
305
|
+
canReadClockOnClient,
|
|
306
|
+
canReadClockOnServer,
|
|
307
|
+
);
|
|
308
|
+
const [now, setNow] = useState<Date | null>(() =>
|
|
309
|
+
needsClock && canReadClock ? new Date() : null,
|
|
310
|
+
);
|
|
311
|
+
// Read the clock during render once it may be read: after hydration (the
|
|
312
|
+
// store re-renders with its client value) and when `format` switches from
|
|
313
|
+
// an absolute value to a clock-dependent one, so the placeholder never
|
|
314
|
+
// commits on the client.
|
|
315
|
+
if (needsClock && canReadClock && now === null) {
|
|
316
|
+
// eslint-disable-next-line @eslint-react/purity -- seeds clock state once, like the initializer; never runs on the server or while hydrating
|
|
317
|
+
setNow(new Date());
|
|
318
|
+
}
|
|
283
319
|
|
|
284
320
|
const date = parseValue(value);
|
|
285
321
|
// An unparseable value (a malformed date string, or a NaN timestamp from
|
|
@@ -289,25 +325,40 @@ export function Timestamp({
|
|
|
289
325
|
const isValidDate = !Number.isNaN(date.getTime());
|
|
290
326
|
const isoString = isValidDate ? date.toISOString() : '';
|
|
291
327
|
|
|
292
|
-
// Determine effective format
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
328
|
+
// Determine effective format. Until the clock is read, a clock-dependent
|
|
329
|
+
// format renders as the full date and time.
|
|
330
|
+
const isClockPending = needsClock && now === null;
|
|
331
|
+
const diffSeconds =
|
|
332
|
+
now === null ? 0 : Math.round((now.getTime() - date.getTime()) / 1000);
|
|
333
|
+
const effectiveFormat: TimestampFormat = isClockPending
|
|
334
|
+
? 'date_time'
|
|
335
|
+
: format === 'auto'
|
|
296
336
|
? Math.abs(diffSeconds) <= autoThreshold
|
|
297
337
|
? 'relative'
|
|
298
338
|
: 'date_time'
|
|
299
339
|
: format;
|
|
300
340
|
|
|
301
|
-
// Format the display text
|
|
302
|
-
//
|
|
341
|
+
// Format the display text in the viewer's own zone; only the tooltip names
|
|
342
|
+
// others. The one exception is the pre-hydration placeholder, written in
|
|
343
|
+
// UTC with its label so every zone produces the same markup.
|
|
303
344
|
const displayText = !isValidDate
|
|
304
345
|
? ''
|
|
305
346
|
: effectiveFormat === 'relative'
|
|
306
|
-
? formatRelativeTime(date, now, locale, 'long')
|
|
347
|
+
? formatRelativeTime(date, now ?? date, locale, 'long')
|
|
307
348
|
: effectiveFormat === 'relative_short'
|
|
308
|
-
? formatRelativeTime(date, now, locale, 'narrow')
|
|
349
|
+
? formatRelativeTime(date, now ?? date, locale, 'narrow')
|
|
309
350
|
: isAbsoluteFormat(effectiveFormat)
|
|
310
|
-
? formatInstant(
|
|
351
|
+
? formatInstant(
|
|
352
|
+
date,
|
|
353
|
+
effectiveFormat,
|
|
354
|
+
locale,
|
|
355
|
+
// The pre-hydration placeholder is written in UTC, labelled, so
|
|
356
|
+
// the server and the hydrating client produce the same text in
|
|
357
|
+
// every time zone.
|
|
358
|
+
isClockPending
|
|
359
|
+
? {timeZone: 'UTC', isTimezoneShown: true}
|
|
360
|
+
: {isTimezoneShown},
|
|
361
|
+
)
|
|
311
362
|
: '';
|
|
312
363
|
|
|
313
364
|
// Full absolute text for the tooltip (visible — keeps the compact timezone
|
|
@@ -942,6 +942,34 @@ describe('TreeList', () => {
|
|
|
942
942
|
expect(screen.queryByText('File')).not.toBeInTheDocument();
|
|
943
943
|
});
|
|
944
944
|
|
|
945
|
+
it("exposes a disabled row's toggle and action as aria-disabled and keeps them inert", () => {
|
|
946
|
+
const onClick = vi.fn();
|
|
947
|
+
const {container} = render(
|
|
948
|
+
<TreeList
|
|
949
|
+
items={underRoot([
|
|
950
|
+
{id: 'run', label: 'Run', onClick, isDisabled: true},
|
|
951
|
+
{...folder, isDisabled: true},
|
|
952
|
+
])}
|
|
953
|
+
/>,
|
|
954
|
+
);
|
|
955
|
+
// spec:AST-085 FR1/FR2: no native disabled anywhere, and a click
|
|
956
|
+
// dispatched straight on the inner controls does nothing.
|
|
957
|
+
expect(container.querySelector('[disabled]')).toBeNull();
|
|
958
|
+
const toggle = container.querySelector<HTMLElement>(
|
|
959
|
+
'[data-tree-toggle][aria-disabled="true"]',
|
|
960
|
+
)!;
|
|
961
|
+
expect(toggle).not.toBeNull();
|
|
962
|
+
toggle.click();
|
|
963
|
+
fireEvent.keyDown(toggle, {key: 'Enter'});
|
|
964
|
+
expect(screen.queryByText('File')).not.toBeInTheDocument();
|
|
965
|
+
const action = Array.from(container.querySelectorAll('button')).find(
|
|
966
|
+
button => button.textContent?.includes('Run'),
|
|
967
|
+
)!;
|
|
968
|
+
expect(action).toHaveAttribute('aria-disabled', 'true');
|
|
969
|
+
action.click();
|
|
970
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
971
|
+
});
|
|
972
|
+
|
|
945
973
|
it('leaves a role-based control in start or end content to itself', () => {
|
|
946
974
|
// Native and role-based controls alike keep their own activation,
|
|
947
975
|
// the same rule link rows get from the shared enlarged-surface hook.
|
|
@@ -38,6 +38,7 @@ import {TreeListBranches} from './TreeListBranches';
|
|
|
38
38
|
import type {TreeListDensity, TreeListVariant} from './TreeListTypes';
|
|
39
39
|
import {themeProps} from '../utils/themeProps';
|
|
40
40
|
import {useTranslator} from '../i18n';
|
|
41
|
+
import {disabledInteractionProps} from '../utils/disabledInteraction';
|
|
41
42
|
|
|
42
43
|
// =============================================================================
|
|
43
44
|
// Styles
|
|
@@ -552,11 +553,13 @@ export function TreeListItem({
|
|
|
552
553
|
// Stable identity for TreeList's activateItem selector — do not
|
|
553
554
|
// remove without also updating TreeList.tsx.
|
|
554
555
|
data-tree-toggle=""
|
|
555
|
-
|
|
556
|
+
// Disabled stays focusable (spec:AST-085 FR1) but inert (FR2).
|
|
557
|
+
aria-disabled={isDisabled || undefined}
|
|
556
558
|
// Roving tabindex lives on the treeitem row; the chevron toggle is not
|
|
557
559
|
// a separate tab stop. Row-level Enter/Space forwards to this button.
|
|
558
560
|
tabIndex={-1}
|
|
559
561
|
onClick={handleToggle}
|
|
562
|
+
{...(isDisabled ? disabledInteractionProps() : null)}
|
|
560
563
|
{...mergeProps(
|
|
561
564
|
// Stable theme target for the expand/collapse control. `data-tree-toggle`
|
|
562
565
|
// stays as the functional activation hook; this adds a themeable
|
|
@@ -608,7 +611,8 @@ export function TreeListItem({
|
|
|
608
611
|
<button
|
|
609
612
|
type="button"
|
|
610
613
|
onClick={onClick}
|
|
611
|
-
disabled={isDisabled}
|
|
614
|
+
aria-disabled={isDisabled || undefined}
|
|
615
|
+
{...(isDisabled ? disabledInteractionProps() : null)}
|
|
612
616
|
aria-labelledby={labelId}
|
|
613
617
|
aria-describedby={description != null ? descriptionId : undefined}
|
|
614
618
|
// Roving tabindex lives on the treeitem row; inner action is not a
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DisabledTriggerFocus.a11y.chromium.spec.ts
|
|
5
|
+
* @input The `a11y/Disabled trigger focus` stories in a built Storybook.
|
|
6
|
+
* @output Real-engine proof of `spec:AST-085` FR4 for Selector and
|
|
7
|
+
* ComplexSelector: a trigger disabled while it holds keyboard focus keeps
|
|
8
|
+
* focus and reports itself disabled.
|
|
9
|
+
* @position Run in `pr-a11y`; a DOM emulator does not move focus off an
|
|
10
|
+
* element that becomes disabled, so it cannot show the loss.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {expect, test} from '@playwright/test';
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_STORYBOOK_DIR,
|
|
16
|
+
serveStorybook,
|
|
17
|
+
type StaticServer,
|
|
18
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
19
|
+
|
|
20
|
+
let storybook: StaticServer;
|
|
21
|
+
|
|
22
|
+
test.beforeAll(async () => {
|
|
23
|
+
storybook = await serveStorybook(
|
|
24
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test.afterAll(async () => {
|
|
29
|
+
await storybook?.close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const CASES = [
|
|
33
|
+
{
|
|
34
|
+
storyId: 'a11y-disabled-trigger-focus--selector-trigger',
|
|
35
|
+
role: 'combobox',
|
|
36
|
+
name: 'Fruit',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
storyId: 'a11y-disabled-trigger-focus--complex-selector-trigger',
|
|
40
|
+
role: 'button',
|
|
41
|
+
name: 'View options',
|
|
42
|
+
},
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
for (const {storyId, role, name} of CASES) {
|
|
46
|
+
test(`${name}: a focused trigger keeps focus when it becomes disabled`, async ({
|
|
47
|
+
page,
|
|
48
|
+
}) => {
|
|
49
|
+
await page.goto(
|
|
50
|
+
`${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`,
|
|
51
|
+
);
|
|
52
|
+
const trigger = page.getByRole(role, {name});
|
|
53
|
+
await trigger.focus();
|
|
54
|
+
await expect(trigger).toBeFocused();
|
|
55
|
+
|
|
56
|
+
await page.evaluate(id => {
|
|
57
|
+
const channel = (
|
|
58
|
+
window as unknown as {
|
|
59
|
+
__STORYBOOK_ADDONS_CHANNEL__?: {
|
|
60
|
+
emit: (event: string, payload: unknown) => void;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
).__STORYBOOK_ADDONS_CHANNEL__;
|
|
64
|
+
if (channel == null) {
|
|
65
|
+
throw new Error('Storybook channel is unavailable');
|
|
66
|
+
}
|
|
67
|
+
channel.emit('updateStoryArgs', {
|
|
68
|
+
storyId: id,
|
|
69
|
+
updatedArgs: {isDisabled: true},
|
|
70
|
+
});
|
|
71
|
+
}, storyId);
|
|
72
|
+
|
|
73
|
+
// Wait for the disabled re-render in either form, so focus is checked
|
|
74
|
+
// after it lands.
|
|
75
|
+
await expect
|
|
76
|
+
.poll(async () =>
|
|
77
|
+
trigger.evaluate(
|
|
78
|
+
el =>
|
|
79
|
+
el.hasAttribute('disabled') ||
|
|
80
|
+
el.getAttribute('aria-disabled') === 'true',
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
.toBe(true);
|
|
84
|
+
await expect(trigger).toBeFocused();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -298,7 +298,22 @@ export function useGridFocus<T extends HTMLElement = HTMLElement>(
|
|
|
298
298
|
for (const el of targets) {
|
|
299
299
|
setTabIndex(el, el === tabbable ? 0 : -1);
|
|
300
300
|
}
|
|
301
|
-
|
|
301
|
+
// A cell that stopped being focusable (an aria-disabled day, for example,
|
|
302
|
+
// which stays reachable by focus) can still hold the stop from earlier
|
|
303
|
+
// navigation; it never stays tabbable, so the grid keeps one Tab stop.
|
|
304
|
+
for (const cell of getCells()) {
|
|
305
|
+
const el = cellFocusable(cell) ? null : resolveFocusTarget(cell);
|
|
306
|
+
if (el != null) {
|
|
307
|
+
setTabIndex(el, -1);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}, [
|
|
311
|
+
getFocusTargets,
|
|
312
|
+
getCells,
|
|
313
|
+
cellFocusable,
|
|
314
|
+
resolveFocusTarget,
|
|
315
|
+
setTabIndex,
|
|
316
|
+
]);
|
|
302
317
|
|
|
303
318
|
// Keep the tab stop valid across renders (cells added/removed, focusability
|
|
304
319
|
// toggled). Runs after every commit but only when roving tabindex is on.
|