@fragments-sdk/ui 0.12.0 → 0.13.0
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/components/Accordion/index.cjs +11 -4
- package/dist/components/Accordion/index.cjs.map +1 -1
- package/dist/components/Accordion/index.d.ts +3 -3
- package/dist/components/Accordion/index.d.ts.map +1 -1
- package/dist/components/Accordion/index.js +11 -4
- package/dist/components/Accordion/index.js.map +1 -1
- package/dist/components/Collapsible/index.cjs +45 -10
- package/dist/components/Collapsible/index.cjs.map +1 -1
- package/dist/components/Collapsible/index.d.ts +6 -12
- package/dist/components/Collapsible/index.d.ts.map +1 -1
- package/dist/components/Collapsible/index.js +45 -10
- package/dist/components/Collapsible/index.js.map +1 -1
- package/dist/components/Combobox/index.cjs +18 -9
- package/dist/components/Combobox/index.cjs.map +1 -1
- package/dist/components/Combobox/index.d.ts +8 -12
- package/dist/components/Combobox/index.d.ts.map +1 -1
- package/dist/components/Combobox/index.js +18 -9
- package/dist/components/Combobox/index.js.map +1 -1
- package/dist/components/Command/index.cjs +54 -21
- package/dist/components/Command/index.cjs.map +1 -1
- package/dist/components/Command/index.d.ts +2 -2
- package/dist/components/Command/index.d.ts.map +1 -1
- package/dist/components/Command/index.js +54 -21
- package/dist/components/Command/index.js.map +1 -1
- package/dist/components/DataTable/index.cjs +13 -1
- package/dist/components/DataTable/index.cjs.map +1 -1
- package/dist/components/DataTable/index.d.ts.map +1 -1
- package/dist/components/DataTable/index.js +13 -1
- package/dist/components/DataTable/index.js.map +1 -1
- package/dist/components/DatePicker/index.d.ts +2 -3
- package/dist/components/DatePicker/index.d.ts.map +1 -1
- package/dist/components/Dialog/index.cjs +12 -9
- package/dist/components/Dialog/index.cjs.map +1 -1
- package/dist/components/Dialog/index.d.ts +8 -12
- package/dist/components/Dialog/index.d.ts.map +1 -1
- package/dist/components/Dialog/index.js +12 -9
- package/dist/components/Dialog/index.js.map +1 -1
- package/dist/components/Drawer/index.cjs +12 -9
- package/dist/components/Drawer/index.cjs.map +1 -1
- package/dist/components/Drawer/index.d.ts +8 -12
- package/dist/components/Drawer/index.d.ts.map +1 -1
- package/dist/components/Drawer/index.js +12 -9
- package/dist/components/Drawer/index.js.map +1 -1
- package/dist/components/Menu/index.cjs +30 -16
- package/dist/components/Menu/index.cjs.map +1 -1
- package/dist/components/Menu/index.d.ts +17 -25
- package/dist/components/Menu/index.d.ts.map +1 -1
- package/dist/components/Menu/index.js +30 -16
- package/dist/components/Menu/index.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenuContext.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenuContext.d.ts +1 -0
- package/dist/components/NavigationMenu/NavigationMenuContext.d.ts.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenuContext.js.map +1 -1
- package/dist/components/NavigationMenu/index.cjs +43 -11
- package/dist/components/NavigationMenu/index.cjs.map +1 -1
- package/dist/components/NavigationMenu/index.d.ts.map +1 -1
- package/dist/components/NavigationMenu/index.js +43 -11
- package/dist/components/NavigationMenu/index.js.map +1 -1
- package/dist/components/NavigationMenu/useNavigationMenu.cjs +2 -0
- package/dist/components/NavigationMenu/useNavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/useNavigationMenu.d.ts +1 -0
- package/dist/components/NavigationMenu/useNavigationMenu.d.ts.map +1 -1
- package/dist/components/NavigationMenu/useNavigationMenu.js +2 -0
- package/dist/components/NavigationMenu/useNavigationMenu.js.map +1 -1
- package/dist/components/Popover/index.cjs +11 -10
- package/dist/components/Popover/index.cjs.map +1 -1
- package/dist/components/Popover/index.d.ts +8 -12
- package/dist/components/Popover/index.d.ts.map +1 -1
- package/dist/components/Popover/index.js +11 -10
- package/dist/components/Popover/index.js.map +1 -1
- package/dist/components/Select/index.cjs +7 -6
- package/dist/components/Select/index.cjs.map +1 -1
- package/dist/components/Select/index.d.ts +6 -9
- package/dist/components/Select/index.d.ts.map +1 -1
- package/dist/components/Select/index.js +7 -6
- package/dist/components/Select/index.js.map +1 -1
- package/dist/components/Sidebar/index.cjs +71 -24
- package/dist/components/Sidebar/index.cjs.map +1 -1
- package/dist/components/Sidebar/index.d.ts +21 -33
- package/dist/components/Sidebar/index.d.ts.map +1 -1
- package/dist/components/Sidebar/index.js +71 -24
- package/dist/components/Sidebar/index.js.map +1 -1
- package/dist/components/Tooltip/index.cjs +12 -6
- package/dist/components/Tooltip/index.cjs.map +1 -1
- package/dist/components/Tooltip/index.d.ts.map +1 -1
- package/dist/components/Tooltip/index.js +12 -6
- package/dist/components/Tooltip/index.js.map +1 -1
- package/dist/datepicker.cjs +24 -10
- package/dist/datepicker.cjs.map +1 -1
- package/dist/datepicker.js +24 -10
- package/dist/datepicker.js.map +1 -1
- package/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.test.tsx +33 -0
- package/src/components/Accordion/index.tsx +10 -3
- package/src/components/Collapsible/Collapsible.test.tsx +41 -0
- package/src/components/Collapsible/index.tsx +53 -16
- package/src/components/Combobox/Combobox.test.tsx +55 -0
- package/src/components/Combobox/index.tsx +23 -17
- package/src/components/Command/Command.test.tsx +93 -0
- package/src/components/Command/index.tsx +61 -18
- package/src/components/DataTable/DataTable.test.tsx +11 -2
- package/src/components/DataTable/index.tsx +22 -2
- package/src/components/DatePicker/DatePicker.test.tsx +79 -0
- package/src/components/DatePicker/index.tsx +29 -14
- package/src/components/Dialog/Dialog.test.tsx +23 -0
- package/src/components/Dialog/index.tsx +15 -16
- package/src/components/Drawer/Drawer.test.tsx +27 -0
- package/src/components/Drawer/index.tsx +15 -16
- package/src/components/Menu/index.tsx +35 -30
- package/src/components/NavigationMenu/NavigationMenu.fragment.tsx +1 -1
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +40 -4
- package/src/components/NavigationMenu/NavigationMenuContext.ts +3 -0
- package/src/components/NavigationMenu/index.tsx +49 -13
- package/src/components/NavigationMenu/useNavigationMenu.ts +4 -0
- package/src/components/Popover/Popover.test.tsx +23 -0
- package/src/components/Popover/index.tsx +15 -18
- package/src/components/Select/Select.test.tsx +41 -0
- package/src/components/Select/index.tsx +10 -12
- package/src/components/Sidebar/Sidebar.test.tsx +83 -4
- package/src/components/Sidebar/index.tsx +87 -45
- package/src/components/Tooltip/Tooltip.test.tsx +17 -0
- package/src/components/Tooltip/index.tsx +46 -32
|
@@ -74,6 +74,16 @@ function defaultFilter(value: string, search: string, keywords?: string[]): numb
|
|
|
74
74
|
return 0;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function getTextContent(node: React.ReactNode): string {
|
|
78
|
+
if (typeof node === 'string' || typeof node === 'number') return String(node);
|
|
79
|
+
if (Array.isArray(node)) return node.map(getTextContent).join(' ');
|
|
80
|
+
if (React.isValidElement(node)) {
|
|
81
|
+
const childProps = node.props as { children?: React.ReactNode };
|
|
82
|
+
return getTextContent(childProps.children);
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
// ============================================
|
|
78
88
|
// Context
|
|
79
89
|
// ============================================
|
|
@@ -95,6 +105,7 @@ interface CommandContextValue {
|
|
|
95
105
|
loop: boolean;
|
|
96
106
|
listRef: React.RefObject<HTMLDivElement | null>;
|
|
97
107
|
visibleCount: number;
|
|
108
|
+
listId: string;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
const CommandContext = React.createContext<CommandContextValue | null>(null);
|
|
@@ -150,6 +161,8 @@ function CommandRoot({
|
|
|
150
161
|
const [items, setItems] = React.useState<Map<string, ItemRegistration>>(new Map());
|
|
151
162
|
const [activeId, setActiveId] = React.useState<string | null>(null);
|
|
152
163
|
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
164
|
+
const generatedListId = React.useId();
|
|
165
|
+
const listId = `command-list-${generatedListId}`;
|
|
153
166
|
|
|
154
167
|
const setSearch = React.useCallback(
|
|
155
168
|
(value: string) => {
|
|
@@ -213,8 +226,9 @@ function CommandRoot({
|
|
|
213
226
|
loop,
|
|
214
227
|
listRef,
|
|
215
228
|
visibleCount,
|
|
229
|
+
listId,
|
|
216
230
|
}),
|
|
217
|
-
[search, setSearch, filter, scores, registerItem, unregisterItem, activeId, loop, visibleCount]
|
|
231
|
+
[search, setSearch, filter, scores, registerItem, unregisterItem, activeId, loop, visibleCount, listId]
|
|
218
232
|
);
|
|
219
233
|
|
|
220
234
|
return (
|
|
@@ -230,8 +244,13 @@ function CommandRoot({
|
|
|
230
244
|
);
|
|
231
245
|
}
|
|
232
246
|
|
|
233
|
-
function CommandInput({
|
|
234
|
-
|
|
247
|
+
function CommandInput({
|
|
248
|
+
className,
|
|
249
|
+
onChange,
|
|
250
|
+
onKeyDown,
|
|
251
|
+
...htmlProps
|
|
252
|
+
}: CommandInputProps) {
|
|
253
|
+
const { search, setSearch, listRef, setActiveId, activeId, loop, listId } = useCommandContext();
|
|
235
254
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
236
255
|
|
|
237
256
|
const getEnabledItems = React.useCallback(() => {
|
|
@@ -243,7 +262,7 @@ function CommandInput({ className, ...htmlProps }: CommandInputProps) {
|
|
|
243
262
|
}, [listRef]);
|
|
244
263
|
|
|
245
264
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
246
|
-
|
|
265
|
+
onKeyDown?.(event);
|
|
247
266
|
if (event.defaultPrevented) return;
|
|
248
267
|
|
|
249
268
|
const items = getEnabledItems();
|
|
@@ -307,32 +326,36 @@ function CommandInput({ className, ...htmlProps }: CommandInputProps) {
|
|
|
307
326
|
type="text"
|
|
308
327
|
role="combobox"
|
|
309
328
|
aria-expanded={true}
|
|
310
|
-
aria-controls=
|
|
329
|
+
aria-controls={listId}
|
|
311
330
|
aria-autocomplete="list"
|
|
312
331
|
aria-activedescendant={activeId ?? undefined}
|
|
313
332
|
autoComplete="off"
|
|
314
333
|
autoCorrect="off"
|
|
315
334
|
spellCheck={false}
|
|
316
335
|
value={search}
|
|
317
|
-
|
|
336
|
+
{...htmlProps}
|
|
337
|
+
onChange={(e) => {
|
|
338
|
+
onChange?.(e);
|
|
339
|
+
if (e.defaultPrevented) return;
|
|
340
|
+
setSearch(e.target.value);
|
|
341
|
+
}}
|
|
318
342
|
onKeyDown={handleKeyDown}
|
|
319
343
|
className={[styles.input, className].filter(Boolean).join(' ')}
|
|
320
|
-
{...htmlProps}
|
|
321
344
|
/>
|
|
322
345
|
</div>
|
|
323
346
|
);
|
|
324
347
|
}
|
|
325
348
|
|
|
326
349
|
function CommandList({ children, className, ...htmlProps }: CommandListProps) {
|
|
327
|
-
const { listRef } = useCommandContext();
|
|
350
|
+
const { listRef, listId } = useCommandContext();
|
|
328
351
|
|
|
329
352
|
return (
|
|
330
353
|
<div
|
|
331
354
|
ref={listRef}
|
|
332
|
-
|
|
355
|
+
{...htmlProps}
|
|
356
|
+
id={listId}
|
|
333
357
|
role="listbox"
|
|
334
358
|
className={[styles.list, className].filter(Boolean).join(' ')}
|
|
335
|
-
{...htmlProps}
|
|
336
359
|
>
|
|
337
360
|
{children}
|
|
338
361
|
</div>
|
|
@@ -346,6 +369,10 @@ function CommandItem({
|
|
|
346
369
|
disabled = false,
|
|
347
370
|
onItemSelect,
|
|
348
371
|
className,
|
|
372
|
+
onClick,
|
|
373
|
+
onKeyDown,
|
|
374
|
+
onMouseEnter,
|
|
375
|
+
style,
|
|
349
376
|
...htmlProps
|
|
350
377
|
}: CommandItemProps) {
|
|
351
378
|
const { scores, registerItem, unregisterItem, activeId, setActiveId } = useCommandContext();
|
|
@@ -356,8 +383,7 @@ function CommandItem({
|
|
|
356
383
|
// Extract text content for filtering if no value prop
|
|
357
384
|
const textValue = React.useMemo(() => {
|
|
358
385
|
if (valueProp) return valueProp;
|
|
359
|
-
|
|
360
|
-
return '';
|
|
386
|
+
return getTextContent(children).trim();
|
|
361
387
|
}, [valueProp, children]);
|
|
362
388
|
|
|
363
389
|
// Register with context
|
|
@@ -377,12 +403,20 @@ function CommandItem({
|
|
|
377
403
|
}
|
|
378
404
|
}, [isActive]);
|
|
379
405
|
|
|
380
|
-
const
|
|
406
|
+
const activateItem = () => {
|
|
381
407
|
if (disabled) return;
|
|
382
408
|
onItemSelect?.();
|
|
383
409
|
};
|
|
384
410
|
|
|
385
|
-
const
|
|
411
|
+
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
412
|
+
onClick?.(event);
|
|
413
|
+
if (event.defaultPrevented) return;
|
|
414
|
+
activateItem();
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
418
|
+
onMouseEnter?.(event);
|
|
419
|
+
if (event.defaultPrevented) return;
|
|
386
420
|
if (!disabled) {
|
|
387
421
|
setActiveId(itemId);
|
|
388
422
|
}
|
|
@@ -400,7 +434,14 @@ function CommandItem({
|
|
|
400
434
|
data-active={isActive || undefined}
|
|
401
435
|
data-disabled={disabled || undefined}
|
|
402
436
|
onClick={handleClick}
|
|
403
|
-
onKeyDown={(e) => {
|
|
437
|
+
onKeyDown={(e) => {
|
|
438
|
+
onKeyDown?.(e);
|
|
439
|
+
if (e.defaultPrevented) return;
|
|
440
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
441
|
+
e.preventDefault();
|
|
442
|
+
activateItem();
|
|
443
|
+
}
|
|
444
|
+
}}
|
|
404
445
|
onMouseEnter={handleMouseEnter}
|
|
405
446
|
className={[
|
|
406
447
|
styles.item,
|
|
@@ -410,7 +451,7 @@ function CommandItem({
|
|
|
410
451
|
]
|
|
411
452
|
.filter(Boolean)
|
|
412
453
|
.join(' ')}
|
|
413
|
-
style={{ display: isVisible ? undefined : 'none' }}
|
|
454
|
+
style={{ ...style, display: isVisible ? undefined : 'none' }}
|
|
414
455
|
>
|
|
415
456
|
{children}
|
|
416
457
|
</div>
|
|
@@ -431,14 +472,16 @@ function CommandGroup({ children, heading, className, ...htmlProps }: CommandGro
|
|
|
431
472
|
setHasVisibleChildren(anyVisible);
|
|
432
473
|
}, [scores]);
|
|
433
474
|
|
|
475
|
+
const { style, ...restHtmlProps } = htmlProps;
|
|
476
|
+
|
|
434
477
|
return (
|
|
435
478
|
<div
|
|
436
479
|
ref={groupRef}
|
|
437
|
-
{...
|
|
480
|
+
{...restHtmlProps}
|
|
438
481
|
role="group"
|
|
439
482
|
aria-labelledby={heading ? labelId : undefined}
|
|
440
483
|
className={[styles.group, className].filter(Boolean).join(' ')}
|
|
441
|
-
style={{ display: hasVisibleChildren ? undefined : 'none' }}
|
|
484
|
+
style={{ ...style, display: hasVisibleChildren ? undefined : 'none' }}
|
|
442
485
|
>
|
|
443
486
|
{heading && (
|
|
444
487
|
<div id={labelId} className={styles.groupHeading}>
|
|
@@ -41,9 +41,18 @@ describe('DataTable', () => {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it('shows empty state message when data is empty', () => {
|
|
44
|
-
render(
|
|
44
|
+
render(
|
|
45
|
+
<DataTable
|
|
46
|
+
columns={columns}
|
|
47
|
+
data={[]}
|
|
48
|
+
emptyMessage="Nothing here"
|
|
49
|
+
caption="People Table"
|
|
50
|
+
aria-label="People"
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
45
53
|
expect(screen.getByText('Nothing here')).toBeInTheDocument();
|
|
46
|
-
expect(screen.
|
|
54
|
+
expect(screen.getByRole('table', { name: /people/i })).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText('People Table')).toBeInTheDocument();
|
|
47
56
|
});
|
|
48
57
|
|
|
49
58
|
it('defaults to "No data available" empty message', () => {
|
|
@@ -270,8 +270,28 @@ function DataTableRoot<T>({
|
|
|
270
270
|
|
|
271
271
|
if (isEmpty) {
|
|
272
272
|
return (
|
|
273
|
-
<div className={styles.
|
|
274
|
-
<
|
|
273
|
+
<div className={[styles.wrapper, bordered && styles.bordered].filter(Boolean).join(' ')}>
|
|
274
|
+
<table
|
|
275
|
+
{...htmlProps}
|
|
276
|
+
className={rootClasses}
|
|
277
|
+
aria-label={ariaLabel}
|
|
278
|
+
aria-describedby={ariaDescribedBy}
|
|
279
|
+
>
|
|
280
|
+
{caption && (
|
|
281
|
+
<caption className={captionHidden ? styles.captionHidden : styles.caption}>
|
|
282
|
+
{caption}
|
|
283
|
+
</caption>
|
|
284
|
+
)}
|
|
285
|
+
<tbody className={styles.tbody}>
|
|
286
|
+
<tr className={styles.row}>
|
|
287
|
+
<td className={styles.td} colSpan={Math.max(columns.length, 1)}>
|
|
288
|
+
<div className={styles.emptyState}>
|
|
289
|
+
<span className={styles.emptyMessage}>{emptyMessage}</span>
|
|
290
|
+
</div>
|
|
291
|
+
</td>
|
|
292
|
+
</tr>
|
|
293
|
+
</tbody>
|
|
294
|
+
</table>
|
|
275
295
|
</div>
|
|
276
296
|
);
|
|
277
297
|
}
|
|
@@ -175,6 +175,38 @@ describe('DatePicker', () => {
|
|
|
175
175
|
|
|
176
176
|
expect(onSelect).toHaveBeenCalledWith(presetDate);
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
it('preset forwards html props', async () => {
|
|
180
|
+
const user = userEvent.setup();
|
|
181
|
+
const onSelect = vi.fn();
|
|
182
|
+
const onPresetClick = vi.fn();
|
|
183
|
+
const presetDate = new Date(2025, 5, 2);
|
|
184
|
+
|
|
185
|
+
render(
|
|
186
|
+
<DatePicker onSelect={onSelect}>
|
|
187
|
+
<DatePicker.Trigger placeholder="Pick a date" />
|
|
188
|
+
<DatePicker.Content>
|
|
189
|
+
<DatePicker.Preset
|
|
190
|
+
id="preset-june-2"
|
|
191
|
+
data-testid="preset"
|
|
192
|
+
aria-label="Preset June 2"
|
|
193
|
+
onClick={onPresetClick}
|
|
194
|
+
date={presetDate}
|
|
195
|
+
>
|
|
196
|
+
June 2nd
|
|
197
|
+
</DatePicker.Preset>
|
|
198
|
+
<DatePicker.Calendar />
|
|
199
|
+
</DatePicker.Content>
|
|
200
|
+
</DatePicker>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await user.click(screen.getByRole('button', { name: /pick a date/i }));
|
|
204
|
+
await user.click(screen.getByTestId('preset'));
|
|
205
|
+
|
|
206
|
+
expect(screen.getByTestId('preset')).toHaveAttribute('id', 'preset-june-2');
|
|
207
|
+
expect(onPresetClick).toHaveBeenCalled();
|
|
208
|
+
expect(onSelect).toHaveBeenCalledWith(presetDate);
|
|
209
|
+
});
|
|
178
210
|
});
|
|
179
211
|
|
|
180
212
|
describe('keyboard', () => {
|
|
@@ -216,6 +248,53 @@ describe('DatePicker', () => {
|
|
|
216
248
|
expect(screen.getByRole('button')).toHaveTextContent('Apr 01, 2025');
|
|
217
249
|
expect(screen.getByRole('button')).toHaveTextContent('Apr 07, 2025');
|
|
218
250
|
});
|
|
251
|
+
|
|
252
|
+
it('requests close after single selection when open is controlled', async () => {
|
|
253
|
+
const user = userEvent.setup();
|
|
254
|
+
const onOpenChange = vi.fn();
|
|
255
|
+
const onSelect = vi.fn();
|
|
256
|
+
|
|
257
|
+
render(
|
|
258
|
+
<DatePicker open onOpenChange={onOpenChange} onSelect={onSelect}>
|
|
259
|
+
<DatePicker.Trigger placeholder="Pick a date" />
|
|
260
|
+
<DatePicker.Content>
|
|
261
|
+
<DatePicker.Calendar />
|
|
262
|
+
</DatePicker.Content>
|
|
263
|
+
</DatePicker>
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await screen.findByRole('grid');
|
|
267
|
+
|
|
268
|
+
const dayButtons = screen.getAllByRole('gridcell');
|
|
269
|
+
const day10 = dayButtons.find((cell) => {
|
|
270
|
+
const btn = cell.querySelector('button');
|
|
271
|
+
return btn?.textContent === '10';
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await user.click(day10!.querySelector('button')!);
|
|
275
|
+
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(onSelect).toHaveBeenCalledWith(expect.any(Date));
|
|
278
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
279
|
+
}, { timeout: 500 });
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('forms', () => {
|
|
284
|
+
it('serializes selected date to a date-only hidden input value', () => {
|
|
285
|
+
render(
|
|
286
|
+
<DatePicker name="appointment" selected={new Date(2025, 0, 15)}>
|
|
287
|
+
<DatePicker.Trigger />
|
|
288
|
+
<DatePicker.Content>
|
|
289
|
+
<DatePicker.Calendar />
|
|
290
|
+
</DatePicker.Content>
|
|
291
|
+
</DatePicker>
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const input = document.querySelector<HTMLInputElement>('input[type="hidden"][name="appointment"]');
|
|
295
|
+
expect(input).toBeTruthy();
|
|
296
|
+
expect(input?.value).toBe('2025-01-15');
|
|
297
|
+
});
|
|
219
298
|
});
|
|
220
299
|
|
|
221
300
|
describe('multi-month', () => {
|
|
@@ -67,13 +67,12 @@ export interface DatePickerCalendarProps {
|
|
|
67
67
|
className?: string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export interface DatePickerPresetProps {
|
|
70
|
+
export interface DatePickerPresetProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
71
71
|
children: React.ReactNode;
|
|
72
72
|
/** Date to select (single mode) */
|
|
73
73
|
date?: Date;
|
|
74
74
|
/** Range to select (range mode) */
|
|
75
75
|
range?: DateRange;
|
|
76
|
-
className?: string;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
// ============================================
|
|
@@ -226,6 +225,14 @@ function defaultFormatRange(range: DateRange): string {
|
|
|
226
225
|
return `${range.from.toLocaleDateString()} - ${range.to.toLocaleDateString()}`;
|
|
227
226
|
}
|
|
228
227
|
|
|
228
|
+
function formatDateForHiddenInput(date?: Date): string {
|
|
229
|
+
if (!date) return '';
|
|
230
|
+
const year = date.getFullYear();
|
|
231
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
232
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
233
|
+
return `${year}-${month}-${day}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
229
236
|
// ============================================
|
|
230
237
|
// ClassNames mapping (built lazily)
|
|
231
238
|
// ============================================
|
|
@@ -327,15 +334,14 @@ function DatePickerRoot({
|
|
|
327
334
|
}
|
|
328
335
|
onSelect?.(date);
|
|
329
336
|
|
|
330
|
-
// Auto-close after single selection (uncontrolled)
|
|
331
|
-
if (
|
|
337
|
+
// Auto-close after single selection (controlled and uncontrolled).
|
|
338
|
+
if (date) {
|
|
332
339
|
setTimeout(() => {
|
|
333
|
-
|
|
334
|
-
onOpenChange?.(false);
|
|
340
|
+
handleOpenChange(false);
|
|
335
341
|
}, 150);
|
|
336
342
|
}
|
|
337
343
|
},
|
|
338
|
-
[selectedProp, onSelect,
|
|
344
|
+
[selectedProp, onSelect, handleOpenChange]
|
|
339
345
|
);
|
|
340
346
|
|
|
341
347
|
const setSelectedRange = React.useCallback(
|
|
@@ -412,10 +418,10 @@ function DatePickerRoot({
|
|
|
412
418
|
type="hidden"
|
|
413
419
|
name={name}
|
|
414
420
|
value={
|
|
415
|
-
|
|
416
|
-
? (contextValue.selected
|
|
421
|
+
mode === 'single'
|
|
422
|
+
? formatDateForHiddenInput(contextValue.selected ?? undefined)
|
|
417
423
|
: contextValue.selectedRange
|
|
418
|
-
? `${contextValue.selectedRange.from
|
|
424
|
+
? `${formatDateForHiddenInput(contextValue.selectedRange.from)},${formatDateForHiddenInput(contextValue.selectedRange.to)}`
|
|
419
425
|
: ''
|
|
420
426
|
}
|
|
421
427
|
/>
|
|
@@ -555,20 +561,29 @@ function DatePickerCalendar({ numberOfMonths: numberOfMonthsProp, className }: D
|
|
|
555
561
|
);
|
|
556
562
|
}
|
|
557
563
|
|
|
558
|
-
function DatePickerPreset({
|
|
564
|
+
function DatePickerPreset({
|
|
565
|
+
children,
|
|
566
|
+
date,
|
|
567
|
+
range,
|
|
568
|
+
className,
|
|
569
|
+
onClick,
|
|
570
|
+
...htmlProps
|
|
571
|
+
}: DatePickerPresetProps) {
|
|
559
572
|
const ctx = useDatePickerContext();
|
|
560
573
|
const classes = [styles.preset, className].filter(Boolean).join(' ');
|
|
561
574
|
|
|
562
|
-
const handleClick = React.useCallback(() => {
|
|
575
|
+
const handleClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
576
|
+
onClick?.(event);
|
|
577
|
+
if (event.defaultPrevented) return;
|
|
563
578
|
if (ctx.mode === 'single' && date) {
|
|
564
579
|
ctx.setSelected(date);
|
|
565
580
|
} else if (ctx.mode === 'range' && range) {
|
|
566
581
|
ctx.setSelectedRange(range);
|
|
567
582
|
}
|
|
568
|
-
}, [ctx, date, range]);
|
|
583
|
+
}, [ctx, date, range, onClick]);
|
|
569
584
|
|
|
570
585
|
return (
|
|
571
|
-
<button type="button" className={classes} onClick={handleClick}>
|
|
586
|
+
<button type="button" {...htmlProps} className={classes} onClick={handleClick}>
|
|
572
587
|
{children}
|
|
573
588
|
</button>
|
|
574
589
|
);
|
|
@@ -71,6 +71,29 @@ describe('Dialog', () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it('forwards html props to trigger, title, description, and close', async () => {
|
|
75
|
+
const user = userEvent.setup();
|
|
76
|
+
render(
|
|
77
|
+
<Dialog>
|
|
78
|
+
<Dialog.Trigger id="dialog-trigger">Open</Dialog.Trigger>
|
|
79
|
+
<Dialog.Content>
|
|
80
|
+
<Dialog.Title id="dialog-title">Dialog Title</Dialog.Title>
|
|
81
|
+
<Dialog.Description id="dialog-description">Dialog Description</Dialog.Description>
|
|
82
|
+
<Dialog.Close data-testid="dialog-close" />
|
|
83
|
+
</Dialog.Content>
|
|
84
|
+
</Dialog>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByRole('button', { name: /open/i })).toHaveAttribute('id', 'dialog-trigger');
|
|
88
|
+
await user.click(screen.getByRole('button', { name: /open/i }));
|
|
89
|
+
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(screen.getByText('Dialog Title')).toHaveAttribute('id', 'dialog-title');
|
|
92
|
+
expect(screen.getByText('Dialog Description')).toHaveAttribute('id', 'dialog-description');
|
|
93
|
+
expect(screen.getByTestId('dialog-close')).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
74
97
|
it('renders compound sub-components (Header, Body, Footer)', async () => {
|
|
75
98
|
renderDialog({ defaultOpen: true });
|
|
76
99
|
|
|
@@ -33,14 +33,12 @@ export interface DialogContentProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
33
33
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export interface DialogTitleProps {
|
|
36
|
+
export interface DialogTitleProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
37
37
|
children: React.ReactNode;
|
|
38
|
-
className?: string;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
export interface DialogDescriptionProps {
|
|
40
|
+
export interface DialogDescriptionProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
42
41
|
children: React.ReactNode;
|
|
43
|
-
className?: string;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
export interface DialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -55,16 +53,14 @@ export interface DialogFooterProps extends React.HTMLAttributes<HTMLDivElement>
|
|
|
55
53
|
children: React.ReactNode;
|
|
56
54
|
}
|
|
57
55
|
|
|
58
|
-
export interface DialogTriggerProps {
|
|
56
|
+
export interface DialogTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
59
57
|
children: React.ReactNode;
|
|
60
58
|
asChild?: boolean;
|
|
61
|
-
className?: string;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
export interface DialogCloseProps {
|
|
61
|
+
export interface DialogCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
65
62
|
children?: React.ReactNode;
|
|
66
63
|
asChild?: boolean;
|
|
67
|
-
className?: string;
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
// ============================================
|
|
@@ -118,17 +114,18 @@ function DialogTrigger({
|
|
|
118
114
|
children,
|
|
119
115
|
asChild,
|
|
120
116
|
className,
|
|
117
|
+
...htmlProps
|
|
121
118
|
}: DialogTriggerProps) {
|
|
122
119
|
if (asChild) {
|
|
123
120
|
return (
|
|
124
|
-
<BaseDialog.Trigger className={className} render={children as React.ReactElement}>
|
|
121
|
+
<BaseDialog.Trigger {...htmlProps} className={className} render={children as React.ReactElement}>
|
|
125
122
|
{null}
|
|
126
123
|
</BaseDialog.Trigger>
|
|
127
124
|
);
|
|
128
125
|
}
|
|
129
126
|
|
|
130
127
|
return (
|
|
131
|
-
<BaseDialog.Trigger className={className}>
|
|
128
|
+
<BaseDialog.Trigger {...htmlProps} className={className}>
|
|
132
129
|
{children}
|
|
133
130
|
</BaseDialog.Trigger>
|
|
134
131
|
);
|
|
@@ -161,15 +158,15 @@ function DialogHeader({ children, className, ...htmlProps }: DialogHeaderProps)
|
|
|
161
158
|
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
162
159
|
}
|
|
163
160
|
|
|
164
|
-
function DialogTitle({ children, className }: DialogTitleProps) {
|
|
161
|
+
function DialogTitle({ children, className, ...htmlProps }: DialogTitleProps) {
|
|
165
162
|
const classes = [styles.title, className].filter(Boolean).join(' ');
|
|
166
|
-
return <BaseDialog.Title className={classes}>{children}</BaseDialog.Title>;
|
|
163
|
+
return <BaseDialog.Title {...htmlProps} className={classes}>{children}</BaseDialog.Title>;
|
|
167
164
|
}
|
|
168
165
|
|
|
169
|
-
function DialogDescription({ children, className }: DialogDescriptionProps) {
|
|
166
|
+
function DialogDescription({ children, className, ...htmlProps }: DialogDescriptionProps) {
|
|
170
167
|
const classes = [styles.description, className].filter(Boolean).join(' ');
|
|
171
168
|
return (
|
|
172
|
-
<BaseDialog.Description className={classes}>
|
|
169
|
+
<BaseDialog.Description {...htmlProps} className={classes}>
|
|
173
170
|
{children}
|
|
174
171
|
</BaseDialog.Description>
|
|
175
172
|
);
|
|
@@ -185,11 +182,12 @@ function DialogFooter({ children, className, ...htmlProps }: DialogFooterProps)
|
|
|
185
182
|
return <div {...htmlProps} className={classes}>{children}</div>;
|
|
186
183
|
}
|
|
187
184
|
|
|
188
|
-
function DialogClose({ children, asChild, className }: DialogCloseProps) {
|
|
185
|
+
function DialogClose({ children, asChild, className, ...htmlProps }: DialogCloseProps) {
|
|
189
186
|
// If no children, render the default X close button
|
|
190
187
|
if (!children) {
|
|
191
188
|
return (
|
|
192
189
|
<BaseDialog.Close
|
|
190
|
+
{...htmlProps}
|
|
193
191
|
data-dialog-close
|
|
194
192
|
aria-label="Close dialog"
|
|
195
193
|
className={[styles.close, className].filter(Boolean).join(' ')}
|
|
@@ -202,6 +200,7 @@ function DialogClose({ children, asChild, className }: DialogCloseProps) {
|
|
|
202
200
|
if (asChild) {
|
|
203
201
|
return (
|
|
204
202
|
<BaseDialog.Close
|
|
203
|
+
{...htmlProps}
|
|
205
204
|
data-dialog-close
|
|
206
205
|
className={className}
|
|
207
206
|
render={children as React.ReactElement}
|
|
@@ -212,7 +211,7 @@ function DialogClose({ children, asChild, className }: DialogCloseProps) {
|
|
|
212
211
|
}
|
|
213
212
|
|
|
214
213
|
return (
|
|
215
|
-
<BaseDialog.Close data-dialog-close className={className}>
|
|
214
|
+
<BaseDialog.Close {...htmlProps} data-dialog-close className={className}>
|
|
216
215
|
{children}
|
|
217
216
|
</BaseDialog.Close>
|
|
218
217
|
);
|
|
@@ -66,6 +66,33 @@ describe('Drawer', () => {
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it('forwards html props to trigger, title, description, and close', async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
render(
|
|
72
|
+
<Drawer>
|
|
73
|
+
<Drawer.Trigger id="drawer-trigger">Open</Drawer.Trigger>
|
|
74
|
+
<Drawer.Content>
|
|
75
|
+
<Drawer.Header>
|
|
76
|
+
<Drawer.Title id="drawer-title">Drawer Title</Drawer.Title>
|
|
77
|
+
<Drawer.Close data-testid="drawer-close" />
|
|
78
|
+
</Drawer.Header>
|
|
79
|
+
<Drawer.Body>
|
|
80
|
+
<Drawer.Description id="drawer-description">Drawer Description</Drawer.Description>
|
|
81
|
+
</Drawer.Body>
|
|
82
|
+
</Drawer.Content>
|
|
83
|
+
</Drawer>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(screen.getByRole('button', { name: /open/i })).toHaveAttribute('id', 'drawer-trigger');
|
|
87
|
+
await user.click(screen.getByRole('button', { name: /open/i }));
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(screen.getByText('Drawer Title')).toHaveAttribute('id', 'drawer-title');
|
|
91
|
+
expect(screen.getByText('Drawer Description')).toHaveAttribute('id', 'drawer-description');
|
|
92
|
+
expect(screen.getByTestId('drawer-close')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
69
96
|
it('renders compound sub-components (Header, Body, Footer)', async () => {
|
|
70
97
|
renderDrawer({ defaultOpen: true });
|
|
71
98
|
|