@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.
- package/CHANGELOG.md +51 -0
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +29 -8
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +49 -20
- package/dist/Calendar/dayCellUtils.d.ts +67 -5
- package/dist/Calendar/dayCellUtils.d.ts.map +1 -1
- package/dist/Calendar/dayCellUtils.js +139 -18
- package/dist/CheckboxInput/CheckboxInput.d.ts +6 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +7 -1
- package/dist/Code/Code.d.ts +15 -1
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +32 -2
- package/dist/Code/index.d.ts +1 -1
- package/dist/Code/index.d.ts.map +1 -1
- package/dist/CodeBlock/index.d.ts +1 -1
- package/dist/CodeBlock/index.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteEmpty.js +24 -4
- package/dist/ContextMenu/ContextMenu.d.ts +9 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +6 -3
- package/dist/DateRangeInput/DateRangeInput.js +2 -2
- package/dist/DateTimeInput/DateTimeInput.d.ts +0 -5
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/Divider/Divider.d.ts +0 -22
- package/dist/Divider/Divider.d.ts.map +1 -1
- package/dist/Markdown/parser.d.ts +7 -0
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +300 -6
- package/dist/MultiSelector/MultiSelector.d.ts +7 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +10 -1
- package/dist/RadioList/RadioList.d.ts +7 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +3 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +6 -1
- package/dist/Selector/Selector.d.ts +6 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +9 -0
- package/dist/Slider/Slider.d.ts +6 -0
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +10 -1
- package/dist/Spinner/Spinner.d.ts.map +1 -1
- package/dist/Spinner/Spinner.js +1 -1
- package/dist/Switch/Switch.d.ts +6 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +7 -1
- package/dist/Table/BaseTable.d.ts.map +1 -1
- package/dist/Table/BaseTable.js +8 -1
- package/dist/Table/TableCell.d.ts.map +1 -1
- package/dist/Table/TableCell.js +73 -4
- package/dist/Table/index.d.ts +2 -0
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +1 -0
- package/dist/Table/plugins/rowExpansion/index.d.ts +3 -0
- package/dist/Table/plugins/rowExpansion/index.d.ts.map +1 -0
- package/dist/Table/plugins/rowExpansion/index.js +3 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts +90 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -0
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +398 -0
- package/dist/Table/tableContextMenu.d.ts +5 -1
- package/dist/Table/tableContextMenu.d.ts.map +1 -1
- package/dist/Table/tableContextMenu.js +10 -3
- package/dist/Text/Text.d.ts +1 -1
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -3
- package/dist/Text/text.stylex.d.ts +35 -0
- package/dist/Text/text.stylex.d.ts.map +1 -1
- package/dist/Text/text.stylex.js +52 -1
- package/dist/Tokenizer/Tokenizer.d.ts +6 -1
- package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
- package/dist/Tokenizer/Tokenizer.js +10 -1
- package/dist/astryx.css +16 -0
- package/dist/astryx.umd.js +51 -49
- package/dist/astryx.umd.js.map +4 -4
- package/dist/docs-types.d.ts +12 -0
- package/dist/docs-types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AspectRatio/AspectRatio.doc.mjs +1 -1
- package/src/Avatar/Avatar.doc.mjs +2 -2
- package/src/Button/Button.tsx +39 -6
- package/src/ButtonGroup/ButtonGroup.doc.mjs +1 -1
- package/src/Calendar/Calendar.test.tsx +106 -0
- package/src/Calendar/Calendar.tsx +59 -22
- package/src/Calendar/dayCellUtils.test.ts +306 -1
- package/src/Calendar/dayCellUtils.ts +206 -13
- package/src/Card/Card.doc.mjs +1 -1
- package/src/Chat/useChatNewMessages.test.tsx +4 -4
- package/src/CheckboxInput/CheckboxInput.doc.mjs +9 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +36 -0
- package/src/CheckboxInput/CheckboxInput.tsx +11 -0
- package/src/CheckboxList/CheckboxList.doc.mjs +1 -1
- package/src/Code/Code.test.tsx +54 -0
- package/src/Code/Code.tsx +48 -4
- package/src/Code/index.ts +1 -1
- package/src/CodeBlock/index.ts +1 -1
- package/src/CommandPalette/CommandPaletteEmpty.tsx +15 -1
- package/src/ContextMenu/ContextMenu.tsx +17 -1
- package/src/DateInput/DateInput.doc.mjs +6 -1
- package/src/DateRangeInput/DateRangeInput.doc.mjs +1 -1
- package/src/DateRangeInput/DateRangeInput.tsx +2 -2
- package/src/DateTimeInput/DateTimeInput.doc.mjs +1 -1
- package/src/DateTimeInput/DateTimeInput.tsx +1 -6
- package/src/Divider/Divider.tsx +1 -23
- package/src/DropdownMenu/DropdownMenu.test.tsx +5 -3
- package/src/EmptyState/EmptyState.doc.mjs +1 -1
- package/src/Field/Field.doc.mjs +1 -1
- package/src/FileInput/FileInput.doc.mjs +1 -1
- package/src/FormLayout/__snapshots__/FormLayout.test.tsx.snap +0 -3
- package/src/Grid/Grid.doc.mjs +3 -3
- package/src/Layout/__tests__/edgeCompensation.test.tsx +72 -0
- package/src/Markdown/Markdown.test.tsx +23 -0
- package/src/Markdown/incremental.test.ts +36 -0
- package/src/Markdown/parser.test.ts +210 -0
- package/src/Markdown/parser.ts +301 -6
- package/src/MobileNav/MobileNav.doc.mjs +20 -0
- package/src/MultiSelector/MultiSelector.doc.mjs +14 -1
- package/src/MultiSelector/MultiSelector.test.tsx +33 -0
- package/src/MultiSelector/MultiSelector.tsx +20 -0
- package/src/PowerSearch/PowerSearch.doc.mjs +2 -2
- package/src/RadioList/RadioList.doc.mjs +7 -1
- package/src/RadioList/RadioList.test.tsx +46 -0
- package/src/RadioList/RadioList.tsx +10 -1
- package/src/RadioList/RadioListItem.tsx +4 -0
- package/src/Section/Section.test.tsx +4 -10
- package/src/SegmentedControl/SegmentedControl.doc.mjs +1 -1
- package/src/Selector/Selector.doc.mjs +12 -1
- package/src/Selector/Selector.test.tsx +30 -0
- package/src/Selector/Selector.tsx +18 -0
- package/src/Slider/Slider.doc.mjs +14 -2
- package/src/Slider/Slider.test.tsx +30 -0
- package/src/Slider/Slider.tsx +20 -0
- package/src/Spinner/Spinner.tsx +1 -0
- package/src/Switch/Switch.doc.mjs +14 -2
- package/src/Switch/Switch.test.tsx +38 -0
- package/src/Switch/Switch.tsx +12 -0
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/Table/BaseTable.tsx +9 -1
- package/src/Table/Table.test.tsx +23 -0
- package/src/Table/TableCell.tsx +87 -8
- package/src/Table/index.ts +5 -0
- package/src/Table/plugins/rowExpansion/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +155 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +590 -0
- package/src/Table/tableContextMenu.test.tsx +27 -3
- package/src/Table/tableContextMenu.tsx +20 -2
- package/src/Table/useTablePagination.doc.mjs +4 -1
- package/src/Table/useTableRowExpansion.doc.mjs +80 -0
- package/src/Table/useTableSortable.doc.mjs +4 -1
- package/src/Text/Text.doc.mjs +2 -2
- package/src/Text/Text.test.tsx +11 -0
- package/src/Text/Text.tsx +4 -2
- package/src/Text/text.stylex.ts +41 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.doc.mjs +11 -6
- package/src/Tokenizer/Tokenizer.doc.mjs +14 -2
- package/src/Tokenizer/Tokenizer.test.tsx +33 -0
- package/src/Tokenizer/Tokenizer.tsx +19 -0
- package/src/Toolbar/Toolbar.test.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.doc.mjs +10 -0
- package/src/Typeahead/Typeahead.doc.mjs +6 -1
- package/src/docs-types.ts +12 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 --
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|
package/src/Slider/Slider.tsx
CHANGED
|
@@ -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)}
|
package/src/Spinner/Spinner.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
});
|