@astryxdesign/core 0.2.0-canary.ea693ac → 0.2.0

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.
Files changed (121) hide show
  1. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
  2. package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
  3. package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -6
  4. package/dist/Button/Button.d.ts +1 -1
  5. package/dist/Button/Button.d.ts.map +1 -1
  6. package/dist/Button/Button.js +2 -4
  7. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  8. package/dist/CheckboxInput/CheckboxInput.js +2 -2
  9. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  10. package/dist/CheckboxList/CheckboxListItem.js +4 -19
  11. package/dist/CodeBlock/CodeBlock.js +2 -2
  12. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  13. package/dist/ContextMenu/ContextMenu.js +1 -10
  14. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +11 -8
  15. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
  16. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +1 -3
  17. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
  18. package/dist/DropdownMenu/DropdownMenuRadioItem.js +1 -1
  19. package/dist/Field/FieldLabel.d.ts +1 -2
  20. package/dist/Field/FieldLabel.d.ts.map +1 -1
  21. package/dist/Field/FieldLabel.js +3 -7
  22. package/dist/Item/Item.d.ts +3 -16
  23. package/dist/Item/Item.d.ts.map +1 -1
  24. package/dist/Item/Item.js +6 -31
  25. package/dist/List/ListItem.d.ts +1 -11
  26. package/dist/List/ListItem.d.ts.map +1 -1
  27. package/dist/List/ListItem.js +0 -2
  28. package/dist/MetadataList/MetadataList.d.ts +1 -1
  29. package/dist/MetadataList/MetadataList.d.ts.map +1 -1
  30. package/dist/MetadataList/MetadataList.js +2 -4
  31. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  32. package/dist/MultiSelector/MultiSelector.js +2 -13
  33. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  34. package/dist/RadioList/RadioListItem.js +1 -1
  35. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  36. package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
  37. package/dist/Selector/Selector.d.ts.map +1 -1
  38. package/dist/Selector/Selector.js +0 -5
  39. package/dist/Skeleton/Skeleton.d.ts.map +1 -1
  40. package/dist/Skeleton/Skeleton.js +2 -2
  41. package/dist/Switch/Switch.d.ts.map +1 -1
  42. package/dist/Switch/Switch.js +4 -15
  43. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  44. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +1 -1
  45. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  46. package/dist/Toast/ToastViewport.js +9 -61
  47. package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
  48. package/dist/ToggleButton/ToggleButton.js +1 -3
  49. package/dist/Token/Token.d.ts +1 -4
  50. package/dist/Token/Token.d.ts.map +1 -1
  51. package/dist/Token/Token.js +43 -68
  52. package/dist/Typeahead/BaseTypeahead.d.ts +0 -9
  53. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  54. package/dist/Typeahead/BaseTypeahead.js +0 -2
  55. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  56. package/dist/Typeahead/Typeahead.js +1 -8
  57. package/dist/astryx.css +3 -16
  58. package/dist/astryx.umd.js +48 -48
  59. package/dist/astryx.umd.js.map +4 -4
  60. package/dist/hooks/useKeyboardHint.d.ts +1 -1
  61. package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
  62. package/dist/hooks/useKeyboardHint.js +2 -4
  63. package/locales/en.json +0 -32
  64. package/locales/pseudo.json +0 -24
  65. package/package.json +1 -1
  66. package/src/AvatarGroup/AvatarGroup.test.tsx +1 -21
  67. package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -4
  68. package/src/Breadcrumbs/Breadcrumbs.test.tsx +1 -1
  69. package/src/Button/Button.test.tsx +0 -16
  70. package/src/Button/Button.tsx +2 -4
  71. package/src/CheckboxInput/CheckboxInput.test.tsx +0 -23
  72. package/src/CheckboxInput/CheckboxInput.tsx +2 -17
  73. package/src/CheckboxList/CheckboxList.test.tsx +3 -94
  74. package/src/CheckboxList/CheckboxListItem.tsx +10 -17
  75. package/src/CodeBlock/CodeBlock.test.tsx +0 -17
  76. package/src/CodeBlock/CodeBlock.tsx +2 -2
  77. package/src/ContextMenu/ContextMenu.test.tsx +2 -17
  78. package/src/ContextMenu/ContextMenu.tsx +1 -10
  79. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +12 -4
  80. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -9
  81. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
  82. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +2 -20
  83. package/src/Field/FieldLabel.test.tsx +0 -25
  84. package/src/Field/FieldLabel.tsx +2 -11
  85. package/src/Item/Item.doc.mjs +0 -5
  86. package/src/Item/Item.test.tsx +0 -88
  87. package/src/Item/Item.tsx +11 -57
  88. package/src/List/ListItem.doc.mjs +0 -12
  89. package/src/List/ListItem.tsx +0 -13
  90. package/src/MetadataList/MetadataList.test.tsx +0 -24
  91. package/src/MetadataList/MetadataList.tsx +2 -6
  92. package/src/MultiSelector/MultiSelector.test.tsx +0 -76
  93. package/src/MultiSelector/MultiSelector.tsx +2 -22
  94. package/src/RadioList/RadioList.test.tsx +4 -26
  95. package/src/RadioList/RadioListItem.tsx +1 -8
  96. package/src/SegmentedControl/SegmentedControl.test.tsx +0 -28
  97. package/src/SegmentedControl/SegmentedControlItem.tsx +2 -20
  98. package/src/Selector/Selector.test.tsx +0 -22
  99. package/src/Selector/Selector.tsx +1 -8
  100. package/src/Skeleton/Skeleton.test.tsx +0 -16
  101. package/src/Skeleton/Skeleton.tsx +1 -13
  102. package/src/Switch/Switch.test.tsx +0 -37
  103. package/src/Switch/Switch.tsx +3 -52
  104. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -19
  105. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +1 -3
  106. package/src/Toast/ToastViewport.test.tsx +8 -218
  107. package/src/Toast/ToastViewport.tsx +12 -71
  108. package/src/ToggleButton/ToggleButton.test.tsx +0 -24
  109. package/src/ToggleButton/ToggleButton.tsx +1 -19
  110. package/src/Token/Token.test.tsx +0 -145
  111. package/src/Token/Token.tsx +33 -73
  112. package/src/Typeahead/BaseTypeahead.tsx +0 -12
  113. package/src/Typeahead/Typeahead.test.tsx +0 -62
  114. package/src/Typeahead/Typeahead.tsx +0 -6
  115. package/src/hooks/useKeyboardHint.test.tsx +0 -22
  116. package/src/hooks/useKeyboardHint.tsx +2 -6
  117. package/dist/Token/TokenLink.d.ts +0 -62
  118. package/dist/Token/TokenLink.d.ts.map +0 -1
  119. package/dist/Token/TokenLink.js +0 -76
  120. package/src/Token/TokenLink.tsx +0 -107
  121. package/src/__tests__/forcedColors.ts +0 -60
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @file useKeyboardHint.tsx
3
- * @input Uses React, StyleX, Kbd, useLayer, i18n (useTranslator)
3
+ * @input Uses React, StyleX, Kbd, useLayer
4
4
  * @output Exports useKeyboardHint hook — ephemeral arrow-key navigation hint
5
5
  * @position Core hook; shows sighted keyboard users how to navigate composite
6
6
  * widgets that use roving tabindex (single Tab stop, arrows inside)
@@ -1 +1 @@
1
- {"version":3,"file":"useKeyboardHint.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardHint.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAiB5E,MAAM,MAAM,uBAAuB,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,WAAW,EAAE,SAAS,CAAC;IAEvB;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEvC;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEtC;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;CAC7C;AAkED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CAgKvB"}
1
+ {"version":3,"file":"useKeyboardHint.d.ts","sourceRoot":"","sources":["../../src/hooks/useKeyboardHint.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAiC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAgB5E,MAAM,MAAM,uBAAuB,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;AAEzE,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAC;IAEtC;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,WAAW,EAAE,SAAS,CAAC;IAEvB;;;OAGG;IACH,OAAO,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEvC;;;OAGG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAEtC;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,KAAK,IAAI,CAAC;CAC7C;AAkED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CA6JvB"}
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useKeyboardHint.tsx
7
- * @input Uses React, StyleX, Kbd, useLayer, i18n (useTranslator)
7
+ * @input Uses React, StyleX, Kbd, useLayer
8
8
  * @output Exports useKeyboardHint hook — ephemeral arrow-key navigation hint
9
9
  * @position Core hook; shows sighted keyboard users how to navigate composite
10
10
  * widgets that use roving tabindex (single Tab stop, arrows inside)
@@ -21,7 +21,6 @@ import "../theme/tokens.stylex.js";
21
21
  import { colorVars, radiusVars, shadowVars, spacingVars, typeScaleVars } from "../theme/tokens.stylex.js";
22
22
  import { Kbd } from "../Kbd/index.js";
23
23
  import { useLayer } from "../Layer/useLayer.js";
24
- import { useTranslator } from "../i18n/index.js";
25
24
 
26
25
  // ---------------------------------------------------------------------------
27
26
  // Types
@@ -91,7 +90,6 @@ export function useKeyboardHint(options = {}) {
91
90
  dismissAfterMs = 3000,
92
91
  isEnabled = true
93
92
  } = options;
94
- const t = useTranslator();
95
93
  const timeoutRef = useRef(null);
96
94
  const dismissedRef = useRef(false);
97
95
  const isVisibleRef = useRef(false);
@@ -203,7 +201,7 @@ export function useKeyboardHint(options = {}) {
203
201
  ...{
204
202
  className: "x11g1kdw"
205
203
  },
206
- children: t('@astryx.keyboardHint.toNavigate')
204
+ children: "to navigate"
207
205
  })]
208
206
  }), {
209
207
  placement: 'below',
package/locales/en.json CHANGED
@@ -247,10 +247,6 @@
247
247
  "defaultMessage": "Use arrow keys to move between avatars",
248
248
  "description": "Screen-reader-only instruction attached (via aria-describedby) to a group of interactive avatars that share a single Tab stop. Tells keyboard users the Left/Right arrow keys move focus between the avatars. Only announced when the group has interactive (link/button) avatars."
249
249
  },
250
- "@astryx.avatarGroup.overflow": {
251
- "defaultMessage": "{count, number} more",
252
- "description": "Accessible name for the \"+N\" overflow indicator at the end of an AvatarGroup — announces how many additional avatars are not shown. Example: `5 more`. The visible \"+N\" text is unaffected; this is the aria-label only."
253
- },
254
250
  "@astryx.banner.dismiss": {
255
251
  "defaultMessage": "Dismiss",
256
252
  "description": "\"Dismiss\" = close/hide this notification (not \"reject a person\"). Aria label AND tooltip on the small X button on a Banner."
@@ -411,10 +407,6 @@
411
407
  "defaultMessage": "Search options",
412
408
  "description": "Screen-reader-only accessible name for that same search input inside a MultiSelector."
413
409
  },
414
- "@astryx.multiSelector.selectAllPartiallySelected": {
415
- "defaultMessage": "{label}, partially selected",
416
- "description": "Accessible name for the MultiSelector select-all option while only some options are selected. `{label}` is the visible select-all label (e.g. \"Select all\"). ARIA forbids aria-selected=\"mixed\" on options, so the indeterminate state is conveyed through the name instead. \"Partially\" = some but not all."
417
- },
418
410
  "@astryx.popover.close": {
419
411
  "defaultMessage": "Close popover",
420
412
  "description": "\"Close\" = shut/dismiss, not \"nearby\". Screen-reader-only label on the close button inside a Popover."
@@ -691,10 +683,6 @@
691
683
  "defaultMessage": "Expand",
692
684
  "description": "\"Expand\" = reveal more, not grow physically. Aria label AND tooltip on the same toggle when currently collapsed. Pairs with `banner.collapse`."
693
685
  },
694
- "@astryx.button.loading": {
695
- "defaultMessage": "Loading",
696
- "description": "Screen-reader-only live-region announcement while a Button is in its loading state (spinner shown, action in flight). Progressive sense (\"work in progress\"), not a noun. Kept separate from `typeahead.loading` — translations may diverge."
697
- },
698
686
  "@astryx.chatComposerDrawer.expand": {
699
687
  "defaultMessage": "Expand {label}",
700
688
  "description": "Screen-reader-only label on the ChatComposerDrawer toggle when the drawer is collapsed. `{label}` is the drawer's visible name — example: `Expand Attachments`. Pairs with `collapse`."
@@ -759,14 +747,6 @@
759
747
  "defaultMessage": "Code",
760
748
  "description": "Screen-reader-only fallback name for a code-snippet scroll container when the code's language is unknown. \"Code\" = source code, not secret code or code of conduct."
761
749
  },
762
- "@astryx.field.optional": {
763
- "defaultMessage": "Optional",
764
- "description": "Visible indicator shown next to a form field's label when the field is optional (isOptional). Mutually exclusive with the required indicator. Very short — appears inline after the label text."
765
- },
766
- "@astryx.field.required": {
767
- "defaultMessage": "Required",
768
- "description": "Visible indicator shown next to a form field's label when the field must be filled in (isRequired). Mutually exclusive with the optional indicator. Very short — appears inline after the label text."
769
- },
770
750
  "@astryx.fileInput.clearLabel": {
771
751
  "defaultMessage": "Clear {label}",
772
752
  "description": "Screen-reader-only label on the X that removes the selected file from a FileInput. Example: `Clear Attachment`, `Clear Résumé`."
@@ -779,22 +759,10 @@
779
759
  "defaultMessage": "{label}, {fileNames}",
780
760
  "description": "Accessible name for a FileInput trigger that has files attached, composing the field label with the selected filenames. Example: `Attachments, report.pdf, notes.txt`."
781
761
  },
782
- "@astryx.keyboardHint.toNavigate": {
783
- "defaultMessage": "to navigate",
784
- "description": "Trailing text in the keyboard-hint badge, shown after arrow-key icons — reads as `← → to navigate`. Lowercase sentence fragment completing the visual phrase; the badge is aria-hidden, so this is for sighted users only."
785
- },
786
762
  "@astryx.lightbox.mediaViewer": {
787
763
  "defaultMessage": "Media viewer",
788
764
  "description": "Screen-reader-only fallback name for the Lightbox dialog when the current media item has no alt text. Should rarely render — consumers should provide alt."
789
765
  },
790
- "@astryx.metadataList.showMore": {
791
- "defaultMessage": "Show more",
792
- "description": "Button label under a MetadataList that reveals items hidden beyond the configured maximum. Toggles with `metadataList.showLess` — keep the pair parallel in your language."
793
- },
794
- "@astryx.metadataList.showLess": {
795
- "defaultMessage": "Show less",
796
- "description": "Button label under a MetadataList that re-hides the extra items revealed by `metadataList.showMore`. Keep the pair parallel."
797
- },
798
766
  "@astryx.mobileNav.navigation": {
799
767
  "defaultMessage": "Navigation",
800
768
  "description": "Screen-reader-only fallback name for the MobileNav overlay dialog when no explicit label was passed."
@@ -185,9 +185,6 @@
185
185
  "@astryx.avatarGroup.keyboardHint": {
186
186
  "defaultMessage": "⟦Úšé àřřóŵ ķéýš ţó ɱóṽé ƀéţŵééñ àṽàţàřš⟧"
187
187
  },
188
- "@astryx.avatarGroup.overflow": {
189
- "defaultMessage": "⟦{count, number} ɱóřé⟧"
190
- },
191
188
  "@astryx.banner.dismiss": {
192
189
  "defaultMessage": "⟦Ðíšɱíšš⟧"
193
190
  },
@@ -308,9 +305,6 @@
308
305
  "@astryx.multiSelector.searchOptions": {
309
306
  "defaultMessage": "⟦Šéàřçĥ óþţíóñš⟧"
310
307
  },
311
- "@astryx.multiSelector.selectAllPartiallySelected": {
312
- "defaultMessage": "⟦{label}, þàřţíàłłý šéłéçţéð⟧"
313
- },
314
308
  "@astryx.popover.close": {
315
309
  "defaultMessage": "⟦Çłóšé þóþóṽéř⟧"
316
310
  },
@@ -518,9 +512,6 @@
518
512
  "@astryx.banner.expand": {
519
513
  "defaultMessage": "⟦Éẋþàñð⟧"
520
514
  },
521
- "@astryx.button.loading": {
522
- "defaultMessage": "⟦Łóàðíñĝ⟧"
523
- },
524
515
  "@astryx.chatComposerDrawer.expand": {
525
516
  "defaultMessage": "⟦Éẋþàñð {label}⟧"
526
517
  },
@@ -569,12 +560,6 @@
569
560
  "@astryx.codeBlock.code": {
570
561
  "defaultMessage": "⟦Çóðé⟧"
571
562
  },
572
- "@astryx.field.optional": {
573
- "defaultMessage": "⟦Óþţíóñàł⟧"
574
- },
575
- "@astryx.field.required": {
576
- "defaultMessage": "⟦Řéɋúířéð⟧"
577
- },
578
563
  "@astryx.fileInput.clearLabel": {
579
564
  "defaultMessage": "⟦Çłéàř {label}⟧"
580
565
  },
@@ -584,18 +569,9 @@
584
569
  "@astryx.fileInput.triggerWithFiles": {
585
570
  "defaultMessage": "⟦{label}, {fileNames}⟧"
586
571
  },
587
- "@astryx.keyboardHint.toNavigate": {
588
- "defaultMessage": "⟦ţó ñàṽíĝàţé⟧"
589
- },
590
572
  "@astryx.lightbox.mediaViewer": {
591
573
  "defaultMessage": "⟦Ṁéðíà ṽíéŵéř⟧"
592
574
  },
593
- "@astryx.metadataList.showMore": {
594
- "defaultMessage": "⟦Šĥóŵ ɱóřé⟧"
595
- },
596
- "@astryx.metadataList.showLess": {
597
- "defaultMessage": "⟦Šĥóŵ łéšš⟧"
598
- },
599
575
  "@astryx.mobileNav.navigation": {
600
576
  "defaultMessage": "⟦Ñàṽíĝàţíóñ⟧"
601
577
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.2.0-canary.ea693ac",
3
+ "version": "0.2.0",
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",
@@ -5,7 +5,6 @@ import userEvent from '@testing-library/user-event';
5
5
  import {AvatarGroup} from './AvatarGroup';
6
6
  import {AvatarGroupOverflow} from './AvatarGroupOverflow';
7
7
  import {Avatar, AvatarStatusDot} from '../Avatar';
8
- import {InternationalizationProvider} from '../i18n';
9
8
 
10
9
  describe('AvatarGroup', () => {
11
10
  it('renders all avatar children', () => {
@@ -239,26 +238,7 @@ describe('AvatarGroupOverflow — hardening', () => {
239
238
  );
240
239
 
241
240
  expect(screen.getByText('+4912')).toBeInTheDocument();
242
- // The aria-label routes through the catalog's `{count, number}` ICU
243
- // argument, so the en locale adds a grouping separator.
244
- expect(screen.getByLabelText('4,912 more')).toBeInTheDocument();
245
- });
246
-
247
- it('localizes the overflow label through the i18n catalog', () => {
248
- render(
249
- <InternationalizationProvider
250
- locale="fr"
251
- overrides={{
252
- fr: {'@astryx.avatarGroup.overflow': '{count, number} de plus'},
253
- }}>
254
- <AvatarGroup>
255
- <Avatar name="Alice" />
256
- <AvatarGroupOverflow count={3} />
257
- </AvatarGroup>
258
- </InternationalizationProvider>,
259
- );
260
-
261
- expect(screen.getByLabelText('3 de plus')).toBeInTheDocument();
241
+ expect(screen.getByLabelText('4912 more')).toBeInTheDocument();
262
242
  });
263
243
  });
264
244
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  /**
5
5
  * @file AvatarGroupOverflow.tsx
6
- * @input Uses React, StyleX, AvatarGroupContext, i18n (useTranslator)
6
+ * @input Uses React, StyleX, AvatarGroupContext
7
7
  * @output Exports AvatarGroupOverflow for overflow indicator
8
8
  * @position Slot component used inside AvatarGroup
9
9
  *
@@ -26,7 +26,6 @@ import {mergeProps} from '../utils';
26
26
  import {useAvatarGroup} from './AvatarGroupContext';
27
27
  import type {BaseProps} from '../BaseProps';
28
28
  import {themeProps} from '../utils/themeProps';
29
- import {useTranslator} from '../i18n';
30
29
 
31
30
  const BORDER_WIDTH = 2;
32
31
  const OVERFLOW_FONT_RATIO = 0.35;
@@ -151,12 +150,11 @@ export function AvatarGroupOverflow({
151
150
  style,
152
151
  ...rest
153
152
  }: AvatarGroupOverflowProps): ReactNode {
154
- const t = useTranslator();
155
153
  const group = useAvatarGroup();
156
154
  const numericSize = group?.numericSize ?? 36;
157
155
  const overlap = group?.overlap ?? 0;
158
156
 
159
- const label = t('@astryx.avatarGroup.overflow', {count});
157
+ const label = `${count} more`;
160
158
  const content = children ?? `+${count}`;
161
159
 
162
160
  if (onClick) {
@@ -519,7 +519,7 @@ describe('BreadcrumbItem menu', () => {
519
519
  <BreadcrumbItem
520
520
  menu={
521
521
  <BreadcrumbMenuRadioGroup
522
- label="Sort by"
522
+ aria-label="Sort by"
523
523
  value="name"
524
524
  onChange={onChange}>
525
525
  <BreadcrumbMenuRadioItem value="name" label="Name" />
@@ -14,7 +14,6 @@ import {render, screen, fireEvent, act} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {Button} from './Button';
16
16
  import {Badge} from '../Badge/Badge';
17
- import {InternationalizationProvider} from '../i18n';
18
17
 
19
18
  describe('Button', () => {
20
19
  it('renders label as visible text', () => {
@@ -447,21 +446,6 @@ describe('Button', () => {
447
446
  expect(liveRegion).toHaveTextContent('Loading');
448
447
  });
449
448
 
450
- it('localizes the loading announcement through the i18n catalog', () => {
451
- render(
452
- <InternationalizationProvider
453
- locale="fr"
454
- overrides={{fr: {'@astryx.button.loading': 'Chargement'}}}>
455
- <Button label="Submit" isLoading />
456
- </InternationalizationProvider>,
457
- );
458
- const button = screen.getByRole('button');
459
- // The Spinner also has role="status", so grab the live region explicitly.
460
- const regions = button.querySelectorAll('[role="status"]');
461
- const liveRegion = regions[regions.length - 1];
462
- expect(liveRegion).toHaveTextContent('Chargement');
463
- });
464
-
465
449
  describe('elevation', () => {
466
450
  it('renders a distinct class for each elevation level', () => {
467
451
  const classFor = (elevation: 'none' | 'low' | 'med' | 'high') => {
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Button.tsx
7
- * @input Uses React, ButtonHTMLAttributes, ReactNode, i18n (useTranslator)
7
+ * @input Uses React, ButtonHTMLAttributes, ReactNode
8
8
  * @output Exports Button component, ButtonProps, ButtonVariant types
9
9
  * @position Core implementation; consumed by index.ts, tested by Button.test.tsx
10
10
  *
@@ -45,7 +45,6 @@ import {mergeProps, mergeRefs} from '../utils';
45
45
  import {useLinkComponent} from '../Link/useLinkComponent';
46
46
  import type {LinkComponentType} from '../Link/types';
47
47
  import {themeProps} from '../utils/themeProps';
48
- import {useTranslator} from '../i18n';
49
48
 
50
49
  /**
51
50
  * Base button styles
@@ -612,7 +611,6 @@ export function Button({
612
611
  ref,
613
612
  ...props
614
613
  }: ButtonProps): ReactNode {
615
- const t = useTranslator();
616
614
  const size = useSize(sizeProp, 'md');
617
615
  const buttonGroup = useButtonGroup();
618
616
 
@@ -762,7 +760,7 @@ export function Button({
762
760
  </span>
763
761
  {/* Live region for loading state announcements */}
764
762
  <VisuallyHidden role="status" aria-live="polite">
765
- {isLoadingState ? t('@astryx.button.loading') : ''}
763
+ {isLoadingState ? 'Loading' : ''}
766
764
  </VisuallyHidden>
767
765
  </>
768
766
  );
@@ -13,7 +13,6 @@ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
13
  import {render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import userEvent from '@testing-library/user-event';
15
15
  import {CheckboxInput} from './CheckboxInput';
16
- import {getForcedColorsRules} from '../__tests__/forcedColors';
17
16
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
18
17
 
19
18
  afterEach(() => {
@@ -544,25 +543,3 @@ describe('CheckboxInput', () => {
544
543
  });
545
544
  });
546
545
  });
547
-
548
- // jsdom cannot emulate forced-colors rendering, so this asserts that the
549
- // compiled output includes the forced-colors rule; visual behavior needs
550
- // manual verification under Windows High Contrast.
551
- describe('forced colors (WCAG 1.4.11)', () => {
552
- it('compiles a forced-colors fill so the indeterminate mark survives Windows High Contrast', () => {
553
- render(
554
- <CheckboxInput label="All" value="indeterminate" onChange={() => {}} />,
555
- );
556
- // The painted indeterminate bar would be stripped to Canvas (invisible);
557
- // CanvasText keeps it perceivable.
558
- expect(getForcedColorsRules()).toContain('background-color: canvastext;');
559
- });
560
-
561
- it('compiles a forced-colors color so the checkmark survives Windows High Contrast', () => {
562
- render(<CheckboxInput label="Accept" value={true} onChange={() => {}} />);
563
- // The check strokes with currentColor; forced colors leaves it the same
564
- // white as the flattened box, so it needs its own CanvasText color to stay
565
- // perceivable on the Canvas box.
566
- expect(getForcedColorsRules()).toContain('color: canvastext;');
567
- });
568
- });
@@ -158,29 +158,14 @@ const styles = stylex.create({
158
158
  },
159
159
  checkmark: {
160
160
  display: 'none',
161
- color: {
162
- default: colorVars['--color-on-accent'],
163
- // Forced colors (Windows High Contrast) does not reliably force an SVG
164
- // stroke painted with currentColor, so the check stays the same white as
165
- // the flattened (Canvas) box fill — a white check on a white box.
166
- // CanvasText keeps it perceivable on the Canvas box, matching the
167
- // indeterminate mark (WCAG 1.4.11).
168
- '@media (forced-colors: active)': 'CanvasText',
169
- },
161
+ color: colorVars['--color-on-accent'],
170
162
  },
171
163
  checkmarkVisible: {
172
164
  display: 'block',
173
165
  },
174
166
  indeterminateMark: {
175
167
  display: 'none',
176
- backgroundColor: {
177
- default: colorVars['--color-on-accent'],
178
- // Forced colors (Windows High Contrast) strips painted backgrounds,
179
- // which would make the indeterminate bar invisible; CanvasText keeps it
180
- // perceivable on the Canvas box fill (WCAG 1.4.11). The checkmark carries
181
- // the matching CanvasText treatment on its own style.
182
- '@media (forced-colors: active)': 'CanvasText',
183
- },
168
+ backgroundColor: colorVars['--color-on-accent'],
184
169
  borderRadius: radiusVars['--radius-full'],
185
170
  },
186
171
  indeterminateMarkVisible: {
@@ -154,105 +154,14 @@ describe('CheckboxList', () => {
154
154
  </CheckboxList>,
155
155
  );
156
156
 
157
- // The row is a pointer-only click surface (no invisible row button — the
158
- // checkbox is the option's only tab stop). Click the row's label area,
159
- // where the composed onClick fires — not the inner checkbox.
160
- expect(
161
- screen.queryByRole('button', {name: 'Option B'}),
162
- ).not.toBeInTheDocument();
163
- // The text also exists in the checkbox's visually hidden <label>; target
164
- // the row's label <span> to click the row surface.
165
- await user.click(screen.getByText('Option B', {selector: 'span'}));
157
+ // Click the interactive row (invisible-button pattern), which is where the
158
+ // composed onClick lives — not the inner checkbox.
159
+ await user.click(screen.getByRole('button', {name: 'Option B'}));
166
160
 
167
161
  expect(handleClick).toHaveBeenCalledTimes(1);
168
162
  expect(handleChange).toHaveBeenCalledWith(['a', 'b']);
169
163
  });
170
164
 
171
- describe('single tab stop per option (WCAG 4.1.2 / APG checkbox pattern)', () => {
172
- it('renders exactly one focusable control per option — the checkbox, no row button', () => {
173
- render(
174
- <CheckboxList label="Preferences" value={['a']} onChange={() => {}}>
175
- <CheckboxListItem label="Option A" value="a" />
176
- <CheckboxListItem label="Option B" value="b" />
177
- </CheckboxList>,
178
- );
179
- // No invisible whole-row button duplicating the checkbox's action.
180
- expect(screen.queryByRole('button')).not.toBeInTheDocument();
181
- for (const item of screen.getAllByRole('listitem')) {
182
- const focusables = item.querySelectorAll(
183
- 'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
184
- );
185
- expect(focusables).toHaveLength(1);
186
- expect(focusables[0]).toBe(within(item).getByRole('checkbox'));
187
- }
188
- });
189
-
190
- it('Tab moves directly from one option checkbox to the next', async () => {
191
- const user = userEvent.setup();
192
- render(
193
- <CheckboxList label="Preferences" value={[]} onChange={() => {}}>
194
- <CheckboxListItem label="Option A" value="a" />
195
- <CheckboxListItem label="Option B" value="b" />
196
- </CheckboxList>,
197
- );
198
- await user.tab();
199
- expect(screen.getByRole('checkbox', {name: 'Option A'})).toHaveFocus();
200
- await user.tab();
201
- expect(screen.getByRole('checkbox', {name: 'Option B'})).toHaveFocus();
202
- // No further tab stop inside the list (the row itself is not focusable).
203
- await user.tab();
204
- expect(document.body).toHaveFocus();
205
- });
206
-
207
- it('clicking the row surface (outside the checkbox) still toggles', async () => {
208
- const user = userEvent.setup();
209
- const handleChange = vi.fn();
210
- render(
211
- <CheckboxList label="Preferences" value={['a']} onChange={handleChange}>
212
- <CheckboxListItem label="Option A" value="a" />
213
- <CheckboxListItem label="Option B" value="b" />
214
- </CheckboxList>,
215
- );
216
- // The text also exists in the checkbox's visually hidden <label>;
217
- // target the row's label <span> to click the row surface.
218
- await user.click(screen.getByText('Option B', {selector: 'span'}));
219
- expect(handleChange).toHaveBeenCalledTimes(1);
220
- expect(handleChange).toHaveBeenCalledWith(['a', 'b']);
221
- });
222
-
223
- it('toggles with Space on the focused checkbox, which reports checked state', async () => {
224
- const user = userEvent.setup();
225
- const handleChange = vi.fn();
226
- render(
227
- <CheckboxList label="Preferences" value={['a']} onChange={handleChange}>
228
- <CheckboxListItem label="Option A" value="a" />
229
- <CheckboxListItem label="Option B" value="b" />
230
- </CheckboxList>,
231
- );
232
- await user.tab();
233
- // The focused control is the checkbox itself, so its accessible name
234
- // and checked state are what assistive tech announces on focus.
235
- const checkboxA = screen.getByRole('checkbox', {name: 'Option A'});
236
- expect(checkboxA).toHaveFocus();
237
- expect(checkboxA).toBeChecked();
238
- await user.keyboard(' ');
239
- expect(handleChange).toHaveBeenCalledTimes(1);
240
- expect(handleChange).toHaveBeenCalledWith([]);
241
- });
242
-
243
- it('the row itself is not focusable', () => {
244
- render(
245
- <CheckboxList label="Preferences" value={[]} onChange={() => {}}>
246
- <CheckboxListItem label="Option A" value="a" />
247
- </CheckboxList>,
248
- );
249
- const item = screen.getByRole('listitem');
250
- expect(item).not.toHaveAttribute('tabindex');
251
- item.focus();
252
- expect(item).not.toHaveFocus();
253
- });
254
- });
255
-
256
165
  it('disables all checkboxes when group isDisabled is true', () => {
257
166
  render(
258
167
  <CheckboxList
@@ -16,11 +16,12 @@
16
16
  * - /packages/cli/templates/blocks/components/CheckboxList/ (showcase blocks)
17
17
  */
18
18
 
19
- import {use, useRef, type ReactNode} from 'react';
19
+ import {use, type MouseEvent, type ReactNode} from 'react';
20
20
  import * as stylex from '@stylexjs/stylex';
21
21
  import type {StyleXStyles} from '@stylexjs/stylex';
22
22
  import {colorVars} from '../theme/tokens.stylex';
23
23
  import type {BaseProps} from '../BaseProps';
24
+ import {composeEventHandlers} from '../utils';
24
25
  import {useDevWarning} from '../hooks/useDevWarning';
25
26
  import {CheckboxInput} from '../CheckboxInput/CheckboxInput';
26
27
  import {ListItem} from '../List/ListItem';
@@ -211,14 +212,6 @@ export function CheckboxListItem({
211
212
  // Whether this item is interactive (has a toggle handler)
212
213
  const isInteractive = !effectiveReadOnly && (ctx != null || onCheck != null);
213
214
 
214
- // The checkbox is the row's single keyboard control and action. The row is
215
- // an enlarged click/tap target that delegates surface clicks to it via
216
- // ListItem's `interactiveRef` (useClickableContainer), so each option is
217
- // exactly one tab stop. Delegate whenever the row should respond to clicks:
218
- // a toggleable item, or one carrying a consumer `onClick`.
219
- const checkboxRef = useRef<HTMLInputElement | null>(null);
220
- const hasRowInteraction = isInteractive || onClickProp != null;
221
-
222
215
  const handleToggle = () => {
223
216
  if (effectiveDisabled || effectiveReadOnly || isBusy) {
224
217
  return;
@@ -251,12 +244,14 @@ export function CheckboxListItem({
251
244
  description={description}
252
245
  endContent={endContent}
253
246
  isDisabled={effectiveDisabled}
254
- // Delegate row clicks to the checkbox instead of wiring onClick (which
255
- // would add an invisible row button = a second tab stop). The checkbox
256
- // stays the option's sole focusable control (WCAG 4.1.2 / APG checkbox
257
- // pattern). A consumer onClick rides on the checkbox itself (below), so
258
- // it still fires for both direct and delegated (row-surface) clicks.
259
- interactiveRef={hasRowInteraction ? checkboxRef : undefined}
247
+ onClick={
248
+ isInteractive || onClickProp
249
+ ? composeEventHandlers<MouseEvent>(
250
+ onClickProp as ((event: MouseEvent) => void) | undefined,
251
+ isInteractive ? handleToggle : undefined,
252
+ )
253
+ : undefined
254
+ }
260
255
  aria-busy={isBusy || undefined}
261
256
  xstyle={
262
257
  [
@@ -271,12 +266,10 @@ export function CheckboxListItem({
271
266
  style={style}
272
267
  startContent={
273
268
  <CheckboxInput
274
- ref={checkboxRef}
275
269
  label={checkboxLabel}
276
270
  isLabelHidden
277
271
  value={resolvedChecked}
278
272
  onChange={() => handleToggle()}
279
- onClick={onClickProp}
280
273
  isDisabled={effectiveDisabled}
281
274
  isReadOnly={effectiveReadOnly}
282
275
  isLoading={isBusy}
@@ -14,7 +14,6 @@ import {act, render, screen, fireEvent, waitFor} from '@testing-library/react';
14
14
  import {CodeBlock} from './CodeBlock';
15
15
  import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
16
16
  import {dracula} from '../theme/syntax';
17
- import {InternationalizationProvider} from '../i18n';
18
17
 
19
18
  function politeRegion(): HTMLElement | null {
20
19
  return document.querySelector('[data-astryx-live-region="polite"]');
@@ -73,22 +72,6 @@ describe('CodeBlock', () => {
73
72
  });
74
73
  });
75
74
 
76
- it('localizes the copy announcement through the i18n catalog', async () => {
77
- render(
78
- <InternationalizationProvider
79
- locale="fr"
80
- overrides={{fr: {'@astryx.codeBlock.copied': 'Copié'}}}>
81
- <CodeBlock code="const x = 1;" language="javascript" />
82
- </InternationalizationProvider>,
83
- );
84
- // The button label and the live-region announcement share the same key.
85
- fireEvent.click(screen.getByRole('button', {name: 'Copy code'}));
86
- await waitFor(() => {
87
- expect(politeRegion()).toHaveTextContent('Copié');
88
- });
89
- expect(screen.getByRole('button', {name: 'Copié'})).toBeInTheDocument();
90
- });
91
-
92
75
  it('keeps the copied indicator a full 2s after a rapid re-copy', async () => {
93
76
  vi.useFakeTimers();
94
77
  try {
@@ -786,7 +786,7 @@ export function CodeBlock({
786
786
  setCopied(true);
787
787
  // Swapping the button's aria-label alone isn't reliably announced by
788
788
  // screen readers, so confirm the copy via a polite live region.
789
- announce(t('@astryx.codeBlock.copied'));
789
+ announce('Copied');
790
790
  onCopy?.();
791
791
  // Restart the reset timer on every copy — otherwise a rapid re-copy
792
792
  // is reverted early by the previous click's timer.
@@ -800,7 +800,7 @@ export function CodeBlock({
800
800
  } catch {
801
801
  // Clipboard failures leave the copied state unchanged.
802
802
  }
803
- }, [code, onCopy, announce, t]);
803
+ }, [code, onCopy, announce]);
804
804
 
805
805
  const sizeStyle = size === 'sm' ? styles.sizeSm : styles.sizeMd;
806
806
  // Digits in the largest line number — sizes the gutter column width.