@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file color.ts
|
|
5
|
+
* @input CSS color strings (hex, rgb()/rgba(), a small set of named colors)
|
|
6
|
+
* @output Shared color parsing/formatting primitives used across the design system
|
|
7
|
+
* @position Package utility; backs theme token resolution, HCT color math, and chart rendering
|
|
8
|
+
*
|
|
9
|
+
* A single home for the color parsers that were previously duplicated across
|
|
10
|
+
* the theme layer, charts, and lab. Consumers get one well-tested definition of
|
|
11
|
+
* "parse this color" rather than several subtly different regexes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A color decomposed into 0-255 RGB channels and a 0-1 alpha. */
|
|
15
|
+
|
|
16
|
+
/** The named colors the design system relies on in token expressions. */
|
|
17
|
+
const NAMED_COLORS = {
|
|
18
|
+
transparent: {
|
|
19
|
+
r: 0,
|
|
20
|
+
g: 0,
|
|
21
|
+
b: 0,
|
|
22
|
+
a: 0
|
|
23
|
+
},
|
|
24
|
+
black: {
|
|
25
|
+
r: 0,
|
|
26
|
+
g: 0,
|
|
27
|
+
b: 0,
|
|
28
|
+
a: 1
|
|
29
|
+
},
|
|
30
|
+
white: {
|
|
31
|
+
r: 255,
|
|
32
|
+
g: 255,
|
|
33
|
+
b: 255,
|
|
34
|
+
a: 1
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
38
|
+
|
|
39
|
+
/** Expand a shorthand hex body (`rgb`/`rgba`) to its full form (`rrggbb`/`rrggbbaa`). */
|
|
40
|
+
function expandShorthand(body) {
|
|
41
|
+
return body.split('').map(c => c + c).join('');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse a hex color into {@link RGBA}. Accepts `#rgb`, `#rgba`, `#rrggbb`, and
|
|
46
|
+
* `#rrggbbaa`, with or without the leading `#`. Returns null for anything else.
|
|
47
|
+
*/
|
|
48
|
+
export function parseHex(hex) {
|
|
49
|
+
if (typeof hex !== 'string') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const body = hex.trim().replace(/^#/, '');
|
|
53
|
+
const normalized = body.length === 3 || body.length === 4 ? expandShorthand(body) : body;
|
|
54
|
+
if (normalized.length !== 6 && normalized.length !== 8 || !/^[0-9a-fA-F]+$/.test(normalized)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
r: parseInt(normalized.slice(0, 2), 16),
|
|
59
|
+
g: parseInt(normalized.slice(2, 4), 16),
|
|
60
|
+
b: parseInt(normalized.slice(4, 6), 16),
|
|
61
|
+
a: normalized.length === 8 ? parseInt(normalized.slice(6, 8), 16) / 255 : 1
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse an `rgb()`/`rgba()` color into {@link RGBA}. Accepts comma- or
|
|
67
|
+
* space-separated channels, an optional `/ alpha`, and percentage channels.
|
|
68
|
+
*/
|
|
69
|
+
export function parseRgb(value) {
|
|
70
|
+
const open = value.indexOf('(');
|
|
71
|
+
if (open === -1 || !value.trim().endsWith(')')) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const body = value.slice(open + 1, value.lastIndexOf(')')).replace(/\//g, ' ');
|
|
75
|
+
const parts = body.split(/[\s,]+/).map(p => p.trim()).filter(Boolean);
|
|
76
|
+
if (parts.length < 3) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const channel = p => {
|
|
80
|
+
const n = p.endsWith('%') ? parseFloat(p) / 100 * 255 : parseFloat(p);
|
|
81
|
+
return clamp(n, 0, 255);
|
|
82
|
+
};
|
|
83
|
+
const r = channel(parts[0]);
|
|
84
|
+
const g = channel(parts[1]);
|
|
85
|
+
const b = channel(parts[2]);
|
|
86
|
+
if ([r, g, b].some(Number.isNaN)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
let a = 1;
|
|
90
|
+
if (parts.length >= 4) {
|
|
91
|
+
const raw = parts[3];
|
|
92
|
+
a = raw.endsWith('%') ? parseFloat(raw) / 100 : parseFloat(raw);
|
|
93
|
+
if (Number.isNaN(a)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
a = clamp(a, 0, 1);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
r,
|
|
100
|
+
g,
|
|
101
|
+
b,
|
|
102
|
+
a
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse a concrete CSS color string into {@link RGBA}. Supports hex,
|
|
108
|
+
* `rgb()`/`rgba()`, and the named colors used in token expressions. Returns
|
|
109
|
+
* null for anything it can't evaluate (e.g. `var()`, `oklch()`, unknown names)
|
|
110
|
+
* so callers can preserve the original expression rather than guessing.
|
|
111
|
+
*/
|
|
112
|
+
export function parseColor(value) {
|
|
113
|
+
const trimmed = value.trim();
|
|
114
|
+
const named = NAMED_COLORS[trimmed.toLowerCase()];
|
|
115
|
+
if (named) {
|
|
116
|
+
return {
|
|
117
|
+
...named
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (trimmed.startsWith('#')) {
|
|
121
|
+
return parseHex(trimmed);
|
|
122
|
+
}
|
|
123
|
+
if (/^rgba?\(/i.test(trimmed)) {
|
|
124
|
+
return parseRgb(trimmed);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Format RGB channels (0-255) as an uppercase `#RRGGBB` string. */
|
|
130
|
+
export function formatHex(r, g, b) {
|
|
131
|
+
const channel = c => clamp(Math.round(c), 0, 255).toString(16).padStart(2, '0').toUpperCase();
|
|
132
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Serialize {@link RGBA} to a compact, JS-usable CSS color: `#RRGGBB` when
|
|
137
|
+
* fully opaque, otherwise `rgba(r, g, b, a)`.
|
|
138
|
+
*/
|
|
139
|
+
export function formatColor({
|
|
140
|
+
r,
|
|
141
|
+
g,
|
|
142
|
+
b,
|
|
143
|
+
a
|
|
144
|
+
}) {
|
|
145
|
+
if (a >= 1) {
|
|
146
|
+
return formatHex(r, g, b);
|
|
147
|
+
}
|
|
148
|
+
const round = n => clamp(Math.round(n), 0, 255);
|
|
149
|
+
return `rgba(${round(r)}, ${round(g)}, ${round(b)}, ${parseFloat(a.toFixed(4))})`;
|
|
150
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -22,4 +22,6 @@ export { observeResize, unobserveResize } from './sharedResizeObserver';
|
|
|
22
22
|
export { isRenderable } from './isRenderable';
|
|
23
23
|
export { getInputARIA } from './inputAria';
|
|
24
24
|
export type { InputARIA, InputARIAInputGroup } from './inputAria';
|
|
25
|
+
export { parseHex, parseRgb, parseColor, formatHex, formatColor } from './color';
|
|
26
|
+
export type { RGBA } from './color';
|
|
25
27
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,YAAY,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAC,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,SAAS,EACT,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,wBAAwB,EACxB,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAC,MAAM,EAAE,KAAK,GAAG,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC7D,YAAY,EACV,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AACzC,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,YAAY,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAC,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,SAAS,EACT,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,wBAAwB,EACxB,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,GAC5B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAC,MAAM,EAAE,KAAK,GAAG,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC7D,YAAY,EACV,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AACzC,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC;AAEhE,OAAO,EAAC,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAC,MAAM,SAAS,CAAC;AAC/E,YAAY,EAAC,IAAI,EAAC,MAAM,SAAS,CAAC"}
|
package/dist/utils/index.js
CHANGED
|
@@ -18,4 +18,5 @@ export { themeProps, themeDataAttributes } from "./themeProps.js";
|
|
|
18
18
|
export { groupItems, getItemGroup } from "./groupItems.js";
|
|
19
19
|
export { observeResize, unobserveResize } from "./sharedResizeObserver.js";
|
|
20
20
|
export { isRenderable } from "./isRenderable.js";
|
|
21
|
-
export { getInputARIA } from "./inputAria.js";
|
|
21
|
+
export { getInputARIA } from "./inputAria.js";
|
|
22
|
+
export { parseHex, parseRgb, parseColor, formatHex, formatColor } from "./color.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeParser.d.ts","sourceRoot":"","sources":["../../src/utils/timeParser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAUf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAmBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,cAAc,GAAE,OAAe,GAC9B,aAAa,GAAG,IAAI,CAuGtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,aAAa,GAAG,SAAS,EAC5B,CAAC,EAAE,aAAa,GAAG,SAAS,GAC3B,MAAM,CA4BR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,GAClB,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAoBf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC9B,aAAa,
|
|
1
|
+
{"version":3,"file":"timeParser.d.ts","sourceRoot":"","sources":["../../src/utils/timeParser.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG;IAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAA;CAAC,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAMvE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA6B5D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAUf;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,cAAc,GAAE,OAAe,GAC9B,MAAM,CAmBR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,cAAc,GAAE,OAAe,GAC9B,aAAa,GAAG,IAAI,CAuGtB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,aAAa,GAAG,SAAS,EAC5B,CAAC,EAAE,aAAa,GAAG,SAAS,GAC3B,MAAM,CA4BR;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,GAClB,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,GAAG,CAAC,EAAE,aAAa,EACnB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAoBf;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,MAAM,EACpB,cAAc,GAAE,OAAe,GAC9B,aAAa,CAwBf"}
|
package/dist/utils/timeParser.js
CHANGED
|
@@ -305,13 +305,16 @@ export function adjustTime(time, deltaMinutes, includeSeconds = false) {
|
|
|
305
305
|
if (!parsed) {
|
|
306
306
|
return time;
|
|
307
307
|
}
|
|
308
|
-
let totalMinutes = parsed.hour * 60 + parsed.minute + deltaMinutes;
|
|
309
308
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
309
|
+
// A non-finite delta would spin the wrap-around forever (-Infinity) or
|
|
310
|
+
// produce "NaN:NaN" (NaN) — return the input unchanged instead.
|
|
311
|
+
if (!Number.isFinite(deltaMinutes)) {
|
|
312
|
+
return time;
|
|
313
313
|
}
|
|
314
|
-
totalMinutes =
|
|
314
|
+
let totalMinutes = parsed.hour * 60 + parsed.minute + deltaMinutes;
|
|
315
|
+
|
|
316
|
+
// Wrap around midnight (double-modulo handles negatives in O(1))
|
|
317
|
+
totalMinutes = (totalMinutes % (24 * 60) + 24 * 60) % (24 * 60);
|
|
315
318
|
const newHour = Math.floor(totalMinutes / 60);
|
|
316
319
|
const newMinute = totalMinutes % 60;
|
|
317
320
|
return formatISOTime({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.1.4-canary.
|
|
3
|
+
"version": "0.1.4-canary.3d64ef2",
|
|
4
4
|
"displayName": "XDS Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -106,4 +106,15 @@ export const docsDense = {
|
|
|
106
106
|
{guidance: false, description: 'Force a square or custom shape. Avatars are always circular.'},
|
|
107
107
|
],
|
|
108
108
|
},
|
|
109
|
+
propDescriptions: {
|
|
110
|
+
src: 'primary image source URL',
|
|
111
|
+
fallbackSrc: 'fallback image when primary fails',
|
|
112
|
+
name: 'user name for initials and alt text',
|
|
113
|
+
alt: 'alt text; falls back to name',
|
|
114
|
+
size: "avatar size. Named ('tiny', 'xsmall', 'small', 'medium', 'large') or numeric px. Short names 'sm'/'md'/'lg' are NOT valid; use full words.",
|
|
115
|
+
status: 'corner content for status indicators',
|
|
116
|
+
},
|
|
117
|
+
components: [
|
|
118
|
+
{name: 'AvatarStatusDot', description: 'size-aware status indicator rendered in the Avatar corner'},
|
|
119
|
+
],
|
|
109
120
|
};
|
|
@@ -113,6 +113,7 @@ export const docs = {
|
|
|
113
113
|
targets: [
|
|
114
114
|
{className: 'astryx-banner', visualProps: ['container', 'status']},
|
|
115
115
|
{className: 'astryx-banner-icon', visualProps: ['status']},
|
|
116
|
+
{className: 'astryx-banner-content', visualProps: ['container', 'status']},
|
|
116
117
|
],
|
|
117
118
|
vars: [
|
|
118
119
|
{name: '--_banner-radius', description: 'Border radius (card container only)', default: 'var(--radius-container)', private: true},
|
|
@@ -180,6 +181,13 @@ export const docsZh = {
|
|
|
180
181
|
'status',
|
|
181
182
|
],
|
|
182
183
|
},
|
|
184
|
+
{
|
|
185
|
+
className: 'astryx-banner-content',
|
|
186
|
+
visualProps: [
|
|
187
|
+
'container',
|
|
188
|
+
'status',
|
|
189
|
+
],
|
|
190
|
+
},
|
|
183
191
|
],
|
|
184
192
|
vars: [
|
|
185
193
|
{name: '--_banner-radius', description: 'Border radius (card container only)', default: 'var(--radius-container)', private: true},
|
|
@@ -270,6 +270,44 @@ describe('Banner', () => {
|
|
|
270
270
|
expect(expandIndex).toBeLessThan(dismissIndex);
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
it('links the expand toggle to its content region via aria-controls', () => {
|
|
274
|
+
render(
|
|
275
|
+
<Banner status="info" title="Controls Test" defaultIsExpanded>
|
|
276
|
+
<div data-testid="region-content">Region content</div>
|
|
277
|
+
</Banner>,
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const toggle = screen.getByRole('button', {name: 'Collapse'});
|
|
281
|
+
const controlsId = toggle.getAttribute('aria-controls');
|
|
282
|
+
// aria-controls must be present and point at the real content region.
|
|
283
|
+
expect(controlsId).toBeTruthy();
|
|
284
|
+
const region = document.getElementById(controlsId as string);
|
|
285
|
+
expect(region).not.toBeNull();
|
|
286
|
+
expect(region).toContainElement(screen.getByTestId('region-content'));
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('sets aria-controls only while the content region is mounted', async () => {
|
|
290
|
+
const user = userEvent.setup();
|
|
291
|
+
render(
|
|
292
|
+
<Banner status="info" title="Controls Toggle">
|
|
293
|
+
<div data-testid="region-content">Region content</div>
|
|
294
|
+
</Banner>,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Collapsed: the region is unmounted, so no dangling aria-controls target.
|
|
298
|
+
const collapsedToggle = screen.getByRole('button', {name: 'Expand'});
|
|
299
|
+
expect(collapsedToggle).not.toHaveAttribute('aria-controls');
|
|
300
|
+
|
|
301
|
+
// Expanded: aria-controls resolves to the mounted region with the children.
|
|
302
|
+
await user.click(collapsedToggle);
|
|
303
|
+
const expandedToggle = screen.getByRole('button', {name: 'Collapse'});
|
|
304
|
+
const controlsId = expandedToggle.getAttribute('aria-controls');
|
|
305
|
+
expect(controlsId).toBeTruthy();
|
|
306
|
+
const region = document.getElementById(controlsId as string);
|
|
307
|
+
expect(region).not.toBeNull();
|
|
308
|
+
expect(region).toContainElement(screen.getByTestId('region-content'));
|
|
309
|
+
});
|
|
310
|
+
|
|
273
311
|
it('does not render content area when no children', () => {
|
|
274
312
|
const {container} = render(<Banner status="info" title="No Children" />);
|
|
275
313
|
const root = container.firstElementChild;
|
package/src/Banner/Banner.tsx
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* - /packages/cli/templates/blocks/components/Banner/ (showcase blocks)
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
import {useState, type ReactNode} from 'react';
|
|
34
|
+
import {useId, useState, type ReactNode} from 'react';
|
|
35
35
|
import * as stylex from '@stylexjs/stylex';
|
|
36
36
|
import type {BaseProps} from '../BaseProps';
|
|
37
37
|
import {Button} from '../Button';
|
|
@@ -387,6 +387,11 @@ export function Banner({
|
|
|
387
387
|
}: BannerProps) {
|
|
388
388
|
const [isDismissed, setIsDismissed] = useState(false);
|
|
389
389
|
const [isExpanded, setIsExpanded] = useState(defaultIsExpanded);
|
|
390
|
+
// Links the expand/collapse toggle to the content region it shows/hides so
|
|
391
|
+
// assistive tech can move from the button to its controlled content
|
|
392
|
+
// (disclosure pattern). The region is conditionally rendered, so aria-controls
|
|
393
|
+
// below is set only while it's mounted to avoid a dangling reference.
|
|
394
|
+
const contentId = useId();
|
|
390
395
|
const defaultIconName = defaultIconNames[status];
|
|
391
396
|
const role = statusRole[status];
|
|
392
397
|
const iconColor = statusIconColor[status];
|
|
@@ -477,6 +482,7 @@ export function Banner({
|
|
|
477
482
|
}
|
|
478
483
|
onClick={handleToggleExpand}
|
|
479
484
|
aria-expanded={isExpanded}
|
|
485
|
+
aria-controls={showContent ? contentId : undefined}
|
|
480
486
|
isIconOnly
|
|
481
487
|
/>
|
|
482
488
|
)}
|
|
@@ -497,6 +503,7 @@ export function Banner({
|
|
|
497
503
|
{/* Content area: collapsible card background — theme target ('banner-content') */}
|
|
498
504
|
{showContent && (
|
|
499
505
|
<div
|
|
506
|
+
id={contentId}
|
|
500
507
|
{...mergeProps(
|
|
501
508
|
themeProps('banner-content', {container, status}),
|
|
502
509
|
stylex.props(styles.contentArea, isCard && styles.contentAreaCard),
|
|
@@ -19,7 +19,7 @@ export const docs = {
|
|
|
19
19
|
{guidance: false, description: 'Disable large blocks of dates without context. The user should understand why dates are unavailable.'},
|
|
20
20
|
],
|
|
21
21
|
anatomy: [
|
|
22
|
-
{name: 'Month header', required: true, description: 'The month name and year with navigation arrows to move between months.'},
|
|
22
|
+
{name: 'Month header', required: true, description: 'The month name and year with navigation arrows to move between months. The arrows mirror automatically under dir="rtl".'},
|
|
23
23
|
{name: 'Day grid', required: true, description: 'A 7-column grid of days with column headers for the day names.'},
|
|
24
24
|
{name: 'Selected day', required: false, description: 'The currently selected date, highlighted. In range mode, the start and end dates plus the days between them.'},
|
|
25
25
|
{name: 'Today marker', required: false, description: 'A subtle indicator on the current date for orientation.'},
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {act, render, screen, within} from '@testing-library/react';
|
|
14
14
|
import userEvent from '@testing-library/user-event';
|
|
15
|
+
import * as stylex from '@stylexjs/stylex';
|
|
15
16
|
import {Calendar} from './Calendar';
|
|
16
17
|
import type {CalendarHandle} from './Calendar';
|
|
18
|
+
import {calendarStyles} from './styles';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Helper to find a day button by its day number.
|
|
@@ -71,6 +73,24 @@ describe('Calendar', () => {
|
|
|
71
73
|
expect(screen.getByText(expectedLabel)).toBeInTheDocument();
|
|
72
74
|
});
|
|
73
75
|
|
|
76
|
+
it("marks today's cell with aria-current='date'", () => {
|
|
77
|
+
render(<Calendar />);
|
|
78
|
+
|
|
79
|
+
const now = new Date();
|
|
80
|
+
const iso = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
81
|
+
|
|
82
|
+
const todayCell = document.querySelector(`button[data-date="${iso}"]`);
|
|
83
|
+
expect(todayCell).not.toBeNull();
|
|
84
|
+
expect(todayCell).toHaveAttribute('aria-current', 'date');
|
|
85
|
+
|
|
86
|
+
// Only today's cell is marked current.
|
|
87
|
+
const others = Array.from(
|
|
88
|
+
document.querySelectorAll('button[data-date]'),
|
|
89
|
+
).filter(el => el.getAttribute('data-date') !== iso);
|
|
90
|
+
expect(others.length).toBeGreaterThan(0);
|
|
91
|
+
others.forEach(el => expect(el).not.toHaveAttribute('aria-current'));
|
|
92
|
+
});
|
|
93
|
+
|
|
74
94
|
it('displays day names', () => {
|
|
75
95
|
render(<Calendar />);
|
|
76
96
|
|
|
@@ -97,7 +117,11 @@ describe('Calendar', () => {
|
|
|
97
117
|
render(<Calendar value="2026-01-15" focusDate="2026-01-01" />);
|
|
98
118
|
|
|
99
119
|
const day15 = getDayButton(15);
|
|
100
|
-
|
|
120
|
+
// In an ARIA grid, selection state lives on the gridcell, not the button
|
|
121
|
+
// (a plain button role does not permit aria-selected).
|
|
122
|
+
const gridcell15 = day15.closest('[role="gridcell"]');
|
|
123
|
+
expect(gridcell15).toHaveAttribute('aria-selected', 'true');
|
|
124
|
+
expect(day15).not.toHaveAttribute('aria-selected');
|
|
101
125
|
});
|
|
102
126
|
|
|
103
127
|
it('calls onChange when date is selected', async () => {
|
|
@@ -215,6 +239,33 @@ describe('Calendar', () => {
|
|
|
215
239
|
expect(screen.getByText(/February 2026.*March 2026/)).toBeInTheDocument();
|
|
216
240
|
});
|
|
217
241
|
|
|
242
|
+
it('clamps out-of-range numberOfMonths to a single month', () => {
|
|
243
|
+
// 1000 would otherwise try to render 1000 month grids and lock the page up.
|
|
244
|
+
render(
|
|
245
|
+
<Calendar
|
|
246
|
+
numberOfMonths={1000 as unknown as 1 | 2}
|
|
247
|
+
focusDate="2026-01-01"
|
|
248
|
+
/>,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Only the focused month renders — no second month, no range separator.
|
|
252
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
253
|
+
expect(screen.queryByText(/February 2026/)).not.toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('clamps numberOfMonths={0} to a single month', () => {
|
|
257
|
+
render(
|
|
258
|
+
<Calendar
|
|
259
|
+
numberOfMonths={0 as unknown as 1 | 2}
|
|
260
|
+
focusDate="2026-01-01"
|
|
261
|
+
/>,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// 0 previously rendered no months at all; now falls back to one.
|
|
265
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
266
|
+
expect(screen.getAllByRole('gridcell').length).toBeGreaterThan(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
218
269
|
// ─── Display Options ─────────────────────────────────────────
|
|
219
270
|
|
|
220
271
|
it('shows week numbers when hasWeekNumbers is true', () => {
|
|
@@ -310,9 +361,22 @@ describe('Calendar', () => {
|
|
|
310
361
|
const day12 = getDayButton(12);
|
|
311
362
|
const day15 = getDayButton(15);
|
|
312
363
|
|
|
313
|
-
|
|
314
|
-
expect(
|
|
315
|
-
|
|
364
|
+
// Selection state lives on the gridcell wrapper, not the day button.
|
|
365
|
+
expect(day10.closest('[role="gridcell"]')).toHaveAttribute(
|
|
366
|
+
'aria-selected',
|
|
367
|
+
'true',
|
|
368
|
+
);
|
|
369
|
+
expect(day12.closest('[role="gridcell"]')).toHaveAttribute(
|
|
370
|
+
'aria-selected',
|
|
371
|
+
'true',
|
|
372
|
+
);
|
|
373
|
+
expect(day15.closest('[role="gridcell"]')).toHaveAttribute(
|
|
374
|
+
'aria-selected',
|
|
375
|
+
'true',
|
|
376
|
+
);
|
|
377
|
+
expect(day10).not.toHaveAttribute('aria-selected');
|
|
378
|
+
expect(day12).not.toHaveAttribute('aria-selected');
|
|
379
|
+
expect(day15).not.toHaveAttribute('aria-selected');
|
|
316
380
|
});
|
|
317
381
|
|
|
318
382
|
it('caps the range highlight next to a disabled mid-range day (#2715)', () => {
|
|
@@ -675,4 +739,48 @@ describe('Calendar', () => {
|
|
|
675
739
|
}
|
|
676
740
|
}
|
|
677
741
|
});
|
|
742
|
+
|
|
743
|
+
// ─── RTL (#3388) ─────────────────────────────────────────────
|
|
744
|
+
|
|
745
|
+
describe('RTL month navigation', () => {
|
|
746
|
+
// jsdom does not apply compiled StyleX CSS, so the scaleX(-1) mirror
|
|
747
|
+
// itself is only observable in a browser (see the dir="rtl" Storybook
|
|
748
|
+
// story). These tests pin the structure the fix depends on: both nav
|
|
749
|
+
// chevrons render inside the navIcon wrapper that carries the
|
|
750
|
+
// ':is([dir="rtl"] *)' conditional transform.
|
|
751
|
+
it('wraps both nav chevrons in the RTL-mirroring navIcon wrapper', () => {
|
|
752
|
+
render(<Calendar focusDate="2026-01-01" />);
|
|
753
|
+
|
|
754
|
+
const {className: navIconClass} = stylex.props(calendarStyles.navIcon);
|
|
755
|
+
expect(navIconClass).toBeTruthy();
|
|
756
|
+
|
|
757
|
+
for (const name of ['Previous month', 'Next month']) {
|
|
758
|
+
const button = screen.getByRole('button', {name});
|
|
759
|
+
const wrappers = Array.from(button.querySelectorAll('span')).filter(
|
|
760
|
+
span => span.className === navIconClass,
|
|
761
|
+
);
|
|
762
|
+
expect(wrappers.length).toBe(1);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('keeps navigation semantics unchanged under dir="rtl"', async () => {
|
|
767
|
+
const user = userEvent.setup();
|
|
768
|
+
|
|
769
|
+
render(
|
|
770
|
+
<div dir="rtl">
|
|
771
|
+
<Calendar focusDate="2026-02-01" />
|
|
772
|
+
</div>,
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
expect(screen.getByText('February 2026')).toBeInTheDocument();
|
|
776
|
+
|
|
777
|
+
// DOM order and handlers must not change in RTL: flexbox already
|
|
778
|
+
// places "Previous month" at the visual right; only the glyph mirrors.
|
|
779
|
+
await user.click(screen.getByRole('button', {name: 'Previous month'}));
|
|
780
|
+
expect(screen.getByText('January 2026')).toBeInTheDocument();
|
|
781
|
+
|
|
782
|
+
await user.click(screen.getByRole('button', {name: 'Next month'}));
|
|
783
|
+
expect(screen.getByText('February 2026')).toBeInTheDocument();
|
|
784
|
+
});
|
|
785
|
+
});
|
|
678
786
|
});
|
|
@@ -203,7 +203,7 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
203
203
|
value,
|
|
204
204
|
defaultValue,
|
|
205
205
|
onChange,
|
|
206
|
-
numberOfMonths = 1,
|
|
206
|
+
numberOfMonths: numberOfMonthsProp = 1,
|
|
207
207
|
min,
|
|
208
208
|
max,
|
|
209
209
|
dateConstraints,
|
|
@@ -223,6 +223,11 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
223
223
|
// DayOfWeek so all downstream date math keeps working with an index.
|
|
224
224
|
const weekStartsOn = normalizeDayOfWeek(weekStartsOnProp);
|
|
225
225
|
|
|
226
|
+
// `numberOfMonths` is typed `1 | 2`; defensively clamp anything else that
|
|
227
|
+
// slips through at runtime to 1 so `Array.from({length})` can't render an
|
|
228
|
+
// absurd number of month grids (e.g. `numberOfMonths={1000}`).
|
|
229
|
+
const numberOfMonths = numberOfMonthsProp === 2 ? 2 : 1;
|
|
230
|
+
|
|
226
231
|
// Today's date (memoized)
|
|
227
232
|
const today = useMemo(() => plainDateToday(), []);
|
|
228
233
|
|
|
@@ -426,7 +431,14 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
426
431
|
<Button
|
|
427
432
|
label="Previous month"
|
|
428
433
|
variant="ghost"
|
|
429
|
-
icon={
|
|
434
|
+
icon={
|
|
435
|
+
// Wrapper span (not Icon props): Icon's string mode clobbers
|
|
436
|
+
// caller classNames, so the RTL mirror must live on its own
|
|
437
|
+
// element.
|
|
438
|
+
<span {...stylex.props(calendarStyles.navIcon)}>
|
|
439
|
+
<Icon icon="chevronLeft" size="sm" color="inherit" />
|
|
440
|
+
</span>
|
|
441
|
+
}
|
|
430
442
|
onClick={() => navigateMonth(-1)}
|
|
431
443
|
isDisabled={!canNavigatePrevious}
|
|
432
444
|
isIconOnly
|
|
@@ -439,7 +451,11 @@ export function Calendar({ref, ...props}: CalendarProps) {
|
|
|
439
451
|
<Button
|
|
440
452
|
label="Next month"
|
|
441
453
|
variant="ghost"
|
|
442
|
-
icon={
|
|
454
|
+
icon={
|
|
455
|
+
<span {...stylex.props(calendarStyles.navIcon)}>
|
|
456
|
+
<Icon icon="chevronRight" size="sm" color="inherit" />
|
|
457
|
+
</span>
|
|
458
|
+
}
|
|
443
459
|
onClick={() => navigateMonth(1)}
|
|
444
460
|
isDisabled={!canNavigateNext}
|
|
445
461
|
isIconOnly
|
|
@@ -912,7 +928,10 @@ function DayCell({
|
|
|
912
928
|
});
|
|
913
929
|
|
|
914
930
|
return (
|
|
915
|
-
<div
|
|
931
|
+
<div
|
|
932
|
+
role="gridcell"
|
|
933
|
+
aria-selected={state.isSelected || state.isInRange || undefined}
|
|
934
|
+
{...stylex.props(dayCellStyles.cell)}>
|
|
916
935
|
{/* Range background */}
|
|
917
936
|
{state.isInRange && (
|
|
918
937
|
<div
|
|
@@ -952,8 +971,10 @@ function DayCell({
|
|
|
952
971
|
type="button"
|
|
953
972
|
data-date={day.iso}
|
|
954
973
|
aria-label={plainDateFormat(date, DATE_FORMAT_WITH_WEEKDAY)}
|
|
955
|
-
aria-selected={state.isSelected || state.isInRange || undefined}
|
|
956
974
|
aria-disabled={state.effectivelyDisabled || undefined}
|
|
975
|
+
// Mark today's cell programmatically (APG date-picker pattern), not just
|
|
976
|
+
// visually, so screen-reader users can identify the current date.
|
|
977
|
+
aria-current={state.isToday ? 'date' : undefined}
|
|
957
978
|
disabled={isDisabled}
|
|
958
979
|
// Initial roving tab-stop seed; useGridFocus owns it after mount.
|
|
959
980
|
tabIndex={isTabbableDay ? 0 : -1}
|
package/src/Calendar/styles.ts
CHANGED
|
@@ -64,9 +64,19 @@ export const calendarStyles = stylex.create({
|
|
|
64
64
|
whiteSpace: 'nowrap',
|
|
65
65
|
borderWidth: 0,
|
|
66
66
|
},
|
|
67
|
+
/**
|
|
68
|
+
* Wrapper for the month nav chevrons. In RTL the flex header already
|
|
69
|
+
* swaps the buttons' visual sides; the glyphs must mirror with them so
|
|
70
|
+
* "Previous month" points outward (visually right) instead of inward.
|
|
71
|
+
* Keyed on the dir attribute (dir="rtl"), same as MobileNav's drawer
|
|
72
|
+
* transform — bare CSS `direction: rtl` alone won't trigger it.
|
|
73
|
+
*/
|
|
67
74
|
navIcon: {
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
display: 'inline-flex',
|
|
76
|
+
transform: {
|
|
77
|
+
default: null,
|
|
78
|
+
':is([dir="rtl"] *)': 'scaleX(-1)',
|
|
79
|
+
},
|
|
70
80
|
},
|
|
71
81
|
});
|
|
72
82
|
|
package/src/Card/Card.tsx
CHANGED
|
@@ -44,8 +44,10 @@ import {themeProps} from '../utils/themeProps';
|
|
|
44
44
|
* - Non-semantic palette: `blue | cyan | gray | green | orange | pink | purple | red | teal | yellow`
|
|
45
45
|
* Each uses the corresponding `--color-background-<name>` token (20% opacity tint).
|
|
46
46
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* Only `default` draws a visible border. Its border width is subtracted from
|
|
48
|
+
* the padding so the total inset (border + padding) equals the padding token —
|
|
49
|
+
* keeping content geometry faithful to the spacing scale and identical to the
|
|
50
|
+
* borderless variants. Themes can override borderWidth/borderColor.
|
|
49
51
|
*/
|
|
50
52
|
export type CardVariant =
|
|
51
53
|
| 'default'
|
|
@@ -71,12 +73,18 @@ const styles = stylex.create({
|
|
|
71
73
|
'--_card-radius': radiusVars['--radius-container'],
|
|
72
74
|
borderRadius: 'var(--_card-radius)',
|
|
73
75
|
overflow: 'clip',
|
|
74
|
-
borderWidth: borderVars['--border-width'],
|
|
75
|
-
borderStyle: 'solid',
|
|
76
|
-
borderColor: 'transparent',
|
|
77
76
|
},
|
|
77
|
+
// The border is drawn *inside* the padding — its width is subtracted from
|
|
78
|
+
// each side — so total inset (border + padding) equals the padding token
|
|
79
|
+
// rather than exceeding it by the border width.
|
|
78
80
|
withBorder: {
|
|
81
|
+
borderWidth: borderVars['--border-width'],
|
|
82
|
+
borderStyle: 'solid',
|
|
79
83
|
borderColor: colorVars['--color-border-emphasized'],
|
|
84
|
+
paddingInlineStart: `calc(var(--container-padding-inline-start) - ${borderVars['--border-width']})`,
|
|
85
|
+
paddingInlineEnd: `calc(var(--container-padding-inline-end) - ${borderVars['--border-width']})`,
|
|
86
|
+
paddingBlockStart: `calc(var(--container-padding-block-start) - ${borderVars['--border-width']})`,
|
|
87
|
+
paddingBlockEnd: `calc(var(--container-padding-block-end) - ${borderVars['--border-width']})`,
|
|
80
88
|
},
|
|
81
89
|
// Fixed-height cards scroll content; overflow: auto also clips to border-radius
|
|
82
90
|
scrollable: {
|
|
@@ -266,7 +274,6 @@ export function Card({
|
|
|
266
274
|
stylex.props(
|
|
267
275
|
styles.card,
|
|
268
276
|
variantStyles[variant],
|
|
269
|
-
variant === 'default' && styles.withBorder,
|
|
270
277
|
hasFixedHeight && styles.scrollable,
|
|
271
278
|
dynamicStyles.sizing(
|
|
272
279
|
width ?? null,
|
|
@@ -296,6 +303,9 @@ export function Card({
|
|
|
296
303
|
!useThemeDefault &&
|
|
297
304
|
effectivePadding !== 4 &&
|
|
298
305
|
containerPaddingBlockEndVarStyles[effectivePadding],
|
|
306
|
+
// Applied after the container padding so the border-inset calc wins;
|
|
307
|
+
// it reads the --container-padding-* vars set above.
|
|
308
|
+
variant === 'default' && styles.withBorder,
|
|
299
309
|
xstyle,
|
|
300
310
|
),
|
|
301
311
|
className,
|