@astryxdesign/core 0.1.3 → 0.1.4-canary.02643db

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 (168) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/Button/Button.d.ts.map +1 -1
  3. package/dist/Button/Button.js +29 -8
  4. package/dist/Calendar/Calendar.d.ts.map +1 -1
  5. package/dist/Calendar/Calendar.js +49 -20
  6. package/dist/Calendar/dayCellUtils.d.ts +67 -5
  7. package/dist/Calendar/dayCellUtils.d.ts.map +1 -1
  8. package/dist/Calendar/dayCellUtils.js +139 -18
  9. package/dist/CheckboxInput/CheckboxInput.d.ts +6 -1
  10. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  11. package/dist/CheckboxInput/CheckboxInput.js +7 -1
  12. package/dist/Code/Code.d.ts +15 -1
  13. package/dist/Code/Code.d.ts.map +1 -1
  14. package/dist/Code/Code.js +32 -2
  15. package/dist/Code/index.d.ts +1 -1
  16. package/dist/Code/index.d.ts.map +1 -1
  17. package/dist/CodeBlock/index.d.ts +1 -1
  18. package/dist/CodeBlock/index.d.ts.map +1 -1
  19. package/dist/CommandPalette/CommandPaletteEmpty.d.ts +1 -1
  20. package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
  21. package/dist/CommandPalette/CommandPaletteEmpty.js +24 -4
  22. package/dist/ContextMenu/ContextMenu.d.ts +9 -1
  23. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  24. package/dist/ContextMenu/ContextMenu.js +6 -3
  25. package/dist/DateRangeInput/DateRangeInput.js +2 -2
  26. package/dist/DateTimeInput/DateTimeInput.d.ts +0 -5
  27. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  28. package/dist/Divider/Divider.d.ts +0 -22
  29. package/dist/Divider/Divider.d.ts.map +1 -1
  30. package/dist/Markdown/parser.d.ts +7 -0
  31. package/dist/Markdown/parser.d.ts.map +1 -1
  32. package/dist/Markdown/parser.js +300 -6
  33. package/dist/MultiSelector/MultiSelector.d.ts +7 -1
  34. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  35. package/dist/MultiSelector/MultiSelector.js +10 -1
  36. package/dist/RadioList/RadioList.d.ts +7 -1
  37. package/dist/RadioList/RadioList.d.ts.map +1 -1
  38. package/dist/RadioList/RadioList.js +3 -1
  39. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  40. package/dist/RadioList/RadioListItem.js +6 -1
  41. package/dist/Selector/Selector.d.ts +6 -0
  42. package/dist/Selector/Selector.d.ts.map +1 -1
  43. package/dist/Selector/Selector.js +9 -0
  44. package/dist/Slider/Slider.d.ts +6 -0
  45. package/dist/Slider/Slider.d.ts.map +1 -1
  46. package/dist/Slider/Slider.js +10 -1
  47. package/dist/Spinner/Spinner.d.ts.map +1 -1
  48. package/dist/Spinner/Spinner.js +1 -1
  49. package/dist/Switch/Switch.d.ts +6 -1
  50. package/dist/Switch/Switch.d.ts.map +1 -1
  51. package/dist/Switch/Switch.js +7 -1
  52. package/dist/Table/BaseTable.d.ts.map +1 -1
  53. package/dist/Table/BaseTable.js +8 -1
  54. package/dist/Table/TableCell.d.ts.map +1 -1
  55. package/dist/Table/TableCell.js +73 -4
  56. package/dist/Table/index.d.ts +2 -0
  57. package/dist/Table/index.d.ts.map +1 -1
  58. package/dist/Table/index.js +1 -0
  59. package/dist/Table/plugins/rowExpansion/index.d.ts +3 -0
  60. package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -0
  61. package/dist/Table/plugins/rowExpansion/index.js +3 -0
  62. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +90 -0
  63. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -0
  64. package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +398 -0
  65. package/dist/Table/tableContextMenu.d.ts +5 -1
  66. package/dist/Table/tableContextMenu.d.ts.map +1 -1
  67. package/dist/Table/tableContextMenu.js +10 -3
  68. package/dist/Text/Text.d.ts +1 -1
  69. package/dist/Text/Text.d.ts.map +1 -1
  70. package/dist/Text/Text.js +4 -3
  71. package/dist/Text/text.stylex.d.ts +35 -0
  72. package/dist/Text/text.stylex.d.ts.map +1 -1
  73. package/dist/Text/text.stylex.js +52 -1
  74. package/dist/Tokenizer/Tokenizer.d.ts +6 -1
  75. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  76. package/dist/Tokenizer/Tokenizer.js +10 -1
  77. package/dist/astryx.css +16 -0
  78. package/dist/astryx.umd.js +51 -49
  79. package/dist/astryx.umd.js.map +4 -4
  80. package/dist/docs-types.d.ts +12 -0
  81. package/dist/docs-types.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/AspectRatio/AspectRatio.doc.mjs +1 -1
  84. package/src/Avatar/Avatar.doc.mjs +2 -2
  85. package/src/Button/Button.tsx +39 -6
  86. package/src/ButtonGroup/ButtonGroup.doc.mjs +1 -1
  87. package/src/Calendar/Calendar.test.tsx +106 -0
  88. package/src/Calendar/Calendar.tsx +59 -22
  89. package/src/Calendar/dayCellUtils.test.ts +306 -1
  90. package/src/Calendar/dayCellUtils.ts +206 -13
  91. package/src/Card/Card.doc.mjs +1 -1
  92. package/src/Chat/useChatNewMessages.test.tsx +4 -4
  93. package/src/CheckboxInput/CheckboxInput.doc.mjs +9 -1
  94. package/src/CheckboxInput/CheckboxInput.test.tsx +36 -0
  95. package/src/CheckboxInput/CheckboxInput.tsx +11 -0
  96. package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
  97. package/src/Code/Code.test.tsx +54 -0
  98. package/src/Code/Code.tsx +48 -4
  99. package/src/Code/index.ts +1 -1
  100. package/src/CodeBlock/index.ts +1 -1
  101. package/src/CommandPalette/CommandPaletteEmpty.tsx +15 -1
  102. package/src/ContextMenu/ContextMenu.tsx +17 -1
  103. package/src/DateInput/DateInput.doc.mjs +6 -1
  104. package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
  105. package/src/DateRangeInput/DateRangeInput.tsx +2 -2
  106. package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
  107. package/src/DateTimeInput/DateTimeInput.tsx +1 -6
  108. package/src/Divider/Divider.tsx +1 -23
  109. package/src/DropdownMenu/DropdownMenu.test.tsx +5 -3
  110. package/src/EmptyState/EmptyState.doc.mjs +1 -1
  111. package/src/Field/Field.doc.mjs +1 -1
  112. package/src/FileInput/FileInput.doc.mjs +1 -1
  113. package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
  114. package/src/Grid/Grid.doc.mjs +3 -3
  115. package/src/Layout/__tests__/edgeCompensation.test.tsx +72 -0
  116. package/src/Markdown/Markdown.test.tsx +23 -0
  117. package/src/Markdown/incremental.test.ts +36 -0
  118. package/src/Markdown/parser.test.ts +210 -0
  119. package/src/Markdown/parser.ts +301 -6
  120. package/src/MobileNav/MobileNav.doc.mjs +20 -0
  121. package/src/MultiSelector/MultiSelector.doc.mjs +14 -1
  122. package/src/MultiSelector/MultiSelector.test.tsx +33 -0
  123. package/src/MultiSelector/MultiSelector.tsx +20 -0
  124. package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
  125. package/src/RadioList/RadioList.doc.mjs +7 -1
  126. package/src/RadioList/RadioList.test.tsx +46 -0
  127. package/src/RadioList/RadioList.tsx +10 -1
  128. package/src/RadioList/RadioListItem.tsx +4 -0
  129. package/src/Section/Section.test.tsx +4 -10
  130. package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
  131. package/src/Selector/Selector.doc.mjs +12 -1
  132. package/src/Selector/Selector.test.tsx +30 -0
  133. package/src/Selector/Selector.tsx +18 -0
  134. package/src/Slider/Slider.doc.mjs +14 -2
  135. package/src/Slider/Slider.test.tsx +30 -0
  136. package/src/Slider/Slider.tsx +20 -0
  137. package/src/Spinner/Spinner.tsx +1 -0
  138. package/src/Switch/Switch.doc.mjs +14 -2
  139. package/src/Switch/Switch.test.tsx +38 -0
  140. package/src/Switch/Switch.tsx +12 -0
  141. package/src/TabList/TabList.doc.mjs +3 -3
  142. package/src/Table/BaseTable.tsx +9 -1
  143. package/src/Table/Table.test.tsx +23 -0
  144. package/src/Table/TableCell.tsx +87 -8
  145. package/src/Table/index.ts +5 -0
  146. package/src/Table/plugins/rowExpansion/index.ts +7 -0
  147. package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +155 -0
  148. package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +590 -0
  149. package/src/Table/tableContextMenu.test.tsx +27 -3
  150. package/src/Table/tableContextMenu.tsx +20 -2
  151. package/src/Table/useTablePagination.doc.mjs +4 -1
  152. package/src/Table/useTableRowExpansion.doc.mjs +80 -0
  153. package/src/Table/useTableSortable.doc.mjs +4 -1
  154. package/src/Text/Text.doc.mjs +2 -2
  155. package/src/Text/Text.test.tsx +11 -0
  156. package/src/Text/Text.tsx +4 -2
  157. package/src/Text/text.stylex.ts +41 -0
  158. package/src/TextArea/TextArea.doc.mjs +1 -1
  159. package/src/TextInput/TextInput.doc.mjs +1 -1
  160. package/src/TimeInput/TimeInput.doc.mjs +11 -6
  161. package/src/Tokenizer/Tokenizer.doc.mjs +14 -2
  162. package/src/Tokenizer/Tokenizer.test.tsx +33 -0
  163. package/src/Tokenizer/Tokenizer.tsx +19 -0
  164. package/src/Toolbar/Toolbar.test.tsx +1 -1
  165. package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
  166. package/src/Typeahead/Typeahead.doc.mjs +6 -1
  167. package/src/docs-types.ts +12 -0
  168. package/src/hooks/useKeyboardHint.doc.mjs +6 -6
@@ -112,11 +112,17 @@ export const docs = {
112
112
  type: 'boolean',
113
113
  description: 'Disables the selector.',
114
114
  },
115
+ {
116
+ name: 'htmlName',
117
+ type: 'string',
118
+ description:
119
+ 'The HTML name attribute for form submissions. Renders one hidden input per selected value, like a native multi-select.',
120
+ },
115
121
  {
116
122
  name: 'disabledMessage',
117
123
  type: 'string',
118
124
  description:
119
- 'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled MultiSelector in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
125
+ 'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled MultiSelector in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
120
126
  },
121
127
  {
122
128
  name: 'isLabelHidden',
@@ -233,6 +239,7 @@ export const docsZh = {
233
239
  hasSearch: '是否显示用于过滤选项的搜索输入。',
234
240
  searchPlaceholder: '搜索输入的占位文本。',
235
241
  isDisabled: '禁用选择器。',
242
+ htmlName: '用于表单提交的 HTML name 属性。为每个已选值渲染一个隐藏输入,类似原生多选。',
236
243
  disabledMessage:
237
244
  '解释选择器被禁用的原因。与 isDisabled 一起使用时,悬停/键盘聚焦时显示工具提示,并通过 aria-disabled 保持触发器可聚焦(仍无法激活)。请使用此属性,而不是用 Tooltip 包裹被禁用的选择器。',
238
245
  isLabelHidden: '视觉上隐藏标签同时保持其可访问性。',
@@ -316,6 +323,11 @@ export const docsDense = {
316
323
  description:
317
324
  'Enable select-all when most users will want all or nearly all options selected.',
318
325
  },
326
+ {
327
+ guidance: true,
328
+ description:
329
+ 'Use inside InputGroup only for a short prefix or suffix addon; prefer count or labels trigger display so the group stays single-line.',
330
+ },
319
331
  {
320
332
  guidance: false,
321
333
  description: 'Use for single-value selection; use Selector instead.',
@@ -352,6 +364,7 @@ export const docsDense = {
352
364
  hasSearch: 'show search input',
353
365
  searchPlaceholder: 'search placeholder',
354
366
  isDisabled: 'disables selector',
367
+ htmlName: 'HTML name attr; one hidden input per selected value.',
355
368
  disabledMessage:
356
369
  'why disabled; w/ isDisabled shows tooltip on hover/focus, trigger stays focusable via aria-disabled; use instead of Tooltip wrapper',
357
370
  isLabelHidden: 'visually hides label',
@@ -1042,4 +1042,37 @@ describe('MultiSelector', () => {
1042
1042
  expect(trigger).toHaveAttribute('tabIndex', '-1');
1043
1043
  });
1044
1044
  });
1045
+ describe('form participation', () => {
1046
+ it('submits one entry per selected value under htmlName', () => {
1047
+ const {container} = render(
1048
+ <form>
1049
+ <MultiSelector
1050
+ label="Fruit"
1051
+ htmlName="fruit"
1052
+ options={['Apple', 'Banana', 'Orange']}
1053
+ value={['Apple', 'Orange']}
1054
+ onChange={() => {}}
1055
+ />
1056
+ </form>,
1057
+ );
1058
+ const data = new FormData(container.querySelector('form')!);
1059
+ expect(data.getAll('fruit')).toEqual(['Apple', 'Orange']);
1060
+ });
1061
+
1062
+ it('is excluded from form data when disabled', () => {
1063
+ const {container} = render(
1064
+ <form>
1065
+ <MultiSelector
1066
+ label="Fruit"
1067
+ htmlName="fruit"
1068
+ options={['Apple']}
1069
+ value={['Apple']}
1070
+ onChange={() => {}}
1071
+ isDisabled
1072
+ />
1073
+ </form>,
1074
+ );
1075
+ expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
1076
+ });
1077
+ });
1045
1078
  });
@@ -439,6 +439,13 @@ export interface MultiSelectorProps<
439
439
  */
440
440
  value: string[];
441
441
 
442
+ /**
443
+ * The HTML name attribute for form submissions. When set, hidden inputs
444
+ * carry one entry per selected value under this name, matching how a
445
+ * native multi-select serializes.
446
+ */
447
+ htmlName?: string;
448
+
442
449
  /**
443
450
  * Callback when selection changes.
444
451
  */
@@ -597,6 +604,7 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
597
604
  renderOption,
598
605
  isDefaultOpen = false,
599
606
  'data-testid': testId,
607
+ htmlName,
600
608
  width,
601
609
  xstyle,
602
610
  className,
@@ -1280,6 +1288,18 @@ export function MultiSelector<T extends MultiSelectorOptionType>({
1280
1288
  {renderTriggerContent()}
1281
1289
  </span>
1282
1290
  </button>
1291
+ {htmlName != null &&
1292
+ value.map(v => (
1293
+ <input
1294
+ key={v}
1295
+ type="hidden"
1296
+ name={htmlName}
1297
+ value={v}
1298
+ // Disabled native controls are excluded from form submission;
1299
+ // mirror that for the hidden carriers.
1300
+ disabled={isDisabled}
1301
+ />
1302
+ ))}
1283
1303
  {isBusy && <Spinner size="sm" />}
1284
1304
  {hasClear && value.length > 0 && !isDisabled && (
1285
1305
  <button
@@ -75,7 +75,7 @@ export const docs = {
75
75
  name: 'disabledMessage',
76
76
  type: 'string',
77
77
  description:
78
- 'Explains why the search is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled PowerSearch in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
78
+ 'Explains why the search is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled PowerSearch in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
79
79
  },
80
80
  {
81
81
  name: 'status',
@@ -223,7 +223,7 @@ export const docsZh = {
223
223
  name: 'disabledMessage',
224
224
  type: 'string',
225
225
  description:
226
- 'Explains why the search is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled PowerSearch in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
226
+ 'Explains why the search is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the input focusable via aria-disabled (input stays blocked). Use this instead of wrapping a disabled PowerSearch in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
227
227
  },
228
228
  {
229
229
  name: 'status',
@@ -74,11 +74,17 @@ export const docs = {
74
74
  description: 'Whether all radio items are disabled.',
75
75
  default: 'false',
76
76
  },
77
+ {
78
+ name: 'htmlName',
79
+ type: 'string',
80
+ description:
81
+ 'The HTML name attribute shared by the radio inputs, useful for form submissions. When omitted, a unique internal name still groups the radios.',
82
+ },
77
83
  {
78
84
  name: 'disabledMessage',
79
85
  type: 'string',
80
86
  description:
81
- 'Explains why the group is disabled. Applies to the whole-group disabled state (isDisabled), not per item. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the radios focusable via aria-disabled (selection stays blocked). Use this instead of wrapping a disabled RadioList in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
87
+ 'Explains why the group is disabled. Applies to the whole-group disabled state (isDisabled), not per item. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the radios focusable via aria-disabled (selection stays blocked). Use this instead of wrapping a disabled RadioList in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
82
88
  },
83
89
  {
84
90
  name: 'isRequired',
@@ -576,4 +576,50 @@ describe('RadioList', () => {
576
576
  }
577
577
  });
578
578
  });
579
+ describe('form participation', () => {
580
+ it('submits the selected value under htmlName', () => {
581
+ const {container} = render(
582
+ <form>
583
+ <RadioList label="Preference" htmlName="pref" value="b" onChange={() => {}}>
584
+ <RadioListItem label="Option A" value="a" />
585
+ <RadioListItem label="Option B" value="b" />
586
+ </RadioList>
587
+ </form>,
588
+ );
589
+ const data = new FormData(container.querySelector('form')!);
590
+ expect(data.get('pref')).toBe('b');
591
+ });
592
+
593
+ it('is excluded from form data when disabled, even with a disabledMessage', () => {
594
+ const {container} = render(
595
+ <form>
596
+ <RadioList
597
+ label="Preference"
598
+ htmlName="pref"
599
+ value="a"
600
+ onChange={() => {}}
601
+ isDisabled
602
+ disabledMessage="Locked"
603
+ >
604
+ <RadioListItem label="Option A" value="a" />
605
+ </RadioList>
606
+ </form>,
607
+ );
608
+ expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
609
+ });
610
+
611
+ it('keeps working as an isolated group when htmlName is omitted', () => {
612
+ const {container} = render(
613
+ <form>
614
+ <RadioList label="Preference" value="a" onChange={() => {}}>
615
+ <RadioListItem label="Option A" value="a" />
616
+ </RadioList>
617
+ </form>,
618
+ );
619
+ // Auto-generated internal name still groups the radios, but the field
620
+ // name is not part of the public form contract.
621
+ const input = container.querySelector('input[type="radio"]')!;
622
+ expect(input.getAttribute('name')).toBeTruthy();
623
+ });
624
+ });
579
625
  });
@@ -111,6 +111,13 @@ export interface RadioListProps extends Omit<
111
111
  * @default false
112
112
  */
113
113
  isDisabled?: boolean;
114
+
115
+ /**
116
+ * The HTML name attribute shared by the radio inputs in the group.
117
+ * Useful for form submissions; when omitted, a unique internal name is
118
+ * generated so the group still roves correctly.
119
+ */
120
+ htmlName?: string;
114
121
  /**
115
122
  * Explains why the radio group is disabled. Applies to the whole-group
116
123
  * disabled state (`isDisabled`), not individual items. When set together with
@@ -199,9 +206,11 @@ export function RadioList({
199
206
  className,
200
207
  style,
201
208
  'data-testid': dataTestId,
209
+ htmlName,
202
210
  children,
203
211
  }: RadioListProps) {
204
- const name = useId();
212
+ const autoName = useId();
213
+ const name = htmlName ?? autoName;
205
214
  const inputID = useId();
206
215
  const labelID = useId();
207
216
  const descriptionID = useId();
@@ -254,6 +254,10 @@ export function RadioListItem({
254
254
  checked={isChecked}
255
255
  disabled={isDisabled && !keepsFocusableForMessage}
256
256
  aria-disabled={keepsFocusableForMessage ? 'true' : undefined}
257
+ // A focusable-disabled radio is not natively disabled, so detach it
258
+ // from the form instead: it keeps its name (grouping) but is excluded
259
+ // from submission, matching a natively disabled control.
260
+ form={keepsFocusableForMessage ? '' : undefined}
257
261
  required={context.isRequired}
258
262
  onChange={() => {
259
263
  if (isDisabled) {
@@ -38,9 +38,7 @@ describe('Section', () => {
38
38
  });
39
39
 
40
40
  it('renders with variant="muted"', () => {
41
- const {container} = render(
42
- <Section variant="muted">Content</Section>,
43
- );
41
+ const {container} = render(<Section variant="muted">Content</Section>);
44
42
  const inner = container.firstElementChild!.firstElementChild!;
45
43
  expect(inner.className).toContain('astryx-section');
46
44
  expect(inner.className).toContain('muted');
@@ -109,9 +107,7 @@ describe('Section', () => {
109
107
  });
110
108
 
111
109
  it('renders variant in astryx class names', () => {
112
- const {container} = render(
113
- <Section variant="muted">Content</Section>,
114
- );
110
+ const {container} = render(<Section variant="muted">Content</Section>);
115
111
  const inner = container.firstElementChild!.firstElementChild!;
116
112
  expect(inner.className).toContain('astryx-section');
117
113
  expect(inner.className).toContain('muted');
@@ -120,9 +116,7 @@ describe('Section', () => {
120
116
  it('accepts xstyle prop without error', () => {
121
117
  // xstyle is a StyleXStyles type; in tests stylex.create returns objects
122
118
  // that may not produce runtime styles, but the prop should be accepted
123
- const {container} = render(
124
- <Section xstyle={undefined}>Content</Section>,
125
- );
119
+ const {container} = render(<Section xstyle={undefined}>Content</Section>);
126
120
  expect(container.firstElementChild).toBeInTheDocument();
127
121
  });
128
122
 
@@ -163,7 +157,7 @@ describe('Section', () => {
163
157
  <Section data-testid="inner">Inner</Section>
164
158
  </Section>,
165
159
  );
166
- // Outer section's inner div should set --xds-section-padding
160
+ // Outer section's inner div should set --astryx-section-padding
167
161
  const outerInner = container.firstElementChild!.firstElementChild!;
168
162
  expect(outerInner.className).toBeDefined();
169
163
  // Inner section should render without error
@@ -69,7 +69,7 @@ export const docs = {
69
69
  name: 'disabledMessage',
70
70
  type: 'string',
71
71
  description:
72
- 'Explains why the control is disabled. Applies to the whole-group disabled state (isDisabled), not per segment. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the control focusable via aria-disabled (selection stays blocked). Use this instead of wrapping a disabled SegmentedControl in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
72
+ 'Explains why the control is disabled. Applies to the whole-group disabled state (isDisabled), not per segment. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the control focusable via aria-disabled (selection stays blocked). Use this instead of wrapping a disabled SegmentedControl in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
73
73
  },
74
74
  {
75
75
  name: 'children',
@@ -86,11 +86,17 @@ export const docs = {
86
86
  description: 'Disables the selector.',
87
87
  default: 'false',
88
88
  },
89
+ {
90
+ name: 'htmlName',
91
+ type: 'string',
92
+ description:
93
+ 'The HTML name attribute for form submissions. Renders a hidden input carrying the selected value, like a native select.',
94
+ },
89
95
  {
90
96
  name: 'disabledMessage',
91
97
  type: 'string',
92
98
  description:
93
- 'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled Selector in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
99
+ 'Explains why the selector is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled Selector in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
94
100
  },
95
101
  {
96
102
  name: 'isLabelHidden',
@@ -346,6 +352,11 @@ export const docsDense = {
346
352
  description:
347
353
  'Set a meaningful placeholder that hints at the expected selection (e.g. "Choose a country" not "Select...").',
348
354
  },
355
+ {
356
+ guidance: true,
357
+ description:
358
+ 'Use inside InputGroup only when the selector needs a short prefix or suffix addon.',
359
+ },
349
360
  {
350
361
  guidance: false,
351
362
  description:
@@ -839,4 +839,34 @@ describe('Selector', () => {
839
839
  expect(trigger).toHaveAttribute('tabIndex', '-1');
840
840
  });
841
841
  });
842
+ describe('form participation', () => {
843
+ it('submits the selected value under htmlName', () => {
844
+ const {container} = render(
845
+ <form>
846
+ <Selector label="Fruit" htmlName="fruit" options={OPTIONS} value="Banana" />
847
+ </form>,
848
+ );
849
+ const data = new FormData(container.querySelector('form')!);
850
+ expect(data.get('fruit')).toBe('Banana');
851
+ });
852
+
853
+ it('submits an empty string when nothing is selected', () => {
854
+ const {container} = render(
855
+ <form>
856
+ <Selector label="Fruit" htmlName="fruit" options={OPTIONS} />
857
+ </form>,
858
+ );
859
+ const data = new FormData(container.querySelector('form')!);
860
+ expect(data.get('fruit')).toBe('');
861
+ });
862
+
863
+ it('is excluded from form data when disabled', () => {
864
+ const {container} = render(
865
+ <form>
866
+ <Selector label="Fruit" htmlName="fruit" options={OPTIONS} value="Banana" isDisabled />
867
+ </form>,
868
+ );
869
+ expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
870
+ });
871
+ });
842
872
  });
@@ -483,6 +483,13 @@ interface SelectorPropsBase<
483
483
  */
484
484
  isDefaultOpen?: boolean;
485
485
 
486
+ /**
487
+ * The HTML name attribute for form submissions. When set, a hidden input
488
+ * carries the selected value under this name, matching how a native
489
+ * select serializes.
490
+ */
491
+ htmlName?: string;
492
+
486
493
  /**
487
494
  * Test ID for testing frameworks.
488
495
  */
@@ -564,6 +571,7 @@ export function Selector<T extends SelectorOptionType>(
564
571
  status,
565
572
  labelTooltip,
566
573
  startIcon,
574
+ htmlName,
567
575
  renderOption,
568
576
  hasSearch = false,
569
577
  searchPlaceholder = 'Search...',
@@ -991,6 +999,16 @@ export function Selector<T extends SelectorOptionType>(
991
999
  {selectedItem?.label ?? placeholder}
992
1000
  </span>
993
1001
  </button>
1002
+ {htmlName != null && (
1003
+ <input
1004
+ type="hidden"
1005
+ name={htmlName}
1006
+ value={value ?? ''}
1007
+ // Disabled native controls are excluded from form submission;
1008
+ // mirror that for the hidden carrier.
1009
+ disabled={isDisabled}
1010
+ />
1011
+ )}
994
1012
  {isBusy && <Spinner size="sm" />}
995
1013
  {hasClear && value != null && !isDisabled && (
996
1014
  <button
@@ -95,11 +95,17 @@ export const docs = {
95
95
  description: 'Whether the slider is disabled.',
96
96
  default: 'false',
97
97
  },
98
+ {
99
+ name: 'htmlName',
100
+ type: 'string',
101
+ description:
102
+ 'The HTML name attribute for form submissions. Renders hidden inputs carrying the current value (two entries in range mode).',
103
+ },
98
104
  {
99
105
  name: 'disabledMessage',
100
106
  type: 'string',
101
107
  description:
102
- 'Explains why the slider is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the thumb focusable via aria-disabled (value changes stay blocked). Use this instead of wrapping a disabled Slider in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
108
+ 'Explains why the slider is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the thumb focusable via aria-disabled (value changes stay blocked). Use this instead of wrapping a disabled Slider in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
103
109
  },
104
110
  {
105
111
  name: 'isOptional',
@@ -244,11 +250,16 @@ export const docsZh = {
244
250
  description: '是否禁用滑块。',
245
251
  default: 'false',
246
252
  },
253
+ {
254
+ name: 'htmlName',
255
+ type: 'string',
256
+ description: '用于表单提交的 HTML name 属性。渲染携带当前值的隐藏输入(范围模式下为两个条目)。',
257
+ },
247
258
  {
248
259
  name: 'disabledMessage',
249
260
  type: 'string',
250
261
  description:
251
- 'Explains why the slider is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the thumb focusable via aria-disabled (value changes stay blocked). Use this instead of wrapping a disabled Slider in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
262
+ 'Explains why the slider is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the thumb focusable via aria-disabled (value changes stay blocked). Use this instead of wrapping a disabled Slider in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
252
263
  },
253
264
  {
254
265
  name: 'isOptional',
@@ -338,6 +349,7 @@ export const docsDense = {
338
349
  marks: 'Tick marks at specified positions w/ optional labels.',
339
350
  minStepsBetweenThumbs: 'Min steps between thumbs in range mode; prevents overlap.',
340
351
  isDisabled: 'Whether slider is disabled.',
352
+ htmlName: 'HTML name attr; hidden inputs carry the value (two in range mode).',
341
353
  isOptional: 'Whether field is optional.',
342
354
  isRequired: 'Whether field is required.',
343
355
  isLabelHidden: 'Visually hide label.',
@@ -535,4 +535,34 @@ describe('Slider', () => {
535
535
  expect(screen.getByRole('slider')).toHaveAttribute('tabindex', '-1');
536
536
  });
537
537
  });
538
+ describe('form participation', () => {
539
+ it('submits the value under htmlName', () => {
540
+ const {container} = render(
541
+ <form>
542
+ <Slider label="Volume" htmlName="volume" value={50} />
543
+ </form>,
544
+ );
545
+ const data = new FormData(container.querySelector('form')!);
546
+ expect(data.get('volume')).toBe('50');
547
+ });
548
+
549
+ it('submits both range values under the same name', () => {
550
+ const {container} = render(
551
+ <form>
552
+ <Slider label="Price" htmlName="price" value={[20, 80] as [number, number]} />
553
+ </form>,
554
+ );
555
+ const data = new FormData(container.querySelector('form')!);
556
+ expect(data.getAll('price')).toEqual(['20', '80']);
557
+ });
558
+
559
+ it('is excluded from form data when disabled', () => {
560
+ const {container} = render(
561
+ <form>
562
+ <Slider label="Volume" htmlName="volume" value={50} isDisabled />
563
+ </form>,
564
+ );
565
+ expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
566
+ });
567
+ });
538
568
  });
@@ -111,6 +111,12 @@ export interface SliderBaseProps extends Omit<
111
111
  valueDisplay?: 'tooltip' | 'text' | 'none';
112
112
  /** Tick marks at specified positions with optional labels. */
113
113
  marks?: {value: number; label?: string}[];
114
+ /**
115
+ * The HTML name attribute for form submissions. When set, the slider
116
+ * renders hidden inputs carrying the current value (two in range mode,
117
+ * matching how paired native range inputs submit).
118
+ */
119
+ htmlName?: string;
114
120
  /** Test ID for the root element. */
115
121
  'data-testid'?: string;
116
122
  }
@@ -363,6 +369,7 @@ export function Slider({ref, ...props}: SliderProps) {
363
369
  step = 1,
364
370
  orientation = 'horizontal',
365
371
  formatValue,
372
+ htmlName,
366
373
  valueDisplay = 'tooltip',
367
374
  marks,
368
375
  width,
@@ -803,6 +810,19 @@ export function Slider({ref, ...props}: SliderProps) {
803
810
  }),
804
811
  stylex.props(styles.sliderRow),
805
812
  )}>
813
+ {htmlName != null &&
814
+ values.map((v, i) => (
815
+ <input
816
+ // Positional identity: index 0 is the start thumb, 1 the end.
817
+ key={i === 0 ? 'start' : 'end'}
818
+ type="hidden"
819
+ name={htmlName}
820
+ value={String(v)}
821
+ // Disabled native controls are excluded from form submission;
822
+ // mirror that for the hidden carrier.
823
+ disabled={isDisabled}
824
+ />
825
+ ))}
806
826
  <div
807
827
  ref={mergeRefs(ref, trackRef, disabledMessageTooltip.ref)}
808
828
  {...(isRange ? {role: 'group', 'aria-label': label} : undefined)}
@@ -70,6 +70,7 @@ const styles = stylex.create({
70
70
  canvas: {
71
71
  backfaceVisibility: 'hidden',
72
72
  display: 'block',
73
+ willChange: 'transform',
73
74
  // Slow the rotation dramatically under reduced-motion rather than freezing
74
75
  // it (a frozen spinner reads as broken), matching ProgressBar's approach.
75
76
  // The role="status" + "Loading" label still convey busy state (obs-6).
@@ -62,11 +62,17 @@ export const docs = {
62
62
  description: 'Whether the switch is disabled.',
63
63
  default: 'false',
64
64
  },
65
+ {
66
+ name: 'htmlName',
67
+ type: 'string',
68
+ description:
69
+ 'The HTML name attribute for the underlying checkbox input, useful for form submissions (submits "on" when the switch is on).',
70
+ },
65
71
  {
66
72
  name: 'disabledMessage',
67
73
  type: 'string',
68
74
  description:
69
- 'Explains why the switch is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the switch focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled Switch in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
75
+ 'Explains why the switch is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the switch focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled Switch in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
70
76
  },
71
77
  {
72
78
  name: 'isOptional',
@@ -203,11 +209,16 @@ export const docsZh = {
203
209
  description: '开关是否被禁用。',
204
210
  default: 'false',
205
211
  },
212
+ {
213
+ name: 'htmlName',
214
+ type: 'string',
215
+ description: '底层复选框输入的 HTML name 属性,用于表单提交(开启时提交 "on")。',
216
+ },
206
217
  {
207
218
  name: 'disabledMessage',
208
219
  type: 'string',
209
220
  description:
210
- 'Explains why the switch is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the switch focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled Switch in Tooltip — disabled controls swallow the hover events an external Tooltip needs.',
221
+ 'Explains why the switch is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the switch focusable via aria-disabled (toggling stays blocked). Use this instead of wrapping a disabled Switch in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.',
211
222
  },
212
223
  {
213
224
  name: 'isOptional',
@@ -309,6 +320,7 @@ export const docsDense = {
309
320
  isLabelHidden: 'Visually hides label; still accessible to screen readers.',
310
321
  description: 'Description text below label.',
311
322
  isDisabled: 'Whether switch is disabled.',
323
+ htmlName: 'HTML name attr for the checkbox; submits "on" when on.',
312
324
  isOptional: 'Whether field is optional; mutually exclusive w/ isRequired.',
313
325
  isRequired: 'Whether switch is required; mutually exclusive w/ isOptional.',
314
326
  status: 'Status indicator w/ type + message; colored message box, sets aria-invalid on error.',
@@ -470,4 +470,42 @@ describe('Switch', () => {
470
470
  expect(screen.getByRole('switch')).toBeDisabled();
471
471
  });
472
472
  });
473
+ describe('form participation', () => {
474
+ it('submits under htmlName when on', () => {
475
+ const {container} = render(
476
+ <form>
477
+ <Switch label="Notify" htmlName="notify" value={true} onChange={() => {}} />
478
+ </form>,
479
+ );
480
+ const data = new FormData(container.querySelector('form')!);
481
+ expect(data.get('notify')).toBe('on');
482
+ });
483
+
484
+ it('is excluded from form data when disabled, even with a disabledMessage', () => {
485
+ const {container} = render(
486
+ <form>
487
+ <Switch
488
+ label="Notify"
489
+ htmlName="notify"
490
+ value={true}
491
+ onChange={() => {}}
492
+ isDisabled
493
+ disabledMessage="Locked"
494
+ />
495
+ </form>,
496
+ );
497
+ expect([...new FormData(container.querySelector('form')!).keys()]).toEqual([]);
498
+ });
499
+
500
+ it('submits nothing when off or when htmlName is omitted', () => {
501
+ const {container} = render(
502
+ <form>
503
+ <Switch label="Off" htmlName="off" value={false} onChange={() => {}} />
504
+ <Switch label="Unnamed" value={true} onChange={() => {}} />
505
+ </form>,
506
+ );
507
+ const data = new FormData(container.querySelector('form')!);
508
+ expect([...data.keys()]).toEqual([]);
509
+ });
510
+ });
473
511
  });