@astryxdesign/core 0.1.7 → 0.1.8-canary.c5fa19b
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 +65 -0
- package/README.md +30 -19
- package/dist/Avatar/Avatar.d.ts +10 -5
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +12 -8
- package/dist/Avatar/AvatarSizeContext.d.ts +1 -1
- package/dist/Avatar/AvatarSizeContext.js +1 -1
- package/dist/Avatar/AvatarStatusDot.d.ts +2 -2
- package/dist/Avatar/AvatarStatusDot.js +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts +2 -2
- package/dist/AvatarGroup/AvatarGroup.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroup.js +2 -2
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +16 -14
- package/dist/Carousel/Carousel.d.ts +3 -2
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +32 -2
- package/dist/Chat/ChatMessage.d.ts +1 -1
- package/dist/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/Chat/ChatMessage.js +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +10 -4
- package/dist/Collapsible/Collapsible.d.ts +11 -1
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +36 -4
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -3
- package/dist/ContextMenu/index.d.ts +3 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +7 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +8 -4
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +89 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +170 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +18 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +70 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +69 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +1 -0
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +178 -0
- package/dist/DropdownMenu/index.d.ts +5 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +10 -1
- package/dist/DropdownMenu/menuItemRoles.d.ts +13 -0
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +1 -0
- package/dist/DropdownMenu/menuItemRoles.js +15 -0
- package/dist/Icon/Icon.d.ts +43 -13
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +62 -27
- package/dist/Outline/Outline.d.ts +69 -3
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +129 -26
- package/dist/Outline/useScrollSpy.d.ts +27 -7
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +202 -70
- package/dist/TabList/Tab.d.ts.map +1 -1
- package/dist/TabList/Tab.js +2 -2
- package/dist/TabList/TabList.d.ts.map +1 -1
- package/dist/TabList/TabList.js +2 -0
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/Table.d.ts +1 -1
- package/dist/Table/Table.js +1 -1
- 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/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +24 -2
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +18 -7
- package/dist/Table/plugins/tree/index.d.ts +5 -0
- package/dist/Table/plugins/tree/index.d.ts.map +1 -0
- package/dist/Table/plugins/tree/index.js +4 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +38 -0
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeData.js +358 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +51 -0
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +1 -0
- package/dist/Table/plugins/tree/useTableTreeState.js +175 -0
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +6 -3
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +8 -1
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +5 -14
- package/dist/astryx.css +24 -4
- package/dist/astryx.umd.js +54 -54
- package/dist/astryx.umd.js.map +4 -4
- package/dist/theme/Theme.js +1 -1
- package/dist/theme/defineTheme.d.ts +1 -1
- package/dist/theme/defineTheme.js +1 -1
- package/dist/theme/hct.d.ts +0 -1
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +0 -3
- package/docs.mjs +32 -461
- package/locales/en.json +200 -172
- package/locales/pseudo.json +21 -0
- package/package.json +3 -1
- package/scripts/postinstall.mjs +93 -0
- package/src/Avatar/Avatar.doc.mjs +6 -6
- package/src/Avatar/Avatar.tsx +16 -11
- package/src/Avatar/AvatarSizeContext.ts +1 -1
- package/src/Avatar/AvatarStatusDot.tsx +2 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +1 -1
- package/src/AvatarGroup/AvatarGroup.test.tsx +17 -3
- package/src/AvatarGroup/AvatarGroup.tsx +3 -3
- package/src/AvatarGroup/AvatarGroupOverflow.doc.mjs +4 -4
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +21 -5
- package/src/Carousel/Carousel.doc.mjs +1 -0
- package/src/Carousel/Carousel.test.tsx +96 -1
- package/src/Carousel/Carousel.tsx +29 -2
- package/src/Chat/Chat.doc.mjs +1 -1
- package/src/Chat/ChatMessage.tsx +4 -2
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/Citation/Citation.test.tsx +51 -0
- package/src/Citation/Citation.tsx +8 -2
- package/src/Collapsible/Collapsible.doc.mjs +7 -0
- package/src/Collapsible/Collapsible.test.tsx +80 -0
- package/src/Collapsible/Collapsible.tsx +58 -5
- package/src/ContextMenu/ContextMenu.test.tsx +75 -1
- package/src/ContextMenu/ContextMenu.tsx +10 -5
- package/src/ContextMenu/index.ts +15 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +23 -0
- package/src/DropdownMenu/DropdownMenu.tsx +11 -6
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +95 -0
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +237 -0
- package/src/DropdownMenu/DropdownMenuContext.tsx +29 -1
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +7 -1
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +119 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +80 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +232 -0
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +163 -0
- package/src/DropdownMenu/index.ts +23 -3
- package/src/DropdownMenu/menuItemRoles.ts +22 -0
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/Icon/Icon.doc.mjs +12 -0
- package/src/Icon/Icon.test.tsx +142 -0
- package/src/Icon/Icon.tsx +98 -30
- package/src/Item/Item.doc.mjs +1 -1
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/Outline/Outline.doc.mjs +123 -3
- package/src/Outline/Outline.test.tsx +735 -0
- package/src/Outline/Outline.tsx +184 -33
- package/src/Outline/useScrollSpy.ts +248 -72
- package/src/Selector/SelectorOption.doc.mjs +1 -1
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/Switch/Switch.doc.mjs +1 -1
- package/src/TabList/Tab.tsx +5 -1
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/TabList/TabList.test.tsx +66 -0
- package/src/TabList/TabList.tsx +8 -0
- package/src/TabList/TabMenu.tsx +4 -1
- package/src/Table/Table.doc.mjs +2 -0
- package/src/Table/Table.tsx +1 -1
- package/src/Table/index.ts +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +90 -1
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +37 -5
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +95 -9
- package/src/Table/plugins/sortable/useTableSortable.tsx +23 -7
- package/src/Table/plugins/tree/index.ts +12 -0
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +163 -0
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +594 -0
- package/src/Table/plugins/tree/useTableTreeData.tsx +495 -0
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +636 -0
- package/src/Table/plugins/tree/useTableTreeState.tsx +253 -0
- package/src/Table/useBaseTablePlugins.ts +6 -2
- package/src/Table/useTableTreeData.doc.mjs +75 -0
- package/src/Table/useTableTreeState.doc.mjs +98 -0
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/Toolbar/Toolbar.tsx +10 -3
- package/src/TreeList/TreeList.test.tsx +32 -1
- package/src/TreeList/TreeListItem.tsx +19 -10
- package/src/docs-script.test.mjs +18 -13
- package/src/postinstall.test.mjs +71 -0
- package/src/theme/Theme.doc.mjs +4 -4
- package/src/theme/Theme.tsx +1 -1
- package/src/theme/defineTheme.ts +1 -1
- package/src/theme/hct.ts +0 -4
- package/dist/TreeList/treeListItem.markers.stylex.d.ts +0 -16
- package/dist/TreeList/treeListItem.markers.stylex.d.ts.map +0 -1
- package/dist/TreeList/treeListItem.markers.stylex.js +0 -22
- package/src/TreeList/treeListItem.markers.stylex.ts +0 -20
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file DropdownMenuRadioItem.tsx
|
|
7
|
+
* @input React, stylex, Item + Icon + DropdownMenu context + tokens from core
|
|
8
|
+
* @output DropdownMenuRadioItem — a single option in a radio group.
|
|
9
|
+
* @position Sub-component; must be used inside a DropdownMenuRadioGroup.
|
|
10
|
+
*
|
|
11
|
+
* A menu item representing one option in a single-select group
|
|
12
|
+
* (role="menuitemradio"). The row owns the role and aria-checked; there is no
|
|
13
|
+
* nested native <input> (per the WAI-ARIA menuitemradio pattern). Selection
|
|
14
|
+
* state + onChange come from DropdownMenuRadioGroupContext. Keyboard nav +
|
|
15
|
+
* Enter/Space activation come from the parent DropdownMenu.
|
|
16
|
+
*
|
|
17
|
+
* The round radio visual (circle + center dot) is decorative (aria-hidden) —
|
|
18
|
+
* the row owns the checked state. Its size is derived from the menu's item size
|
|
19
|
+
* and it swaps to the inline-end of the row on coarse-pointer (touch) devices
|
|
20
|
+
* via CSS `order`. The dot is a shaped element, not an icon (a filled circle
|
|
21
|
+
* has no registry glyph).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {useCallback, type ReactNode} from 'react';
|
|
25
|
+
import * as stylex from '@stylexjs/stylex';
|
|
26
|
+
import {renderIconSlot, type IconType} from '../Icon';
|
|
27
|
+
import {Item} from '../Item';
|
|
28
|
+
import {useDropdownMenuContext} from './DropdownMenuContext';
|
|
29
|
+
import {
|
|
30
|
+
colorVars,
|
|
31
|
+
spacingVars,
|
|
32
|
+
durationVars,
|
|
33
|
+
easeVars,
|
|
34
|
+
borderVars,
|
|
35
|
+
} from '../theme/tokens.stylex';
|
|
36
|
+
import {mergeProps, themeProps} from '../utils';
|
|
37
|
+
import type {BaseProps} from '../BaseProps';
|
|
38
|
+
import {useDropdownMenuRadioGroupContext} from './DropdownMenuContext';
|
|
39
|
+
|
|
40
|
+
const styles = stylex.create({
|
|
41
|
+
root: {
|
|
42
|
+
width: '100%',
|
|
43
|
+
borderRadius: `max(0px, calc(var(--_dropdown-menu-radius, ${spacingVars['--spacing-2']}) - var(--_dropdown-menu-padding, ${spacingVars['--spacing-1']})))`,
|
|
44
|
+
color: colorVars['--color-text-primary'],
|
|
45
|
+
backgroundColor: {
|
|
46
|
+
default: 'transparent',
|
|
47
|
+
':focus': colorVars['--color-overlay-hover'],
|
|
48
|
+
':hover': colorVars['--color-overlay-hover'],
|
|
49
|
+
},
|
|
50
|
+
cursor: 'pointer',
|
|
51
|
+
outline: 'none',
|
|
52
|
+
},
|
|
53
|
+
disabled: {
|
|
54
|
+
opacity: 0.5,
|
|
55
|
+
cursor: 'not-allowed',
|
|
56
|
+
},
|
|
57
|
+
// Rendered in Item's `marker` slot as a raw flex child. On touch it moves to
|
|
58
|
+
// the inline-end of the row via `order`.
|
|
59
|
+
circle: {
|
|
60
|
+
display: 'flex',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
flexShrink: 0,
|
|
64
|
+
boxSizing: 'border-box',
|
|
65
|
+
borderWidth: borderVars['--border-width'],
|
|
66
|
+
borderStyle: 'solid',
|
|
67
|
+
borderRadius: '50%',
|
|
68
|
+
transitionProperty: 'background-color, border-color',
|
|
69
|
+
transitionDuration: {
|
|
70
|
+
default: durationVars['--duration-fast'],
|
|
71
|
+
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
72
|
+
},
|
|
73
|
+
transitionTimingFunction: easeVars['--ease-standard'],
|
|
74
|
+
order: {
|
|
75
|
+
default: 0,
|
|
76
|
+
'@media (pointer: coarse)': 1,
|
|
77
|
+
},
|
|
78
|
+
marginInlineStart: {
|
|
79
|
+
default: 0,
|
|
80
|
+
'@media (pointer: coarse)': 'auto',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
unchecked: {
|
|
84
|
+
borderColor: colorVars['--color-border-emphasized'],
|
|
85
|
+
backgroundColor: colorVars['--color-background-surface'],
|
|
86
|
+
},
|
|
87
|
+
checked: {
|
|
88
|
+
borderColor: colorVars['--color-accent'],
|
|
89
|
+
backgroundColor: colorVars['--color-accent'],
|
|
90
|
+
},
|
|
91
|
+
dot: {
|
|
92
|
+
borderRadius: '50%',
|
|
93
|
+
backgroundColor: colorVars['--color-on-accent'],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const circleSizeStyles = stylex.create({
|
|
98
|
+
sm: {width: 18, height: 18},
|
|
99
|
+
md: {width: 22, height: 22},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const dotSizeStyles = stylex.create({
|
|
103
|
+
sm: {width: 6, height: 6},
|
|
104
|
+
md: {width: 8, height: 8},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export interface DropdownMenuRadioItemProps extends Omit<
|
|
108
|
+
BaseProps,
|
|
109
|
+
'role' | 'aria-checked' | 'tabIndex'
|
|
110
|
+
> {
|
|
111
|
+
/**
|
|
112
|
+
* The value this item represents within its group. The group's `value`
|
|
113
|
+
* matches against this to determine the checked state.
|
|
114
|
+
*/
|
|
115
|
+
value: string;
|
|
116
|
+
/**
|
|
117
|
+
* Primary label text identifying the option.
|
|
118
|
+
*/
|
|
119
|
+
label: ReactNode;
|
|
120
|
+
/**
|
|
121
|
+
* Secondary description text displayed below the label.
|
|
122
|
+
*/
|
|
123
|
+
description?: ReactNode;
|
|
124
|
+
/**
|
|
125
|
+
* Icon to display before the label. Accepts a semantic icon name (see
|
|
126
|
+
* `npx astryx docs icons`) or a rendered node.
|
|
127
|
+
*/
|
|
128
|
+
icon?: ReactNode | IconType;
|
|
129
|
+
/**
|
|
130
|
+
* Whether this individual radio item is disabled. Disabled items stay
|
|
131
|
+
* focusable (via `aria-disabled`) so they remain discoverable by keyboard
|
|
132
|
+
* and assistive technology, but selection is blocked.
|
|
133
|
+
* @default false
|
|
134
|
+
*/
|
|
135
|
+
isDisabled?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Content to render after the label and description, such as a badge or
|
|
138
|
+
* metadata.
|
|
139
|
+
*/
|
|
140
|
+
endContent?: ReactNode;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A single option in a DropdownMenuRadioGroup (role="menuitemradio").
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```
|
|
148
|
+
* <DropdownMenuRadioGroup value={sort} onChange={setSort} aria-label="Sort by">
|
|
149
|
+
* <DropdownMenuRadioItem value="newest" label="Newest" />
|
|
150
|
+
* <DropdownMenuRadioItem value="oldest" label="Oldest" icon="clock" />
|
|
151
|
+
* </DropdownMenuRadioGroup>
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function DropdownMenuRadioItem({
|
|
155
|
+
value,
|
|
156
|
+
label,
|
|
157
|
+
description,
|
|
158
|
+
icon,
|
|
159
|
+
isDisabled = false,
|
|
160
|
+
endContent,
|
|
161
|
+
xstyle,
|
|
162
|
+
className,
|
|
163
|
+
style,
|
|
164
|
+
...rest
|
|
165
|
+
}: DropdownMenuRadioItemProps) {
|
|
166
|
+
const menuCtx = useDropdownMenuContext();
|
|
167
|
+
const groupCtx = useDropdownMenuRadioGroupContext();
|
|
168
|
+
if (!groupCtx) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'DropdownMenuRadioItem must be used within a DropdownMenuRadioGroup',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const menuSize = menuCtx?.menuSize ?? 'md';
|
|
174
|
+
const controlSize = menuSize === 'sm' ? 'sm' : 'md';
|
|
175
|
+
const isChecked = groupCtx.value === value;
|
|
176
|
+
|
|
177
|
+
const handleClick = useCallback(() => {
|
|
178
|
+
if (isDisabled) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
groupCtx.onChange(value);
|
|
182
|
+
if (groupCtx.hasCloseOnSelect) {
|
|
183
|
+
menuCtx?.closeMenu();
|
|
184
|
+
}
|
|
185
|
+
}, [isDisabled, groupCtx, value, menuCtx]);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<Item
|
|
189
|
+
{...rest}
|
|
190
|
+
role="menuitemradio"
|
|
191
|
+
aria-checked={isChecked}
|
|
192
|
+
tabIndex={isDisabled ? undefined : -1}
|
|
193
|
+
marker={
|
|
194
|
+
<span
|
|
195
|
+
aria-hidden="true"
|
|
196
|
+
{...mergeProps(
|
|
197
|
+
themeProps('dropdown-menu-radio', {
|
|
198
|
+
size: controlSize,
|
|
199
|
+
checked: isChecked ? 'checked' : null,
|
|
200
|
+
disabled: isDisabled ? 'disabled' : null,
|
|
201
|
+
}),
|
|
202
|
+
stylex.props(
|
|
203
|
+
styles.circle,
|
|
204
|
+
circleSizeStyles[controlSize],
|
|
205
|
+
isChecked ? styles.checked : styles.unchecked,
|
|
206
|
+
),
|
|
207
|
+
)}>
|
|
208
|
+
{isChecked && (
|
|
209
|
+
<span {...stylex.props(styles.dot, dotSizeStyles[controlSize])} />
|
|
210
|
+
)}
|
|
211
|
+
</span>
|
|
212
|
+
}
|
|
213
|
+
startContent={
|
|
214
|
+
icon
|
|
215
|
+
? renderIconSlot(icon, {size: 'sm', color: 'secondary'})
|
|
216
|
+
: undefined
|
|
217
|
+
}
|
|
218
|
+
label={label}
|
|
219
|
+
description={description}
|
|
220
|
+
endContent={endContent}
|
|
221
|
+
onClick={handleClick}
|
|
222
|
+
isDisabled={isDisabled}
|
|
223
|
+
xstyle={[styles.root, isDisabled && styles.disabled, xstyle]}
|
|
224
|
+
{...mergeProps(themeProps('dropdown-menu-item', {size: menuSize}), {
|
|
225
|
+
className,
|
|
226
|
+
style,
|
|
227
|
+
})}
|
|
228
|
+
/>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
DropdownMenuRadioItem.displayName = 'DropdownMenuRadioItem';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file DropdownMenuSelectable.test.tsx
|
|
5
|
+
* @input vitest, @testing-library/react, DropdownMenu + selectable items
|
|
6
|
+
* @output Unit tests for DropdownMenuCheckboxItem / RadioGroup / RadioItem (#3829)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {describe, it, expect, vi, beforeEach} from 'vitest';
|
|
10
|
+
import {render, screen} from '@testing-library/react';
|
|
11
|
+
import userEvent from '@testing-library/user-event';
|
|
12
|
+
import {DropdownMenu} from './DropdownMenu';
|
|
13
|
+
import {DropdownMenuCheckboxItem} from './DropdownMenuCheckboxItem';
|
|
14
|
+
import {DropdownMenuRadioGroup} from './DropdownMenuRadioGroup';
|
|
15
|
+
import {DropdownMenuRadioItem} from './DropdownMenuRadioItem';
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
19
|
+
this.setAttribute('popover-open', '');
|
|
20
|
+
const event = new Event('toggle', {bubbles: false});
|
|
21
|
+
Object.defineProperty(event, 'newState', {value: 'open'});
|
|
22
|
+
this.dispatchEvent(event);
|
|
23
|
+
});
|
|
24
|
+
HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
|
|
25
|
+
this.removeAttribute('popover-open');
|
|
26
|
+
const event = new Event('toggle', {bubbles: false});
|
|
27
|
+
Object.defineProperty(event, 'newState', {value: 'closed'});
|
|
28
|
+
this.dispatchEvent(event);
|
|
29
|
+
});
|
|
30
|
+
const originalMatches = HTMLElement.prototype.matches;
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
(HTMLElement.prototype as any).matches = function (
|
|
33
|
+
selector: string,
|
|
34
|
+
): boolean {
|
|
35
|
+
if (selector === ':popover-open') {
|
|
36
|
+
return this.hasAttribute('popover-open');
|
|
37
|
+
}
|
|
38
|
+
return originalMatches.call(this, selector);
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('DropdownMenuCheckboxItem', () => {
|
|
43
|
+
it('renders role menuitemcheckbox and reflects checked state', async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
render(
|
|
46
|
+
<DropdownMenu button={{label: 'View'}}>
|
|
47
|
+
<DropdownMenuCheckboxItem label="Show archived" value={true} />
|
|
48
|
+
</DropdownMenu>,
|
|
49
|
+
);
|
|
50
|
+
await user.click(screen.getByRole('button', {name: /View/}));
|
|
51
|
+
expect(
|
|
52
|
+
screen.getByRole('menuitemcheckbox', {
|
|
53
|
+
name: /Show archived/,
|
|
54
|
+
hidden: true,
|
|
55
|
+
}),
|
|
56
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('calls onChange with the toggled value on click', async () => {
|
|
60
|
+
const user = userEvent.setup();
|
|
61
|
+
const onChangeSpy = vi.fn();
|
|
62
|
+
render(
|
|
63
|
+
<DropdownMenu button={{label: 'View'}}>
|
|
64
|
+
<DropdownMenuCheckboxItem
|
|
65
|
+
label="Show archived"
|
|
66
|
+
value={false}
|
|
67
|
+
onChange={onChangeSpy}
|
|
68
|
+
/>
|
|
69
|
+
</DropdownMenu>,
|
|
70
|
+
);
|
|
71
|
+
await user.click(screen.getByRole('button', {name: /View/}));
|
|
72
|
+
await user.click(
|
|
73
|
+
screen.getByRole('menuitemcheckbox', {
|
|
74
|
+
name: /Show archived/,
|
|
75
|
+
hidden: true,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
expect(onChangeSpy).toHaveBeenCalledWith(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('does not toggle when disabled', async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
const onChangeSpy = vi.fn();
|
|
84
|
+
render(
|
|
85
|
+
<DropdownMenu button={{label: 'View'}}>
|
|
86
|
+
<DropdownMenuCheckboxItem
|
|
87
|
+
label="Show archived"
|
|
88
|
+
value={false}
|
|
89
|
+
onChange={onChangeSpy}
|
|
90
|
+
isDisabled
|
|
91
|
+
/>
|
|
92
|
+
</DropdownMenu>,
|
|
93
|
+
);
|
|
94
|
+
await user.click(screen.getByRole('button', {name: /View/}));
|
|
95
|
+
const item = screen.getByRole('menuitemcheckbox', {
|
|
96
|
+
name: /Show archived/,
|
|
97
|
+
hidden: true,
|
|
98
|
+
});
|
|
99
|
+
expect(item).toHaveAttribute('aria-disabled', 'true');
|
|
100
|
+
await user.click(item);
|
|
101
|
+
expect(onChangeSpy).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('DropdownMenuRadioGroup / RadioItem', () => {
|
|
106
|
+
it('renders a named group with radios reflecting the selected value', async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
render(
|
|
109
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
110
|
+
<DropdownMenuRadioGroup
|
|
111
|
+
value="newest"
|
|
112
|
+
onChange={() => {}}
|
|
113
|
+
aria-label="Sort by">
|
|
114
|
+
<DropdownMenuRadioItem value="newest" label="Newest" />
|
|
115
|
+
<DropdownMenuRadioItem value="oldest" label="Oldest" />
|
|
116
|
+
</DropdownMenuRadioGroup>
|
|
117
|
+
</DropdownMenu>,
|
|
118
|
+
);
|
|
119
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
120
|
+
expect(
|
|
121
|
+
screen.getByRole('menuitemradio', {name: 'Newest', hidden: true}),
|
|
122
|
+
).toHaveAttribute('aria-checked', 'true');
|
|
123
|
+
expect(
|
|
124
|
+
screen.getByRole('menuitemradio', {name: 'Oldest', hidden: true}),
|
|
125
|
+
).toHaveAttribute('aria-checked', 'false');
|
|
126
|
+
expect(
|
|
127
|
+
screen.getByRole('group', {name: 'Sort by', hidden: true}),
|
|
128
|
+
).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('calls onChange with the selected value', async () => {
|
|
132
|
+
const user = userEvent.setup();
|
|
133
|
+
const onChange = vi.fn();
|
|
134
|
+
render(
|
|
135
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
136
|
+
<DropdownMenuRadioGroup
|
|
137
|
+
value="newest"
|
|
138
|
+
onChange={onChange}
|
|
139
|
+
aria-label="Sort by">
|
|
140
|
+
<DropdownMenuRadioItem value="newest" label="Newest" />
|
|
141
|
+
<DropdownMenuRadioItem value="oldest" label="Oldest" />
|
|
142
|
+
</DropdownMenuRadioGroup>
|
|
143
|
+
</DropdownMenu>,
|
|
144
|
+
);
|
|
145
|
+
await user.click(screen.getByRole('button', {name: /Sort/}));
|
|
146
|
+
await user.click(
|
|
147
|
+
screen.getByRole('menuitemradio', {name: 'Oldest', hidden: true}),
|
|
148
|
+
);
|
|
149
|
+
expect(onChange).toHaveBeenCalledWith('oldest');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('throws when a radio item is used outside a group', () => {
|
|
153
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
154
|
+
expect(() =>
|
|
155
|
+
render(
|
|
156
|
+
<DropdownMenu button={{label: 'Sort'}}>
|
|
157
|
+
<DropdownMenuRadioItem value="x" label="X" />
|
|
158
|
+
</DropdownMenu>,
|
|
159
|
+
),
|
|
160
|
+
).toThrow(/DropdownMenuRadioGroup/);
|
|
161
|
+
spy.mockRestore();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -18,7 +18,27 @@ export {
|
|
|
18
18
|
type DropdownMenuOption,
|
|
19
19
|
} from './DropdownMenu';
|
|
20
20
|
|
|
21
|
+
export {DropdownMenuItem, type DropdownMenuItemProps} from './DropdownMenuItem';
|
|
22
|
+
|
|
23
|
+
// Selectable items — checkbox (independent) and radio (single-select group).
|
|
24
|
+
export {
|
|
25
|
+
DropdownMenuCheckboxItem,
|
|
26
|
+
type DropdownMenuCheckboxItemProps,
|
|
27
|
+
} from './DropdownMenuCheckboxItem';
|
|
28
|
+
export {
|
|
29
|
+
DropdownMenuRadioGroup,
|
|
30
|
+
type DropdownMenuRadioGroupProps,
|
|
31
|
+
} from './DropdownMenuRadioGroup';
|
|
32
|
+
export {
|
|
33
|
+
DropdownMenuRadioItem,
|
|
34
|
+
type DropdownMenuRadioItemProps,
|
|
35
|
+
} from './DropdownMenuRadioItem';
|
|
36
|
+
|
|
37
|
+
// Menu-coordination context — public so consumers can build custom menu items
|
|
38
|
+
// that read the menu size / close the menu.
|
|
21
39
|
export {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
40
|
+
DropdownMenuContext,
|
|
41
|
+
useDropdownMenuContext,
|
|
42
|
+
type DropdownMenuContextValue,
|
|
43
|
+
type DropdownMenuSize,
|
|
44
|
+
} from './DropdownMenuContext';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file menuItemRoles.ts
|
|
5
|
+
* @output Shared menu-item role set + focus selector.
|
|
6
|
+
* @position Internal; used by DropdownMenu and ContextMenu so their
|
|
7
|
+
* roving-focus, typeahead, and Enter/Space activation stay in sync.
|
|
8
|
+
*
|
|
9
|
+
* Includes the selectable items (menuitemradio/menuitemcheckbox) alongside
|
|
10
|
+
* plain menuitem so checkbox/radio rows are reachable by arrow keys, typeahead,
|
|
11
|
+
* and Enter/Space — not just role="menuitem".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const MENU_ITEM_ROLES: ReadonlySet<string> = new Set([
|
|
15
|
+
'menuitem',
|
|
16
|
+
'menuitemradio',
|
|
17
|
+
'menuitemcheckbox',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const MENU_ITEM_SELECTOR: string = [...MENU_ITEM_ROLES]
|
|
21
|
+
.map(role => `[role="${role}"]:not([aria-disabled="true"])`)
|
|
22
|
+
.join(',');
|
package/src/Field/Field.doc.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export const docs = {
|
|
|
95
95
|
{
|
|
96
96
|
name: 'labelIcon',
|
|
97
97
|
type: 'IconType',
|
|
98
|
-
description: 'Icon to display before the label text. See `
|
|
98
|
+
description: 'Icon to display before the label text. See `astryx docs icons` for valid semantic names.',
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
name: 'labelTooltip',
|
|
@@ -48,7 +48,7 @@ export const docs = {
|
|
|
48
48
|
{
|
|
49
49
|
name: 'labelIcon',
|
|
50
50
|
type: 'IconType',
|
|
51
|
-
description: 'Icon before the label text. See `
|
|
51
|
+
description: 'Icon before the label text. See `astryx docs icons` for valid semantic names.',
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
name: 'labelTooltip',
|
package/src/Icon/Icon.doc.mjs
CHANGED
|
@@ -34,6 +34,11 @@ export const docs = {
|
|
|
34
34
|
description: 'Icon size.',
|
|
35
35
|
default: "'md'",
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: 'label',
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Accessible name for a MEANINGFUL, standalone icon (a status glyph or icon-only indicator with no adjacent text). Setting it exposes the icon to screen readers as role="img" with this text as the accessible name (aria-label) and removes the default aria-hidden. Omit it (default) for decorative icons and the icon stays hidden from assistive tech (aria-hidden="true"). This is the accessible-name / alt-text prop for icons — one prop instead of manually setting aria-label + role + aria-hidden. An empty string is treated as decorative. Do not set it when an interactive parent (Button, IconButton, link) already names the control.',
|
|
41
|
+
},
|
|
37
42
|
],
|
|
38
43
|
theming: {
|
|
39
44
|
targets: [
|
|
@@ -45,6 +50,7 @@ export const docs = {
|
|
|
45
50
|
bestPractices: [
|
|
46
51
|
{ guidance: true, description: 'Use semantic icon names when available; they adapt to theme changes automatically.' },
|
|
47
52
|
{ guidance: true, description: 'Pair icons with text labels for accessibility; icon-only elements need an accessible label.' },
|
|
53
|
+
{ guidance: true, description: 'For a meaningful standalone icon (no adjacent text), give it an accessible name via the `label` prop — it sets role="img" + aria-label and unhides the icon.' },
|
|
48
54
|
{ guidance: true, description: 'Use color tokens for icon colors, not hardcoded hex values.' },
|
|
49
55
|
{ guidance: true, description: 'Be mindful of context; decorative icons in compact components can distract rather than help.' },
|
|
50
56
|
{ guidance: false, description: 'Use icons as the sole means of conveying meaning; always provide a text alternative.' },
|
|
@@ -79,6 +85,11 @@ export const docsZh = {
|
|
|
79
85
|
description: '图标尺寸。',
|
|
80
86
|
default: "'md'",
|
|
81
87
|
},
|
|
88
|
+
{
|
|
89
|
+
name: 'label',
|
|
90
|
+
type: 'string',
|
|
91
|
+
description: '有含义的独立图标的可访问名称(无相邻文字的状态图标或纯图标指示器)。设置后会将图标以 role="img" 暴露给辅助技术,并以该文本作为可访问名称(aria-label),同时移除默认的 aria-hidden。省略(默认)用于装饰性图标,图标对辅助技术保持隐藏(aria-hidden="true")。空字符串按装饰性处理。当交互式父元素(Button、IconButton、链接)已命名该控件时请勿设置。',
|
|
92
|
+
},
|
|
82
93
|
],
|
|
83
94
|
theming: {
|
|
84
95
|
targets: [
|
|
@@ -123,5 +134,6 @@ export const docsDense = {
|
|
|
123
134
|
icon: 'Semantic icon name or SVG component. Valid names: close, chevronDown, chevronLeft, chevronRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. For others, pass an SVG component.',
|
|
124
135
|
color: 'Color variant mapped to Astryx icon color tokens.',
|
|
125
136
|
size: 'Icon size.',
|
|
137
|
+
label: 'Accessible name for a meaningful, standalone icon. Sets role="img" + aria-label and drops the default aria-hidden. Omit (default) for decorative icons (stays aria-hidden). Empty string = decorative. The accessible-name/alt-text prop for icons.',
|
|
126
138
|
},
|
|
127
139
|
};
|
package/src/Icon/Icon.test.tsx
CHANGED
|
@@ -96,6 +96,52 @@ describe('Icon', () => {
|
|
|
96
96
|
expect(screen.getByTestId('icon')).toBeInTheDocument();
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
it('sizes component-mode icons in rem so they scale with root font-size', () => {
|
|
100
|
+
const sizes = {
|
|
101
|
+
xsm: '0.75rem',
|
|
102
|
+
sm: '1rem',
|
|
103
|
+
md: '1.25rem',
|
|
104
|
+
lg: '1.5rem',
|
|
105
|
+
} as const;
|
|
106
|
+
for (const [size, expected] of Object.entries(sizes)) {
|
|
107
|
+
const {unmount} = render(
|
|
108
|
+
<Icon
|
|
109
|
+
icon={TestIcon}
|
|
110
|
+
size={size as keyof typeof sizes}
|
|
111
|
+
data-testid="icon"
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
const style = getComputedStyle(screen.getByTestId('icon'));
|
|
115
|
+
expect(style.width).toBe(expected);
|
|
116
|
+
expect(style.height).toBe(expected);
|
|
117
|
+
unmount();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('sizes registry (string-mode) icons in rem, including fontSize', () => {
|
|
122
|
+
const sizes = {
|
|
123
|
+
xsm: '0.75rem',
|
|
124
|
+
sm: '1rem',
|
|
125
|
+
md: '1.25rem',
|
|
126
|
+
lg: '1.5rem',
|
|
127
|
+
} as const;
|
|
128
|
+
for (const [size, expected] of Object.entries(sizes)) {
|
|
129
|
+
const {unmount} = render(
|
|
130
|
+
<Icon
|
|
131
|
+
icon="check"
|
|
132
|
+
size={size as keyof typeof sizes}
|
|
133
|
+
data-testid="icon"
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
const style = getComputedStyle(screen.getByTestId('icon'));
|
|
137
|
+
expect(style.width).toBe(expected);
|
|
138
|
+
expect(style.height).toBe(expected);
|
|
139
|
+
// fontSize is expressed in rem so 1em-based registry icons scale too.
|
|
140
|
+
expect(style.fontSize).toBe(expected);
|
|
141
|
+
unmount();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
99
145
|
it('forwards ref correctly', () => {
|
|
100
146
|
const ref = vi.fn();
|
|
101
147
|
render(<Icon icon={TestIcon} ref={ref} />);
|
|
@@ -137,4 +183,100 @@ describe('Icon', () => {
|
|
|
137
183
|
expect(icon).toHaveAttribute('aria-label', 'Done');
|
|
138
184
|
expect(icon).toHaveAttribute('aria-hidden', 'false');
|
|
139
185
|
});
|
|
186
|
+
|
|
187
|
+
describe('label (accessible name)', () => {
|
|
188
|
+
it('makes a component-mode icon meaningful: role="img" + aria-label, no aria-hidden', () => {
|
|
189
|
+
render(<Icon icon={TestIcon} label="Completed" data-testid="icon" />);
|
|
190
|
+
const icon = screen.getByTestId('icon');
|
|
191
|
+
expect(icon).toHaveAttribute('role', 'img');
|
|
192
|
+
expect(icon).toHaveAttribute('aria-label', 'Completed');
|
|
193
|
+
expect(icon).not.toHaveAttribute('aria-hidden');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('makes a string-mode (registry) icon meaningful: role="img" + aria-label, no aria-hidden', () => {
|
|
197
|
+
render(<Icon icon="check" label="Completed" data-testid="icon" />);
|
|
198
|
+
const icon = screen.getByTestId('icon');
|
|
199
|
+
expect(icon).toHaveAttribute('role', 'img');
|
|
200
|
+
expect(icon).toHaveAttribute('aria-label', 'Completed');
|
|
201
|
+
expect(icon).not.toHaveAttribute('aria-hidden');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('keeps the decorative default when label is omitted (component mode)', () => {
|
|
205
|
+
render(<Icon icon={TestIcon} data-testid="icon" />);
|
|
206
|
+
const icon = screen.getByTestId('icon');
|
|
207
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
208
|
+
expect(icon).not.toHaveAttribute('role');
|
|
209
|
+
expect(icon).not.toHaveAttribute('aria-label');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('keeps the decorative default when label is omitted (string mode)', () => {
|
|
213
|
+
render(<Icon icon="check" data-testid="icon" />);
|
|
214
|
+
const icon = screen.getByTestId('icon');
|
|
215
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
216
|
+
expect(icon).not.toHaveAttribute('role');
|
|
217
|
+
expect(icon).not.toHaveAttribute('aria-label');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('treats an empty string label as decorative (component mode)', () => {
|
|
221
|
+
render(<Icon icon={TestIcon} label="" data-testid="icon" />);
|
|
222
|
+
const icon = screen.getByTestId('icon');
|
|
223
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
224
|
+
expect(icon).not.toHaveAttribute('role');
|
|
225
|
+
expect(icon).not.toHaveAttribute('aria-label');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('treats an empty string label as decorative (string mode)', () => {
|
|
229
|
+
render(<Icon icon="check" label="" data-testid="icon" />);
|
|
230
|
+
const icon = screen.getByTestId('icon');
|
|
231
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
232
|
+
expect(icon).not.toHaveAttribute('role');
|
|
233
|
+
expect(icon).not.toHaveAttribute('aria-label');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('lets an explicit aria-hidden win over label (component mode)', () => {
|
|
237
|
+
render(
|
|
238
|
+
<Icon icon={TestIcon} label="Close" aria-hidden data-testid="icon" />,
|
|
239
|
+
);
|
|
240
|
+
const icon = screen.getByTestId('icon');
|
|
241
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('lets an explicit aria-hidden win over label (string mode)', () => {
|
|
245
|
+
render(
|
|
246
|
+
<Icon icon="check" label="Close" aria-hidden data-testid="icon" />,
|
|
247
|
+
);
|
|
248
|
+
const icon = screen.getByTestId('icon');
|
|
249
|
+
expect(icon).toHaveAttribute('aria-hidden', 'true');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('lets an explicit aria-label override the label-derived name (component mode)', () => {
|
|
253
|
+
render(
|
|
254
|
+
<Icon
|
|
255
|
+
icon={TestIcon}
|
|
256
|
+
label="Close"
|
|
257
|
+
aria-label="Dismiss"
|
|
258
|
+
data-testid="icon"
|
|
259
|
+
/>,
|
|
260
|
+
);
|
|
261
|
+
expect(screen.getByTestId('icon')).toHaveAttribute(
|
|
262
|
+
'aria-label',
|
|
263
|
+
'Dismiss',
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('lets an explicit role override the label-derived role (string mode)', () => {
|
|
268
|
+
render(
|
|
269
|
+
<Icon
|
|
270
|
+
icon="check"
|
|
271
|
+
label="Close"
|
|
272
|
+
role="presentation"
|
|
273
|
+
data-testid="icon"
|
|
274
|
+
/>,
|
|
275
|
+
);
|
|
276
|
+
expect(screen.getByTestId('icon')).toHaveAttribute(
|
|
277
|
+
'role',
|
|
278
|
+
'presentation',
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
140
282
|
});
|