@fragments-sdk/ui 0.7.5 → 0.8.1
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 +58 -25
- package/fragments.json +1 -1
- package/package.json +15 -5
- package/src/blocks/AppShell.block.ts +2 -2
- package/src/blocks/InsetDashboardLayout.block.ts +1 -1
- package/src/blocks/LoginForm.block.ts +14 -7
- package/src/components/Accordion/Accordion.fragment.tsx +10 -4
- package/src/components/Alert/Alert.fragment.tsx +2 -2
- package/src/components/Alert/Alert.module.scss +4 -4
- package/src/components/AppShell/AppShell.fragment.tsx +3 -3
- package/src/components/AppShell/index.tsx +2 -0
- package/src/components/Avatar/Avatar.fragment.tsx +7 -3
- package/src/components/Avatar/Avatar.module.scss +1 -1
- package/src/components/Avatar/index.tsx +37 -1
- package/src/components/Badge/Badge.fragment.tsx +5 -5
- package/src/components/Badge/Badge.module.scss +4 -4
- package/src/components/Badge/index.tsx +5 -1
- package/src/components/Box/Box.fragment.tsx +2 -2
- package/src/components/Box/index.tsx +5 -1
- package/src/components/Breadcrumbs/Breadcrumbs.fragment.tsx +2 -2
- package/src/components/Button/Button.fragment.tsx +19 -18
- package/src/components/Button/index.tsx +5 -1
- package/src/components/ButtonGroup/ButtonGroup.fragment.tsx +2 -2
- package/src/components/ButtonGroup/index.tsx +5 -1
- package/src/components/Card/Card.fragment.tsx +7 -7
- package/src/components/Chart/Chart.fragment.tsx +11 -3
- package/src/components/Chart/index.tsx +22 -4
- package/src/components/Checkbox/Checkbox.fragment.tsx +2 -2
- package/src/components/Checkbox/index.tsx +5 -1
- package/src/components/Chip/Chip.fragment.tsx +2 -7
- package/src/components/Chip/Chip.module.scss +2 -2
- package/src/components/CodeBlock/CodeBlock.fragment.tsx +11 -5
- package/src/components/CodeBlock/CodeBlock.module.scss +11 -53
- package/src/components/CodeBlock/index.tsx +13 -24
- package/src/components/Collapsible/Collapsible.fragment.tsx +2 -2
- package/src/components/ColorPicker/ColorPicker.fragment.tsx +2 -2
- package/src/components/ColorPicker/index.tsx +5 -1
- package/src/components/Combobox/Combobox.fragment.tsx +17 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +5 -5
- package/src/components/ConversationList/ConversationList.module.scss +1 -1
- package/src/components/DatePicker/DatePicker.fragment.tsx +245 -0
- package/src/components/DatePicker/DatePicker.module.scss +394 -0
- package/src/components/DatePicker/DatePicker.test.tsx +264 -0
- package/src/components/DatePicker/index.tsx +535 -0
- package/src/components/Dialog/Dialog.fragment.tsx +2 -2
- package/src/components/EmptyState/EmptyState.fragment.tsx +2 -2
- package/src/components/Field/Field.fragment.tsx +7 -6
- package/src/components/Fieldset/Fieldset.fragment.tsx +7 -6
- package/src/components/Form/Form.fragment.tsx +11 -5
- package/src/components/Form/index.tsx +5 -1
- package/src/components/Grid/Grid.fragment.tsx +6 -2
- package/src/components/Header/Header.fragment.tsx +38 -15
- package/src/components/Header/Header.module.scss +114 -1
- package/src/components/Header/Header.test.tsx +106 -1
- package/src/components/Header/index.tsx +100 -31
- package/src/components/Icon/Icon.fragment.tsx +8 -3
- package/src/components/Icon/index.tsx +5 -1
- package/src/components/Image/Image.fragment.tsx +4 -4
- package/src/components/Image/index.tsx +5 -1
- package/src/components/Input/Input.fragment.tsx +23 -5
- package/src/components/Input/Input.module.scss +1 -1
- package/src/components/Input/index.tsx +5 -1
- package/src/components/Link/Link.fragment.tsx +2 -6
- package/src/components/Link/index.tsx +5 -1
- package/src/components/List/List.fragment.tsx +2 -2
- package/src/components/Listbox/Listbox.fragment.tsx +2 -14
- package/src/components/Loading/Loading.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.fragment.tsx +2 -2
- package/src/components/Markdown/Markdown.module.scss +11 -3
- package/src/components/Markdown/index.tsx +5 -1
- package/src/components/Menu/Menu.fragment.tsx +2 -2
- package/src/components/Message/Message.fragment.tsx +10 -8
- package/src/components/Message/Message.module.scss +1 -1
- package/src/components/Popover/Popover.fragment.tsx +2 -2
- package/src/components/Progress/Progress.fragment.tsx +16 -2
- package/src/components/Progress/index.tsx +9 -2
- package/src/components/Prompt/Prompt.fragment.tsx +13 -2
- package/src/components/RadioGroup/RadioGroup.fragment.tsx +7 -2
- package/src/components/ScrollArea/ScrollArea.fragment.tsx +185 -0
- package/src/components/ScrollArea/ScrollArea.module.scss +136 -0
- package/src/components/ScrollArea/ScrollArea.test.tsx +38 -0
- package/src/components/ScrollArea/index.tsx +121 -0
- package/src/components/Select/Select.fragment.tsx +15 -7
- package/src/components/Separator/Separator.fragment.tsx +2 -2
- package/src/components/Separator/index.tsx +5 -1
- package/src/components/Sidebar/Sidebar.fragment.tsx +66 -13
- package/src/components/Sidebar/Sidebar.module.scss +69 -21
- package/src/components/Sidebar/Sidebar.test.tsx +31 -2
- package/src/components/Sidebar/index.tsx +69 -45
- package/src/components/Skeleton/Skeleton.fragment.tsx +7 -2
- package/src/components/Slider/Slider.fragment.tsx +2 -2
- package/src/components/Slider/index.tsx +5 -1
- package/src/components/Stack/Stack.fragment.tsx +4 -4
- package/src/components/Stack/index.tsx +5 -1
- package/src/components/Table/Table.fragment.tsx +31 -2
- package/src/components/Table/index.tsx +49 -6
- package/src/components/TableOfContents/TableOfContents.fragment.tsx +149 -0
- package/src/components/TableOfContents/TableOfContents.module.scss +66 -0
- package/src/components/TableOfContents/TableOfContents.test.tsx +126 -0
- package/src/components/TableOfContents/index.tsx +110 -0
- package/src/components/Tabs/Tabs.fragment.tsx +2 -2
- package/src/components/Text/Text.fragment.tsx +2 -2
- package/src/components/Text/Text.module.scss +6 -0
- package/src/components/Text/Text.test.tsx +5 -0
- package/src/components/Text/index.tsx +8 -1
- package/src/components/Textarea/Textarea.fragment.tsx +10 -2
- package/src/components/Textarea/index.tsx +5 -1
- package/src/components/Theme/Theme.fragment.tsx +2 -2
- package/src/components/Theme/ThemeToggle.module.scss +1 -1
- package/src/components/Theme/index.tsx +8 -1
- package/src/components/ThinkingIndicator/ThinkingIndicator.fragment.tsx +5 -4
- package/src/components/Toast/Toast.fragment.tsx +14 -2
- package/src/components/Toggle/Toggle.fragment.tsx +2 -2
- package/src/components/Toggle/index.tsx +5 -1
- package/src/components/ToggleGroup/ToggleGroup.fragment.tsx +5 -5
- package/src/components/Tooltip/Tooltip.fragment.tsx +20 -2
- package/src/components/Tooltip/index.tsx +6 -1
- package/src/components/VisuallyHidden/VisuallyHidden.fragment.tsx +2 -2
- package/src/components/VisuallyHidden/index.tsx +5 -1
- package/src/components/compound-pattern.test.ts +40 -0
- package/src/index.ts +29 -0
- package/src/recipes/AppShell.recipe.ts +2 -2
- package/src/recipes/LoginForm.recipe.ts +14 -7
- package/src/tokens/_computed.scss +12 -0
- package/src/tokens/_derive.scss +71 -0
- package/src/tokens/_mixins.scss +9 -0
- package/src/tokens/_variables.scss +26 -4
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, userEvent, waitFor, expectNoA11yViolations } from '../../test/utils';
|
|
3
|
+
import { DatePicker } from './index';
|
|
4
|
+
import type { DateRange } from './index';
|
|
5
|
+
|
|
6
|
+
function renderDatePicker(props: {
|
|
7
|
+
onSelect?: (date: Date | null) => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
selected?: Date | null;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
} = {}) {
|
|
12
|
+
return render(
|
|
13
|
+
<DatePicker
|
|
14
|
+
placeholder={props.placeholder ?? 'Pick a date'}
|
|
15
|
+
onSelect={props.onSelect}
|
|
16
|
+
disabled={props.disabled}
|
|
17
|
+
selected={props.selected}
|
|
18
|
+
>
|
|
19
|
+
<DatePicker.Trigger />
|
|
20
|
+
<DatePicker.Content>
|
|
21
|
+
<DatePicker.Calendar />
|
|
22
|
+
</DatePicker.Content>
|
|
23
|
+
</DatePicker>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderRangePicker(props: {
|
|
28
|
+
onRangeSelect?: (range: DateRange | null) => void;
|
|
29
|
+
selectedRange?: DateRange | null;
|
|
30
|
+
numberOfMonths?: number;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
} = {}) {
|
|
33
|
+
return render(
|
|
34
|
+
<DatePicker
|
|
35
|
+
mode="range"
|
|
36
|
+
placeholder={props.placeholder ?? 'Select date range'}
|
|
37
|
+
onRangeSelect={props.onRangeSelect}
|
|
38
|
+
selectedRange={props.selectedRange}
|
|
39
|
+
numberOfMonths={props.numberOfMonths ?? 2}
|
|
40
|
+
>
|
|
41
|
+
<DatePicker.Trigger />
|
|
42
|
+
<DatePicker.Content>
|
|
43
|
+
<DatePicker.Calendar />
|
|
44
|
+
</DatePicker.Content>
|
|
45
|
+
</DatePicker>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('DatePicker', () => {
|
|
50
|
+
describe('rendering', () => {
|
|
51
|
+
it('renders a trigger button', () => {
|
|
52
|
+
renderDatePicker();
|
|
53
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('shows placeholder text when no value selected', () => {
|
|
57
|
+
renderDatePicker({ placeholder: 'Choose date' });
|
|
58
|
+
expect(screen.getByText('Choose date')).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('shows formatted date when selected', () => {
|
|
62
|
+
renderDatePicker({ selected: new Date(2025, 0, 15) });
|
|
63
|
+
// format(date, 'PPP') produces "January 15th, 2025"
|
|
64
|
+
expect(screen.getByRole('button')).toHaveTextContent('January');
|
|
65
|
+
expect(screen.getByRole('button')).toHaveTextContent('2025');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('shows formatted range when range selected', () => {
|
|
69
|
+
const range: DateRange = {
|
|
70
|
+
from: new Date(2025, 0, 10),
|
|
71
|
+
to: new Date(2025, 0, 20),
|
|
72
|
+
};
|
|
73
|
+
renderRangePicker({ selectedRange: range });
|
|
74
|
+
expect(screen.getByRole('button')).toHaveTextContent('Jan 10, 2025');
|
|
75
|
+
expect(screen.getByRole('button')).toHaveTextContent('Jan 20, 2025');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('interaction', () => {
|
|
80
|
+
it('opens on click', async () => {
|
|
81
|
+
const user = userEvent.setup();
|
|
82
|
+
renderDatePicker();
|
|
83
|
+
|
|
84
|
+
await user.click(screen.getByRole('button'));
|
|
85
|
+
// DayPicker renders a grid
|
|
86
|
+
expect(await screen.findByRole('grid')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('selects a date on click', async () => {
|
|
90
|
+
const user = userEvent.setup();
|
|
91
|
+
const onSelect = vi.fn();
|
|
92
|
+
renderDatePicker({ onSelect });
|
|
93
|
+
|
|
94
|
+
await user.click(screen.getByRole('button'));
|
|
95
|
+
await screen.findByRole('grid');
|
|
96
|
+
|
|
97
|
+
// Click the 15th day button (find any visible "15" in a grid cell)
|
|
98
|
+
const dayButtons = screen.getAllByRole('gridcell');
|
|
99
|
+
const day15 = dayButtons.find((cell) => {
|
|
100
|
+
const btn = cell.querySelector('button');
|
|
101
|
+
return btn?.textContent === '15';
|
|
102
|
+
});
|
|
103
|
+
expect(day15).toBeDefined();
|
|
104
|
+
const btn = day15!.querySelector('button')!;
|
|
105
|
+
await user.click(btn);
|
|
106
|
+
|
|
107
|
+
expect(onSelect).toHaveBeenCalledWith(expect.any(Date));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('auto-closes after single date selection', async () => {
|
|
111
|
+
const user = userEvent.setup();
|
|
112
|
+
renderDatePicker({ onSelect: vi.fn() });
|
|
113
|
+
|
|
114
|
+
await user.click(screen.getByRole('button'));
|
|
115
|
+
await screen.findByRole('grid');
|
|
116
|
+
|
|
117
|
+
const dayButtons = screen.getAllByRole('gridcell');
|
|
118
|
+
const visibleDay = dayButtons.find((cell) => {
|
|
119
|
+
const btn = cell.querySelector('button');
|
|
120
|
+
return btn?.textContent === '10';
|
|
121
|
+
});
|
|
122
|
+
const btn = visibleDay!.querySelector('button')!;
|
|
123
|
+
await user.click(btn);
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
|
|
127
|
+
}, { timeout: 500 });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('range mode: stays open after both clicks (no auto-close)', async () => {
|
|
131
|
+
const user = userEvent.setup();
|
|
132
|
+
const onRangeSelect = vi.fn();
|
|
133
|
+
renderRangePicker({ onRangeSelect, numberOfMonths: 1 });
|
|
134
|
+
|
|
135
|
+
await user.click(screen.getByRole('button'));
|
|
136
|
+
await screen.findByRole('grid');
|
|
137
|
+
|
|
138
|
+
const dayButtons = screen.getAllByRole('gridcell');
|
|
139
|
+
const day10 = dayButtons.find((cell) => {
|
|
140
|
+
const btn = cell.querySelector('button');
|
|
141
|
+
return btn?.textContent === '10';
|
|
142
|
+
});
|
|
143
|
+
await user.click(day10!.querySelector('button')!);
|
|
144
|
+
|
|
145
|
+
// Should still be open after first click
|
|
146
|
+
expect(screen.getByRole('grid')).toBeInTheDocument();
|
|
147
|
+
|
|
148
|
+
const day20 = dayButtons.find((cell) => {
|
|
149
|
+
const btn = cell.querySelector('button');
|
|
150
|
+
return btn?.textContent === '20';
|
|
151
|
+
});
|
|
152
|
+
await user.click(day20!.querySelector('button')!);
|
|
153
|
+
|
|
154
|
+
// Range mode never auto-closes — user closes via Escape or click-outside
|
|
155
|
+
expect(screen.getByRole('grid')).toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('preset click selects a date', async () => {
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
const onSelect = vi.fn();
|
|
161
|
+
const presetDate = new Date(2025, 5, 1);
|
|
162
|
+
|
|
163
|
+
render(
|
|
164
|
+
<DatePicker onSelect={onSelect}>
|
|
165
|
+
<DatePicker.Trigger placeholder="Pick a date" />
|
|
166
|
+
<DatePicker.Content>
|
|
167
|
+
<DatePicker.Preset date={presetDate}>June 1st</DatePicker.Preset>
|
|
168
|
+
<DatePicker.Calendar />
|
|
169
|
+
</DatePicker.Content>
|
|
170
|
+
</DatePicker>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await user.click(screen.getByRole('button', { name: /pick a date/i }));
|
|
174
|
+
await user.click(await screen.findByText('June 1st'));
|
|
175
|
+
|
|
176
|
+
expect(onSelect).toHaveBeenCalledWith(presetDate);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('keyboard', () => {
|
|
181
|
+
it('Escape closes the calendar', async () => {
|
|
182
|
+
const user = userEvent.setup();
|
|
183
|
+
renderDatePicker();
|
|
184
|
+
|
|
185
|
+
await user.click(screen.getByRole('button'));
|
|
186
|
+
await screen.findByRole('grid');
|
|
187
|
+
|
|
188
|
+
await user.keyboard('{Escape}');
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('disabled', () => {
|
|
196
|
+
it('trigger is disabled when disabled prop is true', () => {
|
|
197
|
+
renderDatePicker({ disabled: true });
|
|
198
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('controlled', () => {
|
|
203
|
+
it('reflects external selected value', () => {
|
|
204
|
+
const date = new Date(2025, 2, 20);
|
|
205
|
+
renderDatePicker({ selected: date });
|
|
206
|
+
expect(screen.getByRole('button')).toHaveTextContent('March');
|
|
207
|
+
expect(screen.getByRole('button')).toHaveTextContent('2025');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('reflects external selectedRange value', () => {
|
|
211
|
+
const range: DateRange = {
|
|
212
|
+
from: new Date(2025, 3, 1),
|
|
213
|
+
to: new Date(2025, 3, 7),
|
|
214
|
+
};
|
|
215
|
+
renderRangePicker({ selectedRange: range });
|
|
216
|
+
expect(screen.getByRole('button')).toHaveTextContent('Apr 01, 2025');
|
|
217
|
+
expect(screen.getByRole('button')).toHaveTextContent('Apr 07, 2025');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('multi-month', () => {
|
|
222
|
+
it('renders correct number of month panels', async () => {
|
|
223
|
+
const user = userEvent.setup();
|
|
224
|
+
renderRangePicker({ numberOfMonths: 2 });
|
|
225
|
+
|
|
226
|
+
await user.click(screen.getByRole('button'));
|
|
227
|
+
const grids = await screen.findAllByRole('grid');
|
|
228
|
+
expect(grids).toHaveLength(2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('a11y', () => {
|
|
233
|
+
it('has no accessibility violations (closed)', async () => {
|
|
234
|
+
const { container } = render(
|
|
235
|
+
<DatePicker>
|
|
236
|
+
<DatePicker.Trigger aria-label="Pick a date" placeholder="Pick a date" />
|
|
237
|
+
<DatePicker.Content>
|
|
238
|
+
<DatePicker.Calendar />
|
|
239
|
+
</DatePicker.Content>
|
|
240
|
+
</DatePicker>
|
|
241
|
+
);
|
|
242
|
+
await expectNoA11yViolations(container);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('has no accessibility violations (open)', async () => {
|
|
246
|
+
const user = userEvent.setup();
|
|
247
|
+
const { container } = render(
|
|
248
|
+
<DatePicker>
|
|
249
|
+
<DatePicker.Trigger aria-label="Pick a date" placeholder="Pick a date" />
|
|
250
|
+
<DatePicker.Content aria-label="Choose date">
|
|
251
|
+
<DatePicker.Calendar />
|
|
252
|
+
</DatePicker.Content>
|
|
253
|
+
</DatePicker>
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
await user.click(screen.getByRole('button'));
|
|
257
|
+
await screen.findByRole('grid');
|
|
258
|
+
// Disable aria-command-name: Base UI focus guard spans have role="button" without names (upstream)
|
|
259
|
+
await expectNoA11yViolations(container, {
|
|
260
|
+
disabledRules: ['aria-command-name'],
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|