@astryxdesign/core 0.4.0 → 0.4.1
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 +2 -2
- 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/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/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/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/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/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 +18 -17
- package/dist/astryx.umd.js +45 -45
- package/dist/astryx.umd.js.map +4 -4
- 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 +40 -0
- package/locales/pseudo.json +30 -0
- package/package.json +2 -2
- 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 +5 -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/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/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/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/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/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +91 -14
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- 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/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/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/locales/en.json
CHANGED
|
@@ -639,6 +639,10 @@
|
|
|
639
639
|
"defaultMessage": "Clear {label}",
|
|
640
640
|
"description": "Aria label for the clear-value button in DateInput / DateRangeInput / DateTimeInput. `{label}` is the field's own label so screen readers announce e.g. 'Clear Start date'."
|
|
641
641
|
},
|
|
642
|
+
"@astryx.dateInput.invalidDate": {
|
|
643
|
+
"defaultMessage": "Invalid date",
|
|
644
|
+
"description": "Screen-reader-only live-region alert in DateInput / DateTimeInput when the typed date cannot be parsed. The field silently reverts on blur, so this is the only rejection feedback a screen-reader user gets."
|
|
645
|
+
},
|
|
642
646
|
"@astryx.dateTimeInput.timeSuffix": {
|
|
643
647
|
"defaultMessage": "{label} time",
|
|
644
648
|
"description": "Screen-reader-only accessible name for the time-of-day slot in a DateTimeInput. Example: `Start date time`; reorder freely (e.g. \"time of {label}\")."
|
|
@@ -711,6 +715,10 @@
|
|
|
711
715
|
"defaultMessage": "Select a time",
|
|
712
716
|
"description": "\"Time\" = clock time (HH:MM), not duration or era. Grey placeholder inside an empty TimeInput field; imperative verb."
|
|
713
717
|
},
|
|
718
|
+
"@astryx.timeInput.invalidTime": {
|
|
719
|
+
"defaultMessage": "Invalid time",
|
|
720
|
+
"description": "Screen-reader-only live-region alert in TimeInput / DateTimeInput when the typed time cannot be parsed. \"Time\" = clock time (HH:MM). The field silently reverts on blur, so this is the only rejection feedback a screen-reader user gets."
|
|
721
|
+
},
|
|
714
722
|
"@astryx.topNav.heading.dialogLabel": {
|
|
715
723
|
"defaultMessage": "Navigation menu",
|
|
716
724
|
"description": "Screen-reader-only accessible name for the dropdown dialog opened from a TopNavHeading's overflow menu. Kept separate from the SideNav sibling."
|
|
@@ -807,6 +815,38 @@
|
|
|
807
815
|
"defaultMessage": "Clear {label}",
|
|
808
816
|
"description": "Screen-reader-only label on the X that removes the selected file from a FileInput. Example: `Clear Attachment`, `Clear Résumé`."
|
|
809
817
|
},
|
|
818
|
+
"@astryx.fileInput.dropHint": {
|
|
819
|
+
"defaultMessage": "Drop files here",
|
|
820
|
+
"description": "Text shown in the dropzone area when a user is dragging files over the FileInput."
|
|
821
|
+
},
|
|
822
|
+
"@astryx.fileInput.errorInvalidType": {
|
|
823
|
+
"defaultMessage": "\"{fileName}\" is not an accepted file type",
|
|
824
|
+
"description": "Validation error shown when a selected file does not match the accepted types."
|
|
825
|
+
},
|
|
826
|
+
"@astryx.fileInput.errorMaxFiles": {
|
|
827
|
+
"defaultMessage": "Maximum {maxFiles} files allowed",
|
|
828
|
+
"description": "Validation error shown when the number of selected files exceeds the maximum."
|
|
829
|
+
},
|
|
830
|
+
"@astryx.fileInput.errorMaxSize": {
|
|
831
|
+
"defaultMessage": "\"{fileName}\" exceeds {maxSize} limit",
|
|
832
|
+
"description": "Validation error shown when a selected file exceeds the maximum size. {maxSize} is a formatted size string like \"2.0 MB\"."
|
|
833
|
+
},
|
|
834
|
+
"@astryx.fileInput.fileSelected": {
|
|
835
|
+
"defaultMessage": "1 file selected: {fileName}",
|
|
836
|
+
"description": "Live-region announcement when a single file is successfully selected."
|
|
837
|
+
},
|
|
838
|
+
"@astryx.fileInput.filesSelected": {
|
|
839
|
+
"defaultMessage": "{count} files selected",
|
|
840
|
+
"description": "Live-region announcement when multiple files are successfully selected."
|
|
841
|
+
},
|
|
842
|
+
"@astryx.fileInput.placeholder": {
|
|
843
|
+
"defaultMessage": "Choose file",
|
|
844
|
+
"description": "Default placeholder text shown when no file is selected in a single-file FileInput."
|
|
845
|
+
},
|
|
846
|
+
"@astryx.fileInput.placeholderMultiple": {
|
|
847
|
+
"defaultMessage": "Choose files",
|
|
848
|
+
"description": "Default placeholder text shown when no files are selected in a multi-file FileInput."
|
|
849
|
+
},
|
|
810
850
|
"@astryx.fileInput.required": {
|
|
811
851
|
"defaultMessage": "Required",
|
|
812
852
|
"description": "Screen-reader-only description on the FileInput trigger indicating the field must be filled in. Mirrors the visible `Required` indicator next to the field label; conveyed via description because aria-required is not supported on role=\"button\"."
|
package/locales/pseudo.json
CHANGED
|
@@ -479,6 +479,9 @@
|
|
|
479
479
|
"@astryx.dateInput.clear": {
|
|
480
480
|
"defaultMessage": "⟦Çłéàř {label}⟧"
|
|
481
481
|
},
|
|
482
|
+
"@astryx.dateInput.invalidDate": {
|
|
483
|
+
"defaultMessage": "⟦Íñṽàłíð ðàţé⟧"
|
|
484
|
+
},
|
|
482
485
|
"@astryx.dateTimeInput.timeSuffix": {
|
|
483
486
|
"defaultMessage": "⟦{label} ţíɱé⟧"
|
|
484
487
|
},
|
|
@@ -533,6 +536,9 @@
|
|
|
533
536
|
"@astryx.timeInput.placeholder": {
|
|
534
537
|
"defaultMessage": "⟦Šéłéçţ à ţíɱé⟧"
|
|
535
538
|
},
|
|
539
|
+
"@astryx.timeInput.invalidTime": {
|
|
540
|
+
"defaultMessage": "⟦Íñṽàłíð ţíɱé⟧"
|
|
541
|
+
},
|
|
536
542
|
"@astryx.topNav.heading.dialogLabel": {
|
|
537
543
|
"defaultMessage": "⟦Ñàṽíĝàţíóñ ɱéñú⟧"
|
|
538
544
|
},
|
|
@@ -605,6 +611,30 @@
|
|
|
605
611
|
"@astryx.fileInput.clearLabel": {
|
|
606
612
|
"defaultMessage": "⟦Çłéàř {label}⟧"
|
|
607
613
|
},
|
|
614
|
+
"@astryx.fileInput.dropHint": {
|
|
615
|
+
"defaultMessage": "⟦Ðřóþ ƒíłéš ĥéřé⟧"
|
|
616
|
+
},
|
|
617
|
+
"@astryx.fileInput.errorInvalidType": {
|
|
618
|
+
"defaultMessage": "⟦\"{fileName}\" íš ñóţ àñ àççéþţéð ƒíłé ţýþé⟧"
|
|
619
|
+
},
|
|
620
|
+
"@astryx.fileInput.errorMaxFiles": {
|
|
621
|
+
"defaultMessage": "⟦Ṁàẋíɱúɱ {maxFiles} ƒíłéš àłłóŵéð⟧"
|
|
622
|
+
},
|
|
623
|
+
"@astryx.fileInput.errorMaxSize": {
|
|
624
|
+
"defaultMessage": "⟦\"{fileName}\" éẋçééðš {maxSize} łíɱíţ⟧"
|
|
625
|
+
},
|
|
626
|
+
"@astryx.fileInput.fileSelected": {
|
|
627
|
+
"defaultMessage": "⟦1 ƒíłé šéłéçţéð: {fileName}⟧"
|
|
628
|
+
},
|
|
629
|
+
"@astryx.fileInput.filesSelected": {
|
|
630
|
+
"defaultMessage": "⟦{count} ƒíłéš šéłéçţéð⟧"
|
|
631
|
+
},
|
|
632
|
+
"@astryx.fileInput.placeholder": {
|
|
633
|
+
"defaultMessage": "⟦Çĥóóšé ƒíłé⟧"
|
|
634
|
+
},
|
|
635
|
+
"@astryx.fileInput.placeholderMultiple": {
|
|
636
|
+
"defaultMessage": "⟦Çĥóóšé ƒíłéš⟧"
|
|
637
|
+
},
|
|
608
638
|
"@astryx.fileInput.required": {
|
|
609
639
|
"defaultMessage": "⟦Řéɋúířéð⟧"
|
|
610
640
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"displayName": "Astryx 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",
|
|
@@ -663,7 +663,7 @@
|
|
|
663
663
|
"@testing-library/jest-dom": "^6.6.0",
|
|
664
664
|
"@testing-library/react": "^16.3.2",
|
|
665
665
|
"rimraf": "^6.0.1",
|
|
666
|
-
"@astryxdesign/cli": "0.4.
|
|
666
|
+
"@astryxdesign/cli": "0.4.1"
|
|
667
667
|
},
|
|
668
668
|
"dependencies": {
|
|
669
669
|
"intl-messageformat": "^11.2.9"
|
|
@@ -203,8 +203,11 @@ export const docsDense = {
|
|
|
203
203
|
bestPractices: [
|
|
204
204
|
{guidance: true, description: 'Choose the right height: use "fill" for dashboards with internal scrolling and "auto" for pages that grow with content.'},
|
|
205
205
|
{guidance: true, description: 'Set `contentPadding` based on content type: 4 for forms and settings, 0 for tables and dashboards.'},
|
|
206
|
+
{guidance: true, description: 'Give every nav slot an accessible name: TopNav and SideNav are separate navigation landmarks, so pass `label` to each.'},
|
|
207
|
+
{guidance: true, description: 'Start the page heading inside `children`: AppShell owns the skip link, banner, and main landmarks but renders no heading, so the first content heading is the page h1.'},
|
|
206
208
|
{guidance: false, description: "Nest one AppShell inside another; it's the outermost layout frame."},
|
|
207
209
|
{guidance: false, description: 'Use for sub-page layouts; use Layout for content areas within AppShell.'},
|
|
210
|
+
{guidance: false, description: 'Add your own skip link or <main>; AppShell renders both, and a second main landmark makes the first ambiguous.'},
|
|
208
211
|
],
|
|
209
212
|
},
|
|
210
213
|
propDescriptions: {
|
package/src/AspectRatio/index.ts
CHANGED
package/src/Badge/Badge.tsx
CHANGED
package/src/Badge/index.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Banner.test.tsx
|
|
5
5
|
* @input Uses vitest, @testing-library/react, Banner component
|
|
6
|
-
* @output Unit tests for Banner component behavior
|
|
6
|
+
* @output Unit tests for Banner component behavior, including the
|
|
7
|
+
* 'banner-icon' theme target riding on the status icon glyph (#4166)
|
|
7
8
|
* @position Testing; validates Banner.tsx implementation
|
|
8
9
|
*
|
|
9
10
|
* SYNC: When modified, update this header
|
|
@@ -337,6 +338,62 @@ describe('Banner', () => {
|
|
|
337
338
|
expect(ref.current).toBeInstanceOf(HTMLDivElement);
|
|
338
339
|
});
|
|
339
340
|
|
|
341
|
+
// =========================================================================
|
|
342
|
+
// Status icon color theming (#4166)
|
|
343
|
+
// =========================================================================
|
|
344
|
+
|
|
345
|
+
it("carries the 'banner-icon' theme target on the default status icon glyph", () => {
|
|
346
|
+
// Theme overrides for 'banner-icon' + 'status:X' compile to
|
|
347
|
+
// '.astryx-banner-icon.<status>' (parseStyleKey). The target must sit on
|
|
348
|
+
// the <Icon> span itself so those same-element rules in
|
|
349
|
+
// @layer astryx-theme beat the Icon's own color variant.
|
|
350
|
+
const statuses = ['info', 'warning', 'error', 'success'] as const;
|
|
351
|
+
for (const status of statuses) {
|
|
352
|
+
const {container, unmount} = render(
|
|
353
|
+
<Banner status={status} title={`${status} banner`} />,
|
|
354
|
+
);
|
|
355
|
+
const glyph = container.querySelector(
|
|
356
|
+
`.astryx-icon.astryx-banner-icon.${status}`,
|
|
357
|
+
);
|
|
358
|
+
expect(glyph).not.toBeNull();
|
|
359
|
+
expect(glyph).toHaveAttribute('data-status', status);
|
|
360
|
+
// Exactly one element carries the target — the layout wrapper no
|
|
361
|
+
// longer does.
|
|
362
|
+
expect(container.querySelectorAll('.astryx-banner-icon')).toHaveLength(1);
|
|
363
|
+
unmount();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('keeps the color variant on the theme-target element (regression pin for #4166)', () => {
|
|
368
|
+
// Pre-fix, '.astryx-banner-icon.info' matched the layout wrapper while
|
|
369
|
+
// the color variant (data-color="accent") sat on an inner span that a
|
|
370
|
+
// theme override could never reach. Target and paint now share one
|
|
371
|
+
// element.
|
|
372
|
+
const {container} = render(<Banner status="info" title="Info" />);
|
|
373
|
+
const target = container.querySelector('.astryx-banner-icon.info');
|
|
374
|
+
expect(target).toHaveAttribute('data-color', 'accent');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("keeps the 'banner-icon' target on the wrapper for a custom icon node", () => {
|
|
378
|
+
// Core never injects props into consumer elements, so with a custom
|
|
379
|
+
// `icon` the target stays on the (layout-only) wrapper and overrides
|
|
380
|
+
// reach the node via inheritance. The node itself is untouched.
|
|
381
|
+
const {container} = render(
|
|
382
|
+
<Banner
|
|
383
|
+
status="info"
|
|
384
|
+
title="Custom icon"
|
|
385
|
+
icon={<span data-testid="custom-glyph">i</span>}
|
|
386
|
+
/>,
|
|
387
|
+
);
|
|
388
|
+
const targets = container.querySelectorAll('.astryx-banner-icon');
|
|
389
|
+
expect(targets).toHaveLength(1);
|
|
390
|
+
expect(targets[0]?.tagName).toBe('DIV');
|
|
391
|
+
expect(targets[0]).toHaveAttribute('aria-hidden', 'true');
|
|
392
|
+
const custom = container.querySelector('[data-testid="custom-glyph"]');
|
|
393
|
+
expect(custom).not.toBeNull();
|
|
394
|
+
expect(custom?.className).toBe('');
|
|
395
|
+
});
|
|
396
|
+
|
|
340
397
|
// =========================================================================
|
|
341
398
|
// Icon registry integration
|
|
342
399
|
// =========================================================================
|
package/src/Banner/Banner.tsx
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - Root container: layout-only wrapper (flex column), no visual styling, no theme target
|
|
13
13
|
* - Header area (themeProps 'banner'): colored status background with icon, title, description, actions, dismiss
|
|
14
14
|
* - Content area (themeProps 'banner-content'): collapsible card background for additional content (children)
|
|
15
|
+
* - Status icon (themeProps 'banner-icon'): the target rides on the default
|
|
16
|
+
* <Icon> itself — the element that paints — so 'status:X' overrides reach
|
|
17
|
+
* the glyph (#4166); for a custom `icon` node it stays on the layout wrapper
|
|
15
18
|
* - No left border accent — color is expressed through the full header background
|
|
16
19
|
* - Each visual area owns its own border-radius (no overflow:clip on the container)
|
|
17
20
|
* - When children are provided, a collapse/expand toggle button appears in the end area
|
|
@@ -445,16 +448,32 @@ export function Banner({
|
|
|
445
448
|
: styles.headerCardStandalone),
|
|
446
449
|
),
|
|
447
450
|
)}>
|
|
451
|
+
{/* The 'banner-icon' target rides on the element that paints: the
|
|
452
|
+
default <Icon> below, or this wrapper when a custom `icon` node
|
|
453
|
+
is passed (core never injects props into consumer elements, so
|
|
454
|
+
overrides reach it via inheritance). The wrapper itself stays
|
|
455
|
+
layout-only. */}
|
|
448
456
|
<div
|
|
449
|
-
{...
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
457
|
+
{...(icon != null
|
|
458
|
+
? mergeProps(
|
|
459
|
+
themeProps('banner-icon', {status}),
|
|
460
|
+
stylex.props(styles.iconWrapper),
|
|
461
|
+
)
|
|
462
|
+
: stylex.props(styles.iconWrapper))}
|
|
453
463
|
aria-hidden="true">
|
|
454
464
|
{icon != null ? (
|
|
455
465
|
icon
|
|
456
466
|
) : (
|
|
457
|
-
<Icon
|
|
467
|
+
// Applied to the status <Icon> itself rather than the wrapper, so
|
|
468
|
+
// the element that paints the glyph is the element a theme
|
|
469
|
+
// targets — a 'banner-icon' 'status:X' color override beats the
|
|
470
|
+
// Icon's own variant from @layer astryx-theme (#4166).
|
|
471
|
+
<Icon
|
|
472
|
+
icon={defaultIconName}
|
|
473
|
+
size="md"
|
|
474
|
+
color={iconColor}
|
|
475
|
+
{...themeProps('banner-icon', {status})}
|
|
476
|
+
/>
|
|
458
477
|
)}
|
|
459
478
|
</div>
|
|
460
479
|
<div {...stylex.props(styles.headerContent)}>
|
package/src/Blockquote/index.ts
CHANGED
|
@@ -163,7 +163,7 @@ export const docs = {
|
|
|
163
163
|
],
|
|
164
164
|
vars: [
|
|
165
165
|
{name: '--_button-radius', description: 'Border radius', default: 'var(--radius-element)', private: true},
|
|
166
|
-
{name: '--button-focus-offset', description: 'Focus ring outline offset', default: '
|
|
166
|
+
{name: '--button-focus-offset', description: 'Focus ring outline offset', default: 'var(--focus-outline-offset)'},
|
|
167
167
|
{name: '--button-icon-only-aspect', description: 'Aspect ratio for icon-only buttons', default: '1 / 1'},
|
|
168
168
|
],
|
|
169
169
|
derived: [
|
|
@@ -245,7 +245,7 @@ export const docsZh = {
|
|
|
245
245
|
],
|
|
246
246
|
vars: [
|
|
247
247
|
{name: '--_button-radius', description: '圆角半径', default: 'var(--radius-element)', private: true},
|
|
248
|
-
{name: '--button-focus-offset', description: '焦点环轮廓偏移', default: '
|
|
248
|
+
{name: '--button-focus-offset', description: '焦点环轮廓偏移', default: 'var(--focus-outline-offset)'},
|
|
249
249
|
{name: '--button-icon-only-aspect', description: '纯图标按钮的宽高比', default: '1 / 1'},
|
|
250
250
|
],
|
|
251
251
|
derived: [
|
package/src/Button/Button.tsx
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
fontWeightVars,
|
|
35
35
|
typeScaleVars,
|
|
36
36
|
shadowVars,
|
|
37
|
+
focusVars,
|
|
37
38
|
} from '../theme/tokens.stylex';
|
|
38
39
|
import {Spinner} from '../Spinner';
|
|
39
40
|
import {VisuallyHidden} from '../VisuallyHidden';
|
|
@@ -57,10 +58,10 @@ import type {ButtonVariantMap} from './index';
|
|
|
57
58
|
const styles = stylex.create({
|
|
58
59
|
base: {
|
|
59
60
|
// Kept as a public themeable var (documented in Button.doc.mjs) even though
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
'--button-focus-offset': '
|
|
61
|
+
// it now defaults to the shared token: removing it would break any theme
|
|
62
|
+
// setting it, for no gain. It overrides the shared offset, so a theme can
|
|
63
|
+
// still tune the ring distance on buttons specifically.
|
|
64
|
+
'--button-focus-offset': focusVars['--focus-outline-offset'],
|
|
64
65
|
outlineOffset: {
|
|
65
66
|
default: '0',
|
|
66
67
|
':focus-visible': 'var(--button-focus-offset)',
|
package/src/Card/Card.tsx
CHANGED
package/src/Card/index.ts
CHANGED
package/src/Center/Center.tsx
CHANGED
package/src/Center/index.ts
CHANGED
|
@@ -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} />);
|