@astryxdesign/core 0.1.7-canary.fb9c68c → 0.1.7

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 (137) hide show
  1. package/README.md +15 -24
  2. package/dist/Collapsible/Collapsible.d.ts +1 -11
  3. package/dist/Collapsible/Collapsible.d.ts.map +1 -1
  4. package/dist/Collapsible/Collapsible.js +4 -36
  5. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  6. package/dist/ContextMenu/ContextMenu.js +3 -4
  7. package/dist/ContextMenu/index.d.ts +0 -3
  8. package/dist/ContextMenu/index.d.ts.map +1 -1
  9. package/dist/ContextMenu/index.js +1 -7
  10. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  11. package/dist/DropdownMenu/DropdownMenu.js +4 -8
  12. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -17
  13. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  14. package/dist/DropdownMenu/DropdownMenuContext.js +0 -18
  15. package/dist/DropdownMenu/index.d.ts +1 -5
  16. package/dist/DropdownMenu/index.d.ts.map +1 -1
  17. package/dist/DropdownMenu/index.js +1 -10
  18. package/dist/Icon/Icon.d.ts +12 -17
  19. package/dist/Icon/Icon.d.ts.map +1 -1
  20. package/dist/Icon/Icon.js +20 -28
  21. package/dist/Outline/Outline.d.ts +3 -69
  22. package/dist/Outline/Outline.d.ts.map +1 -1
  23. package/dist/Outline/Outline.js +26 -129
  24. package/dist/Outline/useScrollSpy.d.ts +7 -27
  25. package/dist/Outline/useScrollSpy.d.ts.map +1 -1
  26. package/dist/Outline/useScrollSpy.js +70 -202
  27. package/dist/TabList/Tab.d.ts.map +1 -1
  28. package/dist/TabList/Tab.js +2 -2
  29. package/dist/TabList/TabList.d.ts.map +1 -1
  30. package/dist/TabList/TabList.js +0 -2
  31. package/dist/TabList/TabMenu.d.ts.map +1 -1
  32. package/dist/TabList/TabMenu.js +1 -1
  33. package/dist/Table/index.d.ts +0 -2
  34. package/dist/Table/index.d.ts.map +1 -1
  35. package/dist/Table/index.js +0 -1
  36. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
  37. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +2 -24
  38. package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
  39. package/dist/Table/plugins/sortable/useTableSortable.js +7 -18
  40. package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
  41. package/dist/Table/useBaseTablePlugins.js +3 -6
  42. package/dist/Toolbar/Toolbar.d.ts.map +1 -1
  43. package/dist/Toolbar/Toolbar.js +1 -8
  44. package/dist/astryx.css +2 -18
  45. package/dist/astryx.umd.js +48 -48
  46. package/dist/astryx.umd.js.map +4 -4
  47. package/dist/theme/Theme.js +1 -1
  48. package/dist/theme/defineTheme.d.ts +1 -1
  49. package/dist/theme/defineTheme.js +1 -1
  50. package/dist/theme/hct.d.ts +1 -0
  51. package/dist/theme/hct.d.ts.map +1 -1
  52. package/dist/theme/hct.js +3 -0
  53. package/locales/en.json +0 -28
  54. package/locales/pseudo.json +0 -21
  55. package/package.json +1 -3
  56. package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
  57. package/src/Collapsible/Collapsible.doc.mjs +0 -7
  58. package/src/Collapsible/Collapsible.test.tsx +0 -80
  59. package/src/Collapsible/Collapsible.tsx +5 -58
  60. package/src/ContextMenu/ContextMenu.test.tsx +1 -75
  61. package/src/ContextMenu/ContextMenu.tsx +5 -10
  62. package/src/ContextMenu/index.ts +0 -15
  63. package/src/DropdownMenu/DropdownMenu.doc.mjs +0 -23
  64. package/src/DropdownMenu/DropdownMenu.tsx +6 -11
  65. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -29
  66. package/src/DropdownMenu/DropdownMenuItem.doc.mjs +1 -7
  67. package/src/DropdownMenu/index.ts +3 -23
  68. package/src/Field/Field.doc.mjs +1 -1
  69. package/src/Field/FieldLabel.doc.mjs +1 -1
  70. package/src/Icon/Icon.test.tsx +0 -46
  71. package/src/Icon/Icon.tsx +24 -37
  72. package/src/NumberInput/NumberInput.doc.mjs +2 -2
  73. package/src/Outline/Outline.doc.mjs +3 -123
  74. package/src/Outline/Outline.test.tsx +0 -735
  75. package/src/Outline/Outline.tsx +33 -184
  76. package/src/Outline/useScrollSpy.ts +72 -248
  77. package/src/Selector/SelectorOption.doc.mjs +1 -1
  78. package/src/SideNav/SideNavItem.doc.mjs +2 -2
  79. package/src/Switch/Switch.doc.mjs +1 -1
  80. package/src/TabList/Tab.tsx +1 -5
  81. package/src/TabList/TabList.doc.mjs +3 -3
  82. package/src/TabList/TabList.test.tsx +0 -66
  83. package/src/TabList/TabList.tsx +0 -8
  84. package/src/TabList/TabMenu.tsx +1 -4
  85. package/src/Table/Table.doc.mjs +0 -2
  86. package/src/Table/index.ts +0 -7
  87. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +1 -90
  88. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +5 -37
  89. package/src/Table/plugins/sortable/useTableSortable.test.tsx +9 -95
  90. package/src/Table/plugins/sortable/useTableSortable.tsx +7 -23
  91. package/src/Table/useBaseTablePlugins.ts +2 -6
  92. package/src/TextArea/TextArea.doc.mjs +1 -1
  93. package/src/TextInput/TextInput.doc.mjs +1 -1
  94. package/src/Toolbar/Toolbar.tsx +3 -10
  95. package/src/theme/Theme.doc.mjs +4 -4
  96. package/src/theme/Theme.tsx +1 -1
  97. package/src/theme/defineTheme.ts +1 -1
  98. package/src/theme/hct.ts +4 -0
  99. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +0 -89
  100. package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +0 -1
  101. package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +0 -170
  102. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +0 -69
  103. package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +0 -1
  104. package/dist/DropdownMenu/DropdownMenuRadioGroup.js +0 -70
  105. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +0 -69
  106. package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +0 -1
  107. package/dist/DropdownMenu/DropdownMenuRadioItem.js +0 -178
  108. package/dist/DropdownMenu/menuItemRoles.d.ts +0 -13
  109. package/dist/DropdownMenu/menuItemRoles.d.ts.map +0 -1
  110. package/dist/DropdownMenu/menuItemRoles.js +0 -15
  111. package/dist/Table/plugins/tree/index.d.ts +0 -5
  112. package/dist/Table/plugins/tree/index.d.ts.map +0 -1
  113. package/dist/Table/plugins/tree/index.js +0 -4
  114. package/dist/Table/plugins/tree/useTableTreeData.d.ts +0 -38
  115. package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +0 -1
  116. package/dist/Table/plugins/tree/useTableTreeData.js +0 -358
  117. package/dist/Table/plugins/tree/useTableTreeState.d.ts +0 -51
  118. package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +0 -1
  119. package/dist/Table/plugins/tree/useTableTreeState.js +0 -175
  120. package/scripts/postinstall.mjs +0 -93
  121. package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +0 -95
  122. package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +0 -237
  123. package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +0 -80
  124. package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +0 -119
  125. package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +0 -80
  126. package/src/DropdownMenu/DropdownMenuRadioItem.tsx +0 -232
  127. package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -163
  128. package/src/DropdownMenu/menuItemRoles.ts +0 -22
  129. package/src/Table/plugins/tree/index.ts +0 -12
  130. package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +0 -163
  131. package/src/Table/plugins/tree/useTableTreeData.test.tsx +0 -594
  132. package/src/Table/plugins/tree/useTableTreeData.tsx +0 -495
  133. package/src/Table/plugins/tree/useTableTreeState.test.tsx +0 -636
  134. package/src/Table/plugins/tree/useTableTreeState.tsx +0 -253
  135. package/src/Table/useTableTreeData.doc.mjs +0 -75
  136. package/src/Table/useTableTreeState.doc.mjs +0 -98
  137. package/src/postinstall.test.mjs +0 -71
@@ -115,7 +115,7 @@ function useThemeStyleInjection(theme) {
115
115
  // One-time perf hint per theme
116
116
  if (!warnedThemes.has(theme.name)) {
117
117
  warnedThemes.add(theme.name);
118
- console.warn(`[Astryx] Theme "${theme.name}" is using runtime style injection. ` + `For better performance, use the pre-built theme:\n\n` + ` import {${theme.name}Theme} from '@astryxdesign/theme-${theme.name}/built';\n` + ` import '@astryxdesign/theme-${theme.name}/theme.css';\n\n` + `For custom themes, run \`npx @astryxdesign/cli theme build <file>\` to generate ` + `the built artifacts.`);
118
+ console.warn(`[Astryx] Theme "${theme.name}" is using runtime style injection. ` + `For better performance, use the pre-built theme:\n\n` + ` import {${theme.name}Theme} from '@astryxdesign/theme-${theme.name}/built';\n` + ` import '@astryxdesign/theme-${theme.name}/theme.css';\n\n` + `For custom themes, run \`npx astryx theme build <file>\` to generate ` + `the built artifacts.`);
119
119
  }
120
120
  const {
121
121
  prose,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Two distribution modes:
5
5
  * - Unbuilt: Theme generates CSS and injects a <style> tag at runtime
6
- * - Built: `astryx theme build` pre-compiles to a CSS file; Theme just
6
+ * - Built: `npx astryx theme build` pre-compiles to a CSS file; Theme just
7
7
  * sets the data-astryx-theme attribute
8
8
  *
9
9
  * Token values can be:
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Two distribution modes:
7
7
  * - Unbuilt: Theme generates CSS and injects a <style> tag at runtime
8
- * - Built: `astryx theme build` pre-compiles to a CSS file; Theme just
8
+ * - Built: `npx astryx theme build` pre-compiles to a CSS file; Theme just
9
9
  * sets the data-astryx-theme attribute
10
10
  *
11
11
  * Token values can be:
@@ -4,6 +4,7 @@ export interface HCT {
4
4
  tone: number;
5
5
  }
6
6
  export declare function toneToY(tone: number): number;
7
+ export declare function yToTone(y: number): number;
7
8
  /**
8
9
  * Convert a hex color to HCT.
9
10
  * Hue: 0-360, Chroma: 0-~120, Tone: 0-100.
@@ -1 +1 @@
1
- {"version":3,"file":"hct.d.ts","sourceRoot":"","sources":["../../src/theme/hct.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAoFD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAkBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAkBzC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CA8BzC;AAkDD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAMD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM/D"}
1
+ {"version":3,"file":"hct.d.ts","sourceRoot":"","sources":["../../src/theme/hct.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,GAAG;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAoFD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAkBD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAkBzC;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CA8BzC;AAkDD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB;AAMD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM/D"}
package/dist/theme/hct.js CHANGED
@@ -79,6 +79,9 @@ function labToXyz(L, a, b) {
79
79
  export function toneToY(tone) {
80
80
  return labFInv((tone + 16) / 116);
81
81
  }
82
+ export function yToTone(y) {
83
+ return 116 * labF(y) - 16;
84
+ }
82
85
 
83
86
  // =============================================================================
84
87
  // Hex <-> RGB
package/locales/en.json CHANGED
@@ -423,26 +423,6 @@
423
423
  "defaultMessage": "Clear sort",
424
424
  "description": "Aria label for a Table column header when clicking it will clear the current sort."
425
425
  },
426
- "@astryx.table.sort.direction.ascending": {
427
- "defaultMessage": "ascending",
428
- "description": "Localized direction word interpolated as `direction` into @astryx.table.sort.sortedBy and @astryx.table.sort.sortedByWithPriority."
429
- },
430
- "@astryx.table.sort.direction.descending": {
431
- "defaultMessage": "descending",
432
- "description": "Localized direction word interpolated as `direction` into @astryx.table.sort.sortedBy and @astryx.table.sort.sortedByWithPriority."
433
- },
434
- "@astryx.table.sort.sortBy": {
435
- "defaultMessage": "Sort by {label}",
436
- "description": "Aria label for a sortable Table header button when the column is unsorted. `label` is the column header text."
437
- },
438
- "@astryx.table.sort.sortedBy": {
439
- "defaultMessage": "Sort by {label}, sorted {direction}",
440
- "description": "Aria label for a sorted Table header button. `direction` is the localized direction word from @astryx.table.sort.direction.*."
441
- },
442
- "@astryx.table.sort.sortedByWithPriority": {
443
- "defaultMessage": "Sort by {label}, sorted {direction}, priority {rank, number} of {total, number}",
444
- "description": "Aria label for a sorted Table header button in multi-sort. `rank` is the 1-based position of this column in the sort order; `total` is the number of sorted columns."
445
- },
446
426
  "@astryx.toast.dismiss": {
447
427
  "defaultMessage": "Dismiss notification",
448
428
  "description": "Aria label for the dismiss button on a Toast."
@@ -743,14 +723,6 @@
743
723
  "defaultMessage": "Expand all rows",
744
724
  "description": "Aria label for the Table expand-all / collapse-all header button when not all rows are currently expanded."
745
725
  },
746
- "@astryx.tableTree.collapseRow": {
747
- "defaultMessage": "Collapse row",
748
- "description": "Aria label for the Table tree-data expander chevron when the row is currently expanded."
749
- },
750
- "@astryx.tableTree.expandRow": {
751
- "defaultMessage": "Expand row",
752
- "description": "Aria label for the Table tree-data expander chevron when the row is currently collapsed."
753
- },
754
726
  "@astryx.textInput.clearLabel": {
755
727
  "defaultMessage": "Clear {label}",
756
728
  "description": "Aria label for the TextInput clear button. `label` is the input's own visible label."
@@ -317,21 +317,6 @@
317
317
  "@astryx.table.sort.clear": {
318
318
  "defaultMessage": "⟦Çłéàř šóřţ⟧"
319
319
  },
320
- "@astryx.table.sort.direction.ascending": {
321
- "defaultMessage": "⟦àšçéñðíñĝ⟧"
322
- },
323
- "@astryx.table.sort.direction.descending": {
324
- "defaultMessage": "⟦ðéšçéñðíñĝ⟧"
325
- },
326
- "@astryx.table.sort.sortBy": {
327
- "defaultMessage": "⟦Šóřţ ƀý {label}⟧"
328
- },
329
- "@astryx.table.sort.sortedBy": {
330
- "defaultMessage": "⟦Šóřţ ƀý {label}, šóřţéð {direction}⟧"
331
- },
332
- "@astryx.table.sort.sortedByWithPriority": {
333
- "defaultMessage": "⟦Šóřţ ƀý {label}, šóřţéð {direction}, þříóříţý {rank, number} óƒ {total, number}⟧"
334
- },
335
320
  "@astryx.toast.dismiss": {
336
321
  "defaultMessage": "⟦Ðíšɱíšš ñóţíƒíçàţíóñ⟧"
337
322
  },
@@ -557,12 +542,6 @@
557
542
  "@astryx.tableRowExpansion.expandAllRows": {
558
543
  "defaultMessage": "⟦Éẋþàñð àłł řóŵš⟧"
559
544
  },
560
- "@astryx.tableTree.collapseRow": {
561
- "defaultMessage": "⟦Çółłàþšé řóŵ⟧"
562
- },
563
- "@astryx.tableTree.expandRow": {
564
- "defaultMessage": "⟦Éẋþàñð řóŵ⟧"
565
- },
566
545
  "@astryx.textInput.clearLabel": {
567
546
  "defaultMessage": "⟦Çłéàř {label}⟧"
568
547
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.1.7-canary.fb9c68c",
3
+ "version": "0.1.7",
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",
@@ -636,7 +636,6 @@
636
636
  "files": [
637
637
  "dist",
638
638
  "src",
639
- "scripts/postinstall.mjs",
640
639
  "locales",
641
640
  "docs.mjs",
642
641
  "groups.doc.mjs",
@@ -663,7 +662,6 @@
663
662
  "intl-messageformat": "^11.2.9"
664
663
  },
665
664
  "scripts": {
666
- "postinstall": "node scripts/postinstall.mjs",
667
665
  "build": "rimraf dist && pnpm build:i18n && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && pnpm build:umd && node ../../scripts/check-no-dev-jsx.mjs",
668
666
  "build:i18n": "node scripts/build-pseudo-locale.mjs",
669
667
  "build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.json",
@@ -113,7 +113,7 @@ export const docs = {
113
113
  {
114
114
  name: 'labelIcon',
115
115
  type: 'IconType',
116
- description: 'Icon to display before the label text. See `astryx docs icons` for valid semantic names.',
116
+ description: 'Icon to display before the label text. See `npx astryx docs icons` for valid semantic names.',
117
117
  },
118
118
  {
119
119
  name: 'status',
@@ -17,7 +17,6 @@ export const docs = {
17
17
  theming: {
18
18
  targets: [
19
19
  {className: 'astryx-collapsible', visualProps: ['density']},
20
- {className: 'astryx-collapsible-content', visualProps: ['density']},
21
20
  {className: 'astryx-collapsible-group', visualProps: ['density']},
22
21
  ],
23
22
  },
@@ -54,12 +53,6 @@ export const docs = {
54
53
  type: 'boolean',
55
54
  description: 'Controlled open state.',
56
55
  },
57
- {
58
- name: 'isDisabled',
59
- type: 'boolean',
60
- description: "Disable the item so its trigger can't be toggled (dimmed, aria-disabled, and out of the tab order). Doesn't collapse an already-open item.",
61
- default: 'false',
62
- },
63
56
  {
64
57
  name: 'onOpenChange',
65
58
  type: '(isOpen: boolean) => void',
@@ -72,12 +72,6 @@ describe('Collapsible', () => {
72
72
  expect(screen.getByTestId('root')).toHaveClass('astryx-collapsible');
73
73
  });
74
74
 
75
- it('renders the stable astryx-collapsible-content class on the content area', () => {
76
- render(<Collapsible trigger="T">c</Collapsible>);
77
- const content = contentFor(screen.getByRole('button'));
78
- expect(content).toHaveClass('astryx-collapsible-content');
79
- });
80
-
81
75
  it('renders a ReactNode trigger, not just a string', () => {
82
76
  render(
83
77
  <Collapsible trigger={<span data-testid="rich">Rich</span>}>
@@ -246,80 +240,6 @@ describe('Collapsible', () => {
246
240
  });
247
241
  });
248
242
 
249
- describe('disabled state', () => {
250
- it('marks the trigger aria-disabled and drops it from the tab order', () => {
251
- render(
252
- <Collapsible trigger="T" isDisabled>
253
- Body
254
- </Collapsible>,
255
- );
256
- const button = screen.getByRole('button');
257
- expect(button).toHaveAttribute('aria-disabled', 'true');
258
- expect(button).toHaveAttribute('tabindex', '-1');
259
- // Never the native disabled attribute — it stays focusable/perceivable.
260
- expect(button).not.toBeDisabled();
261
- });
262
-
263
- it('is enabled by default (no aria-disabled, stays in tab order)', () => {
264
- render(<Collapsible trigger="T">Body</Collapsible>);
265
- const button = screen.getByRole('button');
266
- expect(button).not.toHaveAttribute('aria-disabled');
267
- expect(button).not.toHaveAttribute('tabindex', '-1');
268
- });
269
-
270
- it('does not toggle when the trigger is clicked while disabled', async () => {
271
- const user = userEvent.setup();
272
- render(
273
- <Collapsible trigger="T" isDisabled defaultIsOpen>
274
- Body
275
- </Collapsible>,
276
- );
277
- const button = screen.getByRole('button');
278
- expect(button).toHaveAttribute('aria-expanded', 'true');
279
- await user.click(button);
280
- expect(button).toHaveAttribute('aria-expanded', 'true');
281
- });
282
-
283
- it('does not fire onOpenChange while disabled', async () => {
284
- const user = userEvent.setup();
285
- const onOpenChange = vi.fn();
286
- render(
287
- <Collapsible trigger="T" isDisabled onOpenChange={onOpenChange}>
288
- Body
289
- </Collapsible>,
290
- );
291
- await user.click(screen.getByRole('button'));
292
- expect(onOpenChange).not.toHaveBeenCalled();
293
- });
294
-
295
- it('does not collapse an already-open item — content stays visible', () => {
296
- render(
297
- <Collapsible trigger="T" isDisabled defaultIsOpen>
298
- Body
299
- </Collapsible>,
300
- );
301
- const content = contentFor(screen.getByRole('button'));
302
- expect(content).not.toHaveStyle({display: 'none'});
303
- });
304
-
305
- it('does not toggle its group item when disabled', async () => {
306
- const user = userEvent.setup();
307
- render(
308
- <CollapsibleGroup type="single">
309
- <Collapsible trigger="A" value="a" isDisabled>
310
- Body A
311
- </Collapsible>
312
- <Collapsible trigger="B" value="b">
313
- Body B
314
- </Collapsible>
315
- </CollapsibleGroup>,
316
- );
317
- const triggerA = screen.getByRole('button', {name: /A/});
318
- await user.click(triggerA);
319
- expect(triggerA).toHaveAttribute('aria-expanded', 'false');
320
- });
321
- });
322
-
323
243
  describe('prop forwarding', () => {
324
244
  it('forwards a ref to the root element', () => {
325
245
  const ref = vi.fn();
@@ -39,7 +39,6 @@ import {
39
39
  durationVars,
40
40
  easeVars,
41
41
  } from '../theme/tokens.stylex';
42
-
43
42
  import {useCollapsible} from './useCollapsible';
44
43
  import {CollapsibleGroupPresentationContext} from './CollapsibleGroupContext';
45
44
  import {getIcon} from '../Icon/globalIconRegistry';
@@ -82,13 +81,6 @@ const styles = stylex.create({
82
81
  textBoxEdge: 'cap alphabetic',
83
82
  textBoxTrim: 'trim-both',
84
83
  },
85
- // Disabled trigger — non-interactive, dimmed. Native `disabled` on the
86
- // button blocks click + keyboard activation; these styles restore the
87
- // visual affordance that `all: unset` wipes.
88
- triggerDisabled: {
89
- cursor: 'not-allowed',
90
- opacity: 0.5,
91
- },
92
84
  // Chevron indicator
93
85
  chevron: {
94
86
  display: 'inline-flex',
@@ -110,17 +102,8 @@ const styles = stylex.create({
110
102
  contentHidden: {
111
103
  display: 'none',
112
104
  },
113
- // Anchors body typography so revealed text renders at the system's body
114
- // scale (family/size/weight/leading) instead of inheriting from wherever
115
- // the Collapsible is placed. External themes override via the
116
- // `astryx-collapsible-content` target.
117
105
  content: {
118
106
  paddingBlockStart: spacingVars['--spacing-1'],
119
- fontFamily: typographyVars['--font-family-body'],
120
- fontSize: typeScaleVars['--text-body-size'],
121
- fontWeight: typeScaleVars['--text-body-weight'],
122
- lineHeight: typeScaleVars['--text-body-leading'],
123
- color: colorVars['--color-text-primary'],
124
107
  },
125
108
  // Group divider chrome — a hairline above every item except the first.
126
109
  // The group's wrapper (or 'all' mode) owns the outer edges.
@@ -183,17 +166,6 @@ export interface CollapsibleProps extends BaseProps {
183
166
  */
184
167
  isOpen?: boolean;
185
168
 
186
- /**
187
- * Whether the collapsible is disabled. A disabled item can't be toggled —
188
- * its trigger is non-interactive and dimmed. Following the system-wide
189
- * disabled convention, the trigger uses `aria-disabled` (not the native
190
- * `disabled` attribute) and drops out of the tab order, staying perceivable
191
- * to assistive tech. The content stays in whatever open state it was;
192
- * disabling doesn't collapse an already-open item.
193
- * @default false
194
- */
195
- isDisabled?: boolean;
196
-
197
169
  /**
198
170
  * Callback when the open state changes.
199
171
  */
@@ -253,7 +225,6 @@ export function Collapsible({
253
225
  children,
254
226
  defaultIsOpen,
255
227
  isOpen: controlledIsOpen,
256
- isDisabled = false,
257
228
  onOpenChange,
258
229
  value,
259
230
  ref,
@@ -273,17 +244,6 @@ export function Collapsible({
273
244
  value,
274
245
  });
275
246
 
276
- // Activation is blocked by this guard rather than the native `disabled`
277
- // attribute, so the trigger keeps `aria-disabled` semantics and stays
278
- // discoverable. A native `disabled` button would silently swallow events
279
- // (e.g. a wrapping tooltip's hover) — the system-wide disabled convention.
280
- const handleToggle = () => {
281
- if (isDisabled) {
282
- return;
283
- }
284
- toggle();
285
- };
286
-
287
247
  const presentation = use(CollapsibleGroupPresentationContext);
288
248
  const isDivided = presentation?.hasDividers ?? false;
289
249
  const density = presentation?.density ?? null;
@@ -308,20 +268,12 @@ export function Collapsible({
308
268
  {...props}>
309
269
  <button
310
270
  type="button"
311
- onClick={handleToggle}
312
- aria-disabled={isDisabled || undefined}
271
+ onClick={toggle}
313
272
  aria-expanded={isOpen}
314
273
  aria-controls={contentId}
315
- // A disabled trigger drops out of the tab order so it isn't a silently
316
- // dead tab stop; activation stays blocked by the handleToggle guard,
317
- // and aria-disabled keeps the state perceivable to assistive tech —
318
- // the system-wide disabled convention (never native `disabled`, which
319
- // would swallow events like a wrapping tooltip's hover).
320
- tabIndex={isDisabled ? -1 : undefined}
321
274
  {...stylex.props(
322
275
  styles.trigger,
323
276
  density != null && triggerDensity[density],
324
- isDisabled && styles.triggerDisabled,
325
277
  )}>
326
278
  <span {...stylex.props(styles.triggerLabel)}>{trigger}</span>
327
279
  <span
@@ -334,15 +286,10 @@ export function Collapsible({
334
286
  </button>
335
287
  <div
336
288
  id={contentId}
337
- {...mergeProps(
338
- themeProps('collapsible-content', {
339
- density: density ?? undefined,
340
- }),
341
- stylex.props(
342
- styles.content,
343
- density != null && contentDensity[density],
344
- !isOpen && styles.contentHidden,
345
- ),
289
+ {...stylex.props(
290
+ styles.content,
291
+ density != null && contentDensity[density],
292
+ !isOpen && styles.contentHidden,
346
293
  )}>
347
294
  {presentation != null ? (
348
295
  <CollapsibleGroupPresentationContext value={null}>
@@ -12,12 +12,7 @@ import {describe, it, expect, vi, beforeEach} from 'vitest';
12
12
  import {render, screen, fireEvent, act} from '@testing-library/react';
13
13
  import userEvent from '@testing-library/user-event';
14
14
  import {ContextMenu} from './ContextMenu';
15
- import {
16
- ContextMenuItem,
17
- ContextMenuCheckboxItem,
18
- ContextMenuRadioGroup,
19
- ContextMenuRadioItem,
20
- } from './index';
15
+ import {ContextMenuItem} from './index';
21
16
  import {DropdownMenuItem} from '../DropdownMenu/DropdownMenuItem';
22
17
  import {Divider} from '../Divider';
23
18
 
@@ -542,72 +537,3 @@ describe('ContextMenu compound mode', () => {
542
537
  });
543
538
  });
544
539
  });
545
-
546
- describe('ContextMenu selectable items', () => {
547
- it('renders checkbox and radio items with correct roles and state', () => {
548
- render(
549
- <ContextMenu
550
- menuContent={
551
- <>
552
- <ContextMenuRadioGroup
553
- value="name"
554
- onChange={() => {}}
555
- aria-label="Sort by">
556
- <ContextMenuRadioItem value="name" label="Sort by name" />
557
- <ContextMenuRadioItem value="date" label="Sort by date" />
558
- </ContextMenuRadioGroup>
559
- <ContextMenuCheckboxItem label="Show hidden" value={true} />
560
- </>
561
- }>
562
- <div>Right-click me</div>
563
- </ContextMenu>,
564
- );
565
- expect(
566
- screen.getByRole('menuitemradio', {name: 'Sort by name', hidden: true}),
567
- ).toHaveAttribute('aria-checked', 'true');
568
- expect(
569
- screen.getByRole('menuitemradio', {name: 'Sort by date', hidden: true}),
570
- ).toHaveAttribute('aria-checked', 'false');
571
- expect(
572
- screen.getByRole('menuitemcheckbox', {name: 'Show hidden', hidden: true}),
573
- ).toHaveAttribute('aria-checked', 'true');
574
- expect(
575
- screen.getByRole('group', {name: 'Sort by', hidden: true}),
576
- ).toBeInTheDocument();
577
- });
578
-
579
- it('fires onChange for radio and checkbox items', async () => {
580
- const user = userEvent.setup();
581
- const onSort = vi.fn();
582
- const onToggle = vi.fn();
583
- render(
584
- <ContextMenu
585
- menuContent={
586
- <>
587
- <ContextMenuRadioGroup
588
- value="name"
589
- onChange={onSort}
590
- aria-label="Sort by">
591
- <ContextMenuRadioItem value="name" label="Sort by name" />
592
- <ContextMenuRadioItem value="date" label="Sort by date" />
593
- </ContextMenuRadioGroup>
594
- <ContextMenuCheckboxItem
595
- label="Show hidden"
596
- value={false}
597
- onChange={onToggle}
598
- />
599
- </>
600
- }>
601
- <div>Right-click me</div>
602
- </ContextMenu>,
603
- );
604
- await user.click(
605
- screen.getByRole('menuitemradio', {name: 'Sort by date', hidden: true}),
606
- );
607
- expect(onSort).toHaveBeenCalledWith('date');
608
- await user.click(
609
- screen.getByRole('menuitemcheckbox', {name: 'Show hidden', hidden: true}),
610
- );
611
- expect(onToggle).toHaveBeenCalledWith(true);
612
- });
613
- });
@@ -46,10 +46,6 @@ import {
46
46
  DropdownMenuContext,
47
47
  type DropdownMenuContextValue,
48
48
  } from '../DropdownMenu/DropdownMenuContext';
49
- import {
50
- MENU_ITEM_ROLES,
51
- MENU_ITEM_SELECTOR,
52
- } from '../DropdownMenu/menuItemRoles';
53
49
  import {useListFocus} from '../hooks/useListFocus';
54
50
  import {useTypeahead} from '../hooks/useTypeahead';
55
51
  import {useLongPress} from '../hooks/useLongPress';
@@ -269,7 +265,7 @@ export function ContextMenu({
269
265
  focusFirst,
270
266
  focusItem,
271
267
  } = useListFocus<HTMLDivElement>({
272
- itemSelector: MENU_ITEM_SELECTOR,
268
+ itemSelector: '[role="menuitem"]:not([aria-disabled="true"])',
273
269
  wrap: false,
274
270
  onEscape: closeMenu,
275
271
  });
@@ -279,7 +275,9 @@ export function ContextMenu({
279
275
  (): HTMLElement[] =>
280
276
  listRef.current
281
277
  ? Array.from(
282
- listRef.current.querySelectorAll<HTMLElement>(MENU_ITEM_SELECTOR),
278
+ listRef.current.querySelectorAll<HTMLElement>(
279
+ '[role="menuitem"]:not([aria-disabled="true"])',
280
+ ),
283
281
  )
284
282
  : [],
285
283
  [listRef],
@@ -343,10 +341,7 @@ export function ContextMenu({
343
341
  if (e.key === 'Enter' || e.key === ' ') {
344
342
  e.preventDefault();
345
343
  const focused = document.activeElement as HTMLElement | null;
346
- if (
347
- focused &&
348
- MENU_ITEM_ROLES.has(focused.getAttribute('role') ?? '')
349
- ) {
344
+ if (focused?.getAttribute('role') === 'menuitem') {
350
345
  focused.click();
351
346
  }
352
347
  return;
@@ -20,18 +20,3 @@ export {
20
20
  DropdownMenuItem as ContextMenuItem,
21
21
  type DropdownMenuItemProps as ContextMenuItemProps,
22
22
  } from '../DropdownMenu/DropdownMenuItem';
23
-
24
- // Selectable items work inside a ContextMenu too — re-exported under the
25
- // ContextMenu name for a coherent API.
26
- export {
27
- DropdownMenuCheckboxItem as ContextMenuCheckboxItem,
28
- type DropdownMenuCheckboxItemProps as ContextMenuCheckboxItemProps,
29
- } from '../DropdownMenu/DropdownMenuCheckboxItem';
30
- export {
31
- DropdownMenuRadioGroup as ContextMenuRadioGroup,
32
- type DropdownMenuRadioGroupProps as ContextMenuRadioGroupProps,
33
- } from '../DropdownMenu/DropdownMenuRadioGroup';
34
- export {
35
- DropdownMenuRadioItem as ContextMenuRadioItem,
36
- type DropdownMenuRadioItemProps as ContextMenuRadioItemProps,
37
- } from '../DropdownMenu/DropdownMenuRadioItem';
@@ -8,33 +8,10 @@ export const docs = {
8
8
  group: 'DropdownMenu',
9
9
  category: 'Action',
10
10
  keywords: ["dropdown","menu","popover","select","actions","contextmenu","overflow","kebab","menubutton"],
11
- playground: {
12
- // `items` is required; without seeded entries the properties-tab preview
13
- // renders an empty trigger button. Provide a few actions so the preview
14
- // is meaningful.
15
- defaults: {
16
- button: {label: 'Actions'},
17
- items: [
18
- {label: 'Edit'},
19
- {label: 'Duplicate'},
20
- {label: 'Delete'},
21
- ],
22
- },
23
- },
24
11
  theming: {
25
12
  targets: [
26
13
  {className: 'astryx-dropdown-menu'},
27
14
  {className: 'astryx-dropdown-menu-item', visualProps: ['size']},
28
- {
29
- className: 'astryx-dropdown-menu-checkbox',
30
- visualProps: ['size'],
31
- states: ['checked', 'disabled'],
32
- },
33
- {
34
- className: 'astryx-dropdown-menu-radio',
35
- visualProps: ['size'],
36
- states: ['checked', 'disabled'],
37
- },
38
15
  ],
39
16
  vars: [
40
17
  {name: '--_dropdown-menu-radius', description: 'Border radius of the menu popup', default: 'var(--radius-element)', private: true},
@@ -38,7 +38,6 @@ import {Icon} from '../Icon';
38
38
  import type {IconType} from '../Icon';
39
39
 
40
40
  import {renderDropdownItems} from './renderDropdownItems';
41
- import {MENU_ITEM_ROLES, MENU_ITEM_SELECTOR} from './menuItemRoles';
42
41
  import {
43
42
  DropdownMenuContext,
44
43
  type DropdownMenuContextValue,
@@ -256,17 +255,14 @@ export function DropdownMenu({
256
255
  popover.hide();
257
256
  }, [popover]);
258
257
 
259
- // Single keyboard navigation path for both modes.
260
- // The selector matches plain items plus selectable items
261
- // (menuitemradio/menuitemcheckbox) so lab checkbox/radio rows are reachable
262
- // and roved to alongside plain items — not just role="menuitem".
258
+ // Single keyboard navigation path for both modes
263
259
  const {
264
260
  listRef,
265
261
  handleKeyDown: listNavKeyDown,
266
262
  focusFirst,
267
263
  focusItem,
268
264
  } = useListFocus<HTMLDivElement>({
269
- itemSelector: MENU_ITEM_SELECTOR,
265
+ itemSelector: '[role="menuitem"]:not([aria-disabled="true"])',
270
266
  wrap: false,
271
267
  onEscape: closeMenu,
272
268
  });
@@ -277,7 +273,9 @@ export function DropdownMenu({
277
273
  (): HTMLElement[] =>
278
274
  listRef.current
279
275
  ? Array.from(
280
- listRef.current.querySelectorAll<HTMLElement>(MENU_ITEM_SELECTOR),
276
+ listRef.current.querySelectorAll<HTMLElement>(
277
+ '[role="menuitem"]:not([aria-disabled="true"])',
278
+ ),
281
279
  )
282
280
  : [],
283
281
  [listRef],
@@ -310,10 +308,7 @@ export function DropdownMenu({
310
308
  if (e.key === 'Enter' || e.key === ' ') {
311
309
  e.preventDefault();
312
310
  const focused = document.activeElement as HTMLElement | null;
313
- if (
314
- focused &&
315
- MENU_ITEM_ROLES.has(focused.getAttribute('role') ?? '')
316
- ) {
311
+ if (focused?.getAttribute('role') === 'menuitem') {
317
312
  focused.click();
318
313
  }
319
314
  return;