@astryxdesign/core 0.2.0-canary.ea693ac → 0.2.0-canary.f306f13
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/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +2 -6
- package/dist/Button/Button.d.ts +1 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +2 -4
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +2 -2
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -19
- package/dist/CodeBlock/CodeBlock.js +2 -2
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +1 -10
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +11 -8
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +1 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +1 -1
- package/dist/Field/FieldLabel.d.ts +1 -2
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +3 -7
- package/dist/Item/Item.d.ts +3 -16
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +6 -31
- package/dist/List/ListItem.d.ts +1 -11
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +0 -2
- package/dist/MetadataList/MetadataList.d.ts +1 -1
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +2 -4
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +2 -13
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControlItem.js +2 -3
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +0 -5
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +2 -2
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +4 -15
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +1 -1
- package/dist/Toast/ToastViewport.d.ts.map +1 -1
- package/dist/Toast/ToastViewport.js +9 -61
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +1 -3
- package/dist/Token/Token.d.ts +1 -4
- package/dist/Token/Token.d.ts.map +1 -1
- package/dist/Token/Token.js +43 -68
- package/dist/Typeahead/BaseTypeahead.d.ts +0 -9
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +0 -2
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +1 -8
- package/dist/astryx.css +3 -16
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/useKeyboardHint.d.ts +1 -1
- package/dist/hooks/useKeyboardHint.d.ts.map +1 -1
- package/dist/hooks/useKeyboardHint.js +2 -4
- package/locales/en.json +0 -32
- package/locales/pseudo.json +0 -24
- package/package.json +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +1 -21
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -4
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +1 -1
- package/src/Button/Button.test.tsx +0 -16
- package/src/Button/Button.tsx +2 -4
- package/src/CheckboxInput/CheckboxInput.test.tsx +0 -23
- package/src/CheckboxInput/CheckboxInput.tsx +2 -17
- package/src/CheckboxList/CheckboxList.test.tsx +3 -94
- package/src/CheckboxList/CheckboxListItem.tsx +10 -17
- package/src/CodeBlock/CodeBlock.test.tsx +0 -17
- package/src/CodeBlock/CodeBlock.tsx +2 -2
- package/src/ContextMenu/ContextMenu.test.tsx +2 -17
- package/src/ContextMenu/ContextMenu.tsx +1 -10
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +12 -4
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +10 -9
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +2 -20
- package/src/Field/FieldLabel.test.tsx +0 -25
- package/src/Field/FieldLabel.tsx +2 -11
- package/src/Item/Item.doc.mjs +0 -5
- package/src/Item/Item.test.tsx +0 -88
- package/src/Item/Item.tsx +11 -57
- package/src/List/ListItem.doc.mjs +0 -12
- package/src/List/ListItem.tsx +0 -13
- package/src/MetadataList/MetadataList.test.tsx +0 -24
- package/src/MetadataList/MetadataList.tsx +2 -6
- package/src/MultiSelector/MultiSelector.test.tsx +0 -76
- package/src/MultiSelector/MultiSelector.tsx +2 -22
- package/src/RadioList/RadioList.test.tsx +4 -26
- package/src/RadioList/RadioListItem.tsx +1 -8
- package/src/SegmentedControl/SegmentedControl.test.tsx +0 -28
- package/src/SegmentedControl/SegmentedControlItem.tsx +2 -20
- package/src/Selector/Selector.test.tsx +0 -22
- package/src/Selector/Selector.tsx +1 -8
- package/src/Skeleton/Skeleton.test.tsx +0 -16
- package/src/Skeleton/Skeleton.tsx +1 -13
- package/src/Switch/Switch.test.tsx +0 -37
- package/src/Switch/Switch.tsx +3 -52
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +0 -19
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +1 -3
- package/src/Toast/ToastViewport.test.tsx +8 -218
- package/src/Toast/ToastViewport.tsx +12 -71
- package/src/ToggleButton/ToggleButton.test.tsx +0 -24
- package/src/ToggleButton/ToggleButton.tsx +1 -19
- package/src/Token/Token.test.tsx +0 -145
- package/src/Token/Token.tsx +33 -73
- package/src/Typeahead/BaseTypeahead.tsx +0 -12
- package/src/Typeahead/Typeahead.test.tsx +0 -62
- package/src/Typeahead/Typeahead.tsx +0 -6
- package/src/hooks/useKeyboardHint.test.tsx +0 -22
- package/src/hooks/useKeyboardHint.tsx +2 -6
- package/dist/Token/TokenLink.d.ts +0 -62
- package/dist/Token/TokenLink.d.ts.map +0 -1
- package/dist/Token/TokenLink.js +0 -76
- package/src/Token/TokenLink.tsx +0 -107
- package/src/__tests__/forcedColors.ts +0 -60
|
@@ -136,21 +136,6 @@ describe('ContextMenu', () => {
|
|
|
136
136
|
expect(HTMLElement.prototype.hidePopover).toHaveBeenCalled();
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
it('closes the menu when Tab is pressed inside it (APG menu pattern)', () => {
|
|
140
|
-
render(
|
|
141
|
-
<ContextMenu items={[{label: 'Item 1'}]}>
|
|
142
|
-
<div>Right-click me</div>
|
|
143
|
-
</ContextMenu>,
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
fireEvent.contextMenu(screen.getByText('Right-click me'));
|
|
147
|
-
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
148
|
-
|
|
149
|
-
const menu = screen.getByRole('menu', {hidden: true});
|
|
150
|
-
fireEvent.keyDown(menu, {key: 'Tab'});
|
|
151
|
-
expect(HTMLElement.prototype.hidePopover).toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
139
|
it('ignores Escape during IME composition', () => {
|
|
155
140
|
render(
|
|
156
141
|
<ContextMenu items={[{label: 'Item 1'}]}>
|
|
@@ -567,7 +552,7 @@ describe('ContextMenu selectable items', () => {
|
|
|
567
552
|
<ContextMenuRadioGroup
|
|
568
553
|
value="name"
|
|
569
554
|
onChange={() => {}}
|
|
570
|
-
label="Sort by">
|
|
555
|
+
aria-label="Sort by">
|
|
571
556
|
<ContextMenuRadioItem value="name" label="Sort by name" />
|
|
572
557
|
<ContextMenuRadioItem value="date" label="Sort by date" />
|
|
573
558
|
</ContextMenuRadioGroup>
|
|
@@ -602,7 +587,7 @@ describe('ContextMenu selectable items', () => {
|
|
|
602
587
|
<ContextMenuRadioGroup
|
|
603
588
|
value="name"
|
|
604
589
|
onChange={onSort}
|
|
605
|
-
label="Sort by">
|
|
590
|
+
aria-label="Sort by">
|
|
606
591
|
<ContextMenuRadioItem value="name" label="Sort by name" />
|
|
607
592
|
<ContextMenuRadioItem value="date" label="Sort by date" />
|
|
608
593
|
</ContextMenuRadioGroup>
|
|
@@ -353,22 +353,13 @@ export function ContextMenu({
|
|
|
353
353
|
}
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
356
|
-
// APG menu pattern: Tab closes the menu. Menu items are tabIndex={-1}
|
|
357
|
-
// so Tab would otherwise leak focus into the page while the menu stayed
|
|
358
|
-
// open (menus-5). Do NOT preventDefault — closing restores focus to the
|
|
359
|
-
// previously focused element, and the browser's default Tab then
|
|
360
|
-
// continues from there to the next element.
|
|
361
|
-
if (e.key === 'Tab') {
|
|
362
|
-
closeMenu();
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
356
|
if (typeahead.onKeyDown(e)) {
|
|
366
357
|
e.preventDefault();
|
|
367
358
|
return;
|
|
368
359
|
}
|
|
369
360
|
listNavKeyDown(e);
|
|
370
361
|
},
|
|
371
|
-
[listNavKeyDown,
|
|
362
|
+
[listNavKeyDown, typeahead, ownsEvent],
|
|
372
363
|
);
|
|
373
364
|
|
|
374
365
|
// Place the zero-size cursor anchor at a point in the trigger's local
|
|
@@ -22,10 +22,16 @@ export const docs = {
|
|
|
22
22
|
description: 'Callback fired when the selected value changes.',
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
name: 'label',
|
|
25
|
+
name: 'aria-label',
|
|
26
26
|
type: 'string',
|
|
27
27
|
description:
|
|
28
|
-
'Accessible
|
|
28
|
+
'Accessible label for the group. Required (or aria-labelledby) so screen readers announce the radios as a named set, e.g. "Sort by".',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'aria-labelledby',
|
|
32
|
+
type: 'string',
|
|
33
|
+
description:
|
|
34
|
+
'The id of an element that labels the group, as an alternative to aria-label.',
|
|
29
35
|
},
|
|
30
36
|
{
|
|
31
37
|
name: 'hasCloseOnSelect',
|
|
@@ -51,7 +57,8 @@ export const docsZh = {
|
|
|
51
57
|
propDescriptions: {
|
|
52
58
|
value: '组中当前选中的值。尚无选中项时传 undefined。',
|
|
53
59
|
onChange: '所选值变化时触发的回调。',
|
|
54
|
-
|
|
60
|
+
'aria-label': '组的无障碍标签。必需(或 aria-labelledby),以便屏幕阅读器将单选项作为命名集合朗读。',
|
|
61
|
+
'aria-labelledby': '标记该组的元素 id,作为 aria-label 的替代。',
|
|
55
62
|
hasCloseOnSelect: '选择某值是否关闭菜单。默认关闭。',
|
|
56
63
|
children: '组成该组的 DropdownMenuRadioItem。',
|
|
57
64
|
},
|
|
@@ -65,7 +72,8 @@ export const docsDense = {
|
|
|
65
72
|
propDescriptions: {
|
|
66
73
|
value: 'selected value (undefined = none)',
|
|
67
74
|
onChange: 'fired when selected value changes',
|
|
68
|
-
label: 'accessible group name (
|
|
75
|
+
'aria-label': 'accessible group name (or aria-labelledby); required',
|
|
76
|
+
'aria-labelledby': 'id of labelling element (alt to aria-label)',
|
|
69
77
|
hasCloseOnSelect: 'close menu on select (default true)',
|
|
70
78
|
children: 'the DropdownMenuRadioItems',
|
|
71
79
|
},
|
|
@@ -49,13 +49,16 @@ export interface DropdownMenuRadioGroupProps extends Omit<
|
|
|
49
49
|
*/
|
|
50
50
|
onChange: (value: string) => void;
|
|
51
51
|
/**
|
|
52
|
-
* Accessible
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `aria-labelledby` (via base props) instead if the name already exists as a
|
|
56
|
-
* visible element on the page.
|
|
52
|
+
* Accessible label for the group. Required (together with `aria-labelledby`,
|
|
53
|
+
* one of the two) so screen readers announce the radios as a named set,
|
|
54
|
+
* e.g. "Sort by".
|
|
57
55
|
*/
|
|
58
|
-
label
|
|
56
|
+
'aria-label'?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The id of an element that labels the group, as an alternative to
|
|
59
|
+
* `aria-label`.
|
|
60
|
+
*/
|
|
61
|
+
'aria-labelledby'?: string;
|
|
59
62
|
/**
|
|
60
63
|
* Whether selecting a value closes the menu. Radio items default to closing
|
|
61
64
|
* on selection (a single-choice commit), unlike checkbox items which stay
|
|
@@ -79,7 +82,7 @@ export interface DropdownMenuRadioGroupProps extends Omit<
|
|
|
79
82
|
* DropdownMenuRadioItem,
|
|
80
83
|
* } from '@astryxdesign/core/DropdownMenu';
|
|
81
84
|
* <DropdownMenu button={{label: 'Sort'}}>
|
|
82
|
-
* <DropdownMenuRadioGroup value={sort} onChange={setSort} label="Sort by">
|
|
85
|
+
* <DropdownMenuRadioGroup value={sort} onChange={setSort} aria-label="Sort by">
|
|
83
86
|
* <DropdownMenuRadioItem value="newest" label="Newest" />
|
|
84
87
|
* <DropdownMenuRadioItem value="oldest" label="Oldest" />
|
|
85
88
|
* </DropdownMenuRadioGroup>
|
|
@@ -89,7 +92,6 @@ export interface DropdownMenuRadioGroupProps extends Omit<
|
|
|
89
92
|
export function DropdownMenuRadioGroup({
|
|
90
93
|
value,
|
|
91
94
|
onChange,
|
|
92
|
-
label,
|
|
93
95
|
hasCloseOnSelect = true,
|
|
94
96
|
children,
|
|
95
97
|
className,
|
|
@@ -105,7 +107,6 @@ export function DropdownMenuRadioGroup({
|
|
|
105
107
|
<div
|
|
106
108
|
{...rest}
|
|
107
109
|
role="group"
|
|
108
|
-
aria-label={label}
|
|
109
110
|
{...mergeProps(stylex.props(styles.group), {className, style})}>
|
|
110
111
|
<DropdownMenuRadioGroupContext value={contextValue}>
|
|
111
112
|
{children}
|
|
@@ -145,7 +145,7 @@ export interface DropdownMenuRadioItemProps extends Omit<
|
|
|
145
145
|
*
|
|
146
146
|
* @example
|
|
147
147
|
* ```
|
|
148
|
-
* <DropdownMenuRadioGroup value={sort} onChange={setSort} label="Sort by">
|
|
148
|
+
* <DropdownMenuRadioGroup value={sort} onChange={setSort} aria-label="Sort by">
|
|
149
149
|
* <DropdownMenuRadioItem value="newest" label="Newest" />
|
|
150
150
|
* <DropdownMenuRadioItem value="oldest" label="Oldest" icon="clock" />
|
|
151
151
|
* </DropdownMenuRadioGroup>
|
|
@@ -110,7 +110,7 @@ describe('DropdownMenuRadioGroup / RadioItem', () => {
|
|
|
110
110
|
<DropdownMenuRadioGroup
|
|
111
111
|
value="newest"
|
|
112
112
|
onChange={() => {}}
|
|
113
|
-
label="Sort by">
|
|
113
|
+
aria-label="Sort by">
|
|
114
114
|
<DropdownMenuRadioItem value="newest" label="Newest" />
|
|
115
115
|
<DropdownMenuRadioItem value="oldest" label="Oldest" />
|
|
116
116
|
</DropdownMenuRadioGroup>
|
|
@@ -136,7 +136,7 @@ describe('DropdownMenuRadioGroup / RadioItem', () => {
|
|
|
136
136
|
<DropdownMenuRadioGroup
|
|
137
137
|
value="newest"
|
|
138
138
|
onChange={onChange}
|
|
139
|
-
label="Sort by">
|
|
139
|
+
aria-label="Sort by">
|
|
140
140
|
<DropdownMenuRadioItem value="newest" label="Newest" />
|
|
141
141
|
<DropdownMenuRadioItem value="oldest" label="Oldest" />
|
|
142
142
|
</DropdownMenuRadioGroup>
|
|
@@ -149,24 +149,6 @@ describe('DropdownMenuRadioGroup / RadioItem', () => {
|
|
|
149
149
|
expect(onChange).toHaveBeenCalledWith('oldest');
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
-
it('names the group from the required label prop', async () => {
|
|
153
|
-
const user = userEvent.setup();
|
|
154
|
-
render(
|
|
155
|
-
<DropdownMenu button={{label: 'Sort'}}>
|
|
156
|
-
<DropdownMenuRadioGroup
|
|
157
|
-
value="newest"
|
|
158
|
-
onChange={() => {}}
|
|
159
|
-
label="Sort by">
|
|
160
|
-
<DropdownMenuRadioItem value="newest" label="Newest" />
|
|
161
|
-
</DropdownMenuRadioGroup>
|
|
162
|
-
</DropdownMenu>,
|
|
163
|
-
);
|
|
164
|
-
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
165
|
-
expect(
|
|
166
|
-
screen.getByRole('group', {name: 'Sort by', hidden: true}),
|
|
167
|
-
).toBeInTheDocument();
|
|
168
|
-
});
|
|
169
|
-
|
|
170
152
|
it('throws when a radio item is used outside a group', () => {
|
|
171
153
|
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
172
154
|
expect(() =>
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
13
13
|
import {render, screen} from '@testing-library/react';
|
|
14
14
|
import {TestIcon} from '../__tests__/TestIcon';
|
|
15
|
-
import {InternationalizationProvider} from '../i18n';
|
|
16
15
|
import {FieldLabel} from './FieldLabel';
|
|
17
16
|
|
|
18
17
|
describe('FieldLabel', () => {
|
|
@@ -37,30 +36,6 @@ describe('FieldLabel', () => {
|
|
|
37
36
|
expect(screen.getByText(/Required/)).toBeInTheDocument();
|
|
38
37
|
});
|
|
39
38
|
|
|
40
|
-
it('localizes the required indicator via the i18n provider', () => {
|
|
41
|
-
render(
|
|
42
|
-
<InternationalizationProvider
|
|
43
|
-
locale="fr"
|
|
44
|
-
overrides={{fr: {'@astryx.field.required': 'Obligatoire'}}}>
|
|
45
|
-
<FieldLabel label="Nom" inputID="name-input" isRequired />
|
|
46
|
-
</InternationalizationProvider>,
|
|
47
|
-
);
|
|
48
|
-
expect(screen.getByText(/Obligatoire/)).toBeInTheDocument();
|
|
49
|
-
expect(screen.queryByText(/Required/)).not.toBeInTheDocument();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('localizes the optional indicator via the i18n provider', () => {
|
|
53
|
-
render(
|
|
54
|
-
<InternationalizationProvider
|
|
55
|
-
locale="fr"
|
|
56
|
-
overrides={{fr: {'@astryx.field.optional': 'Facultatif'}}}>
|
|
57
|
-
<FieldLabel label="Nom" inputID="name-input" isOptional />
|
|
58
|
-
</InternationalizationProvider>,
|
|
59
|
-
);
|
|
60
|
-
expect(screen.getByText(/Facultatif/)).toBeInTheDocument();
|
|
61
|
-
expect(screen.queryByText(/Optional/)).not.toBeInTheDocument();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
39
|
it('shows Optional when both isOptional and isRequired are true', () => {
|
|
65
40
|
render(
|
|
66
41
|
<FieldLabel label="Name" inputID="name-input" isOptional isRequired />,
|
package/src/Field/FieldLabel.tsx
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
-
'use client';
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* @file FieldLabel.tsx
|
|
7
|
-
* @input Uses React, Icon, IconType
|
|
5
|
+
* @input Uses React, Icon, IconType
|
|
8
6
|
* @output Exports FieldLabel component, FieldLabelProps
|
|
9
7
|
* @position Core label implementation; used by Field, CheckboxInput, Switch
|
|
10
8
|
*
|
|
@@ -12,7 +10,6 @@
|
|
|
12
10
|
* - /packages/core/src/Field/Field.doc.mjs (props table, features, implementation notes)
|
|
13
11
|
* - /packages/core/src/Field/index.ts (exports if types change)
|
|
14
12
|
* - /packages/cli/templates/blocks/components/Field/ (showcase blocks)
|
|
15
|
-
* - /packages/core/locales/en.json (@astryx.field.required / @astryx.field.optional)
|
|
16
13
|
*/
|
|
17
14
|
|
|
18
15
|
import type {ReactNode} from 'react';
|
|
@@ -29,7 +26,6 @@ import {
|
|
|
29
26
|
} from '../theme/tokens.stylex';
|
|
30
27
|
import {Icon, renderIconSlot, type IconType} from '../Icon';
|
|
31
28
|
import {Tooltip} from '../Tooltip';
|
|
32
|
-
import {useTranslator} from '../i18n';
|
|
33
29
|
import {themeProps} from '../utils/themeProps';
|
|
34
30
|
|
|
35
31
|
const styles = stylex.create({
|
|
@@ -181,12 +177,7 @@ export function FieldLabel({
|
|
|
181
177
|
ref,
|
|
182
178
|
...rest
|
|
183
179
|
}: FieldLabelProps) {
|
|
184
|
-
const
|
|
185
|
-
const statusText = isOptional
|
|
186
|
-
? t('@astryx.field.optional')
|
|
187
|
-
: isRequired
|
|
188
|
-
? t('@astryx.field.required')
|
|
189
|
-
: null;
|
|
180
|
+
const statusText = isOptional ? 'Optional' : isRequired ? 'Required' : null;
|
|
190
181
|
|
|
191
182
|
// A group label (e.g. for a radiogroup) must not be a literal `<label>`
|
|
192
183
|
// element: a `<label>` semantically names a single form control and can't be
|
package/src/Item/Item.doc.mjs
CHANGED
|
@@ -37,7 +37,6 @@ export const docs = {
|
|
|
37
37
|
{name: 'labelLines', type: 'number', description: 'Max lines before label truncates with ellipsis.'},
|
|
38
38
|
{name: 'descriptionLines', type: 'number', description: 'Max lines before description truncates with ellipsis.'},
|
|
39
39
|
{name: 'onClick', type: '(event: MouseEvent) => void', description: 'Click handler. Makes the item clickable with button semantics.'},
|
|
40
|
-
{name: 'interactiveRef', type: 'RefObject<HTMLElement | null>', description: 'Ref to a nested control (e.g. a checkbox in startContent) that owns the item\'s keyboard access and action. The row becomes an enlarged click/tap target that delegates surface clicks to it (useClickableContainer) and renders no invisible button/anchor, so the row adds no second tab stop (WCAG 4.1.2). Mutually exclusive with onClick/href — those are ignored when set.'},
|
|
41
40
|
{name: 'href', type: 'string', description: 'Link URL. Makes the item a link via an invisible anchor element.'},
|
|
42
41
|
{name: 'target', type: "'_blank' | '_self'", description: 'Link target. Only used with href. target="_blank" automatically adds noopener noreferrer.'},
|
|
43
42
|
{name: 'rel', type: 'string', description: 'Link relationship tokens. noopener noreferrer are merged automatically for target="_blank".'},
|
|
@@ -91,8 +90,6 @@ export const docsZh = {
|
|
|
91
90
|
labelLines: '标签截断前的最大行数。',
|
|
92
91
|
descriptionLines: '描述截断前的最大行数。',
|
|
93
92
|
onClick: '点击处理函数。使项目可点击,具有按钮语义。',
|
|
94
|
-
interactiveRef:
|
|
95
|
-
'指向嵌套控件(如 startContent 中的复选框)的 ref,该控件承载项目的键盘访问和操作。行成为更大的点击/触摸目标,将表面点击委托给该控件(useClickableContainer),且不渲染不可见按钮/锚点,因此行不会增加第二个 Tab 停留点(WCAG 4.1.2)。与 onClick/href 互斥——设置后二者将被忽略。',
|
|
96
93
|
href: '链接 URL。通过不可见锚点元素使项目成为链接。',
|
|
97
94
|
target: '链接目标。仅与 href 一起使用。target="_blank" 会自动添加 noopener noreferrer。',
|
|
98
95
|
rel: '链接关系标记。target="_blank" 会自动合并 noopener noreferrer。',
|
|
@@ -160,8 +157,6 @@ export const docsDense = {
|
|
|
160
157
|
labelLines: 'Max label lines before truncation.',
|
|
161
158
|
descriptionLines: 'Max description lines before truncation.',
|
|
162
159
|
onClick: 'Click handler; enables button semantics.',
|
|
163
|
-
interactiveRef:
|
|
164
|
-
'Ref to a nested control that owns the item\'s keyboard access/action; row delegates surface clicks to it (useClickableContainer), no invisible button/anchor, no second tab stop (WCAG 4.1.2). Mutually exclusive with onClick/href.',
|
|
165
160
|
href: 'Link URL; enables anchor semantics.',
|
|
166
161
|
target:
|
|
167
162
|
'Link target, only with href. target="_blank" auto-adds noopener noreferrer.',
|
package/src/Item/Item.test.tsx
CHANGED
|
@@ -9,35 +9,11 @@
|
|
|
9
9
|
* SYNC: When Item component changes, update tests to match new behavior
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {useRef} from 'react';
|
|
13
12
|
import {describe, it, expect, vi} from 'vitest';
|
|
14
13
|
import {render, screen} from '@testing-library/react';
|
|
15
14
|
import userEvent from '@testing-library/user-event';
|
|
16
15
|
import {Item} from './Item';
|
|
17
16
|
|
|
18
|
-
/**
|
|
19
|
-
* Item in delegation mode: `interactiveRef` points at a nested control that
|
|
20
|
-
* owns the row's keyboard access and action. The row is an enlarged tap target
|
|
21
|
-
* that forwards surface clicks to that control (useClickableContainer).
|
|
22
|
-
*/
|
|
23
|
-
function DelegatingItem({onToggle}: {onToggle?: () => void}) {
|
|
24
|
-
const ref = useRef<HTMLInputElement>(null);
|
|
25
|
-
return (
|
|
26
|
-
<Item
|
|
27
|
-
label="Row"
|
|
28
|
-
interactiveRef={ref}
|
|
29
|
-
startContent={
|
|
30
|
-
<input
|
|
31
|
-
ref={ref}
|
|
32
|
-
type="checkbox"
|
|
33
|
-
aria-label="Pick row"
|
|
34
|
-
onChange={onToggle}
|
|
35
|
-
/>
|
|
36
|
-
}
|
|
37
|
-
/>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
17
|
describe('Item', () => {
|
|
42
18
|
// ===========================================================================
|
|
43
19
|
// Basic rendering
|
|
@@ -214,70 +190,6 @@ describe('Item', () => {
|
|
|
214
190
|
expect(buttons).toHaveLength(1);
|
|
215
191
|
});
|
|
216
192
|
|
|
217
|
-
// ===========================================================================
|
|
218
|
-
// Interactive — interactiveRef (delegation to a nested control)
|
|
219
|
-
// ===========================================================================
|
|
220
|
-
|
|
221
|
-
it('renders no invisible button in interactiveRef (delegation) mode', () => {
|
|
222
|
-
const {container} = render(<DelegatingItem />);
|
|
223
|
-
// The nested control provides keyboard access — the row must not add a
|
|
224
|
-
// second focusable control for the same action (WCAG 4.1.2).
|
|
225
|
-
expect(container.querySelector('button')).not.toBeInTheDocument();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('keeps the nested control as the only tab stop in interactiveRef mode', async () => {
|
|
229
|
-
const user = userEvent.setup();
|
|
230
|
-
render(<DelegatingItem />);
|
|
231
|
-
await user.tab();
|
|
232
|
-
expect(screen.getByRole('checkbox')).toHaveFocus();
|
|
233
|
-
// Next tab leaves the item entirely — the row itself is not focusable.
|
|
234
|
-
await user.tab();
|
|
235
|
-
expect(screen.getByRole('checkbox')).not.toHaveFocus();
|
|
236
|
-
expect(document.body).toHaveFocus();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('delegates a row-surface click to the interactive control', async () => {
|
|
240
|
-
const user = userEvent.setup();
|
|
241
|
-
const onToggle = vi.fn();
|
|
242
|
-
render(<DelegatingItem onToggle={onToggle} />);
|
|
243
|
-
// Clicking the label (row surface) is forwarded to the checkbox.
|
|
244
|
-
await user.click(screen.getByText('Row'));
|
|
245
|
-
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('does not double-fire when the interactive control itself is clicked', async () => {
|
|
249
|
-
const user = userEvent.setup();
|
|
250
|
-
const onToggle = vi.fn();
|
|
251
|
-
render(<DelegatingItem onToggle={onToggle} />);
|
|
252
|
-
await user.click(screen.getByRole('checkbox'));
|
|
253
|
-
// The row must not re-forward the control's own click back to it.
|
|
254
|
-
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('ignores onClick when interactiveRef is set (delegation wins, single tab stop)', async () => {
|
|
258
|
-
const user = userEvent.setup();
|
|
259
|
-
const onClick = vi.fn();
|
|
260
|
-
function ItemWithBoth() {
|
|
261
|
-
const ref = useRef<HTMLInputElement>(null);
|
|
262
|
-
return (
|
|
263
|
-
<Item
|
|
264
|
-
label="Row"
|
|
265
|
-
onClick={onClick}
|
|
266
|
-
interactiveRef={ref}
|
|
267
|
-
startContent={
|
|
268
|
-
<input ref={ref} type="checkbox" aria-label="Pick row" />
|
|
269
|
-
}
|
|
270
|
-
/>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
const {container} = render(<ItemWithBoth />);
|
|
274
|
-
// No invisible button (onClick is ignored in delegation mode)...
|
|
275
|
-
expect(container.querySelector('button')).not.toBeInTheDocument();
|
|
276
|
-
// ...and the checkbox is still the sole tab stop.
|
|
277
|
-
await user.tab();
|
|
278
|
-
expect(screen.getByRole('checkbox')).toHaveFocus();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
193
|
// ===========================================================================
|
|
282
194
|
// Interactive — href (invisible anchor pattern)
|
|
283
195
|
// ===========================================================================
|
package/src/Item/Item.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file Item.tsx
|
|
7
|
-
* @input Uses React, ReactNode, StyleXStyles, theme tokens
|
|
7
|
+
* @input Uses React, ReactNode, StyleXStyles, theme tokens
|
|
8
8
|
* @output Exports Item component, ItemProps type
|
|
9
9
|
* @position Core layout primitive; consumed by index.ts, tested by Item.test.tsx
|
|
10
10
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* - /packages/cli/templates/blocks/components/Item/ (showcase blocks)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import
|
|
19
|
+
import type {ReactNode} from 'react';
|
|
20
20
|
import * as stylex from '@stylexjs/stylex';
|
|
21
21
|
import {
|
|
22
22
|
colorVars,
|
|
@@ -27,11 +27,9 @@ import {
|
|
|
27
27
|
typeScaleVars,
|
|
28
28
|
} from '../theme/tokens.stylex';
|
|
29
29
|
import type {BaseProps} from '../BaseProps';
|
|
30
|
-
import {mergeProps
|
|
30
|
+
import {mergeProps} from '../utils';
|
|
31
31
|
import {computeTargetAndRel} from '../Link/computeTargetAndRel';
|
|
32
32
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
33
|
-
import {useClickableContainer} from '../hooks/useClickableContainer';
|
|
34
|
-
import {useDevWarning} from '../hooks/useDevWarning';
|
|
35
33
|
import {themeProps} from '../utils/themeProps';
|
|
36
34
|
|
|
37
35
|
// =============================================================================
|
|
@@ -110,20 +108,6 @@ export interface ItemProps extends BaseProps<HTMLElement> {
|
|
|
110
108
|
*/
|
|
111
109
|
onClick?: (event: React.MouseEvent) => void;
|
|
112
110
|
|
|
113
|
-
/**
|
|
114
|
-
* Ref to a nested control inside the item (e.g. a checkbox in
|
|
115
|
-
* `startContent`) that already provides the item's keyboard access and
|
|
116
|
-
* action. When set, the item becomes an enlarged click/tap target that
|
|
117
|
-
* delegates surface clicks to that control via the `useClickableContainer`
|
|
118
|
-
* pattern: it renders no invisible button/anchor, so the row adds no second
|
|
119
|
-
* tab stop (WCAG 4.1.2 — one focusable control per option). Clicks on the
|
|
120
|
-
* control itself, and on any other nested interactive element, are left to
|
|
121
|
-
* that element. Mutually exclusive with `onClick`/`href` — when
|
|
122
|
-
* `interactiveRef` is set those are ignored (the nested control is the sole
|
|
123
|
-
* action).
|
|
124
|
-
*/
|
|
125
|
-
interactiveRef?: React.RefObject<HTMLElement | null>;
|
|
126
|
-
|
|
127
111
|
/**
|
|
128
112
|
* Link URL. Makes the item a link via an invisible anchor element.
|
|
129
113
|
*/
|
|
@@ -365,7 +349,6 @@ export function Item({
|
|
|
365
349
|
labelLines,
|
|
366
350
|
descriptionLines,
|
|
367
351
|
onClick,
|
|
368
|
-
interactiveRef,
|
|
369
352
|
href,
|
|
370
353
|
target: targetFromProps,
|
|
371
354
|
rel: relFromProps,
|
|
@@ -380,29 +363,7 @@ export function Item({
|
|
|
380
363
|
...restProps
|
|
381
364
|
}: ItemProps) {
|
|
382
365
|
const LinkComponent = useLinkComponent();
|
|
383
|
-
|
|
384
|
-
// Delegation mode: the row is an enlarged click/tap target for a nested
|
|
385
|
-
// control (e.g. a checkbox) that owns the keyboard access and action. The
|
|
386
|
-
// control is the row's only tab stop; the row proxies surface clicks to it.
|
|
387
|
-
const isDelegate = interactiveRef != null;
|
|
388
|
-
const containerRef = useRef<HTMLElement | null>(null);
|
|
389
|
-
// Only onClick is needed: onMouseUp handles middle-click href navigation,
|
|
390
|
-
// which delegation mode never has (href is ignored here).
|
|
391
|
-
const {onClick: delegatedOnClick} = useClickableContainer({
|
|
392
|
-
containerRef,
|
|
393
|
-
interactiveRef: interactiveRef ?? undefined,
|
|
394
|
-
disabled: isDisabled,
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
useDevWarning(
|
|
398
|
-
'Item',
|
|
399
|
-
'`interactiveRef` is mutually exclusive with `onClick`/`href`. In ' +
|
|
400
|
-
'delegation mode the row only forwards clicks to the referenced control, ' +
|
|
401
|
-
'so `onClick`/`href` are ignored. Drop one of them.',
|
|
402
|
-
isDelegate && (onClick != null || href != null),
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
const isInteractive = onClick != null || href != null || isDelegate;
|
|
366
|
+
const isInteractive = onClick != null || href != null;
|
|
406
367
|
const {target, rel} = computeTargetAndRel(targetFromProps, relFromProps);
|
|
407
368
|
// When a semantic role is provided (e.g. "menuitem"), a parent component
|
|
408
369
|
// handles keyboard access. Skip the invisible button/anchor and put
|
|
@@ -480,10 +441,7 @@ export function Item({
|
|
|
480
441
|
<span {...stylex.props(styles.startContent)}>{startContent}</span>
|
|
481
442
|
)}
|
|
482
443
|
|
|
483
|
-
{hasParentRole
|
|
484
|
-
// Delegation mode (and parent-role mode) put the label in a plain span:
|
|
485
|
-
// keyboard access lives on the nested control, so no invisible
|
|
486
|
-
// button/anchor is rendered and the row adds no second tab stop.
|
|
444
|
+
{hasParentRole ? (
|
|
487
445
|
<span
|
|
488
446
|
{...stylex.props(
|
|
489
447
|
styles.content,
|
|
@@ -539,9 +497,7 @@ export function Item({
|
|
|
539
497
|
|
|
540
498
|
return (
|
|
541
499
|
<Component
|
|
542
|
-
ref={
|
|
543
|
-
(isDelegate ? mergeRefs(ref, containerRef) : ref) as React.Ref<never>
|
|
544
|
-
}
|
|
500
|
+
ref={ref as React.Ref<never>}
|
|
545
501
|
{...restProps}
|
|
546
502
|
aria-selected={(allowsAriaSelected && isSelected) || undefined}
|
|
547
503
|
// aria-selected is invalid on roles that don't permit it (listitem, a
|
|
@@ -571,13 +527,11 @@ export function Item({
|
|
|
571
527
|
)}
|
|
572
528
|
role={role}
|
|
573
529
|
onClick={
|
|
574
|
-
|
|
575
|
-
?
|
|
576
|
-
:
|
|
577
|
-
?
|
|
578
|
-
:
|
|
579
|
-
? handleContainerClick
|
|
580
|
-
: undefined
|
|
530
|
+
hasParentRole
|
|
531
|
+
? onClick
|
|
532
|
+
: isInteractive
|
|
533
|
+
? handleContainerClick
|
|
534
|
+
: undefined
|
|
581
535
|
}>
|
|
582
536
|
{innerContent}
|
|
583
537
|
</Component>
|
|
@@ -59,11 +59,6 @@ export const docs = {
|
|
|
59
59
|
type: '(e: MouseEvent) => void',
|
|
60
60
|
description: 'Click handler; enables the invisible button pattern.',
|
|
61
61
|
},
|
|
62
|
-
{
|
|
63
|
-
name: 'interactiveRef',
|
|
64
|
-
type: 'RefObject<HTMLElement | null>',
|
|
65
|
-
description: 'Ref to a nested control (e.g. a checkbox in startContent) that owns the item\'s keyboard access and action. The row becomes an enlarged click/tap target that delegates surface clicks to it (useClickableContainer) and renders no invisible button/anchor, so the row adds no second tab stop (WCAG 4.1.2). Mutually exclusive with onClick/href — those are ignored when set.',
|
|
66
|
-
},
|
|
67
62
|
{
|
|
68
63
|
name: 'href',
|
|
69
64
|
type: 'string',
|
|
@@ -126,11 +121,6 @@ export const docsZh = {
|
|
|
126
121
|
type: '(e: MouseEvent) => void',
|
|
127
122
|
description: '点击处理函数;启用隐形按钮模式。',
|
|
128
123
|
},
|
|
129
|
-
{
|
|
130
|
-
name: 'interactiveRef',
|
|
131
|
-
type: 'RefObject<HTMLElement | null>',
|
|
132
|
-
description: '指向嵌套控件(如 startContent 中的复选框)的 ref,该控件承载项目的键盘访问和操作。行成为更大的点击/触摸目标,将表面点击委托给该控件(useClickableContainer),且不渲染不可见按钮/锚点,因此行不会增加第二个 Tab 停留点(WCAG 4.1.2)。与 onClick/href 互斥——设置后二者将被忽略。',
|
|
133
|
-
},
|
|
134
124
|
{
|
|
135
125
|
name: 'href',
|
|
136
126
|
type: 'string',
|
|
@@ -172,8 +162,6 @@ export const docsDense = {
|
|
|
172
162
|
startContent: 'Content before label area (e.g. icon, avatar).',
|
|
173
163
|
endContent: 'Content after label area (e.g. badge, chevron).',
|
|
174
164
|
onClick: 'Click handler; enables invisible button pattern.',
|
|
175
|
-
interactiveRef:
|
|
176
|
-
'Ref to a nested control that owns the item\'s keyboard access/action; row delegates surface clicks to it (useClickableContainer), no invisible button/anchor, no second tab stop (WCAG 4.1.2). Mutually exclusive with onClick/href.',
|
|
177
165
|
href: 'Link URL; enables invisible anchor pattern.',
|
|
178
166
|
target: 'Link target attribute, only when href provided. target="_blank" auto-adds noopener noreferrer.',
|
|
179
167
|
rel: 'Link relationship tokens. noopener noreferrer are merged for target="_blank".',
|
package/src/List/ListItem.tsx
CHANGED
|
@@ -75,17 +75,6 @@ export interface ListItemProps extends BaseProps<HTMLLIElement> {
|
|
|
75
75
|
*/
|
|
76
76
|
onClick?: (e: React.MouseEvent) => void;
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
* Ref to a nested control inside the item (e.g. a checkbox in
|
|
80
|
-
* `startContent`) that already provides the item's keyboard access and
|
|
81
|
-
* action. When set, the item becomes an enlarged click/tap target that
|
|
82
|
-
* delegates surface clicks to that control via the `useClickableContainer`
|
|
83
|
-
* pattern: it renders no invisible button/anchor, so the row adds no second
|
|
84
|
-
* tab stop (WCAG 4.1.2 — one focusable control per option). Mutually
|
|
85
|
-
* exclusive with `onClick`/`href` — when set those are ignored.
|
|
86
|
-
*/
|
|
87
|
-
interactiveRef?: React.RefObject<HTMLElement | null>;
|
|
88
|
-
|
|
89
78
|
/**
|
|
90
79
|
* URL for link items. Renders an invisible anchor element.
|
|
91
80
|
* Automatically enables hover/press styles when provided.
|
|
@@ -210,7 +199,6 @@ export function ListItem({
|
|
|
210
199
|
startContent,
|
|
211
200
|
endContent,
|
|
212
201
|
onClick,
|
|
213
|
-
interactiveRef,
|
|
214
202
|
href,
|
|
215
203
|
target,
|
|
216
204
|
rel,
|
|
@@ -251,7 +239,6 @@ export function ListItem({
|
|
|
251
239
|
description={description}
|
|
252
240
|
endContent={endContent}
|
|
253
241
|
onClick={onClick}
|
|
254
|
-
interactiveRef={interactiveRef}
|
|
255
242
|
href={href}
|
|
256
243
|
target={target as '_blank' | '_self'}
|
|
257
244
|
rel={rel}
|
|
@@ -5,7 +5,6 @@ import {render, screen} from '@testing-library/react';
|
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import {MetadataList} from './MetadataList';
|
|
7
7
|
import {MetadataListItem} from './MetadataListItem';
|
|
8
|
-
import {InternationalizationProvider} from '../i18n';
|
|
9
8
|
|
|
10
9
|
describe('MetadataList', () => {
|
|
11
10
|
it('renders a description list with items', () => {
|
|
@@ -88,29 +87,6 @@ describe('MetadataList', () => {
|
|
|
88
87
|
expect(screen.queryByText('B')).not.toBeInTheDocument();
|
|
89
88
|
});
|
|
90
89
|
|
|
91
|
-
it('localizes the show more / show less labels through the i18n catalog', async () => {
|
|
92
|
-
const user = userEvent.setup();
|
|
93
|
-
|
|
94
|
-
render(
|
|
95
|
-
<InternationalizationProvider
|
|
96
|
-
locale="fr"
|
|
97
|
-
overrides={{
|
|
98
|
-
fr: {
|
|
99
|
-
'@astryx.metadataList.showMore': 'Afficher plus',
|
|
100
|
-
'@astryx.metadataList.showLess': 'Afficher moins',
|
|
101
|
-
},
|
|
102
|
-
}}>
|
|
103
|
-
<MetadataList maxNumOfItems={1}>
|
|
104
|
-
<MetadataListItem label="A">1</MetadataListItem>
|
|
105
|
-
<MetadataListItem label="B">2</MetadataListItem>
|
|
106
|
-
</MetadataList>
|
|
107
|
-
</InternationalizationProvider>,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
await user.click(screen.getByText('Afficher plus'));
|
|
111
|
-
expect(screen.getByText('Afficher moins')).toBeInTheDocument();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
90
|
it('does not show toggle in horizontal mode even with maxNumOfItems', () => {
|
|
115
91
|
render(
|
|
116
92
|
<MetadataList orientation="horizontal" maxNumOfItems={1}>
|