@astryxdesign/core 0.4.0 → 0.4.1-canary.1ce0b4c
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/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 +48 -48
- 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/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
package/src/theme/index.ts
CHANGED
|
@@ -90,6 +90,7 @@ export {
|
|
|
90
90
|
spacingDefaults,
|
|
91
91
|
sizeDefaults,
|
|
92
92
|
borderDefaults,
|
|
93
|
+
focusDefaults,
|
|
93
94
|
radiusDefaults,
|
|
94
95
|
shadowDefaults,
|
|
95
96
|
durationDefaults,
|
|
@@ -102,6 +103,7 @@ export {
|
|
|
102
103
|
spacingVars,
|
|
103
104
|
sizeVars,
|
|
104
105
|
borderVars,
|
|
106
|
+
focusVars,
|
|
105
107
|
radiusVars,
|
|
106
108
|
shadowVars,
|
|
107
109
|
durationVars,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import {describe, it, expect} from 'vitest';
|
|
28
28
|
import {readdirSync, readFileSync} from 'node:fs';
|
|
29
|
-
import {join} from 'node:path';
|
|
29
|
+
import {join, relative} from 'node:path';
|
|
30
30
|
import ts from 'typescript';
|
|
31
31
|
import {stableClassName} from '../naming';
|
|
32
32
|
|
|
@@ -45,6 +45,61 @@ interface ThemeTargetSite {
|
|
|
45
45
|
isOpaque: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Keys of a spread whose operand is a conditional object literal:
|
|
50
|
+
* `...(cond && {a})`, `...(cond ? {a} : null)`, `...(cond || {a})`.
|
|
51
|
+
* Returns null when the operand is anything else (a bag like `...rest`, or an
|
|
52
|
+
* object with a computed key), which the caller treats as opaque.
|
|
53
|
+
*/
|
|
54
|
+
function conditionalSpreadKeys(expression: ts.Expression): string[] | null {
|
|
55
|
+
const unwrap = (node: ts.Expression): ts.Expression =>
|
|
56
|
+
ts.isParenthesizedExpression(node) ? unwrap(node.expression) : node;
|
|
57
|
+
|
|
58
|
+
const expr = unwrap(expression);
|
|
59
|
+
const branches: ts.Expression[] = [];
|
|
60
|
+
if (
|
|
61
|
+
ts.isBinaryExpression(expr) &&
|
|
62
|
+
(expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
63
|
+
expr.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
|
64
|
+
) {
|
|
65
|
+
branches.push(expr.right);
|
|
66
|
+
} else if (ts.isConditionalExpression(expr)) {
|
|
67
|
+
branches.push(expr.whenTrue, expr.whenFalse);
|
|
68
|
+
} else if (ts.isObjectLiteralExpression(expr)) {
|
|
69
|
+
branches.push(expr);
|
|
70
|
+
} else {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const keys: string[] = [];
|
|
75
|
+
for (const branch of branches) {
|
|
76
|
+
const object = unwrap(branch);
|
|
77
|
+
// A falsy filler arm (`null`, `undefined`) contributes no keys; anything
|
|
78
|
+
// else that is not an object literal hides its keys, so give up.
|
|
79
|
+
if (
|
|
80
|
+
object.kind === ts.SyntaxKind.NullKeyword ||
|
|
81
|
+
(ts.isIdentifier(object) && object.text === 'undefined')
|
|
82
|
+
) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!ts.isObjectLiteralExpression(object)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
for (const prop of object.properties) {
|
|
89
|
+
const name = prop.name;
|
|
90
|
+
if (
|
|
91
|
+
name != null &&
|
|
92
|
+
(ts.isIdentifier(name) || ts.isStringLiteralLike(name))
|
|
93
|
+
) {
|
|
94
|
+
keys.push(name.text);
|
|
95
|
+
} else {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return keys;
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
/**
|
|
49
104
|
* Extract every `themeProps('name', {...})` call from a source file.
|
|
50
105
|
*
|
|
@@ -88,8 +143,16 @@ function extractThemeTargets(
|
|
|
88
143
|
if (ts.isObjectLiteralExpression(propsArg)) {
|
|
89
144
|
for (const prop of propsArg.properties) {
|
|
90
145
|
if (ts.isSpreadAssignment(prop)) {
|
|
91
|
-
//
|
|
92
|
-
|
|
146
|
+
// A conditional spread of an object literal — `...(type &&
|
|
147
|
+
// {type})` — has statically known keys. Treating it as opaque
|
|
148
|
+
// is how `astryx-heading`'s `type` stayed undocumented: the
|
|
149
|
+
// same drift this file exists to catch (#3652, #3680).
|
|
150
|
+
const spreadKeys = conditionalSpreadKeys(prop.expression);
|
|
151
|
+
if (spreadKeys == null) {
|
|
152
|
+
site.isOpaque = true;
|
|
153
|
+
} else {
|
|
154
|
+
site.propKeys.push(...spreadKeys);
|
|
155
|
+
}
|
|
93
156
|
continue;
|
|
94
157
|
}
|
|
95
158
|
const name = prop.name;
|
|
@@ -194,12 +257,35 @@ describe('extractThemeTargets', () => {
|
|
|
194
257
|
]);
|
|
195
258
|
});
|
|
196
259
|
|
|
260
|
+
it('reads the keys of a conditional spread', () => {
|
|
261
|
+
// Heading's real call site: `{level, color, ...(type && {type})}`.
|
|
262
|
+
const [site] = extractThemeTargets(
|
|
263
|
+
`themeProps('heading', {level, color, ...(type && {type})})`,
|
|
264
|
+
);
|
|
265
|
+
expect(site.propKeys).toEqual(['level', 'color', 'type']);
|
|
266
|
+
expect(site.isOpaque).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('reads both arms of a ternary spread', () => {
|
|
270
|
+
const [site] = extractThemeTargets(
|
|
271
|
+
`themeProps('card', {...(isOpen ? {expanded} : {collapsed})})`,
|
|
272
|
+
);
|
|
273
|
+
expect(site.propKeys).toEqual(['expanded', 'collapsed']);
|
|
274
|
+
expect(site.isOpaque).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
197
277
|
it('marks a spread props bag opaque rather than guessing its keys', () => {
|
|
198
278
|
const [site] = extractThemeTargets(`themeProps('card', {...rest})`);
|
|
199
279
|
expect(site.isOpaque).toBe(true);
|
|
200
280
|
expect(site.propKeys).toEqual([]);
|
|
201
281
|
});
|
|
202
282
|
|
|
283
|
+
it('marks a conditional spread of a non-literal opaque', () => {
|
|
284
|
+
const [site] = extractThemeTargets(`themeProps('card', {...(on && rest)})`);
|
|
285
|
+
expect(site.isOpaque).toBe(true);
|
|
286
|
+
expect(site.propKeys).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
|
|
203
289
|
it('marks a non-literal props bag opaque', () => {
|
|
204
290
|
const [site] = extractThemeTargets(`themeProps('card', visualProps)`);
|
|
205
291
|
expect(site.isOpaque).toBe(true);
|
|
@@ -231,7 +317,37 @@ interface ComponentInfo {
|
|
|
231
317
|
dir: string;
|
|
232
318
|
sites: ThemeTargetSite[];
|
|
233
319
|
/** The doc blocks that carry theming.targets, by the key they live under. */
|
|
234
|
-
docBlocks: {key: 'docs' | 'docsZh'; targets: DocTarget[]}[];
|
|
320
|
+
docBlocks: {key: 'docs' | 'docsZh'; file: string; targets: DocTarget[]}[];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Every `*.doc.mjs` under src that declares a theming target for one of
|
|
325
|
+
* `classNames`. Lets a component be checked against the doc that documents it,
|
|
326
|
+
* wherever that file lives.
|
|
327
|
+
*
|
|
328
|
+
* Reads the file as text rather than requiring it: this runs for directories
|
|
329
|
+
* that have no doc of their own, so most candidates are misses.
|
|
330
|
+
*/
|
|
331
|
+
function docFilesDocumenting(classNames: Set<string>): string[] {
|
|
332
|
+
const matches: string[] = [];
|
|
333
|
+
const walk = (dir: string): void => {
|
|
334
|
+
for (const entry of readdirSync(dir, {withFileTypes: true})) {
|
|
335
|
+
const p = join(dir, entry.name);
|
|
336
|
+
if (entry.isDirectory()) {
|
|
337
|
+
walk(p);
|
|
338
|
+
} else if (entry.name.endsWith('.doc.mjs')) {
|
|
339
|
+
const source = readFileSync(p, 'utf-8');
|
|
340
|
+
for (const className of classNames) {
|
|
341
|
+
if (source.includes(`'${className}'`)) {
|
|
342
|
+
matches.push(p);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
walk(SRC_DIR);
|
|
350
|
+
return matches;
|
|
235
351
|
}
|
|
236
352
|
|
|
237
353
|
function discoverComponents(): ComponentInfo[] {
|
|
@@ -262,27 +378,37 @@ function discoverComponents(): ComponentInfo[] {
|
|
|
262
378
|
continue;
|
|
263
379
|
}
|
|
264
380
|
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
381
|
+
// A component's doc file usually sits beside its source, but not always:
|
|
382
|
+
// `Heading/Heading.tsx` is documented by `Text/Text.doc.mjs`. Requiring a
|
|
383
|
+
// same-directory doc silently exempted every such component — Heading
|
|
384
|
+
// among them. Fall back to whichever doc file documents the classes this
|
|
385
|
+
// directory renders.
|
|
386
|
+
//
|
|
387
|
+
// Both paths match the on-disk listing rather than existsSync: on
|
|
388
|
+
// case-insensitive filesystems existsSync would match a differently-cased
|
|
389
|
+
// doc file that CI never checks. (Same guard as derivedVarRegistry.test.ts.)
|
|
390
|
+
const docFiles = dirEntries.includes(`${dir}.doc.mjs`)
|
|
391
|
+
? [join(dirPath, `${dir}.doc.mjs`)]
|
|
392
|
+
: docFilesDocumenting(new Set(sites.map(s => s.className)));
|
|
393
|
+
if (docFiles.length === 0) {
|
|
276
394
|
continue;
|
|
277
395
|
}
|
|
278
396
|
|
|
279
397
|
const docBlocks: ComponentInfo['docBlocks'] = [];
|
|
280
|
-
for (const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
398
|
+
for (const docFile of docFiles) {
|
|
399
|
+
let mod: ComponentDocModule;
|
|
400
|
+
try {
|
|
401
|
+
mod = require(docFile) as ComponentDocModule;
|
|
402
|
+
} catch {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
for (const key of ['docs', 'docsZh'] as const) {
|
|
406
|
+
const targets = mod[key]?.theming?.targets;
|
|
407
|
+
// Only blocks that already document a theming surface are held to it —
|
|
408
|
+
// a doc with no theming block at all is a separate (documentation) gap.
|
|
409
|
+
if (targets != null) {
|
|
410
|
+
docBlocks.push({key, file: relative(SRC_DIR, docFile), targets});
|
|
411
|
+
}
|
|
286
412
|
}
|
|
287
413
|
}
|
|
288
414
|
if (docBlocks.length === 0) {
|
|
@@ -309,22 +435,22 @@ describe('theming.targets matches the themeProps() call sites', () => {
|
|
|
309
435
|
for (const {dir, sites, docBlocks} of components) {
|
|
310
436
|
const renderedClasses = [...new Set(sites.map(s => s.className))].sort();
|
|
311
437
|
|
|
312
|
-
for (const {key, targets} of docBlocks) {
|
|
438
|
+
for (const {key, file, targets} of docBlocks) {
|
|
313
439
|
const documented = new Set(targets.map(t => t.className));
|
|
314
440
|
|
|
315
|
-
it(`${dir} (${key}): every rendered class is documented`, () => {
|
|
441
|
+
it(`${dir} (${file} ${key}): every rendered class is documented`, () => {
|
|
316
442
|
const undocumented = renderedClasses.filter(c => !documented.has(c));
|
|
317
443
|
expect(
|
|
318
444
|
undocumented,
|
|
319
445
|
`${dir} renders ${undocumented.length} astryx-* class(es) that ` +
|
|
320
|
-
`${
|
|
446
|
+
`${file} ${key}.theming.targets does not document: ` +
|
|
321
447
|
`${undocumented.join(', ')}. An undocumented class is an ` +
|
|
322
448
|
`unthemeable element — theme authors and codegen read targets[] ` +
|
|
323
449
|
`to learn which selectors exist. Add {className: '...'} entries.`,
|
|
324
450
|
).toEqual([]);
|
|
325
451
|
});
|
|
326
452
|
|
|
327
|
-
it(`${dir} (${key}): every visual prop passed to themeProps is documented`, () => {
|
|
453
|
+
it(`${dir} (${file} ${key}): every visual prop passed to themeProps is documented`, () => {
|
|
328
454
|
const missing: string[] = [];
|
|
329
455
|
for (const site of sites) {
|
|
330
456
|
const target = targets.find(t => t.className === site.className);
|
|
@@ -343,7 +469,7 @@ describe('theming.targets matches the themeProps() call sites', () => {
|
|
|
343
469
|
}
|
|
344
470
|
expect(
|
|
345
471
|
[...new Set(missing)],
|
|
346
|
-
`${dir} passes prop keys to themeProps() that ${
|
|
472
|
+
`${dir} passes prop keys to themeProps() that ${file} ` +
|
|
347
473
|
`${key}.theming.targets does not list under visualProps/states. ` +
|
|
348
474
|
`Each one is a [data-*] selector consumers cannot discover.`,
|
|
349
475
|
).toEqual([]);
|
|
@@ -193,6 +193,24 @@ export const borderVars = stylex.defineVars(borderDefaults);
|
|
|
193
193
|
|
|
194
194
|
export type BorderVarName = keyof typeof borderDefaults;
|
|
195
195
|
|
|
196
|
+
// =============================================================================
|
|
197
|
+
// Focus Tokens
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// The keyboard focus ring, as one definition for the whole system. Values come
|
|
200
|
+
// from Design Conventions §User Interaction States: 2px --color-accent at 3px
|
|
201
|
+
// offset. Components read these through utils/focusOutline.stylex.ts; the
|
|
202
|
+
// `:focus-visible` condition stays in core, so a theme can restyle the ring
|
|
203
|
+
// but cannot show it to pointer users.
|
|
204
|
+
|
|
205
|
+
export const focusDefaults = {
|
|
206
|
+
'--focus-outline-width': '2px',
|
|
207
|
+
'--focus-outline-style': 'solid',
|
|
208
|
+
'--focus-outline-color': 'var(--color-accent)',
|
|
209
|
+
'--focus-outline-offset': '3px',
|
|
210
|
+
} as const;
|
|
211
|
+
|
|
212
|
+
export const focusVars = stylex.defineVars(focusDefaults);
|
|
213
|
+
|
|
196
214
|
// =============================================================================
|
|
197
215
|
// Radius Tokens
|
|
198
216
|
// =============================================================================
|
package/src/theme/tokens.test.ts
CHANGED
|
@@ -280,3 +280,40 @@ describe('resolveThemeTokens — generated themes end to end', () => {
|
|
|
280
280
|
expect(light['--color-icon-accent']).toBe(light['--color-accent']);
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
|
+
|
|
284
|
+
describe('focus outline tokens', () => {
|
|
285
|
+
it('default to the accent ring from Design Conventions', () => {
|
|
286
|
+
const tokens = resolveThemeTokens(null, {mode: 'light'});
|
|
287
|
+
|
|
288
|
+
expect(tokens['--focus-outline-width']).toBe('2px');
|
|
289
|
+
expect(tokens['--focus-outline-style']).toBe('solid');
|
|
290
|
+
expect(tokens['--focus-outline-offset']).toBe('3px');
|
|
291
|
+
// Tracks the theme's accent unless a theme says otherwise.
|
|
292
|
+
expect(tokens['--focus-outline-color']).toBe(tokens['--color-accent']);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('follows a theme accent without an explicit focus override', () => {
|
|
296
|
+
const theme = defineTheme({name: 'brand', color: {accent: '#AA0000'}});
|
|
297
|
+
const tokens = resolveThemeTokens(theme, {mode: 'light'});
|
|
298
|
+
|
|
299
|
+
expect(tokens['--focus-outline-color']).toBe(tokens['--color-accent']);
|
|
300
|
+
expect(tokens['--focus-outline-color']).not.toContain('var(');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('takes an explicit ring over the accent', () => {
|
|
304
|
+
const theme = defineTheme({
|
|
305
|
+
name: 'brand',
|
|
306
|
+
color: {accent: '#AA0000'},
|
|
307
|
+
tokens: {
|
|
308
|
+
'--focus-outline-color': '#00FF00',
|
|
309
|
+
'--focus-outline-width': '4px',
|
|
310
|
+
'--focus-outline-offset': '0px',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const tokens = resolveThemeTokens(theme, {mode: 'light'});
|
|
314
|
+
|
|
315
|
+
expect(tokens['--focus-outline-color']).toBe('#00FF00');
|
|
316
|
+
expect(tokens['--focus-outline-width']).toBe('4px');
|
|
317
|
+
expect(tokens['--focus-outline-offset']).toBe('0px');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -7,28 +7,42 @@
|
|
|
7
7
|
* @position Internal utility for consistent keyboard focus outlines across core components
|
|
8
8
|
*
|
|
9
9
|
* Centralizes the standard Astryx focus outline, shown only for keyboard focus.
|
|
10
|
-
* Every focusable surface drew the same 2px accent ring
|
|
11
|
-
*
|
|
10
|
+
* Every focusable surface drew the same 2px accent ring already; writing it per
|
|
11
|
+
* component meant dozens of identical definitions to keep in step, and they did
|
|
12
|
+
* not stay in step — offsets drifted and one ring was a border-width thick.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Every ring in core and lab is drawn from here, with one exception recorded in
|
|
15
|
+
* Switch: its condition is a component-scoped ancestor marker, which cannot be
|
|
16
|
+
* shared without leaking focus state between components. A component may still
|
|
17
|
+
* override the OFFSET (a ring that must sit inset, or clear of a field border);
|
|
18
|
+
* width, style and color are not restated anywhere.
|
|
19
|
+
*
|
|
20
|
+
* Every value comes from the `--focus-outline-*` tokens, which is how a theme
|
|
21
|
+
* restyles the ring: one override reaches every component at once. The
|
|
22
|
+
* `:focus-visible` condition is not themeable and stays here, so a theme can
|
|
23
|
+
* change what the ring looks like but cannot show it to pointer users.
|
|
20
24
|
*/
|
|
21
25
|
|
|
22
26
|
import * as stylex from '@stylexjs/stylex';
|
|
23
|
-
import {
|
|
27
|
+
import {focusVars} from '../theme/tokens.stylex';
|
|
28
|
+
|
|
29
|
+
const FOCUS_OUTLINE_WIDTH = focusVars['--focus-outline-width'];
|
|
30
|
+
const FOCUS_OUTLINE_STYLE = focusVars['--focus-outline-style'];
|
|
31
|
+
const FOCUS_OUTLINE_COLOR = focusVars['--focus-outline-color'];
|
|
32
|
+
|
|
33
|
+
const FOCUS_OUTLINE_OFFSET = focusVars['--focus-outline-offset'];
|
|
24
34
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The ring as a single `outline` value, for reading it at runtime — an
|
|
37
|
+
* assertion, or an element styled imperatively.
|
|
38
|
+
*
|
|
39
|
+
* NOT usable inside another file's `stylex.create`: StyleX resolves imported
|
|
40
|
+
* `defineVars` and nothing else, so a component whose condition
|
|
41
|
+
* {@link focusOutlineStyles} cannot express (Switch, whose ring keys off a
|
|
42
|
+
* component-scoped ancestor marker) has to compose the `focusVars` tokens
|
|
43
|
+
* itself.
|
|
44
|
+
*/
|
|
45
|
+
export const FOCUS_OUTLINE = `${FOCUS_OUTLINE_WIDTH} ${FOCUS_OUTLINE_STYLE} ${FOCUS_OUTLINE_COLOR}`;
|
|
32
46
|
|
|
33
47
|
/**
|
|
34
48
|
* Written as longhands rather than the `outline` shorthand.
|
|
@@ -40,7 +54,7 @@ const FOCUS_OUTLINE_COLOR = colorVars['--color-accent'];
|
|
|
40
54
|
*/
|
|
41
55
|
const focusOutlineLonghands = {
|
|
42
56
|
outlineWidth: FOCUS_OUTLINE_WIDTH,
|
|
43
|
-
outlineStyle:
|
|
57
|
+
outlineStyle: FOCUS_OUTLINE_STYLE,
|
|
44
58
|
outlineColor: FOCUS_OUTLINE_COLOR,
|
|
45
59
|
} as const;
|
|
46
60
|
|
|
@@ -53,8 +67,8 @@ const focusOutlineLonghands = {
|
|
|
53
67
|
* Longhands, not the `outline` shorthand, for the same reason the styles are:
|
|
54
68
|
* a shorthand resets every longhand it covers, so a caller could not re-color
|
|
55
69
|
* the ring without restating its width and style. Split, each part is
|
|
56
|
-
* independently overridable — and
|
|
57
|
-
*
|
|
70
|
+
* independently overridable — and every part stays a var, so a theme's
|
|
71
|
+
* `--focus-outline-*` overrides still flow through.
|
|
58
72
|
*
|
|
59
73
|
* Keys are camelCase to match `HTMLElement.style`, so this spreads straight
|
|
60
74
|
* onto an element:
|
|
@@ -66,7 +80,7 @@ const focusOutlineLonghands = {
|
|
|
66
80
|
*/
|
|
67
81
|
export const FOCUS_OUTLINE_PARTS = {
|
|
68
82
|
outlineWidth: FOCUS_OUTLINE_WIDTH,
|
|
69
|
-
outlineStyle:
|
|
83
|
+
outlineStyle: FOCUS_OUTLINE_STYLE,
|
|
70
84
|
outlineColor: FOCUS_OUTLINE_COLOR,
|
|
71
85
|
outlineOffset: FOCUS_OUTLINE_OFFSET,
|
|
72
86
|
} as const;
|
|
@@ -113,7 +127,7 @@ export const focusOutlineStyles = stylex.create({
|
|
|
113
127
|
publishFocusVisibleVars: {
|
|
114
128
|
'--_focus-outline': {
|
|
115
129
|
default: 'none',
|
|
116
|
-
':focus-visible':
|
|
130
|
+
':focus-visible': FOCUS_OUTLINE,
|
|
117
131
|
},
|
|
118
132
|
'--_focus-outline-offset': {
|
|
119
133
|
default: '0',
|
|
@@ -123,7 +137,7 @@ export const focusOutlineStyles = stylex.create({
|
|
|
123
137
|
focusWithinOrPublished: {
|
|
124
138
|
outline: {
|
|
125
139
|
default: 'var(--_focus-outline, none)',
|
|
126
|
-
':has(:focus-visible)':
|
|
140
|
+
':has(:focus-visible)': FOCUS_OUTLINE,
|
|
127
141
|
},
|
|
128
142
|
outlineOffset: {
|
|
129
143
|
default: 'var(--_focus-outline-offset, 0)',
|
package/src/utils/index.ts
CHANGED
|
@@ -101,3 +101,7 @@ export type {RGBA} from './color';
|
|
|
101
101
|
export {devWarn, devError, warnOnce, formatDevMessage} from './devWarning';
|
|
102
102
|
|
|
103
103
|
export {rtlStyles} from './rtlStyles';
|
|
104
|
+
|
|
105
|
+
// The shared focus ring, exported for @astryxdesign/lab — same reason
|
|
106
|
+
// rtlStyles is: a style that must be identical across packages, not copied.
|
|
107
|
+
export {focusOutlineStyles, focusOutlineProps} from './focusOutline.stylex';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file interactionModality.ts
|
|
5
|
+
* @input Global pointerdown/keydown events
|
|
6
|
+
* @output `getInteractionModality()` — how the user last interacted
|
|
7
|
+
* @position Internal utility; used where `:focus-visible` alone is too broad
|
|
8
|
+
*
|
|
9
|
+
* `:focus-visible` is the right selector for a focus ring and should stay the
|
|
10
|
+
* condition that draws one. It is not, on its own, "focused by keyboard":
|
|
11
|
+
* per CSS Selectors 4 a pointer-focused element that supports text entry
|
|
12
|
+
* matches it too, deliberately, so that a clicked text field still shows where
|
|
13
|
+
* typing will go. Measured in Chromium — a bare `<input>` clicked with a mouse
|
|
14
|
+
* matches `:focus-visible`.
|
|
15
|
+
*
|
|
16
|
+
* A component that wants a ring for keyboard focus ONLY therefore needs one bit
|
|
17
|
+
* `:focus-visible` cannot give it: which device moved focus. This records that,
|
|
18
|
+
* to be used as a gate ALONGSIDE `:focus-visible`, never as a replacement for
|
|
19
|
+
* it — the browser's heuristic still decides everything else, including
|
|
20
|
+
* `focus({focusVisible: true})`.
|
|
21
|
+
*
|
|
22
|
+
* Listeners are registered once for the document, in the capture phase so they
|
|
23
|
+
* run before the focus they explain, and are never removed: two passive
|
|
24
|
+
* listeners for the whole app, versus a pair per mounted input.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export type InteractionModality = 'keyboard' | 'pointer';
|
|
28
|
+
|
|
29
|
+
// Default to keyboard: with no interaction yet, focus arrived programmatically
|
|
30
|
+
// or from the browser restoring it, and showing a ring is the safe error.
|
|
31
|
+
let modality: InteractionModality = 'keyboard';
|
|
32
|
+
let isListening = false;
|
|
33
|
+
|
|
34
|
+
function onPointerDown(): void {
|
|
35
|
+
modality = 'pointer';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
39
|
+
// Modifier-only presses are not navigation — holding Shift before a click
|
|
40
|
+
// must not turn that click into "keyboard".
|
|
41
|
+
if (event.metaKey || event.altKey || event.ctrlKey) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
modality = 'keyboard';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Start tracking, once per document. Safe to call repeatedly and on the server
|
|
49
|
+
* (where it does nothing).
|
|
50
|
+
*/
|
|
51
|
+
export function trackInteractionModality(): void {
|
|
52
|
+
if (isListening || typeof document === 'undefined') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
isListening = true;
|
|
56
|
+
document.addEventListener('pointerdown', onPointerDown, {
|
|
57
|
+
capture: true,
|
|
58
|
+
passive: true,
|
|
59
|
+
});
|
|
60
|
+
document.addEventListener('keydown', onKeyDown, {
|
|
61
|
+
capture: true,
|
|
62
|
+
passive: true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** How the user last interacted with the page. */
|
|
67
|
+
export function getInteractionModality(): InteractionModality {
|
|
68
|
+
return modality;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Test-only: restore the initial state between cases. */
|
|
72
|
+
export function __resetInteractionModalityForTest(): void {
|
|
73
|
+
modality = 'keyboard';
|
|
74
|
+
}
|