@astryxdesign/core 0.1.7-canary.fb9c68c → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -24
- package/dist/Collapsible/Collapsible.d.ts +1 -11
- package/dist/Collapsible/Collapsible.d.ts.map +1 -1
- package/dist/Collapsible/Collapsible.js +4 -36
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +3 -4
- package/dist/ContextMenu/index.d.ts +0 -3
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/ContextMenu/index.js +1 -7
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +4 -8
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -17
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +0 -18
- package/dist/DropdownMenu/index.d.ts +1 -5
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/index.js +1 -10
- package/dist/Icon/Icon.d.ts +12 -17
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +20 -28
- package/dist/Outline/Outline.d.ts +3 -69
- package/dist/Outline/Outline.d.ts.map +1 -1
- package/dist/Outline/Outline.js +26 -129
- package/dist/Outline/useScrollSpy.d.ts +7 -27
- package/dist/Outline/useScrollSpy.d.ts.map +1 -1
- package/dist/Outline/useScrollSpy.js +70 -202
- 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 +0 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +1 -1
- package/dist/Table/index.d.ts +0 -2
- package/dist/Table/index.d.ts.map +1 -1
- package/dist/Table/index.js +0 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +2 -24
- package/dist/Table/plugins/sortable/useTableSortable.d.ts.map +1 -1
- package/dist/Table/plugins/sortable/useTableSortable.js +7 -18
- package/dist/Table/useBaseTablePlugins.d.ts.map +1 -1
- package/dist/Table/useBaseTablePlugins.js +3 -6
- package/dist/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar/Toolbar.js +1 -8
- package/dist/astryx.css +2 -18
- package/dist/astryx.umd.js +48 -48
- 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 +1 -0
- package/dist/theme/hct.d.ts.map +1 -1
- package/dist/theme/hct.js +3 -0
- package/locales/en.json +0 -28
- package/locales/pseudo.json +0 -21
- package/package.json +1 -3
- package/src/CheckboxInput/CheckboxInput.doc.mjs +1 -1
- package/src/Collapsible/Collapsible.doc.mjs +0 -7
- package/src/Collapsible/Collapsible.test.tsx +0 -80
- package/src/Collapsible/Collapsible.tsx +5 -58
- package/src/ContextMenu/ContextMenu.test.tsx +1 -75
- package/src/ContextMenu/ContextMenu.tsx +5 -10
- package/src/ContextMenu/index.ts +0 -15
- package/src/DropdownMenu/DropdownMenu.doc.mjs +0 -23
- package/src/DropdownMenu/DropdownMenu.tsx +6 -11
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -29
- package/src/DropdownMenu/DropdownMenuItem.doc.mjs +1 -7
- package/src/DropdownMenu/index.ts +3 -23
- package/src/Field/Field.doc.mjs +1 -1
- package/src/Field/FieldLabel.doc.mjs +1 -1
- package/src/Icon/Icon.test.tsx +0 -46
- package/src/Icon/Icon.tsx +24 -37
- package/src/NumberInput/NumberInput.doc.mjs +2 -2
- package/src/Outline/Outline.doc.mjs +3 -123
- package/src/Outline/Outline.test.tsx +0 -735
- package/src/Outline/Outline.tsx +33 -184
- package/src/Outline/useScrollSpy.ts +72 -248
- 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 +1 -5
- package/src/TabList/TabList.doc.mjs +3 -3
- package/src/TabList/TabList.test.tsx +0 -66
- package/src/TabList/TabList.tsx +0 -8
- package/src/TabList/TabMenu.tsx +1 -4
- package/src/Table/Table.doc.mjs +0 -2
- package/src/Table/index.ts +0 -7
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.test.tsx +1 -90
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +5 -37
- package/src/Table/plugins/sortable/useTableSortable.test.tsx +9 -95
- package/src/Table/plugins/sortable/useTableSortable.tsx +7 -23
- package/src/Table/useBaseTablePlugins.ts +2 -6
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextInput/TextInput.doc.mjs +1 -1
- package/src/Toolbar/Toolbar.tsx +3 -10
- 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 +4 -0
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +0 -89
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts.map +0 -1
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.js +0 -170
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +0 -69
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +0 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +0 -70
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +0 -69
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts.map +0 -1
- package/dist/DropdownMenu/DropdownMenuRadioItem.js +0 -178
- package/dist/DropdownMenu/menuItemRoles.d.ts +0 -13
- package/dist/DropdownMenu/menuItemRoles.d.ts.map +0 -1
- package/dist/DropdownMenu/menuItemRoles.js +0 -15
- package/dist/Table/plugins/tree/index.d.ts +0 -5
- package/dist/Table/plugins/tree/index.d.ts.map +0 -1
- package/dist/Table/plugins/tree/index.js +0 -4
- package/dist/Table/plugins/tree/useTableTreeData.d.ts +0 -38
- package/dist/Table/plugins/tree/useTableTreeData.d.ts.map +0 -1
- package/dist/Table/plugins/tree/useTableTreeData.js +0 -358
- package/dist/Table/plugins/tree/useTableTreeState.d.ts +0 -51
- package/dist/Table/plugins/tree/useTableTreeState.d.ts.map +0 -1
- package/dist/Table/plugins/tree/useTableTreeState.js +0 -175
- package/scripts/postinstall.mjs +0 -93
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +0 -95
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +0 -237
- package/src/DropdownMenu/DropdownMenuRadioGroup.doc.mjs +0 -80
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +0 -119
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +0 -80
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +0 -232
- package/src/DropdownMenu/DropdownMenuSelectable.test.tsx +0 -163
- package/src/DropdownMenu/menuItemRoles.ts +0 -22
- package/src/Table/plugins/tree/index.ts +0 -12
- package/src/Table/plugins/tree/useTableTreeData-perf.test.tsx +0 -163
- package/src/Table/plugins/tree/useTableTreeData.test.tsx +0 -594
- package/src/Table/plugins/tree/useTableTreeData.tsx +0 -495
- package/src/Table/plugins/tree/useTableTreeState.test.tsx +0 -636
- package/src/Table/plugins/tree/useTableTreeState.tsx +0 -253
- package/src/Table/useTableTreeData.doc.mjs +0 -75
- package/src/Table/useTableTreeState.doc.mjs +0 -98
- package/src/postinstall.test.mjs +0 -71
|
@@ -1,232 +0,0 @@
|
|
|
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';
|
|
@@ -1,163 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
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(',');
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
export {useTableTreeData} from './useTableTreeData';
|
|
4
|
-
export type {
|
|
5
|
-
TableTreeRowMeta,
|
|
6
|
-
UseTableTreeDataConfig,
|
|
7
|
-
} from './useTableTreeData';
|
|
8
|
-
export {useTableTreeState} from './useTableTreeState';
|
|
9
|
-
export type {
|
|
10
|
-
UseTableTreeStateConfig,
|
|
11
|
-
UseTableTreeStateResult,
|
|
12
|
-
} from './useTableTreeState';
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file useTableTreeData-perf.test.tsx
|
|
5
|
-
* @input Table, useTableTreeData, useTableTreeState, React testing utilities
|
|
6
|
-
* @output Performance tests for tree plugin render behavior
|
|
7
|
-
* @position Test file; validates toggle render efficiency and DOM cost
|
|
8
|
-
*
|
|
9
|
-
* The tree plugin uses an external store so a toggle re-renders only the
|
|
10
|
-
* affected cells — not the whole table body. Note: expanding inserts rows,
|
|
11
|
-
* which shifts the rowIndex of every row *after* the insertion point (the
|
|
12
|
-
* row memo comparator includes rowIndex by design), so the guarantee is
|
|
13
|
-
* scoped to rows before the toggled row.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {describe, it, expect} from 'vitest';
|
|
17
|
-
import {render, screen, act} from '@testing-library/react';
|
|
18
|
-
import {useMemo} from 'react';
|
|
19
|
-
import userEvent from '@testing-library/user-event';
|
|
20
|
-
import {Table} from '../../Table';
|
|
21
|
-
import type {TableColumn} from '../../types';
|
|
22
|
-
import {useTableTreeData} from './useTableTreeData';
|
|
23
|
-
import {useTableTreeState} from './useTableTreeState';
|
|
24
|
-
|
|
25
|
-
interface TreeRow extends Record<string, unknown> {
|
|
26
|
-
id: string;
|
|
27
|
-
name: string;
|
|
28
|
-
children?: TreeRow[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** 5 roots; the last root has 2 children. */
|
|
32
|
-
const roots: TreeRow[] = [
|
|
33
|
-
...Array.from({length: 4}, (_, i) => ({
|
|
34
|
-
id: `root-${i}`,
|
|
35
|
-
name: `Root ${i}`,
|
|
36
|
-
})),
|
|
37
|
-
{
|
|
38
|
-
id: 'root-4',
|
|
39
|
-
name: 'Root 4',
|
|
40
|
-
children: [
|
|
41
|
-
{id: 'child-0', name: 'Child 0'},
|
|
42
|
-
{id: 'child-1', name: 'Child 1'},
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
function TreeRenderCountTable({
|
|
48
|
-
renderCounts,
|
|
49
|
-
}: {
|
|
50
|
-
renderCounts: Record<string, number>;
|
|
51
|
-
}) {
|
|
52
|
-
const columns = useMemo<TableColumn<TreeRow>[]>(
|
|
53
|
-
() => [
|
|
54
|
-
{
|
|
55
|
-
key: 'name',
|
|
56
|
-
header: 'Name',
|
|
57
|
-
renderCell: (item: TreeRow) => {
|
|
58
|
-
renderCounts[item.id] = (renderCounts[item.id] ?? 0) + 1;
|
|
59
|
-
return item.name;
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
[renderCounts],
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const {visibleData, treeConfig} = useTableTreeState<TreeRow>({
|
|
67
|
-
data: roots,
|
|
68
|
-
idKey: 'id',
|
|
69
|
-
});
|
|
70
|
-
const tree = useTableTreeData(treeConfig);
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<Table data={visibleData} columns={columns} idKey="id" plugins={{tree}} />
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
describe('Tree plugin render performance', () => {
|
|
78
|
-
it('expanding the last root does not re-render preceding rows', async () => {
|
|
79
|
-
const user = userEvent.setup();
|
|
80
|
-
const renderCounts: Record<string, number> = {};
|
|
81
|
-
|
|
82
|
-
render(<TreeRenderCountTable renderCounts={renderCounts} />);
|
|
83
|
-
|
|
84
|
-
const initialCounts = {...renderCounts};
|
|
85
|
-
expect(Object.keys(initialCounts)).toHaveLength(5);
|
|
86
|
-
|
|
87
|
-
await act(async () => {
|
|
88
|
-
await user.click(screen.getByRole('button', {name: 'Expand row'}));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i < 4; i++) {
|
|
92
|
-
expect(renderCounts[`root-${i}`]).toBe(initialCounts[`root-${i}`]);
|
|
93
|
-
}
|
|
94
|
-
// The children mounted exactly once.
|
|
95
|
-
expect(renderCounts['child-0']).toBe(1);
|
|
96
|
-
expect(renderCounts['child-1']).toBe(1);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('collapsing unmounts the subtree rows entirely', async () => {
|
|
100
|
-
const user = userEvent.setup();
|
|
101
|
-
const renderCounts: Record<string, number> = {};
|
|
102
|
-
|
|
103
|
-
render(<TreeRenderCountTable renderCounts={renderCounts} />);
|
|
104
|
-
|
|
105
|
-
await act(async () => {
|
|
106
|
-
await user.click(screen.getByRole('button', {name: 'Expand row'}));
|
|
107
|
-
});
|
|
108
|
-
expect(screen.getAllByRole('row')).toHaveLength(8); // header + 5 roots + 2 children
|
|
109
|
-
|
|
110
|
-
await act(async () => {
|
|
111
|
-
await user.click(screen.getByRole('button', {name: 'Collapse row'}));
|
|
112
|
-
});
|
|
113
|
-
expect(screen.getAllByRole('row')).toHaveLength(6); // header + 5 roots
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('adds at most one wrapper, one expander/spacer to the tree column and zero DOM to other columns', () => {
|
|
117
|
-
const {container} = render(
|
|
118
|
-
<Table
|
|
119
|
-
data={
|
|
120
|
-
[{id: 'a', name: 'a', children: [{id: 'b', name: 'b'}]}] as TreeRow[]
|
|
121
|
-
}
|
|
122
|
-
columns={[
|
|
123
|
-
{key: 'name', header: 'Name'},
|
|
124
|
-
{key: 'id', header: 'Id'},
|
|
125
|
-
]}
|
|
126
|
-
idKey="id"
|
|
127
|
-
plugins={{}}
|
|
128
|
-
/>,
|
|
129
|
-
);
|
|
130
|
-
const plainSecondCellHTML =
|
|
131
|
-
container.querySelectorAll('tbody td')[1].innerHTML;
|
|
132
|
-
|
|
133
|
-
function DomBudgetTable() {
|
|
134
|
-
const {visibleData, treeConfig} = useTableTreeState<TreeRow>({
|
|
135
|
-
data: [{id: 'a', name: 'a', children: [{id: 'b', name: 'b'}]}],
|
|
136
|
-
idKey: 'id',
|
|
137
|
-
});
|
|
138
|
-
const tree = useTableTreeData(treeConfig);
|
|
139
|
-
return (
|
|
140
|
-
<Table
|
|
141
|
-
data={visibleData}
|
|
142
|
-
columns={[
|
|
143
|
-
{key: 'name', header: 'Name'},
|
|
144
|
-
{key: 'id', header: 'Id'},
|
|
145
|
-
]}
|
|
146
|
-
idKey="id"
|
|
147
|
-
plugins={{tree}}
|
|
148
|
-
/>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
const {container: treeContainer} = render(<DomBudgetTable />);
|
|
152
|
-
const cells = treeContainer.querySelectorAll('tbody td');
|
|
153
|
-
|
|
154
|
-
// Tree column: one flex wrapper containing one expander button + text.
|
|
155
|
-
const wrapper = cells[0].firstElementChild as HTMLElement;
|
|
156
|
-
expect(wrapper.tagName).toBe('DIV');
|
|
157
|
-
expect(wrapper.children).toHaveLength(1);
|
|
158
|
-
expect(wrapper.querySelector('button')).not.toBeNull();
|
|
159
|
-
|
|
160
|
-
// Other columns: byte-identical to the plugin-free render.
|
|
161
|
-
expect(cells[1].innerHTML).toBe(plainSecondCellHTML);
|
|
162
|
-
});
|
|
163
|
-
});
|