@astryxdesign/core 0.4.0 → 0.4.1-canary.0ad35c3
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 +39 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +1 -2
- package/dist/AspectRatio/index.d.ts.map +1 -1
- package/dist/AspectRatio/index.js +1 -2
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +1 -2
- package/dist/Badge/index.d.ts.map +1 -1
- package/dist/Badge/index.js +1 -2
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +17 -3
- package/dist/Blockquote/Blockquote.d.ts.map +1 -1
- package/dist/Blockquote/Blockquote.js +1 -2
- package/dist/Blockquote/index.d.ts.map +1 -1
- package/dist/Blockquote/index.js +1 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -9
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +1 -2
- package/dist/Card/index.d.ts.map +1 -1
- package/dist/Card/index.js +1 -2
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +1 -2
- package/dist/Center/index.d.ts.map +1 -1
- package/dist/Center/index.js +1 -2
- package/dist/Chat/ChatMessage.d.ts +7 -0
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.d.ts +13 -1
- package/dist/Chat/ChatMessageBubble.d.ts.map +1 -1
- package/dist/Chat/ChatMessageBubble.js +19 -1
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +1 -2
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +5 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +7 -8
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +23 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +44 -16
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +32 -13
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
- package/dist/Field/PanelSearchInput.d.ts +81 -0
- package/dist/Field/PanelSearchInput.d.ts.map +1 -0
- package/dist/Field/PanelSearchInput.js +151 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +21 -10
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +1 -2
- package/dist/Grid/index.d.ts.map +1 -1
- package/dist/Grid/index.js +1 -2
- package/dist/HoverCard/HoverCard.d.ts +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +5 -13
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -5
- package/dist/Icon/globalIconRegistry.d.ts +5 -4
- package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
- package/dist/Icon/globalIconRegistry.js +5 -4
- package/dist/Indicator/index.d.ts +1 -1
- package/dist/Indicator/index.d.ts.map +1 -1
- package/dist/Indicator/types.d.ts +10 -0
- package/dist/Indicator/types.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +7 -2
- package/dist/Layer/layerHost.d.ts +24 -0
- package/dist/Layer/layerHost.d.ts.map +1 -0
- package/dist/Layer/layerHost.js +79 -0
- package/dist/Layer/useLayer.d.ts +14 -6
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +155 -29
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +24 -50
- package/dist/MultiSelector/MultiSelector.d.ts +8 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +134 -102
- package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +11 -5
- package/dist/Popover/usePopover.d.ts +15 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +14 -2
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -3
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +1 -2
- package/dist/Section/index.d.ts.map +1 -1
- package/dist/Section/index.js +1 -2
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +135 -95
- package/dist/SideNav/SideNav.d.ts +7 -9
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +32 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +39 -15
- package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
- package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseContext.js +16 -2
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +70 -24
- package/dist/SideNav/SideNavItem.d.ts +7 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +114 -75
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +7 -16
- package/dist/SideNav/index.d.ts +1 -1
- package/dist/SideNav/index.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -2
- package/dist/Skeleton/index.d.ts.map +1 -1
- package/dist/Skeleton/index.js +1 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +2 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +3 -3
- package/dist/Table/TableRow.d.ts +1 -1
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +4 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +7 -2
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +14 -3
- package/dist/Timestamp/formatInstant.d.ts +11 -1
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +3 -1
- package/dist/TopNav/TopNavMegaMenu.js +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +2 -2
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
- package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
- package/dist/VisuallyHidden/index.d.ts.map +1 -1
- package/dist/VisuallyHidden/index.js +1 -2
- package/dist/astryx.css +23 -24
- package/dist/astryx.umd.js +47 -47
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +16 -2
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +17 -4
- package/dist/theme/defineTheme.d.ts +2 -2
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +2 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.stylex.d.ts +12 -0
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +23 -0
- package/dist/utils/focusOutline.stylex.d.ts +34 -19
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +46 -33
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -1
- package/dist/utils/interactionModality.d.ts +34 -0
- package/dist/utils/interactionModality.d.ts.map +1 -0
- package/dist/utils/interactionModality.js +70 -0
- package/locales/en.json +44 -0
- package/locales/pseudo.json +33 -0
- package/package.json +3 -3
- package/src/AppShell/AppShell.doc.mjs +3 -0
- package/src/AspectRatio/AspectRatio.tsx +0 -2
- package/src/AspectRatio/index.ts +0 -2
- package/src/Badge/Badge.tsx +0 -2
- package/src/Badge/index.ts +0 -2
- package/src/Banner/Banner.test.tsx +58 -1
- package/src/Banner/Banner.tsx +24 -5
- package/src/Blockquote/Blockquote.tsx +0 -2
- package/src/Blockquote/index.ts +0 -2
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.tsx +10 -7
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
- package/src/Card/Card.tsx +0 -2
- package/src/Card/index.ts +0 -2
- package/src/Center/Center.tsx +0 -2
- package/src/Center/index.ts +0 -2
- package/src/Chat/Chat.doc.mjs +4 -1
- package/src/Chat/ChatMessage.doc.mjs +3 -3
- package/src/Chat/ChatMessage.tsx +7 -0
- package/src/Chat/ChatMessageBubble.doc.mjs +7 -0
- package/src/Chat/ChatMessageBubble.test.tsx +95 -6
- package/src/Chat/ChatMessageBubble.tsx +25 -0
- package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
- package/src/Code/Code.tsx +0 -2
- package/src/CommandPalette/CommandPalette.test.tsx +54 -0
- package/src/CommandPalette/CommandPalette.tsx +5 -0
- package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
- package/src/ComplexSelector/ComplexSelector.tsx +6 -10
- package/src/DateInput/DateInput.test.tsx +19 -0
- package/src/DateInput/DateInput.tsx +3 -6
- package/src/DateRangeInput/DateRangeInput.tsx +3 -9
- package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
- package/src/DateTimeInput/DateTimeInput.tsx +23 -9
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
- package/src/Field/PanelSearchInput.tsx +271 -0
- package/src/FileInput/FileInput.tsx +21 -8
- package/src/Grid/Grid.tsx +0 -2
- package/src/Grid/index.ts +0 -2
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.test.tsx +283 -55
- package/src/HoverCard/HoverCard.tsx +5 -13
- package/src/HoverCard/useHoverCard.tsx +8 -11
- package/src/Icon/globalIconRegistry.tsx +5 -4
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/Indicator/index.ts +1 -0
- package/src/Indicator/types.ts +11 -0
- package/src/InputGroup/groupStyles.ts +7 -8
- package/src/Layer/layerHost.test.ts +99 -0
- package/src/Layer/layerHost.ts +141 -0
- package/src/Layer/useLayer.doc.mjs +14 -4
- package/src/Layer/useLayer.test.tsx +318 -7
- package/src/Layer/useLayer.tsx +229 -35
- package/src/Lightbox/Lightbox.tsx +2 -11
- package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
- package/src/MultiSelector/MultiSelector.test.tsx +135 -5
- package/src/MultiSelector/MultiSelector.tsx +170 -127
- package/src/NavItem/navItemStyles.stylex.ts +31 -5
- package/src/Popover/Popover.doc.mjs +2 -0
- package/src/Popover/usePopover.tsx +37 -4
- package/src/ProgressBar/ProgressBar.tsx +2 -8
- package/src/Section/Section.tsx +0 -2
- package/src/Section/index.ts +0 -2
- package/src/Selector/Selector.doc.mjs +10 -0
- package/src/Selector/Selector.test.tsx +386 -2
- package/src/Selector/Selector.tsx +182 -122
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +771 -2
- package/src/SideNav/SideNav.tsx +37 -16
- package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
- package/src/SideNav/SideNavCollapseButton.tsx +67 -15
- package/src/SideNav/SideNavCollapseContext.ts +29 -9
- package/src/SideNav/SideNavHeading.tsx +28 -10
- package/src/SideNav/SideNavItem.doc.mjs +13 -0
- package/src/SideNav/SideNavItem.tsx +92 -84
- package/src/SideNav/SideNavSection.tsx +6 -20
- package/src/SideNav/index.ts +2 -0
- package/src/Skeleton/Skeleton.tsx +0 -2
- package/src/Skeleton/index.ts +0 -2
- package/src/Slider/Slider.tsx +2 -11
- package/src/Switch/Switch.tsx +10 -2
- package/src/Table/Table.test.tsx +31 -0
- package/src/Table/TableRow.tsx +6 -0
- package/src/Text/Text.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +125 -30
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/Toolbar/Toolbar.test.tsx +14 -14
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
- package/src/TopNav/TopNavMegaMenu.tsx +4 -1
- package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
- package/src/TopNav/TopNavMenu.test.tsx +41 -0
- package/src/TopNav/TopNavMenu.tsx +8 -2
- package/src/TreeList/TreeList.test.tsx +3 -2
- package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
- package/src/VisuallyHidden/index.ts +0 -2
- package/src/hooks/index.ts +5 -1
- package/src/hooks/useFocusTrap.test.tsx +16 -0
- package/src/hooks/useFocusTrap.ts +20 -2
- package/src/hooks/useInputStatusIcon.tsx +8 -8
- package/src/serverSafeComponents.test.ts +665 -0
- package/src/theme/defineTheme.ts +3 -0
- package/src/theme/generateThemeRules.test.ts +17 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/themingTargets.test.ts +152 -26
- package/src/theme/tokens.stylex.ts +18 -0
- package/src/theme/tokens.test.ts +37 -0
- package/src/utils/focusOutline.stylex.ts +37 -23
- package/src/utils/index.ts +4 -0
- package/src/utils/interactionModality.ts +74 -0
|
@@ -45,6 +45,11 @@ export const docs = {
|
|
|
45
45
|
type: "'first' | 'middle' | 'last'",
|
|
46
46
|
description: 'Position within a multi-bubble group. Controls corner radius reduction on the sender side. Leave unset for standalone bubbles (full radius).',
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: 'width',
|
|
50
|
+
type: 'SizeValue',
|
|
51
|
+
description: 'Width of the bubble (number = pixels, string = used as-is). When set, replaces the default max(80%, 280px) width cap. Combine with variant="ghost" to let custom content (an artifact card, attachments) span the full message column.',
|
|
52
|
+
},
|
|
48
53
|
],
|
|
49
54
|
};
|
|
50
55
|
|
|
@@ -59,6 +64,7 @@ export const docsZh = {
|
|
|
59
64
|
name: '气泡上方渲染的发送者名称,与气泡文本内边距对齐。用于消息中的第一个气泡。',
|
|
60
65
|
metadata: '气泡下方渲染的元数据内容,与气泡文本内边距对齐。用于消息中的最后一个气泡。',
|
|
61
66
|
group: '多气泡组中的位置。控制发送者侧的圆角缩减。',
|
|
67
|
+
width: '气泡宽度(数字 = 像素,字符串 = 按原样使用)。设置后替代默认的 max(80%, 280px) 宽度上限;与 ghost 变体组合可让自定义内容占满消息列宽度。',
|
|
62
68
|
},
|
|
63
69
|
};
|
|
64
70
|
|
|
@@ -74,5 +80,6 @@ export const docsDense = {
|
|
|
74
80
|
displayName: 'sender name above bubble, aligned w/ bubble padding',
|
|
75
81
|
metadata: 'metadata below bubble, aligned w/ bubble padding',
|
|
76
82
|
group: 'position in multi-bubble group; controls corner radius reduction',
|
|
83
|
+
width: 'bubble width (number=px, string=as-is); replaces default max(80%,280px) cap; ghost+width="100%" spans full message column',
|
|
77
84
|
},
|
|
78
85
|
};
|
|
@@ -7,6 +7,16 @@ import {ChatMessageBubble} from './ChatMessageBubble';
|
|
|
7
7
|
import {ChatMessageList} from './ChatMessageList';
|
|
8
8
|
import {ChatMessageMetadata} from './ChatMessageMetadata';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Classes shared between two elements. StyleX emits one atomic class per
|
|
12
|
+
* style declaration, so two elements share a class exactly when they share
|
|
13
|
+
* a declaration — e.g. the same density paddingInline.
|
|
14
|
+
*/
|
|
15
|
+
function sharedClasses(a: Element, b: Element): string[] {
|
|
16
|
+
const bClasses = new Set(Array.from(b.classList));
|
|
17
|
+
return Array.from(a.classList).filter(c => bClasses.has(c));
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
describe('ChatMessageBubble', () => {
|
|
11
21
|
it('renders children', () => {
|
|
12
22
|
render(
|
|
@@ -29,9 +39,7 @@ describe('ChatMessageBubble', () => {
|
|
|
29
39
|
|
|
30
40
|
it('defaults to assistant when no context', () => {
|
|
31
41
|
render(
|
|
32
|
-
<ChatMessageBubble data-testid="bubble">
|
|
33
|
-
Standalone
|
|
34
|
-
</ChatMessageBubble>,
|
|
42
|
+
<ChatMessageBubble data-testid="bubble">Standalone</ChatMessageBubble>,
|
|
35
43
|
);
|
|
36
44
|
const el = screen.getByTestId('bubble');
|
|
37
45
|
expect(el.className).toContain('assistant');
|
|
@@ -41,9 +49,7 @@ describe('ChatMessageBubble', () => {
|
|
|
41
49
|
render(
|
|
42
50
|
<ChatMessageList density="compact">
|
|
43
51
|
<ChatMessage sender="assistant">
|
|
44
|
-
<ChatMessageBubble data-testid="bubble">
|
|
45
|
-
Compact
|
|
46
|
-
</ChatMessageBubble>
|
|
52
|
+
<ChatMessageBubble data-testid="bubble">Compact</ChatMessageBubble>
|
|
47
53
|
</ChatMessage>
|
|
48
54
|
</ChatMessageList>,
|
|
49
55
|
);
|
|
@@ -59,6 +65,89 @@ describe('ChatMessageBubble', () => {
|
|
|
59
65
|
);
|
|
60
66
|
expect(screen.getByTestId('my-bubble')).toBeTruthy();
|
|
61
67
|
});
|
|
68
|
+
|
|
69
|
+
it('ghost variant aligns custom content with the bubble text column (#2574)', () => {
|
|
70
|
+
// Repro from the issue: a raw child renders flush with the message
|
|
71
|
+
// edge — it carries none of the inset the bubble's name slot gets.
|
|
72
|
+
const {container} = render(
|
|
73
|
+
<ChatMessage sender="assistant">
|
|
74
|
+
<ChatMessageBubble name="Navi">Hello</ChatMessageBubble>
|
|
75
|
+
<div data-testid="raw">Artifact card</div>
|
|
76
|
+
<ChatMessageBubble variant="ghost" data-testid="ghost">
|
|
77
|
+
Artifact card
|
|
78
|
+
</ChatMessageBubble>
|
|
79
|
+
</ChatMessage>,
|
|
80
|
+
);
|
|
81
|
+
const nameSlot = container.querySelector('[data-chat-name]')!;
|
|
82
|
+
expect(nameSlot).toBeTruthy();
|
|
83
|
+
|
|
84
|
+
// Unwrapped custom content: no shared inset — the misalignment case.
|
|
85
|
+
expect(sharedClasses(screen.getByTestId('raw'), nameSlot)).toEqual([]);
|
|
86
|
+
|
|
87
|
+
// Ghost-wrapped content: shares the bubble slot's paddingInline
|
|
88
|
+
// declaration, so its text column matches the filled bubble exactly.
|
|
89
|
+
expect(
|
|
90
|
+
sharedClasses(screen.getByTestId('ghost'), nameSlot).length,
|
|
91
|
+
).toBeGreaterThan(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('ghost inset tracks message density', () => {
|
|
95
|
+
const renderAtDensity = (density: 'balanced' | 'spacious') => {
|
|
96
|
+
const {container} = render(
|
|
97
|
+
<ChatMessage sender="assistant" density={density}>
|
|
98
|
+
<ChatMessageBubble name="Navi">Hello</ChatMessageBubble>
|
|
99
|
+
<ChatMessageBubble variant="ghost" data-testid={`ghost-${density}`}>
|
|
100
|
+
Card
|
|
101
|
+
</ChatMessageBubble>
|
|
102
|
+
</ChatMessage>,
|
|
103
|
+
);
|
|
104
|
+
return sharedClasses(
|
|
105
|
+
screen.getByTestId(`ghost-${density}`),
|
|
106
|
+
container.querySelector('[data-chat-name]')!,
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const balancedInset = renderAtDensity('balanced');
|
|
111
|
+
const spaciousInset = renderAtDensity('spacious');
|
|
112
|
+
|
|
113
|
+
// Both densities align with their own bubble's slot padding...
|
|
114
|
+
expect(balancedInset.length).toBeGreaterThan(0);
|
|
115
|
+
expect(spaciousInset.length).toBeGreaterThan(0);
|
|
116
|
+
// ...and spacious uses a wider inset than balanced.
|
|
117
|
+
expect(spaciousInset).not.toEqual(balancedInset);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('width prop replaces the default width cap', () => {
|
|
121
|
+
render(
|
|
122
|
+
<ChatMessage sender="assistant">
|
|
123
|
+
<ChatMessageBubble data-testid="capped">Text</ChatMessageBubble>
|
|
124
|
+
<ChatMessageBubble variant="ghost" width="100%" data-testid="full">
|
|
125
|
+
Card
|
|
126
|
+
</ChatMessageBubble>
|
|
127
|
+
</ChatMessage>,
|
|
128
|
+
);
|
|
129
|
+
const capped = screen.getByTestId('capped');
|
|
130
|
+
const full = screen.getByTestId('full');
|
|
131
|
+
|
|
132
|
+
// Default bubbles keep the cap; a width bubble replaces it with none.
|
|
133
|
+
expect(getComputedStyle(capped).maxWidth).toMatch(/^max\(80%,\s*280px\)$/);
|
|
134
|
+
expect(getComputedStyle(full).maxWidth).toBe('none');
|
|
135
|
+
// The dynamic width value is set on the element (string passes through).
|
|
136
|
+
expect(full.getAttribute('style')).toContain('100%');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('numeric width is treated as pixels', () => {
|
|
140
|
+
render(
|
|
141
|
+
<ChatMessage sender="assistant">
|
|
142
|
+
<ChatMessageBubble width={420} data-testid="fixed">
|
|
143
|
+
Artifact
|
|
144
|
+
</ChatMessageBubble>
|
|
145
|
+
</ChatMessage>,
|
|
146
|
+
);
|
|
147
|
+
expect(screen.getByTestId('fixed').getAttribute('style')).toContain(
|
|
148
|
+
'420px',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
62
151
|
});
|
|
63
152
|
|
|
64
153
|
describe('ChatMessageMetadata', () => {
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* - If you use bubbles on one side (e.g. assistant), use them consistently
|
|
16
16
|
* for all messages on that side. Use `ghost` variant for content that
|
|
17
17
|
* needs alignment without a visual boundary.
|
|
18
|
+
* - For custom content (cards, attachments, citations) that should span
|
|
19
|
+
* the full message column instead of the default width cap, combine
|
|
20
|
+
* `ghost` with `width="100%"` (#2574).
|
|
18
21
|
* - Put `name` on the first bubble in a message, `metadata` on the last.
|
|
19
22
|
* - For unbubbled messages, use ChatMessage's `name` and `metadata`
|
|
20
23
|
* props instead.
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
} from '../theme/tokens.stylex';
|
|
37
40
|
import {useChatMessageContext} from './ChatContext';
|
|
38
41
|
import {mergeProps} from '../utils';
|
|
42
|
+
import type {SizeValue} from '../utils/types';
|
|
39
43
|
import type {BaseProps} from '../BaseProps';
|
|
40
44
|
import {themeProps} from '../utils/themeProps';
|
|
41
45
|
|
|
@@ -83,6 +87,15 @@ export interface ChatMessageBubbleProps extends BaseProps<HTMLDivElement> {
|
|
|
83
87
|
* Leave unset for standalone bubbles (full radius).
|
|
84
88
|
*/
|
|
85
89
|
group?: 'first' | 'middle' | 'last';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Width of the bubble.
|
|
93
|
+
* Numbers are treated as pixels, strings are used as-is (e.g. "100%").
|
|
94
|
+
* When set, replaces the default `max(80%, 280px)` width cap; leave unset
|
|
95
|
+
* to keep the cap. Combine with `variant="ghost"` to let custom content
|
|
96
|
+
* (an artifact card, attachments) span the full message column.
|
|
97
|
+
*/
|
|
98
|
+
width?: SizeValue;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
// =============================================================================
|
|
@@ -189,6 +202,16 @@ const styles = stylex.create({
|
|
|
189
202
|
},
|
|
190
203
|
});
|
|
191
204
|
|
|
205
|
+
// Dynamic styles for sizing props
|
|
206
|
+
const dynamicStyles = stylex.create({
|
|
207
|
+
sizing: (width: SizeValue) => ({
|
|
208
|
+
width,
|
|
209
|
+
// An explicit width replaces the default cap — a full-column or
|
|
210
|
+
// fixed-width bubble shouldn't also be clamped by max(80%, 280px).
|
|
211
|
+
maxWidth: 'none',
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
|
|
192
215
|
// =============================================================================
|
|
193
216
|
// Component
|
|
194
217
|
// =============================================================================
|
|
@@ -216,6 +239,7 @@ export function ChatMessageBubble({
|
|
|
216
239
|
name,
|
|
217
240
|
metadata,
|
|
218
241
|
group,
|
|
242
|
+
width,
|
|
219
243
|
xstyle,
|
|
220
244
|
className,
|
|
221
245
|
style: styleProp,
|
|
@@ -292,6 +316,7 @@ export function ChatMessageBubble({
|
|
|
292
316
|
paddingStyle,
|
|
293
317
|
variant === 'ghost' && styles.paddingBlockNone,
|
|
294
318
|
groupStyle,
|
|
319
|
+
width != null && dynamicStyles.sizing(width),
|
|
295
320
|
xstyle,
|
|
296
321
|
),
|
|
297
322
|
className,
|
|
@@ -17,6 +17,7 @@ import {Theme} from '../theme/Theme';
|
|
|
17
17
|
import {defineTheme} from '../theme/defineTheme';
|
|
18
18
|
import {getForcedColorsRules} from '../__tests__/forcedColors';
|
|
19
19
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
20
|
+
import {FOCUS_OUTLINE_PARTS} from '../utils/focusOutline.stylex';
|
|
20
21
|
|
|
21
22
|
afterEach(() => {
|
|
22
23
|
__resetLiveRegionsForTest();
|
|
@@ -686,7 +687,9 @@ describe('focus ring ownership (WCAG 2.4.7)', () => {
|
|
|
686
687
|
<CheckboxInput label="Accept" value={false} onChange={() => {}} />,
|
|
687
688
|
);
|
|
688
689
|
focusInput(container);
|
|
689
|
-
expect(indicatorOf(container).style.outlineStyle).toBe(
|
|
690
|
+
expect(indicatorOf(container).style.outlineStyle).toBe(
|
|
691
|
+
FOCUS_OUTLINE_PARTS.outlineStyle,
|
|
692
|
+
);
|
|
690
693
|
});
|
|
691
694
|
|
|
692
695
|
it('paints it on a replacement that forwards nothing', () => {
|
|
@@ -701,7 +704,7 @@ describe('focus ring ownership (WCAG 2.4.7)', () => {
|
|
|
701
704
|
expect(replaced.className).toBe('');
|
|
702
705
|
// ...and it still gets a ring, because the owner drew it.
|
|
703
706
|
focusInput(container);
|
|
704
|
-
expect(replaced.style.outlineStyle).toBe(
|
|
707
|
+
expect(replaced.style.outlineStyle).toBe(FOCUS_OUTLINE_PARTS.outlineStyle);
|
|
705
708
|
});
|
|
706
709
|
|
|
707
710
|
it('clears the ring on blur', () => {
|
|
@@ -709,7 +712,9 @@ describe('focus ring ownership (WCAG 2.4.7)', () => {
|
|
|
709
712
|
<CheckboxInput label="Accept" value={false} onChange={() => {}} />,
|
|
710
713
|
);
|
|
711
714
|
const input = focusInput(container);
|
|
712
|
-
expect(indicatorOf(container).style.outlineStyle).toBe(
|
|
715
|
+
expect(indicatorOf(container).style.outlineStyle).toBe(
|
|
716
|
+
FOCUS_OUTLINE_PARTS.outlineStyle,
|
|
717
|
+
);
|
|
713
718
|
fireEvent.blur(input);
|
|
714
719
|
expect(indicatorOf(container).style.outlineStyle).toBe('');
|
|
715
720
|
});
|
package/src/Code/Code.tsx
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
11
11
|
import {render, screen, waitFor, fireEvent, act} from '@testing-library/react';
|
|
12
|
+
import {useState} from 'react';
|
|
12
13
|
import {CommandPalette} from './CommandPalette';
|
|
13
14
|
import {createStaticSource} from '@astryxdesign/core/Typeahead';
|
|
14
15
|
import type {SearchSource, SearchableItem} from '@astryxdesign/core/Typeahead';
|
|
@@ -274,6 +275,59 @@ describe('CommandPalette', () => {
|
|
|
274
275
|
);
|
|
275
276
|
});
|
|
276
277
|
|
|
278
|
+
it('discards a search response that resolves after the palette closed', async () => {
|
|
279
|
+
// A source whose search resolves only when released, and no cancel()
|
|
280
|
+
// implementation — closing the palette must invalidate the request itself.
|
|
281
|
+
const resolvers: ((items: SearchableItem[]) => void)[] = [];
|
|
282
|
+
const source: SearchSource = {
|
|
283
|
+
bootstrap: () => [],
|
|
284
|
+
async search(): Promise<SearchableItem[]> {
|
|
285
|
+
return new Promise<SearchableItem[]>(resolve => {
|
|
286
|
+
resolvers.push(resolve);
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
function Harness() {
|
|
292
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
293
|
+
return (
|
|
294
|
+
<CommandPalette
|
|
295
|
+
isOpen={isOpen}
|
|
296
|
+
onOpenChange={setIsOpen}
|
|
297
|
+
searchSource={source}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
render(<Harness />);
|
|
302
|
+
|
|
303
|
+
const input = screen.getByRole('combobox');
|
|
304
|
+
fireEvent.change(input, {target: {value: 'a'}});
|
|
305
|
+
await waitFor(() => expect(resolvers).toHaveLength(1));
|
|
306
|
+
|
|
307
|
+
// Close while the search is still in flight.
|
|
308
|
+
const dialog = screen.getByRole('dialog');
|
|
309
|
+
const escapeEvent = new KeyboardEvent('keydown', {
|
|
310
|
+
key: 'Escape',
|
|
311
|
+
bubbles: true,
|
|
312
|
+
cancelable: true,
|
|
313
|
+
});
|
|
314
|
+
dialog.dispatchEvent(escapeEvent);
|
|
315
|
+
await waitFor(() =>
|
|
316
|
+
expect(screen.getByRole('dialog', {hidden: true})).not.toHaveAttribute(
|
|
317
|
+
'open',
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// The stale response arrives after close — it must not re-commit the
|
|
322
|
+
// abandoned query/results into the closed palette.
|
|
323
|
+
await act(async () => {
|
|
324
|
+
resolvers[0]([{id: 'stale', label: 'Stale item'}]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(screen.getByRole('combobox', {hidden: true})).toHaveValue('');
|
|
328
|
+
expect(screen.queryByText('Stale item')).not.toBeInTheDocument();
|
|
329
|
+
});
|
|
330
|
+
|
|
277
331
|
it('does not move the highlight while typing in the search input', async () => {
|
|
278
332
|
// The palette's input already filters; letting the shared combobox
|
|
279
333
|
// typeahead also chase prefixes would drag the highlight — and Enter —
|
|
@@ -319,6 +319,11 @@ export function CommandPalette<T extends SearchableItem = SearchableItem>({
|
|
|
319
319
|
);
|
|
320
320
|
|
|
321
321
|
const handleClose = useCallback(() => {
|
|
322
|
+
// Invalidate any in-flight search. Most sources don't implement cancel(),
|
|
323
|
+
// and a response that resolves after close would still pass runSearch's
|
|
324
|
+
// version check and re-commit the stale query/results into the closed
|
|
325
|
+
// palette (visible as a ghost query on reopen while bootstrap is pending).
|
|
326
|
+
searchVersionRef.current++;
|
|
322
327
|
// Reset both committed and optimistic search on close
|
|
323
328
|
setSearch('');
|
|
324
329
|
setSearchResults([]);
|
|
@@ -177,7 +177,7 @@ describe('ComplexSelector', () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
describe('ComplexSelector popup theme target', () => {
|
|
180
|
-
it('puts astryx-complex-selector-popup on the
|
|
180
|
+
it('puts astryx-complex-selector-popup on the surface that paints, not the content box', async () => {
|
|
181
181
|
const user = userEvent.setup();
|
|
182
182
|
render(
|
|
183
183
|
<ComplexSelector label="Fruit blend" value="Apple" triggerLabel="Apple">
|
|
@@ -186,18 +186,27 @@ describe('ComplexSelector popup theme target', () => {
|
|
|
186
186
|
);
|
|
187
187
|
await user.click(screen.getByRole('button', {name: 'Fruit blend'}));
|
|
188
188
|
|
|
189
|
-
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
190
189
|
const popup = document.querySelector(
|
|
191
190
|
'.astryx-complex-selector-popup',
|
|
192
191
|
) as HTMLElement;
|
|
193
192
|
expect(popup).not.toBeNull();
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
|
|
194
|
+
// The surface is the element usePopover renders: it carries the dialog
|
|
195
|
+
// role and the shared surface class, and the component's content box —
|
|
196
|
+
// the one with the padding and the scroll — sits INSIDE it. A target on
|
|
197
|
+
// that inner box cannot paint the popup's background or radius, which is
|
|
198
|
+
// what a theme reaches for this class to do.
|
|
199
|
+
expect(popup).toHaveAttribute('role', 'dialog');
|
|
200
|
+
expect(popup).toHaveClass('astryx-popover-surface');
|
|
201
|
+
expect(popup.querySelector('[id]')).not.toBeNull();
|
|
198
202
|
expect(popup).toContainElement(
|
|
199
203
|
screen.getByRole('button', {name: 'Done', ...h}),
|
|
200
204
|
);
|
|
205
|
+
|
|
206
|
+
// And it is not the bare positioning layer either.
|
|
207
|
+
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
208
|
+
expect(popup).not.toBe(layer);
|
|
209
|
+
expect(layer.contains(popup)).toBe(true);
|
|
201
210
|
});
|
|
202
211
|
|
|
203
212
|
it('keeps the target when the consumer also passes contentXstyle', async () => {
|
|
@@ -292,6 +292,11 @@ export function ComplexSelector<Value>({
|
|
|
292
292
|
dialogLabel: label,
|
|
293
293
|
hasCloseButton: false,
|
|
294
294
|
hasAutoFocus: true,
|
|
295
|
+
// The popup's theme target belongs on the SURFACE — the element painting
|
|
296
|
+
// background, radius and elevation — which `usePopover` owns. Rendered on
|
|
297
|
+
// the content box below it, a theme's background or radius rule paints the
|
|
298
|
+
// wrong box.
|
|
299
|
+
surfaceTarget: 'complex-selector-popup',
|
|
295
300
|
onHide: () => {
|
|
296
301
|
document.getElementById(triggerId)?.focus();
|
|
297
302
|
},
|
|
@@ -313,16 +318,7 @@ export function ComplexSelector<Value>({
|
|
|
313
318
|
const triggerContent = triggerLabel ?? placeholder;
|
|
314
319
|
|
|
315
320
|
const content = (
|
|
316
|
-
|
|
317
|
-
// bare positioning box (useLayer zeroes its borders, padding and
|
|
318
|
-
// background), so this content box is the surface a theme has to reach to
|
|
319
|
-
// paint the popup.
|
|
320
|
-
<div
|
|
321
|
-
id={contentId}
|
|
322
|
-
{...mergeProps(
|
|
323
|
-
themeProps('complex-selector-popup'),
|
|
324
|
-
stylex.props(styles.content, contentXstyle),
|
|
325
|
-
)}>
|
|
321
|
+
<div id={contentId} {...stylex.props(styles.content, contentXstyle)}>
|
|
326
322
|
{children(optimisticValue, commitValue, popover.hide, {
|
|
327
323
|
isOpen: popover.isOpen,
|
|
328
324
|
isBusy,
|
|
@@ -25,6 +25,7 @@ import {InputGroup} from '../InputGroup';
|
|
|
25
25
|
import {InputGroupText} from '../InputGroup/InputGroupText';
|
|
26
26
|
import {defineTheme} from '../theme/defineTheme';
|
|
27
27
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
28
|
+
import {InternationalizationProvider} from '../i18n';
|
|
28
29
|
|
|
29
30
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
30
31
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -170,6 +171,24 @@ describe('DateInput', () => {
|
|
|
170
171
|
expect(screen.queryByText('Invalid date')).not.toBeInTheDocument();
|
|
171
172
|
});
|
|
172
173
|
|
|
174
|
+
it('resolves the invalid-date announcement from the i18n catalog', () => {
|
|
175
|
+
const {container} = render(
|
|
176
|
+
<InternationalizationProvider
|
|
177
|
+
locale="en"
|
|
178
|
+
overrides={{en: {'@astryx.dateInput.invalidDate': 'Ungültiges Datum'}}}>
|
|
179
|
+
<DateInput label="Date" onChange={() => {}} />
|
|
180
|
+
</InternationalizationProvider>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
fireEvent.change(screen.getByRole('combobox'), {
|
|
184
|
+
target: {value: '13/45/2024'},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(within(container).getByRole('alert')).toHaveTextContent(
|
|
188
|
+
'Ungültiges Datum',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
173
192
|
it('reverts to previous value on blur when input is invalid', async () => {
|
|
174
193
|
const onChange = vi.fn();
|
|
175
194
|
render(<DateInput label="Date" value="2026-01-25" onChange={onChange} />);
|
|
@@ -80,11 +80,6 @@ const styles = stylex.create({
|
|
|
80
80
|
backgroundColor: 'transparent',
|
|
81
81
|
cursor: 'pointer',
|
|
82
82
|
borderRadius: radiusVars['--radius-element'],
|
|
83
|
-
outline: {
|
|
84
|
-
default: 'none',
|
|
85
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
86
|
-
},
|
|
87
|
-
outlineOffset: 1,
|
|
88
83
|
},
|
|
89
84
|
iconButtonDisabled: {
|
|
90
85
|
cursor: 'not-allowed',
|
|
@@ -163,6 +158,7 @@ import {mergeProps, mergeRefs, isFocusDetached} from '../utils';
|
|
|
163
158
|
import type {BaseProps} from '../BaseProps';
|
|
164
159
|
import type {SizeValue} from '../utils/types';
|
|
165
160
|
import {themeProps} from '../utils/themeProps';
|
|
161
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
166
162
|
import {stableClassName} from '../naming';
|
|
167
163
|
import {useTranslator} from '../i18n';
|
|
168
164
|
|
|
@@ -694,6 +690,7 @@ export function DateInput({
|
|
|
694
690
|
: t('@astryx.dateInput.openCalendar')
|
|
695
691
|
}
|
|
696
692
|
{...stylex.props(
|
|
693
|
+
focusOutlineStyles.focusVisible,
|
|
697
694
|
styles.iconButton,
|
|
698
695
|
isEffectivelyDisabled && styles.iconButtonDisabled,
|
|
699
696
|
)}>
|
|
@@ -754,7 +751,7 @@ export function DateInput({
|
|
|
754
751
|
user would get no feedback that their entry was rejected (WCAG 3.3.1).
|
|
755
752
|
*/}
|
|
756
753
|
<VisuallyHidden as="div" role="alert" aria-live="assertive">
|
|
757
|
-
{!isInputValid ? '
|
|
754
|
+
{!isInputValid ? t('@astryx.dateInput.invalidDate') : ''}
|
|
758
755
|
</VisuallyHidden>
|
|
759
756
|
{hasClear && value !== undefined && !isEffectivelyDisabled && (
|
|
760
757
|
<InputClearButton
|
|
@@ -61,6 +61,7 @@ import type {SizeValue} from '../utils/types';
|
|
|
61
61
|
import {useSize} from '../SizeContext/SizeContext';
|
|
62
62
|
import {useInputStatusIcon} from '../hooks/useInputStatusIcon';
|
|
63
63
|
import {themeProps} from '../utils/themeProps';
|
|
64
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
64
65
|
import {stableClassName} from '../naming';
|
|
65
66
|
import {useTranslator} from '../i18n';
|
|
66
67
|
|
|
@@ -118,11 +119,6 @@ const styles = stylex.create({
|
|
|
118
119
|
backgroundColor: 'transparent',
|
|
119
120
|
cursor: 'pointer',
|
|
120
121
|
borderRadius: radiusVars['--radius-element'],
|
|
121
|
-
outline: {
|
|
122
|
-
default: 'none',
|
|
123
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
124
|
-
},
|
|
125
|
-
outlineOffset: 1,
|
|
126
122
|
},
|
|
127
123
|
iconButtonDisabled: {
|
|
128
124
|
cursor: 'not-allowed',
|
|
@@ -160,10 +156,6 @@ const styles = stylex.create({
|
|
|
160
156
|
color: colorVars['--color-text-primary'],
|
|
161
157
|
cursor: 'pointer',
|
|
162
158
|
textAlign: 'start',
|
|
163
|
-
outline: {
|
|
164
|
-
default: 'none',
|
|
165
|
-
':focus-visible': `${borderVars['--border-width']} solid ${colorVars['--color-accent']}`,
|
|
166
|
-
},
|
|
167
159
|
},
|
|
168
160
|
presetButtonActive: {
|
|
169
161
|
backgroundColor: colorVars['--color-accent-muted'],
|
|
@@ -592,6 +584,7 @@ export function DateRangeInput({
|
|
|
592
584
|
}
|
|
593
585
|
tabIndex={-1}
|
|
594
586
|
{...stylex.props(
|
|
587
|
+
focusOutlineStyles.focusVisible,
|
|
595
588
|
styles.iconButton,
|
|
596
589
|
isEffectivelyDisabled && styles.iconButtonDisabled,
|
|
597
590
|
)}>
|
|
@@ -661,6 +654,7 @@ export function DateRangeInput({
|
|
|
661
654
|
aria-current={isActive ? 'true' : undefined}
|
|
662
655
|
onClick={() => handlePresetClick(preset)}
|
|
663
656
|
{...stylex.props(
|
|
657
|
+
focusOutlineStyles.focusVisible,
|
|
664
658
|
styles.presetButton,
|
|
665
659
|
isActive && styles.presetButtonActive,
|
|
666
660
|
)}>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* SYNC: When DateTimeInput.tsx changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
12
|
+
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
13
13
|
import {render, screen, fireEvent, waitFor} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
15
|
import {getButton, queryButton} from '../__tests__/fastRoleQueries';
|
|
@@ -17,6 +17,16 @@ import {DateTimeInput} from './DateTimeInput';
|
|
|
17
17
|
import type {ISODateTimeString} from './DateTimeInput';
|
|
18
18
|
import {defineTheme} from '../theme/defineTheme';
|
|
19
19
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
20
|
+
import {InternationalizationProvider} from '../i18n';
|
|
21
|
+
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
22
|
+
|
|
23
|
+
function politeRegion(): HTMLElement | null {
|
|
24
|
+
return document.querySelector('[data-astryx-live-region="polite"]');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
__resetLiveRegionsForTest();
|
|
29
|
+
});
|
|
20
30
|
|
|
21
31
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
22
32
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -626,6 +636,35 @@ describe('DateTimeInput', () => {
|
|
|
626
636
|
|
|
627
637
|
expect(screen.getByText('Invalid time')).toBeInTheDocument();
|
|
628
638
|
});
|
|
639
|
+
|
|
640
|
+
it('resolves the invalid date and time announcements from the i18n catalog', () => {
|
|
641
|
+
render(
|
|
642
|
+
<InternationalizationProvider
|
|
643
|
+
locale="en"
|
|
644
|
+
overrides={{
|
|
645
|
+
en: {
|
|
646
|
+
'@astryx.dateInput.invalidDate': 'Ungültiges Datum',
|
|
647
|
+
'@astryx.timeInput.invalidTime': 'Ungültige Zeit',
|
|
648
|
+
},
|
|
649
|
+
}}>
|
|
650
|
+
<DateTimeInput
|
|
651
|
+
label="Meeting"
|
|
652
|
+
value={'2026-03-15T10:00' as ISODateTimeString}
|
|
653
|
+
onChange={() => {}}
|
|
654
|
+
/>
|
|
655
|
+
</InternationalizationProvider>,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
fireEvent.change(screen.getByRole('combobox'), {
|
|
659
|
+
target: {value: '13/45/2024'},
|
|
660
|
+
});
|
|
661
|
+
expect(screen.getByText('Ungültiges Datum')).toBeInTheDocument();
|
|
662
|
+
|
|
663
|
+
fireEvent.change(screen.getByLabelText('Meeting time'), {
|
|
664
|
+
target: {value: '99:99 zz'},
|
|
665
|
+
});
|
|
666
|
+
expect(screen.getByText('Ungültige Zeit')).toBeInTheDocument();
|
|
667
|
+
});
|
|
629
668
|
});
|
|
630
669
|
describe('disabledMessage', () => {
|
|
631
670
|
// jsdom does not implement the Popover API used by the tooltip, so mock
|
|
@@ -819,6 +858,45 @@ describe('DateTimeInput', () => {
|
|
|
819
858
|
expect(onChange).toHaveBeenCalledTimes(1);
|
|
820
859
|
expect(onChange.mock.calls[0][0]).toContain('14:31');
|
|
821
860
|
});
|
|
861
|
+
|
|
862
|
+
// Arrow-key stepping mutates a plain textbox programmatically, and screen
|
|
863
|
+
// readers do not announce programmatic textbox changes — the new value
|
|
864
|
+
// must be announced through the polite live region (WCAG 4.1.2).
|
|
865
|
+
it('politely announces the new time after ArrowUp stepping', async () => {
|
|
866
|
+
render(
|
|
867
|
+
<DateTimeInput
|
|
868
|
+
label="Meeting"
|
|
869
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
870
|
+
onChange={() => {}}
|
|
871
|
+
/>,
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
875
|
+
key: 'ArrowUp',
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
await waitFor(() => {
|
|
879
|
+
expect(politeRegion()).toHaveTextContent('2:31 PM');
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('politely announces the new time after ArrowDown stepping', async () => {
|
|
884
|
+
render(
|
|
885
|
+
<DateTimeInput
|
|
886
|
+
label="Meeting"
|
|
887
|
+
value={'2026-03-15T14:30' as ISODateTimeString}
|
|
888
|
+
onChange={() => {}}
|
|
889
|
+
/>,
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
fireEvent.keyDown(screen.getByLabelText('Meeting time'), {
|
|
893
|
+
key: 'ArrowDown',
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
await waitFor(() => {
|
|
897
|
+
expect(politeRegion()).toHaveTextContent('2:29 PM');
|
|
898
|
+
});
|
|
899
|
+
});
|
|
822
900
|
});
|
|
823
901
|
|
|
824
902
|
describe('weekStartsOn', () => {
|