@astryxdesign/core 0.7.2 → 0.7.3-canary.0bf23bd
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/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/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +19 -24
- package/dist/ClickableCard/ClickableCard.d.ts +2 -2
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +11 -9
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +11 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +23 -5
- package/dist/DateInput/DateInput.d.ts +4 -4
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +27 -20
- package/dist/DateInput/NativeDateField.d.ts.map +1 -1
- package/dist/DateInput/NativeDateField.js +14 -8
- package/dist/DateInput/TouchDateField.d.ts.map +1 -1
- package/dist/DateInput/TouchDateField.js +14 -10
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +9 -4
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +52 -10
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +34 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +15 -0
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +21 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +375 -42
- package/dist/DropdownMenu/menuRegionStops.d.ts +2 -2
- package/dist/DropdownMenu/menuRegionStops.js +2 -2
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +14 -12
- package/dist/Link/Link.d.ts +2 -3
- package/dist/Link/Link.d.ts.map +1 -1
- package/dist/Link/Link.js +11 -19
- 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/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +8 -4
- package/dist/Selector/Selector.d.ts +4 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +23 -14
- package/dist/Selector/SelectorOption.d.ts.map +1 -1
- package/dist/Selector/SelectorOption.js +1 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +7 -14
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +25 -17
- package/dist/Switch/switch.markers.stylex.d.ts +2 -0
- package/dist/Switch/switch.markers.stylex.d.ts.map +1 -1
- package/dist/Switch/switch.markers.stylex.js +6 -0
- package/dist/TimeInput/TimeInput.d.ts +2 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +8 -3
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.js +36 -14
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -2
- package/dist/TopNav/TopNav.d.ts.map +1 -1
- package/dist/TopNav/TopNav.js +2 -2
- package/dist/TopNav/TopNavHeading.d.ts.map +1 -1
- package/dist/TopNav/TopNavHeading.js +1 -0
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +14 -2
- package/dist/astryx.css +8 -2
- package/dist/hooks/useMenuPress.d.ts +11 -1
- package/dist/hooks/useMenuPress.d.ts.map +1 -1
- package/dist/hooks/useMenuPress.js +43 -5
- package/dist/utils/disabledInteraction.d.ts +21 -0
- package/dist/utils/disabledInteraction.d.ts.map +1 -0
- package/dist/utils/disabledInteraction.js +33 -0
- package/dist/utils/timeParser.d.ts +4 -0
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +9 -2
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +35 -8
- package/src/Banner/Banner.spec.md +37 -32
- package/src/BottomSheet/BottomSheet.spec.md +13 -6
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +8 -4
- package/src/BottomSheet/BottomSheetSwitcher.spec.md +121 -51
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +26 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +6 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +44 -5
- package/src/CheckboxInput/CheckboxInput.tsx +16 -26
- package/src/CheckboxInput/__tests__/Checkbox.a11y.states.ts +3 -3
- package/src/CheckboxList/CheckboxList.test.tsx +9 -6
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/ClickableCard/ClickableCard.test.tsx +5 -4
- package/src/ClickableCard/ClickableCard.tsx +9 -9
- package/src/CodeBlock/CodeBlock.highlightStyles.test.tsx +87 -0
- package/src/CodeBlock/CodeBlock.tsx +11 -1
- package/src/ComplexSelector/ComplexSelector.focusableDisabled.test.tsx +76 -0
- package/src/ComplexSelector/ComplexSelector.tsx +21 -4
- package/src/ContextMenu/ContextMenu.test.tsx +41 -0
- package/src/DateInput/DateInput.doc.mjs +9 -8
- package/src/DateInput/DateInput.test.tsx +88 -13
- package/src/DateInput/DateInput.tsx +49 -21
- package/src/DateInput/DateInputTouch.test.tsx +22 -1
- package/src/DateInput/NativeDateField.test.tsx +29 -3
- package/src/DateInput/NativeDateField.tsx +20 -7
- package/src/DateInput/TouchDateField.tsx +24 -9
- package/src/DateRangeInput/DateRangeInput.spec.md +13 -13
- package/src/Dialog/Dialog.test.tsx +39 -0
- package/src/Dialog/Dialog.tsx +9 -4
- package/src/DropdownMenu/DropdownMenu.spec.md +33 -2
- package/src/DropdownMenu/DropdownMenu.tsx +79 -8
- package/src/DropdownMenu/DropdownMenuContext.public.test.ts +22 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +39 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +16 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +623 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +556 -99
- package/src/DropdownMenu/__tests__/SubMenuHeaderFooter.a11y.chromium.spec.ts +267 -0
- package/src/DropdownMenu/menuRegionStops.ts +2 -2
- package/src/DropdownMenu/modules/useMenuPress.spec.md +9 -0
- package/src/FieldStatus/FieldStatus.spec.md +3 -2
- package/src/FileInput/FileInput.test.tsx +30 -7
- package/src/FileInput/FileInput.tsx +21 -11
- package/src/Link/Link.test.tsx +21 -7
- package/src/Link/Link.tsx +16 -24
- package/src/Link/__tests__/Link.a11y.states.ts +1 -1
- 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/Popover/Popover.test.tsx +15 -0
- package/src/Popover/Popover.tsx +19 -8
- package/src/PowerSearch/PowerSearchFilterEditor.test.tsx +3 -2
- package/src/PowerSearch/PowerSearchToken.test.tsx +7 -5
- package/src/Selector/Selector.doc.mjs +4 -4
- package/src/Selector/Selector.focusableDisabled.test.tsx +127 -0
- package/src/Selector/Selector.spec.md +11 -11
- package/src/Selector/Selector.test.tsx +64 -6
- package/src/Selector/Selector.tsx +51 -21
- package/src/Selector/SelectorOption.tsx +4 -0
- package/src/SideNav/SideNav.test.tsx +35 -0
- package/src/Slider/Slider.spec.md +11 -11
- package/src/Slider/Slider.test.tsx +3 -3
- package/src/Slider/Slider.tsx +8 -15
- package/src/Switch/Switch.test.tsx +85 -8
- package/src/Switch/Switch.tsx +26 -16
- package/src/Switch/__tests__/Switch.a11y.states.ts +2 -2
- package/src/Switch/switch.markers.stylex.ts +4 -0
- package/src/Table/Table.spec.md +2 -0
- package/src/Table/plugins/infiniteScroll/useTableInfiniteScroll.spec.md +244 -0
- package/src/Table/plugins/selection/useTableSelection.test.tsx +5 -3
- package/src/Table/plugins/virtualization/useTableVirtualization.spec.md +266 -0
- package/src/TimeInput/TimeInput.doc.mjs +5 -3
- package/src/TimeInput/TimeInput.test.tsx +20 -0
- package/src/TimeInput/TimeInput.tsx +2 -1
- package/src/Token/Token.test.tsx +26 -4
- package/src/Token/Token.tsx +6 -2
- package/src/Tokenizer/Tokenizer.spec.md +28 -28
- package/src/Tooltip/Tooltip.test.tsx +51 -6
- package/src/Tooltip/Tooltip.tsx +41 -18
- package/src/Tooltip/__tests__/Tooltip.a11y.chromium.spec.ts +129 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.renders.tsx +86 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.states.ts +132 -0
- package/src/Tooltip/__tests__/Tooltip.a11y.test.tsx +126 -0
- package/src/Tooltip/__tests__/TooltipHoverBridge.a11y.chromium.spec.ts +74 -0
- package/src/Tooltip/useTooltip.test.tsx +23 -0
- package/src/Tooltip/useTooltip.tsx +22 -9
- package/src/TopNav/TopNav.tsx +11 -1
- package/src/TopNav/TopNavHeading.tsx +1 -0
- package/src/TopNav/__tests__/TopNavMobileLongHeading.a11y.chromium.spec.ts +74 -0
- package/src/Typeahead/Typeahead.test.tsx +89 -0
- package/src/Typeahead/Typeahead.tsx +15 -2
- package/src/hooks/useMenuPress.ts +45 -7
- package/src/utils/disabledInteraction.ts +40 -0
- package/src/utils/timeParser.test.ts +11 -0
- package/src/utils/timeParser.ts +9 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Markdown.rerender.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, Markdown, a synthetic long document
|
|
6
|
+
* @output Tests that an unrelated caller re-render reuses the rendered document
|
|
7
|
+
* @position Performance regression evidence for long documents. Counting
|
|
8
|
+
* renders of a caller-supplied block renderer is deterministic, unlike a
|
|
9
|
+
* timing budget, and fails when Markdown rebuilds every block element on a
|
|
10
|
+
* re-render that changed nothing it reads.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {useState} from 'react';
|
|
14
|
+
import {act, cleanup, fireEvent, render} from '@testing-library/react';
|
|
15
|
+
import {afterEach, describe, expect, it, vi} from 'vitest';
|
|
16
|
+
import {Markdown} from './Markdown';
|
|
17
|
+
import type {MarkdownComponents} from './Markdown';
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
cleanup();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** A long, review-style document: headings, prose, lists, a table, code. */
|
|
24
|
+
function longDocument(sections: number): string {
|
|
25
|
+
return Array.from({length: sections}, (_, i) =>
|
|
26
|
+
[
|
|
27
|
+
`## Section ${i}`,
|
|
28
|
+
'',
|
|
29
|
+
`Paragraph ${i} with **bold**, *emphasis*, \`code\` and a [link](/docs/${i}).`,
|
|
30
|
+
'',
|
|
31
|
+
`- Point ${i}`,
|
|
32
|
+
` - Nested point ${i}`,
|
|
33
|
+
`- [x] Done ${i}`,
|
|
34
|
+
'',
|
|
35
|
+
'| Name | Count |',
|
|
36
|
+
'| --- | ---: |',
|
|
37
|
+
`| row ${i} | ${i * 3} |`,
|
|
38
|
+
'',
|
|
39
|
+
'```ts',
|
|
40
|
+
`const value${i} = ${i};`,
|
|
41
|
+
'```',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
).join('\n\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SECTIONS = 40;
|
|
47
|
+
|
|
48
|
+
function createCountingComponents(): {
|
|
49
|
+
components: Partial<MarkdownComponents>;
|
|
50
|
+
renders: () => number;
|
|
51
|
+
} {
|
|
52
|
+
let count = 0;
|
|
53
|
+
function CountingCode({code}: {code: string; language?: string}) {
|
|
54
|
+
count++;
|
|
55
|
+
return <pre data-testid="code">{code}</pre>;
|
|
56
|
+
}
|
|
57
|
+
return {components: {code: CountingCode}, renders: () => count};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('Markdown re-render reuse', () => {
|
|
61
|
+
it('does not re-render any block when a caller re-render changes nothing Markdown reads', () => {
|
|
62
|
+
const {components, renders} = createCountingComponents();
|
|
63
|
+
const source = longDocument(SECTIONS);
|
|
64
|
+
let bump: () => void = () => {};
|
|
65
|
+
function Page() {
|
|
66
|
+
const [tick, setTick] = useState(0);
|
|
67
|
+
bump = () => setTick(t => t + 1);
|
|
68
|
+
return (
|
|
69
|
+
<section data-tick={tick}>
|
|
70
|
+
{/* A new link handler each render, as inline callers write it. */}
|
|
71
|
+
<Markdown components={components} onLinkClick={() => {}}>
|
|
72
|
+
{source}
|
|
73
|
+
</Markdown>
|
|
74
|
+
</section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const {container} = render(<Page />);
|
|
78
|
+
const document = container.querySelector('[role="document"]');
|
|
79
|
+
const html = document?.innerHTML;
|
|
80
|
+
expect(renders()).toBe(SECTIONS);
|
|
81
|
+
|
|
82
|
+
act(() => bump());
|
|
83
|
+
act(() => bump());
|
|
84
|
+
|
|
85
|
+
expect(renders()).toBe(SECTIONS);
|
|
86
|
+
expect(container.querySelector('[role="document"]')).toBe(document);
|
|
87
|
+
expect(document?.innerHTML).toBe(html);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('re-renders when the text changes', () => {
|
|
91
|
+
const {components, renders} = createCountingComponents();
|
|
92
|
+
const {rerender, getAllByTestId} = render(
|
|
93
|
+
<Markdown components={components}>{longDocument(2)}</Markdown>,
|
|
94
|
+
);
|
|
95
|
+
expect(renders()).toBe(2);
|
|
96
|
+
rerender(
|
|
97
|
+
<Markdown components={components}>
|
|
98
|
+
{`${longDocument(2)}\n\n\`\`\`ts\nconst added = true;\n\`\`\``}
|
|
99
|
+
</Markdown>,
|
|
100
|
+
);
|
|
101
|
+
expect(getAllByTestId('code')).toHaveLength(3);
|
|
102
|
+
expect(getAllByTestId('code')[2].textContent).toBe('const added = true;');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('re-renders when a rendering prop changes', () => {
|
|
106
|
+
const {container, rerender} = render(
|
|
107
|
+
<Markdown headingLevelStart={1}>{'# Title'}</Markdown>,
|
|
108
|
+
);
|
|
109
|
+
expect(container.querySelector('h1')).not.toBeNull();
|
|
110
|
+
rerender(<Markdown headingLevelStart={3}>{'# Title'}</Markdown>);
|
|
111
|
+
expect(container.querySelector('h1')).toBeNull();
|
|
112
|
+
expect(container.querySelector('h3')?.textContent).toBe('Title');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('calls the latest link handler after a re-render that reused the document', () => {
|
|
116
|
+
const first = vi.fn();
|
|
117
|
+
const latest = vi.fn(() => false as const);
|
|
118
|
+
const {container, rerender} = render(
|
|
119
|
+
<Markdown onLinkClick={first}>{'[go](/next)'}</Markdown>,
|
|
120
|
+
);
|
|
121
|
+
rerender(<Markdown onLinkClick={latest}>{'[go](/next)'}</Markdown>);
|
|
122
|
+
const link = container.querySelector('a');
|
|
123
|
+
expect(link).not.toBeNull();
|
|
124
|
+
const event = new MouseEvent('click', {bubbles: true, cancelable: true});
|
|
125
|
+
act(() => {
|
|
126
|
+
link?.dispatchEvent(event);
|
|
127
|
+
});
|
|
128
|
+
expect(first).not.toHaveBeenCalled();
|
|
129
|
+
expect(latest).toHaveBeenCalledWith('/next', expect.anything());
|
|
130
|
+
expect(event.defaultPrevented).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('starts and stops handling link clicks when the handler is added or removed', () => {
|
|
134
|
+
const handler = vi.fn();
|
|
135
|
+
const {container, rerender} = render(<Markdown>{'[go](/next)'}</Markdown>);
|
|
136
|
+
rerender(<Markdown onLinkClick={handler}>{'[go](/next)'}</Markdown>);
|
|
137
|
+
fireEvent.click(container.querySelector('a') as HTMLAnchorElement);
|
|
138
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
139
|
+
rerender(<Markdown>{'[go](/next)'}</Markdown>);
|
|
140
|
+
fireEvent.click(container.querySelector('a') as HTMLAnchorElement);
|
|
141
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -41,6 +41,7 @@ import {TableHeaderCell} from '../Table/TableHeaderCell';
|
|
|
41
41
|
import {TableHeader} from '../Table/TableHeader';
|
|
42
42
|
import {TableBody} from '../Table/TableBody';
|
|
43
43
|
import {mergeProps} from '../utils';
|
|
44
|
+
import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
44
45
|
import {useStreamingText} from '../hooks/useStreamingText';
|
|
45
46
|
import {computeBoundaries, computeSegments} from './streaming';
|
|
46
47
|
import type {BaseProps} from '../BaseProps';
|
|
@@ -2122,6 +2123,21 @@ export function Markdown<
|
|
|
2122
2123
|
}: MarkdownProps<Plugins>): React.ReactElement {
|
|
2123
2124
|
const t = useTranslator();
|
|
2124
2125
|
const LinkComponent = useLinkComponent();
|
|
2126
|
+
// Links read the caller's handler when clicked, so a handler recreated on
|
|
2127
|
+
// every caller render does not invalidate the memoized document below.
|
|
2128
|
+
// Only whether a handler exists changes the rendered links.
|
|
2129
|
+
const onLinkClickRef = useRef(onLinkClick);
|
|
2130
|
+
useIsomorphicLayoutEffect(() => {
|
|
2131
|
+
onLinkClickRef.current = onLinkClick;
|
|
2132
|
+
});
|
|
2133
|
+
const hasLinkClick = onLinkClick != null;
|
|
2134
|
+
const stableOnLinkClick = useMemo<MarkdownProps['onLinkClick']>(
|
|
2135
|
+
() =>
|
|
2136
|
+
hasLinkClick
|
|
2137
|
+
? (href, event) => onLinkClickRef.current?.(href, event)
|
|
2138
|
+
: undefined,
|
|
2139
|
+
[hasLinkClick],
|
|
2140
|
+
);
|
|
2125
2141
|
// Derive the set of source IDs for the parser (stable across renders when sources don't change)
|
|
2126
2142
|
const sourceIds = useMemo(
|
|
2127
2143
|
() => (sources ? new Set(Object.keys(sources)) : undefined),
|
|
@@ -2327,19 +2343,80 @@ export function Markdown<
|
|
|
2327
2343
|
return next;
|
|
2328
2344
|
}, [display, smoothedLen, maxSpans]);
|
|
2329
2345
|
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
};
|
|
2335
|
-
|
|
2336
|
-
// Build citation context — numbers are assigned in encounter order during rendering.
|
|
2337
|
-
// This is recreated each render so numbering stays consistent with the AST.
|
|
2338
|
-
const citationCtx: CitationContext | null = sources
|
|
2339
|
-
? {sources, numberMap: new Map(), nextNumber: 1, style: citationStyle}
|
|
2346
|
+
const contentWidthValue = contentWidth
|
|
2347
|
+
? typeof contentWidth === 'number'
|
|
2348
|
+
? `${contentWidth}px`
|
|
2349
|
+
: contentWidth
|
|
2340
2350
|
: null;
|
|
2341
2351
|
|
|
2352
|
+
// The block document is the expensive part of a long Markdown render. It
|
|
2353
|
+
// depends only on these inputs, so a caller re-render that changes none of
|
|
2354
|
+
// them returns the same elements and React skips the whole subtree. The
|
|
2355
|
+
// cursor and citation numbering are rebuilt inside, from the start of the
|
|
2356
|
+
// document, exactly as a full render builds them.
|
|
2357
|
+
const renderedBlocks = useMemo(() => {
|
|
2358
|
+
if (display === 'inline') {
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
const blockCursor: StreamingCursor = {
|
|
2362
|
+
offset: 0,
|
|
2363
|
+
boundaries,
|
|
2364
|
+
active: isStreaming,
|
|
2365
|
+
};
|
|
2366
|
+
// Numbers are assigned in encounter order during rendering.
|
|
2367
|
+
const blockCitationCtx: CitationContext | null = sources
|
|
2368
|
+
? {sources, numberMap: new Map(), nextNumber: 1, style: citationStyle}
|
|
2369
|
+
: null;
|
|
2370
|
+
return blocks.map((block, i) =>
|
|
2371
|
+
renderBlock(
|
|
2372
|
+
block,
|
|
2373
|
+
i,
|
|
2374
|
+
blocks.length,
|
|
2375
|
+
density,
|
|
2376
|
+
headingLevelStart,
|
|
2377
|
+
stableOnLinkClick,
|
|
2378
|
+
blockCursor,
|
|
2379
|
+
blockCitationCtx,
|
|
2380
|
+
contentWidthValue,
|
|
2381
|
+
contentAlign,
|
|
2382
|
+
LinkComponent,
|
|
2383
|
+
inlinePlugins,
|
|
2384
|
+
components,
|
|
2385
|
+
preparedPlugins,
|
|
2386
|
+
t,
|
|
2387
|
+
headingProjection,
|
|
2388
|
+
),
|
|
2389
|
+
);
|
|
2390
|
+
}, [
|
|
2391
|
+
display,
|
|
2392
|
+
blocks,
|
|
2393
|
+
boundaries,
|
|
2394
|
+
isStreaming,
|
|
2395
|
+
sources,
|
|
2396
|
+
citationStyle,
|
|
2397
|
+
density,
|
|
2398
|
+
headingLevelStart,
|
|
2399
|
+
stableOnLinkClick,
|
|
2400
|
+
contentWidthValue,
|
|
2401
|
+
contentAlign,
|
|
2402
|
+
LinkComponent,
|
|
2403
|
+
inlinePlugins,
|
|
2404
|
+
components,
|
|
2405
|
+
preparedPlugins,
|
|
2406
|
+
t,
|
|
2407
|
+
headingProjection,
|
|
2408
|
+
]);
|
|
2409
|
+
|
|
2342
2410
|
if (display === 'inline') {
|
|
2411
|
+
const cursor: StreamingCursor = {
|
|
2412
|
+
offset: 0,
|
|
2413
|
+
boundaries,
|
|
2414
|
+
active: isStreaming,
|
|
2415
|
+
};
|
|
2416
|
+
// Numbers are assigned in encounter order during rendering.
|
|
2417
|
+
const citationCtx: CitationContext | null = sources
|
|
2418
|
+
? {sources, numberMap: new Map(), nextNumber: 1, style: citationStyle}
|
|
2419
|
+
: null;
|
|
2343
2420
|
const renderedInline = (
|
|
2344
2421
|
<span
|
|
2345
2422
|
ref={ref}
|
|
@@ -2357,7 +2434,7 @@ export function Markdown<
|
|
|
2357
2434
|
renderInline(
|
|
2358
2435
|
node,
|
|
2359
2436
|
i,
|
|
2360
|
-
|
|
2437
|
+
stableOnLinkClick,
|
|
2361
2438
|
cursor,
|
|
2362
2439
|
citationCtx,
|
|
2363
2440
|
LinkComponent,
|
|
@@ -2391,30 +2468,7 @@ export function Markdown<
|
|
|
2391
2468
|
className,
|
|
2392
2469
|
style,
|
|
2393
2470
|
)}>
|
|
2394
|
-
{
|
|
2395
|
-
renderBlock(
|
|
2396
|
-
block,
|
|
2397
|
-
i,
|
|
2398
|
-
blocks.length,
|
|
2399
|
-
density,
|
|
2400
|
-
headingLevelStart,
|
|
2401
|
-
onLinkClick,
|
|
2402
|
-
cursor,
|
|
2403
|
-
citationCtx,
|
|
2404
|
-
contentWidth
|
|
2405
|
-
? typeof contentWidth === 'number'
|
|
2406
|
-
? `${contentWidth}px`
|
|
2407
|
-
: contentWidth
|
|
2408
|
-
: null,
|
|
2409
|
-
contentAlign,
|
|
2410
|
-
LinkComponent,
|
|
2411
|
-
inlinePlugins,
|
|
2412
|
-
components,
|
|
2413
|
-
preparedPlugins,
|
|
2414
|
-
t,
|
|
2415
|
-
headingProjection,
|
|
2416
|
-
),
|
|
2417
|
-
)}
|
|
2471
|
+
{renderedBlocks}
|
|
2418
2472
|
</div>
|
|
2419
2473
|
);
|
|
2420
2474
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MultiSelector.disabledFocus.a11y.chromium.spec.ts
|
|
5
|
+
* @input The `Core/MultiSelector` Default story in a built Storybook.
|
|
6
|
+
* @output Real-engine proof of `spec:AST-085` FR4: a trigger disabled while it
|
|
7
|
+
* holds keyboard focus keeps focus and reports itself disabled.
|
|
8
|
+
* @position Run with `pnpm test:a11y-contract`; a DOM emulator does not move
|
|
9
|
+
* focus off an element that becomes disabled, so it cannot show the loss.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {expect, test} from '@playwright/test';
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_STORYBOOK_DIR,
|
|
15
|
+
serveStorybook,
|
|
16
|
+
type StaticServer,
|
|
17
|
+
} from '@astryxdesign/a11y-spec/storybook';
|
|
18
|
+
|
|
19
|
+
let storybook: StaticServer;
|
|
20
|
+
|
|
21
|
+
test.beforeAll(async () => {
|
|
22
|
+
storybook = await serveStorybook(
|
|
23
|
+
process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test.afterAll(async () => {
|
|
28
|
+
await storybook?.close();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('a focused trigger keeps focus when it becomes disabled', async ({
|
|
32
|
+
page,
|
|
33
|
+
}) => {
|
|
34
|
+
const storyId = 'core-multiselector--default';
|
|
35
|
+
await page.goto(
|
|
36
|
+
`${storybook.origin}/iframe.html?id=${storyId}&viewMode=story`,
|
|
37
|
+
);
|
|
38
|
+
const trigger = page.getByRole('combobox', {name: 'Columns'});
|
|
39
|
+
await trigger.focus();
|
|
40
|
+
await expect(trigger).toBeFocused();
|
|
41
|
+
|
|
42
|
+
await page.evaluate(id => {
|
|
43
|
+
const channel = (
|
|
44
|
+
window as unknown as {
|
|
45
|
+
__STORYBOOK_ADDONS_CHANNEL__?: {
|
|
46
|
+
emit: (event: string, payload: unknown) => void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
).__STORYBOOK_ADDONS_CHANNEL__;
|
|
50
|
+
if (channel == null) {
|
|
51
|
+
throw new Error('Storybook channel is unavailable');
|
|
52
|
+
}
|
|
53
|
+
channel.emit('updateStoryArgs', {
|
|
54
|
+
storyId: id,
|
|
55
|
+
updatedArgs: {isDisabled: true},
|
|
56
|
+
});
|
|
57
|
+
}, storyId);
|
|
58
|
+
|
|
59
|
+
// Wait for the disabled re-render in either form, so focus is checked
|
|
60
|
+
// after it lands.
|
|
61
|
+
await expect
|
|
62
|
+
.poll(async () =>
|
|
63
|
+
trigger.evaluate(
|
|
64
|
+
el =>
|
|
65
|
+
el.hasAttribute('disabled') ||
|
|
66
|
+
el.getAttribute('aria-disabled') === 'true',
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
.toBe(true);
|
|
70
|
+
await expect(trigger).toBeFocused();
|
|
71
|
+
});
|
|
@@ -304,7 +304,7 @@ export const docs = {
|
|
|
304
304
|
name: 'disabledMessage',
|
|
305
305
|
type: 'string',
|
|
306
306
|
description:
|
|
307
|
-
'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover
|
|
307
|
+
'Explains why the selector is disabled. With isDisabled, shows the reason as a tooltip on hover and keyboard focus of the trigger, which stays focusable (aria-disabled) like every disabled selector; activation stays blocked. Use this instead of wrapping a disabled MultiSelector in Tooltip.',
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
name: 'isLabelHidden',
|
|
@@ -511,7 +511,7 @@ export const docs = {
|
|
|
511
511
|
{
|
|
512
512
|
guidance: false,
|
|
513
513
|
description:
|
|
514
|
-
'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled
|
|
514
|
+
'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
|
|
515
515
|
},
|
|
516
516
|
],
|
|
517
517
|
},
|
|
@@ -552,7 +552,7 @@ export const docsZh = {
|
|
|
552
552
|
htmlName:
|
|
553
553
|
'用于表单提交的 HTML name 属性。为每个已选值渲染一个隐藏输入,类似原生多选。',
|
|
554
554
|
disabledMessage:
|
|
555
|
-
'解释选择器被禁用的原因。与 isDisabled
|
|
555
|
+
'解释选择器被禁用的原因。与 isDisabled 一起使用时,悬停/键盘聚焦时显示工具提示,触发器与所有被禁用的选择器一样通过 aria-disabled 保持可聚焦(仍无法激活)。请使用此属性,而不是用 Tooltip 包裹被禁用的选择器。',
|
|
556
556
|
isLabelHidden: '视觉上隐藏标签同时保持其可访问性。',
|
|
557
557
|
description: '标签下方显示的辅助文本。',
|
|
558
558
|
isOptional: '将字段标记为可选。',
|
|
@@ -619,7 +619,7 @@ export const docsZh = {
|
|
|
619
619
|
{
|
|
620
620
|
guidance: false,
|
|
621
621
|
description:
|
|
622
|
-
'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled
|
|
622
|
+
'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
|
|
623
623
|
},
|
|
624
624
|
],
|
|
625
625
|
},
|
|
@@ -674,7 +674,7 @@ export const docsDense = {
|
|
|
674
674
|
{
|
|
675
675
|
guidance: false,
|
|
676
676
|
description:
|
|
677
|
-
'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled
|
|
677
|
+
'Wrap a disabled MultiSelector in Tooltip to explain why it is disabled. Use the disabledMessage prop, which attaches the reason to the trigger itself.',
|
|
678
678
|
},
|
|
679
679
|
],
|
|
680
680
|
},
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file MultiSelector.focusableDisabled.test.tsx
|
|
5
|
+
* @input Uses vitest, @testing-library/react, StyleX, MultiSelector
|
|
6
|
+
* @output Conformance tests for spec:AST-085 FR1–FR3 and FR6 on MultiSelector;
|
|
7
|
+
* FR4 needs a real engine (MultiSelector.disabledFocus.a11y.chromium.spec.ts)
|
|
8
|
+
* @position Testing; validates the disabled trigger of MultiSelector.tsx
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {describe, it, expect, vi} from 'vitest';
|
|
12
|
+
import {render, screen} from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import * as stylex from '@stylexjs/stylex';
|
|
15
|
+
import {colorVars} from '../theme/tokens.stylex';
|
|
16
|
+
import {MultiSelector} from './MultiSelector';
|
|
17
|
+
|
|
18
|
+
type Handler = () => void;
|
|
19
|
+
function Harness(props: {
|
|
20
|
+
isDisabled: boolean;
|
|
21
|
+
variant?: 'input' | 'secondary' | 'ghost';
|
|
22
|
+
onClick?: Handler;
|
|
23
|
+
onKeyDown?: Handler;
|
|
24
|
+
onMouseDown?: Handler;
|
|
25
|
+
onDoubleClick?: Handler;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<form aria-label="Form">
|
|
29
|
+
<button type="button">Before</button>
|
|
30
|
+
<MultiSelector
|
|
31
|
+
label="Fruit"
|
|
32
|
+
htmlName="fruit"
|
|
33
|
+
options={['Apple', 'Banana']}
|
|
34
|
+
value={['Banana']}
|
|
35
|
+
onChange={() => {}}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
<button type="button">After</button>
|
|
39
|
+
</form>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const trigger = () => screen.getByRole('combobox', {name: 'Fruit'});
|
|
44
|
+
|
|
45
|
+
describe('MultiSelector disabled (spec:AST-085)', () => {
|
|
46
|
+
it('FR1: renders no native disabled and keeps its tab stop', async () => {
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
const {container} = render(<Harness isDisabled />);
|
|
49
|
+
expect(container.querySelector('[disabled]')).toBeNull();
|
|
50
|
+
expect(trigger()).toHaveAttribute('aria-disabled', 'true');
|
|
51
|
+
await user.click(screen.getByRole('button', {name: 'Before'}));
|
|
52
|
+
await user.tab();
|
|
53
|
+
expect(trigger()).toHaveFocus();
|
|
54
|
+
await user.tab();
|
|
55
|
+
expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('FR2: no press or key opens it or reaches a caller or ancestor handler', async () => {
|
|
59
|
+
const user = userEvent.setup();
|
|
60
|
+
const handlers = {
|
|
61
|
+
onClick: vi.fn(),
|
|
62
|
+
onKeyDown: vi.fn(),
|
|
63
|
+
onMouseDown: vi.fn(),
|
|
64
|
+
onDoubleClick: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
const onAncestorClick = vi.fn();
|
|
67
|
+
render(
|
|
68
|
+
<div onClick={onAncestorClick}>
|
|
69
|
+
<Harness isDisabled {...handlers} />
|
|
70
|
+
</div>,
|
|
71
|
+
);
|
|
72
|
+
await user.dblClick(trigger());
|
|
73
|
+
trigger().focus();
|
|
74
|
+
for (const key of ['{Enter}', ' ', '{ArrowDown}', 'b']) {
|
|
75
|
+
await user.keyboard(key);
|
|
76
|
+
}
|
|
77
|
+
expect(trigger()).toHaveAttribute('aria-expanded', 'false');
|
|
78
|
+
expect(screen.queryByRole('listbox')).toBeNull();
|
|
79
|
+
for (const handler of [...Object.values(handlers), onAncestorClick]) {
|
|
80
|
+
expect(handler).not.toHaveBeenCalled();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('FR3: submits no value while disabled', () => {
|
|
85
|
+
const {rerender} = render(<Harness isDisabled={false} />);
|
|
86
|
+
const form = () =>
|
|
87
|
+
screen.getByRole<HTMLFormElement>('form', {name: 'Form'});
|
|
88
|
+
expect(new FormData(form()).getAll('fruit')).toEqual(['Banana']);
|
|
89
|
+
rerender(<Harness isDisabled />);
|
|
90
|
+
expect(new FormData(form()).getAll('fruit')).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// jsdom cannot match `:focus-visible`: compare the ring's StyleX class hash.
|
|
94
|
+
const RING = stylex
|
|
95
|
+
.props(
|
|
96
|
+
stylex.create({
|
|
97
|
+
ring: {
|
|
98
|
+
boxShadow: {
|
|
99
|
+
default: null,
|
|
100
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}).ring,
|
|
104
|
+
)
|
|
105
|
+
.className!.split(' ')
|
|
106
|
+
.pop()!;
|
|
107
|
+
|
|
108
|
+
it.each([
|
|
109
|
+
['input', true],
|
|
110
|
+
['secondary', false],
|
|
111
|
+
['ghost', false],
|
|
112
|
+
] as const)(
|
|
113
|
+
'FR6: a disabled %s trigger carries its own ring: %s',
|
|
114
|
+
(variant, ring) => {
|
|
115
|
+
render(<Harness isDisabled variant={variant} />);
|
|
116
|
+
const wrapper = trigger().parentElement!;
|
|
117
|
+
expect(wrapper.classList.contains(RING)).toBe(ring);
|
|
118
|
+
expect(getComputedStyle(wrapper).cursor).toBe('default');
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
});
|
|
@@ -35,7 +35,7 @@ import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
|
35
35
|
import {selectorPresentationStyles} from '../Selector/selectorPresentation.stylex';
|
|
36
36
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
37
37
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
38
|
-
import {focusVars} from '../theme/tokens.stylex';
|
|
38
|
+
import {colorVars, focusVars} from '../theme/tokens.stylex';
|
|
39
39
|
|
|
40
40
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
41
41
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -210,6 +210,45 @@ describe('MultiSelector', () => {
|
|
|
210
210
|
);
|
|
211
211
|
});
|
|
212
212
|
|
|
213
|
+
it('shows no disabled focus ring after a pointer pick (spec:AST-085 FR6)', async () => {
|
|
214
|
+
const field = (isDisabled: boolean) => (
|
|
215
|
+
<MultiSelector
|
|
216
|
+
label="Fruit"
|
|
217
|
+
options={defaultOptions}
|
|
218
|
+
value={[]}
|
|
219
|
+
onChange={() => {}}
|
|
220
|
+
presentation="bottom-sheet"
|
|
221
|
+
isDisabled={isDisabled}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
const {rerender} = render(field(false));
|
|
225
|
+
const trigger = screen.getByRole('combobox');
|
|
226
|
+
fireEvent.pointerDown(trigger, {pointerType: 'touch'});
|
|
227
|
+
fireEvent.click(trigger, {detail: 1});
|
|
228
|
+
const dialog = await screen.findByRole('dialog', {name: 'Fruit'});
|
|
229
|
+
fireEvent.pointerDown(dialog, {pointerType: 'touch'});
|
|
230
|
+
fireEvent.click(dialog, {detail: 1});
|
|
231
|
+
rerender(field(true));
|
|
232
|
+
const ring = stylex
|
|
233
|
+
.props(
|
|
234
|
+
stylex.create({
|
|
235
|
+
ring: {
|
|
236
|
+
boxShadow: {
|
|
237
|
+
default: null,
|
|
238
|
+
':has(> [aria-disabled="true"]:focus-visible)': `inset 0px 0px 0px 2px ${colorVars['--color-accent-muted']}`,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
}).ring,
|
|
242
|
+
)
|
|
243
|
+
.className!.split(' ')
|
|
244
|
+
.pop()!;
|
|
245
|
+
const wrapper = screen.getByRole('combobox').parentElement!;
|
|
246
|
+
expect(wrapper).toHaveClass(
|
|
247
|
+
stylex.props(selectorPresentationStyles.pointerRestoredFocus).className!,
|
|
248
|
+
);
|
|
249
|
+
expect(wrapper).not.toHaveClass(ring);
|
|
250
|
+
});
|
|
251
|
+
|
|
213
252
|
it('renders with label', () => {
|
|
214
253
|
render(
|
|
215
254
|
<MultiSelector
|
|
@@ -515,7 +554,9 @@ describe('MultiSelector', () => {
|
|
|
515
554
|
isDisabled
|
|
516
555
|
/>,
|
|
517
556
|
);
|
|
518
|
-
|
|
557
|
+
const trigger = screen.getByRole('combobox');
|
|
558
|
+
expect(trigger).not.toHaveAttribute('disabled');
|
|
559
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
519
560
|
});
|
|
520
561
|
|
|
521
562
|
it('has correct ARIA attributes', () => {
|
|
@@ -1602,7 +1643,7 @@ describe('MultiSelector', () => {
|
|
|
1602
1643
|
expect(screen.getByRole('combobox')).toHaveFocus();
|
|
1603
1644
|
});
|
|
1604
1645
|
|
|
1605
|
-
it('trigger
|
|
1646
|
+
it('trigger keeps its tab stop when disabled (spec:AST-085 FR1)', () => {
|
|
1606
1647
|
render(
|
|
1607
1648
|
<MultiSelector
|
|
1608
1649
|
label="Fruit"
|
|
@@ -1612,7 +1653,7 @@ describe('MultiSelector', () => {
|
|
|
1612
1653
|
isDisabled
|
|
1613
1654
|
/>,
|
|
1614
1655
|
);
|
|
1615
|
-
expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '
|
|
1656
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('tabIndex', '0');
|
|
1616
1657
|
});
|
|
1617
1658
|
|
|
1618
1659
|
it('opens the listbox with ArrowDown from a focused trigger', async () => {
|
|
@@ -2076,7 +2117,7 @@ describe('MultiSelector', () => {
|
|
|
2076
2117
|
expect(onChange).not.toHaveBeenCalled();
|
|
2077
2118
|
});
|
|
2078
2119
|
|
|
2079
|
-
it('
|
|
2120
|
+
it('stays focusable when disabled without a reason (spec:AST-085 FR1)', () => {
|
|
2080
2121
|
render(
|
|
2081
2122
|
<MultiSelector
|
|
2082
2123
|
label="Fruit"
|
|
@@ -2087,8 +2128,9 @@ describe('MultiSelector', () => {
|
|
|
2087
2128
|
/>,
|
|
2088
2129
|
);
|
|
2089
2130
|
const trigger = screen.getByRole('combobox');
|
|
2090
|
-
expect(trigger).
|
|
2091
|
-
expect(trigger).toHaveAttribute('
|
|
2131
|
+
expect(trigger).not.toHaveAttribute('disabled');
|
|
2132
|
+
expect(trigger).toHaveAttribute('aria-disabled', 'true');
|
|
2133
|
+
expect(trigger).toHaveAttribute('tabIndex', '0');
|
|
2092
2134
|
});
|
|
2093
2135
|
});
|
|
2094
2136
|
describe('isReadOnly', () => {
|