@astryxdesign/core 0.1.4-canary.39ffd21 → 0.1.4-canary.3d64ef2
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/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +8 -1
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +31 -11
- package/dist/Calendar/styles.d.ts +2 -2
- package/dist/Calendar/styles.d.ts.map +1 -1
- package/dist/Calendar/styles.js +2 -2
- package/dist/Card/Card.d.ts +4 -2
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +15 -7
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +1 -0
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +8 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/ClickableCard/ClickableCard.d.ts.map +1 -1
- package/dist/ClickableCard/ClickableCard.js +26 -3
- package/dist/CodeBlock/CodeBlock.d.ts +10 -1
- package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
- package/dist/CodeBlock/CodeBlock.js +23 -5
- package/dist/Collapsible/Collapsible.d.ts +9 -3
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +101 -13
- package/dist/Collapsible/CollapsibleGroup.d.ts +39 -7
- package/dist/Collapsible/CollapsibleGroup.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroup.js +70 -9
- package/dist/Collapsible/CollapsibleGroupContext.d.ts +31 -0
- package/dist/Collapsible/CollapsibleGroupContext.d.ts.map +1 -1
- package/dist/Collapsible/CollapsibleGroupContext.js +33 -3
- package/dist/Collapsible/index.d.ts +1 -0
- package/dist/Collapsible/index.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +2 -0
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +2 -0
- package/dist/Grid/Grid.d.ts +4 -3
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +26 -9
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +5 -1
- package/dist/InputGroup/InputGroup.js +2 -2
- package/dist/Item/Item.js +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +26 -2
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +32 -16
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +87 -36
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +39 -21
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -1
- package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
- package/dist/Resizable/ResizeHandle.js +10 -2
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +6 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +34 -7
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +14 -3
- package/dist/Table/plugins/columnResize/useTableColumnResize.d.ts.map +1 -1
- package/dist/Table/plugins/columnResize/useTableColumnResize.js +5 -2
- package/dist/Table/plugins/pagination/paginateData.d.ts.map +1 -1
- package/dist/Table/plugins/pagination/paginateData.js +5 -1
- package/dist/Table/plugins/selection/useTableSelectionState.d.ts.map +1 -1
- package/dist/Table/plugins/selection/useTableSelectionState.js +11 -1
- package/dist/Table/plugins/sortable/useTableSortableState.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortableState.js +8 -4
- package/dist/Thumbnail/Thumbnail.d.ts +3 -2
- package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
- package/dist/Thumbnail/Thumbnail.js +1 -0
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +6 -1
- package/dist/Toast/Toast.d.ts.map +1 -1
- package/dist/Toast/Toast.js +11 -3
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +12 -0
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -2
- package/dist/astryx.css +7 -0
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/focusableSelector.d.ts +1 -1
- package/dist/hooks/focusableSelector.d.ts.map +1 -1
- package/dist/hooks/focusableSelector.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts +3 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +38 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +10 -3
- package/dist/theme/hct.d.ts +0 -15
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +9 -9
- package/dist/theme/tokens.d.ts +10 -2
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +220 -4
- package/dist/utils/color.d.ts +42 -0
- package/dist/utils/color.d.ts.map +1 -0
- package/dist/utils/color.js +150 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/timeParser.d.ts.map +1 -1
- package/dist/utils/timeParser.js +8 -5
- package/package.json +1 -1
- package/src/Avatar/Avatar.doc.mjs +11 -0
- package/src/Banner/Banner.doc.mjs +8 -0
- package/src/Banner/Banner.test.tsx +38 -0
- package/src/Banner/Banner.tsx +8 -1
- package/src/Calendar/Calendar.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +112 -4
- package/src/Calendar/Calendar.tsx +26 -5
- package/src/Calendar/styles.ts +12 -2
- package/src/Card/Card.tsx +16 -6
- package/src/Carousel/Carousel.doc.mjs +12 -12
- package/src/Carousel/Carousel.test.tsx +13 -0
- package/src/Carousel/Carousel.tsx +1 -0
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatToolCalls.test.tsx +55 -0
- package/src/Chat/ChatToolCalls.tsx +10 -12
- package/src/CheckboxInput/CheckboxInput.doc.mjs +3 -3
- package/src/CheckboxInput/CheckboxInput.test.tsx +71 -4
- package/src/CheckboxInput/CheckboxInput.tsx +2 -0
- package/src/Citation/Citation.doc.mjs +5 -0
- package/src/ClickableCard/ClickableCard.tsx +45 -2
- package/src/CodeBlock/CodeBlock.doc.mjs +7 -2
- package/src/CodeBlock/CodeBlock.test.tsx +87 -3
- package/src/CodeBlock/CodeBlock.tsx +43 -3
- package/src/Collapsible/Collapsible.doc.mjs +8 -4
- package/src/Collapsible/Collapsible.tsx +86 -10
- package/src/Collapsible/CollapsibleGroup.doc.mjs +27 -3
- package/src/Collapsible/CollapsibleGroup.test.tsx +288 -0
- package/src/Collapsible/CollapsibleGroup.tsx +107 -9
- package/src/Collapsible/CollapsibleGroupContext.tsx +42 -2
- package/src/Collapsible/index.ts +4 -0
- package/src/CommandPalette/CommandPaletteEmpty.doc.mjs +5 -0
- package/src/CommandPalette/CommandPaletteGroup.doc.mjs +10 -0
- package/src/CommandPalette/CommandPaletteInput.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteItem.doc.mjs +6 -0
- package/src/CommandPalette/CommandPaletteList.doc.mjs +9 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +15 -0
- package/src/ContextMenu/ContextMenu.test.tsx +15 -0
- package/src/ContextMenu/ContextMenu.tsx +2 -0
- package/src/ContextMenu/ContextMenuItem.doc.mjs +6 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +70 -0
- package/src/DateTimeInput/DateTimeInput.tsx +2 -0
- package/src/Dialog/DialogHeader.doc.mjs +45 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +1 -1
- package/src/Field/FieldLabel.test.tsx +2 -2
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.test.tsx +43 -18
- package/src/Grid/Grid.tsx +26 -9
- package/src/Icon/Icon.test.tsx +41 -29
- package/src/Icon/Icon.tsx +6 -2
- package/src/InputGroup/InputGroup.tsx +2 -2
- package/src/Item/Item.tsx +1 -1
- package/src/Layout/LayoutContent.doc.mjs +12 -0
- package/src/Layout/LayoutPanel.doc.mjs +40 -0
- package/src/Lightbox/Lightbox.doc.mjs +5 -0
- package/src/Lightbox/Lightbox.test.tsx +123 -2
- package/src/Lightbox/Lightbox.tsx +29 -1
- package/src/NavMenu/NavMenu.doc.mjs +15 -0
- package/src/NumberInput/NumberInput.test.tsx +48 -3
- package/src/NumberInput/NumberInput.tsx +32 -15
- package/src/Overlay/Overlay.doc.mjs +6 -0
- package/src/Pagination/Pagination.test.tsx +143 -0
- package/src/Pagination/Pagination.tsx +75 -23
- package/src/Popover/Popover.test.tsx +65 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/PowerSearch/PowerSearch.test.tsx +109 -1
- package/src/PowerSearch/PowerSearch.tsx +37 -16
- package/src/ProgressBar/ProgressBar.test.tsx +10 -0
- package/src/ProgressBar/ProgressBar.tsx +2 -1
- package/src/Resizable/ResizeHandle.test.tsx +180 -0
- package/src/Resizable/ResizeHandle.tsx +10 -2
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/SelectableCard/SelectableCard.doc.mjs +1 -1
- package/src/SideNav/SideNav.doc.mjs +7 -1
- package/src/SideNav/SideNav.test.tsx +20 -0
- package/src/SideNav/SideNav.tsx +6 -2
- package/src/TabList/TabList.test.tsx +185 -1
- package/src/TabList/TabMenu.tsx +45 -9
- package/src/Table/BaseTable.tsx +13 -2
- package/src/Table/Table.test.tsx +51 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.test.tsx +13 -0
- package/src/Table/plugins/columnResize/useTableColumnResize.tsx +3 -1
- package/src/Table/plugins/pagination/paginateData.ts +5 -1
- package/src/Table/plugins/pagination/useTablePagination.test.tsx +13 -0
- package/src/Table/plugins/selection/useTableSelectionState.test.tsx +23 -0
- package/src/Table/plugins/selection/useTableSelectionState.tsx +11 -6
- package/src/Table/plugins/sortable/useTableSortableState.test.tsx +35 -1
- package/src/Table/plugins/sortable/useTableSortableState.tsx +8 -4
- package/src/TextArea/TextArea.test.tsx +3 -3
- package/src/TextInput/TextInput.test.tsx +3 -3
- package/src/Thumbnail/Thumbnail.test.tsx +36 -2
- package/src/Thumbnail/Thumbnail.tsx +4 -2
- package/src/TimeInput/TimeInput.test.tsx +37 -0
- package/src/TimeInput/TimeInput.tsx +11 -1
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/Toast/Toast.tsx +11 -3
- package/src/Toast/ToastViewport.test.tsx +64 -0
- package/src/Toast/ToastViewport.tsx +12 -0
- package/src/ToggleButton/ToggleButton.doc.mjs +1 -0
- package/src/Tokenizer/Tokenizer.doc.mjs +2 -2
- package/src/TopNav/TopNav.doc.mjs +1 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/Typeahead/BaseTypeahead.doc.mjs +19 -0
- package/src/Typeahead/BaseTypeahead.tsx +5 -2
- package/src/Typeahead/Typeahead.doc.mjs +1 -1
- package/src/Typeahead/Typeahead.test.tsx +45 -1
- package/src/__tests__/TestIcon.tsx +26 -0
- package/src/docPropReferences.test.ts +178 -0
- package/src/hooks/focusableSelector.ts +1 -1
- package/src/hooks/useFocusTrap.doc.mjs +4 -2
- package/src/hooks/useFocusTrap.test.tsx +149 -1
- package/src/hooks/useFocusTrap.ts +50 -1
- package/src/theme/SyntaxTheme.doc.mjs +4 -4
- package/src/theme/defineTheme.ts +4 -13
- package/src/theme/derivedVarRegistry.test.ts +2 -2
- package/src/theme/expandColorScale.test.ts +29 -0
- package/src/theme/expandColorScale.ts +11 -6
- package/src/theme/hct.ts +9 -17
- package/src/theme/tokens.test.ts +182 -9
- package/src/theme/tokens.ts +239 -4
- package/src/theme/useTheme.test.tsx +20 -0
- package/src/utils/color.test.ts +133 -0
- package/src/utils/color.ts +148 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/timeParser.test.ts +13 -0
- package/src/utils/timeParser.ts +8 -5
|
@@ -20,15 +20,15 @@ export const docs = {
|
|
|
20
20
|
],
|
|
21
21
|
anatomy: [
|
|
22
22
|
{name: 'Scroll container', required: true, description: 'The horizontal overflow area that holds all items.'},
|
|
23
|
-
{name: 'Items', required: true, description: 'The children rendered in a row.
|
|
23
|
+
{name: 'Items', required: true, description: 'The children rendered in a row inside the scroll container. With hasSnap, each item snaps to the start edge.'},
|
|
24
24
|
{name: 'Fade edges', required: false, description: 'Gradient fades on the left and right edges that indicate more content is available. Enabled by default, disable with hasEdgeFade={false}.'},
|
|
25
|
-
{name: 'Navigation buttons', required: false, description: 'Prev/next buttons that appear
|
|
25
|
+
{name: 'Navigation buttons', required: false, description: 'Prev/next buttons that appear when the content can scroll in that direction. Enabled by default, disable with hasButtons={false}.'},
|
|
26
26
|
],
|
|
27
27
|
},
|
|
28
28
|
props: [
|
|
29
29
|
{name: 'children', type: 'ReactNode', description: 'Carousel items rendered in a horizontal scroll container.', required: true},
|
|
30
30
|
{name: 'gap', type: "0 | 0.5 | 1 | 1.5 | 2 | 3 | 4", description: 'Gap between items using the spacing token scale.', default: '1'},
|
|
31
|
-
{name: 'hasButtons', type: 'boolean', description: 'Show prev/next navigation buttons
|
|
31
|
+
{name: 'hasButtons', type: 'boolean', description: 'Show prev/next navigation buttons when content is scrollable.', default: 'true'},
|
|
32
32
|
{name: 'hasEdgeFade', type: 'boolean', description: 'Show a gradient edge-fade mask when content overflows, signalling that more items exist off-screen.', default: 'true'},
|
|
33
33
|
{name: 'hasSnap', type: 'boolean', description: 'Enable scroll-snap so each child snaps to the start edge.', default: 'false'},
|
|
34
34
|
{name: 'padding', type: '0 | 0.5 | 1 | 1.5 | 2 | 3 | 4 | 5 | 6 | 8 | 10', description: 'Inline padding inside the scroll container, with matching scroll-padding so snap points align to the content edge.'},
|
|
@@ -59,20 +59,20 @@ export const docs = {
|
|
|
59
59
|
export const docsZh = {
|
|
60
60
|
usage: {
|
|
61
61
|
description:
|
|
62
|
-
'Carousel
|
|
62
|
+
'Carousel 在项目超出可用宽度时将一行项目水平滚动。适用于卡片网格、图片画廊、产品列表,或任何希望无需占据整页即可浏览的项目集合。',
|
|
63
63
|
bestPractices: [
|
|
64
|
-
{guidance: true, description: '
|
|
65
|
-
{guidance: true, description: '
|
|
66
|
-
{guidance: true, description: '
|
|
67
|
-
{guidance: false, description: '
|
|
68
|
-
{guidance: false, description: '
|
|
69
|
-
{guidance: false, description: '
|
|
64
|
+
{guidance: true, description: '当每个项目应精确对齐到起始边缘时启用滚动吸附,例如画廊或产品列表。'},
|
|
65
|
+
{guidance: true, description: '始终提供描述轮播内容的 aria-label,例如"精选产品"或"团队成员"。'},
|
|
66
|
+
{guidance: true, description: '使用一致的间距和项目宽度,让轮播看起来是有意为之,而不是内容意外溢出。'},
|
|
67
|
+
{guidance: false, description: '将每位用户都必须看到的内容放入轮播。并非所有人都会水平滚动,关键内容应放在首屏。'},
|
|
68
|
+
{guidance: false, description: '自动轮播项目。让用户按自己的节奏滚动。'},
|
|
69
|
+
{guidance: false, description: '嵌套轮播。轮播中嵌套轮播会造成困惑并破坏键盘导航。'},
|
|
70
70
|
],
|
|
71
71
|
},
|
|
72
72
|
propDescriptions: {
|
|
73
73
|
children: '在水平滚动容器中渲染的轮播项目。',
|
|
74
74
|
gap: '使用间距令牌比例的项目间距。',
|
|
75
|
-
hasButtons: '
|
|
75
|
+
hasButtons: '内容可滚动时显示上一个/下一个导航按钮。',
|
|
76
76
|
hasEdgeFade: '内容溢出时显示渐变边缘遮罩,提示屏幕外还有更多项目。',
|
|
77
77
|
hasSnap: '启用滚动吸附,使每个子元素吸附到起始边缘。',
|
|
78
78
|
padding: '滚动容器内的内联内边距,并设置匹配的 scroll-padding,使吸附点与内容边缘对齐。',
|
|
@@ -98,7 +98,7 @@ export const docsDense = {
|
|
|
98
98
|
propDescriptions: {
|
|
99
99
|
children: 'carousel items in horizontal scroll',
|
|
100
100
|
gap: 'item spacing via spacing token scale',
|
|
101
|
-
hasButtons: 'prev/next buttons
|
|
101
|
+
hasButtons: 'prev/next buttons when content is scrollable',
|
|
102
102
|
hasEdgeFade: 'gradient edge-fade mask on overflow',
|
|
103
103
|
hasSnap: 'scroll-snap; children snap to start edge',
|
|
104
104
|
padding: 'inline padding; scroll-padding keeps snap points on content edge',
|
|
@@ -38,6 +38,19 @@ describe('Carousel', () => {
|
|
|
38
38
|
expect(region).toHaveAttribute('aria-roledescription', 'carousel');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it('makes the scroll container keyboard-focusable', () => {
|
|
42
|
+
render(
|
|
43
|
+
<Carousel aria-label="Photos">
|
|
44
|
+
<div>Item</div>
|
|
45
|
+
</Carousel>,
|
|
46
|
+
);
|
|
47
|
+
// The inner scroll container overflows, so it must be reachable by
|
|
48
|
+
// keyboard (axe: scrollable-region-focusable).
|
|
49
|
+
const region = screen.getByRole('region', {name: 'Photos'});
|
|
50
|
+
const scroller = region.firstElementChild;
|
|
51
|
+
expect(scroller).toHaveAttribute('tabindex', '0');
|
|
52
|
+
});
|
|
53
|
+
|
|
41
54
|
it('applies data-testid', () => {
|
|
42
55
|
render(
|
|
43
56
|
<Carousel data-testid="my-carousel">
|
package/src/Chat/Chat.doc.mjs
CHANGED
|
@@ -17,7 +17,7 @@ export const docs = {
|
|
|
17
17
|
{className: 'astryx-chat-composer', visualProps: ['density']},
|
|
18
18
|
{className: 'astryx-chat-composer-input'},
|
|
19
19
|
{className: 'astryx-chat-composer-drawer', visualProps: ['collapsed']},
|
|
20
|
-
{className: 'astryx-chat-message', visualProps: ['sender']},
|
|
20
|
+
{className: 'astryx-chat-message', visualProps: ['sender', 'density']},
|
|
21
21
|
{
|
|
22
22
|
className: 'astryx-chat-message-bubble',
|
|
23
23
|
visualProps: ['sender', 'variant', 'density'],
|
|
@@ -97,4 +97,59 @@ describe('ChatToolCalls', () => {
|
|
|
97
97
|
);
|
|
98
98
|
expect(screen.getByText('git status')).toBeInTheDocument();
|
|
99
99
|
});
|
|
100
|
+
|
|
101
|
+
it('exposes no aria-expanded on a call row without resultDetail', () => {
|
|
102
|
+
const {container} = render(
|
|
103
|
+
<ChatToolCalls calls={[{name: 'bash', status: 'complete'}]} />,
|
|
104
|
+
);
|
|
105
|
+
expect(container.querySelector('[aria-expanded]')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('wires disclosure semantics on an expandable call row', () => {
|
|
109
|
+
render(
|
|
110
|
+
<ChatToolCalls
|
|
111
|
+
calls={[
|
|
112
|
+
{
|
|
113
|
+
name: 'readFile',
|
|
114
|
+
status: 'complete',
|
|
115
|
+
resultDetail: <div>file contents here</div>,
|
|
116
|
+
},
|
|
117
|
+
]}
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
const row = screen.getByRole('button');
|
|
121
|
+
expect(row).toHaveAttribute('aria-expanded', 'false');
|
|
122
|
+
// Detail panel is conditionally mounted, so no aria-controls while closed.
|
|
123
|
+
expect(row).not.toHaveAttribute('aria-controls');
|
|
124
|
+
expect(screen.queryByText('file contents here')).not.toBeInTheDocument();
|
|
125
|
+
|
|
126
|
+
fireEvent.click(row);
|
|
127
|
+
|
|
128
|
+
expect(row).toHaveAttribute('aria-expanded', 'true');
|
|
129
|
+
const detailId = row.getAttribute('aria-controls');
|
|
130
|
+
expect(detailId).toBeTruthy();
|
|
131
|
+
const panel = document.getElementById(detailId as string);
|
|
132
|
+
expect(panel).not.toBeNull();
|
|
133
|
+
expect(panel).toHaveTextContent('file contents here');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('points the group header aria-controls at the content region', () => {
|
|
137
|
+
render(
|
|
138
|
+
<ChatToolCalls
|
|
139
|
+
defaultIsExpanded={true}
|
|
140
|
+
calls={[
|
|
141
|
+
{name: 'searchCode', status: 'complete'},
|
|
142
|
+
{name: 'readFile', status: 'complete'},
|
|
143
|
+
]}
|
|
144
|
+
/>,
|
|
145
|
+
);
|
|
146
|
+
const header = screen.getByRole('button');
|
|
147
|
+
expect(header).toHaveAttribute('aria-expanded', 'true');
|
|
148
|
+
const regionId = header.getAttribute('aria-controls');
|
|
149
|
+
expect(regionId).toBeTruthy();
|
|
150
|
+
const region = document.getElementById(regionId as string);
|
|
151
|
+
expect(region).not.toBeNull();
|
|
152
|
+
expect(region).toHaveTextContent('searchCode');
|
|
153
|
+
expect(region).toHaveTextContent('readFile');
|
|
154
|
+
});
|
|
100
155
|
});
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* for builders to wire up.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import React, {useState, useCallback, type ReactNode} from 'react';
|
|
21
|
+
import React, {useState, useCallback, useId, type ReactNode} from 'react';
|
|
22
22
|
import type {BaseProps} from '../BaseProps';
|
|
23
23
|
import * as stylex from '@stylexjs/stylex';
|
|
24
24
|
import {
|
|
@@ -41,11 +41,7 @@ import {themeProps} from '../utils/themeProps';
|
|
|
41
41
|
// Types
|
|
42
42
|
// =============================================================================
|
|
43
43
|
|
|
44
|
-
export type ChatToolCallStatus =
|
|
45
|
-
| 'pending'
|
|
46
|
-
| 'running'
|
|
47
|
-
| 'complete'
|
|
48
|
-
| 'error';
|
|
44
|
+
export type ChatToolCallStatus = 'pending' | 'running' | 'complete' | 'error';
|
|
49
45
|
|
|
50
46
|
export interface ChatToolCallItem {
|
|
51
47
|
/** Tool/function name. */
|
|
@@ -356,6 +352,7 @@ function CallRow({call}: {call: ChatToolCallItem}) {
|
|
|
356
352
|
const status = call.status ?? 'complete';
|
|
357
353
|
const hasDetail = call.resultDetail != null;
|
|
358
354
|
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
|
355
|
+
const detailId = useId();
|
|
359
356
|
|
|
360
357
|
const toggleDetail = hasDetail
|
|
361
358
|
? () => setIsDetailOpen(prev => !prev)
|
|
@@ -365,6 +362,8 @@ function CallRow({call}: {call: ChatToolCallItem}) {
|
|
|
365
362
|
<div
|
|
366
363
|
role={hasDetail ? 'button' : undefined}
|
|
367
364
|
tabIndex={hasDetail ? 0 : undefined}
|
|
365
|
+
aria-expanded={hasDetail ? isDetailOpen : undefined}
|
|
366
|
+
aria-controls={hasDetail && isDetailOpen ? detailId : undefined}
|
|
368
367
|
onClick={toggleDetail}
|
|
369
368
|
onKeyDown={
|
|
370
369
|
hasDetail
|
|
@@ -399,11 +398,7 @@ function CallRow({call}: {call: ChatToolCallItem}) {
|
|
|
399
398
|
</span>
|
|
400
399
|
<span {...stylex.props(styles.callName)}>{call.name}</span>
|
|
401
400
|
{call.node != null && (
|
|
402
|
-
<Badge
|
|
403
|
-
label={call.node}
|
|
404
|
-
variant="neutral"
|
|
405
|
-
xstyle={styles.nodePill}
|
|
406
|
-
/>
|
|
401
|
+
<Badge label={call.node} variant="neutral" xstyle={styles.nodePill} />
|
|
407
402
|
)}
|
|
408
403
|
{call.target != null && (
|
|
409
404
|
<span {...stylex.props(styles.callLabel)}>{call.target}</span>
|
|
@@ -448,7 +443,7 @@ function CallRow({call}: {call: ChatToolCallItem}) {
|
|
|
448
443
|
<div>
|
|
449
444
|
{row}
|
|
450
445
|
{isDetailOpen && (
|
|
451
|
-
<div {...stylex.props(styles.callDetailContent)}>
|
|
446
|
+
<div id={detailId} {...stylex.props(styles.callDetailContent)}>
|
|
452
447
|
{call.resultDetail}
|
|
453
448
|
</div>
|
|
454
449
|
)}
|
|
@@ -497,6 +492,7 @@ export function ChatToolCalls(props: ChatToolCallsProps) {
|
|
|
497
492
|
|
|
498
493
|
const autoDefault = defaultIsExpanded ?? false;
|
|
499
494
|
const [internalExpanded, setInternalExpanded] = useState(autoDefault);
|
|
495
|
+
const contentId = useId();
|
|
500
496
|
const isControlled = controlledExpanded !== undefined;
|
|
501
497
|
const isExpanded = isControlled ? controlledExpanded : internalExpanded;
|
|
502
498
|
|
|
@@ -548,6 +544,7 @@ export function ChatToolCalls(props: ChatToolCallsProps) {
|
|
|
548
544
|
role="button"
|
|
549
545
|
tabIndex={0}
|
|
550
546
|
aria-expanded={isExpanded}
|
|
547
|
+
aria-controls={contentId}
|
|
551
548
|
onClick={toggle}
|
|
552
549
|
onKeyDown={e => {
|
|
553
550
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
@@ -613,6 +610,7 @@ export function ChatToolCalls(props: ChatToolCallsProps) {
|
|
|
613
610
|
|
|
614
611
|
{/* Expanded: all calls with full metadata */}
|
|
615
612
|
<div
|
|
613
|
+
id={contentId}
|
|
616
614
|
{...stylex.props(
|
|
617
615
|
styles.groupContent,
|
|
618
616
|
isExpanded && styles.groupContentExpanded,
|
|
@@ -125,7 +125,7 @@ export const docs = {
|
|
|
125
125
|
theming: {
|
|
126
126
|
targets: [
|
|
127
127
|
{className: 'astryx-checkbox-input', visualProps: ['size']},
|
|
128
|
-
{className: 'astryx-checkbox'},
|
|
128
|
+
{className: 'astryx-checkbox', visualProps: ['size'], states: ['checked', 'disabled']},
|
|
129
129
|
],
|
|
130
130
|
},
|
|
131
131
|
usage: {
|
|
@@ -178,7 +178,7 @@ export const docsZh = {
|
|
|
178
178
|
{name: 'isLoading', type: 'boolean', description: '复选框是否处于加载状态。显示旋转器并阻止交互。', default: 'false'},
|
|
179
179
|
{name: 'isDisabled', type: 'boolean', description: '复选框是否禁用。', default: 'false'},
|
|
180
180
|
{name: 'htmlName', type: 'string', description: '底层复选框输入的 HTML name 属性,用于表单提交(勾选时提交 "on")。'},
|
|
181
|
-
{name: 'disabledMessage', type: 'string', description: 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip
|
|
181
|
+
{name: 'disabledMessage', type: 'string', description: 'Explains why the checkbox is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the checkbox focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled CheckboxInput in Tooltip: disabled controls swallow the hover events an external Tooltip needs.'},
|
|
182
182
|
{name: 'isReadOnly', type: 'boolean', description: '复选框是否为只读。以完整不透明度显示当前状态但阻止交互。与 isDisabled 不同,只读复选框不会变暗。', default: 'false'},
|
|
183
183
|
{name: 'isOptional', type: 'boolean', description: '字段是否可选。与 isRequired 互斥。', default: 'false'},
|
|
184
184
|
{name: 'isRequired', type: 'boolean', description: '复选框是否必填。与 isOptional 互斥。', default: 'false'},
|
|
@@ -200,7 +200,7 @@ export const docsZh = {
|
|
|
200
200
|
'size',
|
|
201
201
|
],
|
|
202
202
|
},
|
|
203
|
-
{className: 'astryx-checkbox'},
|
|
203
|
+
{className: 'astryx-checkbox', visualProps: ['size'], states: ['checked', 'disabled']},
|
|
204
204
|
],
|
|
205
205
|
},
|
|
206
206
|
};
|
|
@@ -167,6 +167,59 @@ describe('CheckboxInput', () => {
|
|
|
167
167
|
expect(handleChange).not.toHaveBeenCalled();
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
it('forwards data-testid to the input', () => {
|
|
171
|
+
render(
|
|
172
|
+
<CheckboxInput
|
|
173
|
+
label="Accept terms"
|
|
174
|
+
value={false}
|
|
175
|
+
onChange={() => {}}
|
|
176
|
+
data-testid="accept-terms-checkbox"
|
|
177
|
+
/>,
|
|
178
|
+
);
|
|
179
|
+
expect(screen.getByTestId('accept-terms-checkbox')).toBe(
|
|
180
|
+
screen.getByRole('checkbox'),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('forwards arbitrary data-* attributes to the input', () => {
|
|
185
|
+
render(
|
|
186
|
+
<CheckboxInput
|
|
187
|
+
label="Accept terms"
|
|
188
|
+
value={false}
|
|
189
|
+
onChange={() => {}}
|
|
190
|
+
data-tracking-id="checkbox-42"
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
expect(screen.getByRole('checkbox')).toHaveAttribute(
|
|
194
|
+
'data-tracking-id',
|
|
195
|
+
'checkbox-42',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not let rest props override checked, disabled, or type', () => {
|
|
200
|
+
// Not part of CheckboxInputProps; spread (rather than named attributes)
|
|
201
|
+
// to sidestep JSX excess-property checks the same way a real caller's
|
|
202
|
+
// spread rest-props object would arrive untyped at runtime.
|
|
203
|
+
const foreignAttrs: Record<string, unknown> = {
|
|
204
|
+
checked: false,
|
|
205
|
+
disabled: false,
|
|
206
|
+
type: 'text',
|
|
207
|
+
};
|
|
208
|
+
render(
|
|
209
|
+
<CheckboxInput
|
|
210
|
+
{...foreignAttrs}
|
|
211
|
+
label="Accept terms"
|
|
212
|
+
value={true}
|
|
213
|
+
onChange={() => {}}
|
|
214
|
+
isDisabled
|
|
215
|
+
/>,
|
|
216
|
+
);
|
|
217
|
+
const checkbox = screen.getByRole('checkbox');
|
|
218
|
+
expect(checkbox).toBeChecked();
|
|
219
|
+
expect(checkbox).toBeDisabled();
|
|
220
|
+
expect(checkbox).toHaveAttribute('type', 'checkbox');
|
|
221
|
+
});
|
|
222
|
+
|
|
170
223
|
it('forwards ref correctly', () => {
|
|
171
224
|
const ref = vi.fn();
|
|
172
225
|
render(
|
|
@@ -396,7 +449,12 @@ describe('CheckboxInput', () => {
|
|
|
396
449
|
it('submits under htmlName when checked', () => {
|
|
397
450
|
const {container} = render(
|
|
398
451
|
<form>
|
|
399
|
-
<CheckboxInput
|
|
452
|
+
<CheckboxInput
|
|
453
|
+
label="Terms"
|
|
454
|
+
htmlName="terms"
|
|
455
|
+
value={true}
|
|
456
|
+
onChange={() => {}}
|
|
457
|
+
/>
|
|
400
458
|
</form>,
|
|
401
459
|
);
|
|
402
460
|
const data = new FormData(container.querySelector('form')!);
|
|
@@ -416,16 +474,25 @@ describe('CheckboxInput', () => {
|
|
|
416
474
|
/>
|
|
417
475
|
</form>,
|
|
418
476
|
);
|
|
419
|
-
expect([
|
|
477
|
+
expect([
|
|
478
|
+
...new FormData(container.querySelector('form')!).keys(),
|
|
479
|
+
]).toEqual([]);
|
|
420
480
|
});
|
|
421
481
|
|
|
422
482
|
it('submits nothing when unchecked', () => {
|
|
423
483
|
const {container} = render(
|
|
424
484
|
<form>
|
|
425
|
-
<CheckboxInput
|
|
485
|
+
<CheckboxInput
|
|
486
|
+
label="Terms"
|
|
487
|
+
htmlName="terms"
|
|
488
|
+
value={false}
|
|
489
|
+
onChange={() => {}}
|
|
490
|
+
/>
|
|
426
491
|
</form>,
|
|
427
492
|
);
|
|
428
|
-
expect([
|
|
493
|
+
expect([
|
|
494
|
+
...new FormData(container.querySelector('form')!).keys(),
|
|
495
|
+
]).toEqual([]);
|
|
429
496
|
});
|
|
430
497
|
});
|
|
431
498
|
});
|
|
@@ -395,6 +395,7 @@ export function CheckboxInput({
|
|
|
395
395
|
className,
|
|
396
396
|
style,
|
|
397
397
|
ref,
|
|
398
|
+
...rest
|
|
398
399
|
}: CheckboxInputProps) {
|
|
399
400
|
const id = useId();
|
|
400
401
|
const descriptionID = useId();
|
|
@@ -484,6 +485,7 @@ export function CheckboxInput({
|
|
|
484
485
|
)}>
|
|
485
486
|
<div {...stylex.props(styles.checkboxWrapper, wrapperSizeStyles[size])}>
|
|
486
487
|
<input
|
|
488
|
+
{...rest}
|
|
487
489
|
ref={mergeRefs(
|
|
488
490
|
ref,
|
|
489
491
|
indeterminateRef,
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
import {type ReactNode, type MouseEvent, useRef, type Ref} from 'react';
|
|
32
32
|
import * as stylex from '@stylexjs/stylex';
|
|
33
33
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
borderVars,
|
|
36
|
+
colorVars,
|
|
37
|
+
durationVars,
|
|
38
|
+
easeVars,
|
|
39
|
+
} from '../theme/tokens.stylex';
|
|
35
40
|
import type {SizeValue, SpacingStep} from '../utils/types';
|
|
36
41
|
import {mergeProps, mergeRefs} from '../utils';
|
|
37
42
|
import {Card} from '../Card/Card';
|
|
@@ -66,7 +71,6 @@ const styles = stylex.create({
|
|
|
66
71
|
content: '""',
|
|
67
72
|
position: 'absolute',
|
|
68
73
|
inset: 0,
|
|
69
|
-
borderRadius: 'inherit',
|
|
70
74
|
pointerEvents: 'none',
|
|
71
75
|
transitionProperty: 'background-color',
|
|
72
76
|
transitionDuration: durationVars['--duration-fast'],
|
|
@@ -84,6 +88,38 @@ const styles = stylex.create({
|
|
|
84
88
|
},
|
|
85
89
|
},
|
|
86
90
|
},
|
|
91
|
+
// Borderless variants (everything except `default`): drop the transparent
|
|
92
|
+
// 1px border Card applies. The overlay is inset to the padding box — inside
|
|
93
|
+
// that border — so a transparent border strip stays untinted on hover and
|
|
94
|
+
// reads as a faint ring. With no border, the overlay covers the full box
|
|
95
|
+
// edge-to-edge and the ring disappears.
|
|
96
|
+
borderless: {
|
|
97
|
+
borderWidth: 0,
|
|
98
|
+
},
|
|
99
|
+
// Bordered variant (`default`): draw the 1px border *within* the padding by
|
|
100
|
+
// subtracting the border width from every side. Total inset (border +
|
|
101
|
+
// padding) then equals the borderless variants' padding, so content geometry
|
|
102
|
+
// and outer dimensions stay identical across all variants. The border rests
|
|
103
|
+
// at the subtle token and emphasizes on hover.
|
|
104
|
+
bordered: {
|
|
105
|
+
borderColor: colorVars['--color-border'],
|
|
106
|
+
paddingInlineStart: `calc(var(--container-padding-inline-start) - ${borderVars['--border-width']})`,
|
|
107
|
+
paddingInlineEnd: `calc(var(--container-padding-inline-end) - ${borderVars['--border-width']})`,
|
|
108
|
+
paddingBlockStart: `calc(var(--container-padding-block-start) - ${borderVars['--border-width']})`,
|
|
109
|
+
paddingBlockEnd: `calc(var(--container-padding-block-end) - ${borderVars['--border-width']})`,
|
|
110
|
+
transitionProperty: 'border-color',
|
|
111
|
+
transitionDuration: durationVars['--duration-fast'],
|
|
112
|
+
transitionTimingFunction: easeVars['--ease-standard'],
|
|
113
|
+
},
|
|
114
|
+
// Emphasize the bordered variant's border on hover. Guarded by
|
|
115
|
+
// @media (hover: hover) so touch devices don't get a stuck hover state.
|
|
116
|
+
borderedHoverOnPointer: {
|
|
117
|
+
'@media (hover: hover)': {
|
|
118
|
+
':hover': {
|
|
119
|
+
borderColor: colorVars['--color-border-emphasized'],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
87
123
|
disabled: {
|
|
88
124
|
cursor: 'not-allowed',
|
|
89
125
|
opacity: 0.5,
|
|
@@ -248,6 +284,11 @@ export function ClickableCard({
|
|
|
248
284
|
|
|
249
285
|
const isLink = href != null;
|
|
250
286
|
|
|
287
|
+
// Only the `default` variant has a visible border. Card draws a transparent
|
|
288
|
+
// 1px border on every other variant purely to avoid layout jitter; we drop
|
|
289
|
+
// it here so the hover overlay covers the full box with no untinted ring.
|
|
290
|
+
const hasBorder = variant === 'default';
|
|
291
|
+
|
|
251
292
|
return (
|
|
252
293
|
<Card
|
|
253
294
|
ref={mergeRefs(ref, containerRef)}
|
|
@@ -264,8 +305,10 @@ export function ClickableCard({
|
|
|
264
305
|
[
|
|
265
306
|
styles.interactive,
|
|
266
307
|
styles.focusWithin,
|
|
308
|
+
hasBorder ? styles.bordered : styles.borderless,
|
|
267
309
|
!isDisabled && styles.overlay,
|
|
268
310
|
!isDisabled && styles.hoverOnPointer,
|
|
311
|
+
!isDisabled && hasBorder && styles.borderedHoverOnPointer,
|
|
269
312
|
isDisabled && styles.disabled,
|
|
270
313
|
xstyleProp,
|
|
271
314
|
] as unknown as StyleXStyles
|
|
@@ -90,6 +90,11 @@ export const docs = {
|
|
|
90
90
|
type: '(code: string, language: string) => Array<{type: string; start: number; end: number}>',
|
|
91
91
|
description: 'Custom tokenizer override for unsupported languages.',
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'syntaxTheme',
|
|
95
|
+
type: 'SyntaxThemeDefinition',
|
|
96
|
+
description: 'Per-instance syntax theme override. Shorthand for wrapping the block in <SyntaxTheme theme={...}>. Accepts a preset from @astryxdesign/core/theme/syntax or a theme created with defineSyntaxTheme(). Defaults to the nearest SyntaxTheme ancestor or the theme-level syntax colors.',
|
|
97
|
+
},
|
|
93
98
|
{
|
|
94
99
|
name: 'isCollapsible',
|
|
95
100
|
type: 'boolean',
|
|
@@ -135,8 +140,8 @@ export const docs = {
|
|
|
135
140
|
},
|
|
136
141
|
theming: {
|
|
137
142
|
targets: [
|
|
138
|
-
{className: 'astryx-code'},
|
|
139
|
-
{className: 'astryx-codeblock', visualProps: ['size', 'language']},
|
|
143
|
+
{className: 'astryx-code', visualProps: ['color']},
|
|
144
|
+
{className: 'astryx-codeblock', visualProps: ['size', 'language', 'container']},
|
|
140
145
|
],
|
|
141
146
|
},
|
|
142
147
|
usage: {
|
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file CodeBlock.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, CodeBlock component
|
|
6
|
-
* @output Unit tests for CodeBlock (copy button, collapse, scroll region a11y)
|
|
6
|
+
* @output Unit tests for CodeBlock (copy button, collapse, scroll region a11y, syntaxTheme)
|
|
7
7
|
* @position Testing; validates CodeBlock implementation
|
|
8
8
|
*
|
|
9
9
|
* SYNC: When CodeBlock.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
13
|
-
import {render, screen, fireEvent} from '@testing-library/react';
|
|
12
|
+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
13
|
+
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import {CodeBlock} from './CodeBlock';
|
|
15
|
+
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
16
|
+
import {dracula} from '../theme/syntax';
|
|
17
|
+
|
|
18
|
+
function politeRegion(): HTMLElement | null {
|
|
19
|
+
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
// A code sample long enough to exceed the default collapsible threshold (10).
|
|
17
23
|
const LONG_CODE = Array.from(
|
|
@@ -27,6 +33,10 @@ describe('CodeBlock', () => {
|
|
|
27
33
|
});
|
|
28
34
|
});
|
|
29
35
|
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
__resetLiveRegionsForTest();
|
|
38
|
+
});
|
|
39
|
+
|
|
30
40
|
it('renders the code', () => {
|
|
31
41
|
render(<CodeBlock code="const x = 1;" language="javascript" />);
|
|
32
42
|
expect(screen.getByText(/const/)).toBeInTheDocument();
|
|
@@ -53,6 +63,15 @@ describe('CodeBlock', () => {
|
|
|
53
63
|
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('const x = 1;');
|
|
54
64
|
});
|
|
55
65
|
|
|
66
|
+
it('announces "Copied" to a polite live region after copying', async () => {
|
|
67
|
+
render(<CodeBlock code="const x = 1;" language="javascript" />);
|
|
68
|
+
const copyButton = screen.getByRole('button', {name: 'Copy code'});
|
|
69
|
+
fireEvent.click(copyButton);
|
|
70
|
+
await waitFor(() => {
|
|
71
|
+
expect(politeRegion()).toHaveTextContent('Copied');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
56
75
|
it('does NOT collapse the block when the copy button is clicked', () => {
|
|
57
76
|
render(
|
|
58
77
|
<CodeBlock
|
|
@@ -112,4 +131,69 @@ describe('CodeBlock', () => {
|
|
|
112
131
|
fireEvent.click(header);
|
|
113
132
|
expect(header).toHaveAttribute('aria-expanded', 'false');
|
|
114
133
|
});
|
|
134
|
+
|
|
135
|
+
it('links the collapsible header to its code region via aria-controls', () => {
|
|
136
|
+
render(
|
|
137
|
+
<CodeBlock
|
|
138
|
+
code={LONG_CODE}
|
|
139
|
+
language="javascript"
|
|
140
|
+
title="example"
|
|
141
|
+
isCollapsible
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
const header = screen
|
|
145
|
+
.getAllByRole('button')
|
|
146
|
+
.find(el => el.hasAttribute('aria-expanded'))!;
|
|
147
|
+
const controlsId = header.getAttribute('aria-controls');
|
|
148
|
+
// aria-controls must be present and point at the real code region.
|
|
149
|
+
expect(controlsId).toBeTruthy();
|
|
150
|
+
const region = document.getElementById(controlsId as string);
|
|
151
|
+
expect(region).not.toBeNull();
|
|
152
|
+
// The region contains the scrollable code body (role="group").
|
|
153
|
+
expect(region).toContainElement(screen.getByRole('group'));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('keeps aria-controls resolvable when collapsed (region stays mounted)', () => {
|
|
157
|
+
render(
|
|
158
|
+
<CodeBlock
|
|
159
|
+
code={LONG_CODE}
|
|
160
|
+
language="javascript"
|
|
161
|
+
title="example"
|
|
162
|
+
isCollapsible
|
|
163
|
+
/>,
|
|
164
|
+
);
|
|
165
|
+
const header = screen
|
|
166
|
+
.getAllByRole('button')
|
|
167
|
+
.find(el => el.hasAttribute('aria-expanded'))!;
|
|
168
|
+
fireEvent.click(header);
|
|
169
|
+
expect(header).toHaveAttribute('aria-expanded', 'false');
|
|
170
|
+
// The code region uses a CSS grid animation to collapse, so it stays in
|
|
171
|
+
// the DOM — aria-controls stays a valid, resolvable reference (unlike a
|
|
172
|
+
// conditionally-mounted region, which would need a conditional attribute).
|
|
173
|
+
const controlsId = header.getAttribute('aria-controls');
|
|
174
|
+
expect(controlsId).toBeTruthy();
|
|
175
|
+
expect(document.getElementById(controlsId as string)).not.toBeNull();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('applies a per-instance syntax theme via the syntaxTheme prop', () => {
|
|
179
|
+
const {container} = render(
|
|
180
|
+
<CodeBlock
|
|
181
|
+
code="const x = 1;"
|
|
182
|
+
language="javascript"
|
|
183
|
+
syntaxTheme={dracula}
|
|
184
|
+
/>,
|
|
185
|
+
);
|
|
186
|
+
const wrapper = container.querySelector('[data-astryx-syntax-theme]');
|
|
187
|
+
expect(wrapper).not.toBeNull();
|
|
188
|
+
expect(wrapper).toHaveAttribute('data-astryx-syntax-theme', 'dracula');
|
|
189
|
+
expect(wrapper!.querySelector('pre')).not.toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('renders no syntax theme wrapper when syntaxTheme is not set', () => {
|
|
193
|
+
const {container} = render(
|
|
194
|
+
<CodeBlock code="const x = 1;" language="javascript" />,
|
|
195
|
+
);
|
|
196
|
+
expect(container.querySelector('[data-astryx-syntax-theme]')).toBeNull();
|
|
197
|
+
expect(container.firstElementChild?.tagName).toBe('PRE');
|
|
198
|
+
});
|
|
115
199
|
});
|