paris 0.19.0 → 0.20.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +17 -2
  3. package/src/stories/accordion/Accordion.test.tsx +140 -0
  4. package/src/stories/accordionselect/AccordionSelect.test.tsx +252 -0
  5. package/src/stories/avatar/Avatar.test.tsx +77 -0
  6. package/src/stories/button/Button.test.tsx +266 -0
  7. package/src/stories/callout/Callout.test.tsx +79 -0
  8. package/src/stories/card/Card.test.tsx +81 -0
  9. package/src/stories/cardbutton/CardButton.test.tsx +174 -0
  10. package/src/stories/checkbox/Checkbox.test.tsx +531 -0
  11. package/src/stories/combobox/Combobox.test.tsx +164 -0
  12. package/src/stories/dialog/Dialog.module.scss +2 -2
  13. package/src/stories/dialog/Dialog.test.tsx +244 -0
  14. package/src/stories/drawer/Drawer.module.scss +2 -2
  15. package/src/stories/drawer/Drawer.test.tsx +259 -0
  16. package/src/stories/field/Field.test.tsx +146 -0
  17. package/src/stories/icon/Icon.test.tsx +59 -0
  18. package/src/stories/informationaltooltip/InformationalTooltip.test.tsx +178 -0
  19. package/src/stories/input/Input.test.tsx +174 -0
  20. package/src/stories/markdown/Markdown.test.tsx +228 -0
  21. package/src/stories/markdowneditor/FixedToolbar.tsx +44 -14
  22. package/src/stories/markdowneditor/LinkPopover.module.scss +1 -1
  23. package/src/stories/markdowneditor/MarkdownEditor.stories.tsx +4 -1
  24. package/src/stories/markdowneditor/MarkdownEditor.test.tsx +115 -0
  25. package/src/stories/markdowneditor/MarkdownEditor.tsx +11 -1
  26. package/src/stories/markdowneditor/MarkdownEditorContext.tsx +3 -0
  27. package/src/stories/markdowneditor/index.ts +1 -0
  28. package/src/stories/menu/Menu.module.scss +1 -1
  29. package/src/stories/menu/Menu.test.tsx +211 -0
  30. package/src/stories/pagination/usePagination.test.ts +259 -0
  31. package/src/stories/popover/Popover.test.tsx +152 -0
  32. package/src/stories/select/Select.module.scss +2 -1
  33. package/src/stories/select/Select.test.tsx +233 -0
  34. package/src/stories/styledlink/StyledLink.test.tsx +59 -0
  35. package/src/stories/table/Table.test.tsx +156 -0
  36. package/src/stories/tabs/Tabs.module.scss +1 -1
  37. package/src/stories/tabs/Tabs.test.tsx +167 -0
  38. package/src/stories/tag/Tag.test.tsx +90 -0
  39. package/src/stories/text/Text.test.tsx +81 -0
  40. package/src/stories/textarea/TextArea.test.tsx +147 -0
  41. package/src/stories/theme/themes.ts +16 -0
  42. package/src/stories/tilt/Tilt.test.tsx +203 -0
  43. package/src/stories/toast/Toast.test.tsx +86 -0
  44. package/src/stories/utility/Dropdown.module.scss +1 -1
  45. package/src/stories/utility/Utility.test.tsx +96 -0
  46. package/src/test/render.tsx +20 -0
  47. package/src/test/setup.ts +32 -0
@@ -0,0 +1,531 @@
1
+ import { useState } from 'react';
2
+ import { render, screen } from '../../test/render';
3
+ import { Checkbox } from './Checkbox';
4
+
5
+ // Helper wrapper for controlled checkbox behavior
6
+ function ControlledCheckbox({
7
+ defaultChecked = false,
8
+ ...props
9
+ }: Omit<React.ComponentProps<typeof Checkbox>, 'checked'> & { defaultChecked?: boolean }) {
10
+ const [checked, setChecked] = useState(defaultChecked);
11
+ return <Checkbox {...props} checked={checked} onChange={(val) => setChecked(!!val)} />;
12
+ }
13
+
14
+ describe('Checkbox', () => {
15
+ // ─── Rendering ───────────────────────────────────────────────────
16
+
17
+ describe('rendering', () => {
18
+ it('renders with default kind', () => {
19
+ render(<Checkbox checked={false}>Accept terms</Checkbox>);
20
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
21
+ expect(screen.getByText('Accept terms')).toBeInTheDocument();
22
+ });
23
+
24
+ it('renders unchecked by default', () => {
25
+ render(<Checkbox checked={false}>Label</Checkbox>);
26
+ const checkbox = screen.getByRole('checkbox');
27
+ expect(checkbox).not.toBeChecked();
28
+ });
29
+
30
+ it('renders checked when checked prop is true', () => {
31
+ render(<Checkbox checked={true}>Label</Checkbox>);
32
+ const checkbox = screen.getByRole('checkbox');
33
+ expect(checkbox).toBeChecked();
34
+ });
35
+
36
+ it('renders label text as children', () => {
37
+ render(<Checkbox checked={false}>My label text</Checkbox>);
38
+ expect(screen.getByText('My label text')).toBeInTheDocument();
39
+ });
40
+
41
+ it('renders ReactNode children', () => {
42
+ render(
43
+ <Checkbox checked={false}>
44
+ <span data-testid="custom-label">Custom element</span>
45
+ </Checkbox>,
46
+ );
47
+ expect(screen.getByTestId('custom-label')).toBeInTheDocument();
48
+ expect(screen.getByText('Custom element')).toBeInTheDocument();
49
+ });
50
+
51
+ it('renders without children', () => {
52
+ render(<Checkbox checked={false} />);
53
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
54
+ });
55
+ });
56
+
57
+ // ─── Interaction ─────────────────────────────────────────────────
58
+
59
+ describe('interaction', () => {
60
+ it('calls onChange when clicked', async () => {
61
+ const handleChange = vi.fn();
62
+ const { user } = render(
63
+ <Checkbox checked={false} onChange={handleChange}>
64
+ Toggle me
65
+ </Checkbox>,
66
+ );
67
+
68
+ await user.click(screen.getByRole('checkbox'));
69
+ expect(handleChange).toHaveBeenCalledTimes(1);
70
+ expect(handleChange).toHaveBeenCalledWith(true);
71
+ });
72
+
73
+ it('calls onChange with false when unchecking', async () => {
74
+ const handleChange = vi.fn();
75
+ const { user } = render(
76
+ <Checkbox checked={true} onChange={handleChange}>
77
+ Toggle me
78
+ </Checkbox>,
79
+ );
80
+
81
+ await user.click(screen.getByRole('checkbox'));
82
+ expect(handleChange).toHaveBeenCalledWith(false);
83
+ });
84
+
85
+ it('toggles checked state in a controlled component', async () => {
86
+ const { user } = render(<ControlledCheckbox>Toggle me</ControlledCheckbox>);
87
+
88
+ const checkbox = screen.getByRole('checkbox');
89
+ expect(checkbox).not.toBeChecked();
90
+
91
+ await user.click(checkbox);
92
+ expect(checkbox).toBeChecked();
93
+
94
+ await user.click(checkbox);
95
+ expect(checkbox).not.toBeChecked();
96
+ });
97
+
98
+ it('clicking the label toggles the checkbox', async () => {
99
+ const handleChange = vi.fn();
100
+ const { user } = render(
101
+ <Checkbox checked={false} onChange={handleChange}>
102
+ Click this label
103
+ </Checkbox>,
104
+ );
105
+
106
+ // The label wraps the checkbox, so clicking text should also trigger
107
+ await user.click(screen.getByText('Click this label'));
108
+ expect(handleChange).toHaveBeenCalled();
109
+ });
110
+ });
111
+
112
+ // ─── Disabled ────────────────────────────────────────────────────
113
+
114
+ describe('disabled', () => {
115
+ it('applies disabled styling class', () => {
116
+ const { container } = render(
117
+ <Checkbox checked={false} disabled>
118
+ Disabled
119
+ </Checkbox>,
120
+ );
121
+ const label = container.querySelector('label');
122
+ expect(label).toHaveClass('disabled');
123
+ });
124
+
125
+ it('sets data-disabled on the checkbox root', () => {
126
+ render(
127
+ <Checkbox checked={false} disabled>
128
+ Disabled
129
+ </Checkbox>,
130
+ );
131
+ const checkbox = screen.getByRole('checkbox');
132
+ expect(checkbox).toHaveAttribute('data-disabled', 'true');
133
+ });
134
+
135
+ it('sets data-disabled on the switch when kind is switch', () => {
136
+ render(
137
+ <Checkbox kind="switch" checked={false} disabled>
138
+ Disabled switch
139
+ </Checkbox>,
140
+ );
141
+ const switchEl = screen.getByRole('switch');
142
+ expect(switchEl).toHaveAttribute('data-disabled', 'true');
143
+ });
144
+ });
145
+
146
+ // ─── Kinds ───────────────────────────────────────────────────────
147
+
148
+ describe('kind variants', () => {
149
+ describe('default kind', () => {
150
+ it('renders a Radix checkbox with default class', () => {
151
+ const { container } = render(
152
+ <Checkbox kind="default" checked={false}>
153
+ Default
154
+ </Checkbox>,
155
+ );
156
+ const root = container.querySelector('[class*="root"]');
157
+ expect(root).toHaveClass('default');
158
+ });
159
+
160
+ it('shows label text next to checkbox', () => {
161
+ render(
162
+ <Checkbox kind="default" checked={false}>
163
+ Default label
164
+ </Checkbox>,
165
+ );
166
+ expect(screen.getByText('Default label')).toBeInTheDocument();
167
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
168
+ });
169
+
170
+ it('renders an SVG check icon when checked', () => {
171
+ const { container } = render(
172
+ <Checkbox kind="default" checked={true}>
173
+ Checked
174
+ </Checkbox>,
175
+ );
176
+ const svg = container.querySelector('svg');
177
+ expect(svg).toBeInTheDocument();
178
+ });
179
+ });
180
+
181
+ describe('surface kind', () => {
182
+ it('renders a Radix checkbox with surface class', () => {
183
+ const { container } = render(
184
+ <Checkbox kind="surface" checked={false}>
185
+ Surface
186
+ </Checkbox>,
187
+ );
188
+ const root = container.querySelector('[class*="root"]');
189
+ expect(root).toHaveClass('surface');
190
+ });
191
+
192
+ it('shows children inside the Radix root (not outside)', () => {
193
+ render(
194
+ <Checkbox kind="surface" checked={false}>
195
+ Surface label
196
+ </Checkbox>,
197
+ );
198
+ expect(screen.getByText('Surface label')).toBeInTheDocument();
199
+ });
200
+
201
+ it('does not show label outside root when kind is surface', () => {
202
+ const { container } = render(
203
+ <Checkbox kind="surface" checked={false}>
204
+ Surface label
205
+ </Checkbox>,
206
+ );
207
+ // In surface mode, the label text is inside the Radix root, not adjacent
208
+ const root = container.querySelector('[class*="root"]');
209
+ expect(root).toContainElement(screen.getByText('Surface label'));
210
+ });
211
+ });
212
+
213
+ describe('panel kind', () => {
214
+ it('renders a Radix checkbox with panel class', () => {
215
+ const { container } = render(
216
+ <Checkbox kind="panel" checked={false}>
217
+ Panel
218
+ </Checkbox>,
219
+ );
220
+ const root = container.querySelector('[class*="root"]');
221
+ expect(root).toHaveClass('panel');
222
+ });
223
+
224
+ it('shows children inside the Radix root', () => {
225
+ const { container } = render(
226
+ <Checkbox kind="panel" checked={false}>
227
+ Panel label
228
+ </Checkbox>,
229
+ );
230
+ const root = container.querySelector('[class*="root"]');
231
+ expect(root).toContainElement(screen.getByText('Panel label'));
232
+ });
233
+
234
+ it('renders a box element when unchecked', () => {
235
+ const { container } = render(
236
+ <Checkbox kind="panel" checked={false}>
237
+ Panel
238
+ </Checkbox>,
239
+ );
240
+ const box = container.querySelector('.box');
241
+ expect(box).toBeInTheDocument();
242
+ });
243
+ });
244
+
245
+ describe('switch kind', () => {
246
+ it('renders a switch element instead of a Radix checkbox', () => {
247
+ render(
248
+ <Checkbox kind="switch" checked={false}>
249
+ Switch
250
+ </Checkbox>,
251
+ );
252
+ // HeadlessUI Switch renders a button with role="switch"
253
+ expect(screen.getByRole('switch')).toBeInTheDocument();
254
+ });
255
+
256
+ it('does not render a Radix checkbox role', () => {
257
+ render(
258
+ <Checkbox kind="switch" checked={false}>
259
+ Switch
260
+ </Checkbox>,
261
+ );
262
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
263
+ });
264
+
265
+ it('shows label text', () => {
266
+ render(
267
+ <Checkbox kind="switch" checked={false}>
268
+ Switch label
269
+ </Checkbox>,
270
+ );
271
+ expect(screen.getByText('Switch label')).toBeInTheDocument();
272
+ });
273
+
274
+ it('toggles switch state on click', async () => {
275
+ const handleChange = vi.fn();
276
+ const { user } = render(
277
+ <Checkbox kind="switch" checked={false} onChange={handleChange}>
278
+ Switch
279
+ </Checkbox>,
280
+ );
281
+
282
+ await user.click(screen.getByRole('switch'));
283
+ expect(handleChange).toHaveBeenCalledWith(true);
284
+ });
285
+
286
+ it('renders knob element', () => {
287
+ const { container } = render(
288
+ <Checkbox kind="switch" checked={false}>
289
+ Switch
290
+ </Checkbox>,
291
+ );
292
+ const knob = container.querySelector('[class*="knob"]');
293
+ expect(knob).toBeInTheDocument();
294
+ });
295
+
296
+ it('applies knobChecked class when checked', () => {
297
+ const { container } = render(
298
+ <Checkbox kind="switch" checked={true}>
299
+ Switch
300
+ </Checkbox>,
301
+ );
302
+ const knob = container.querySelector('[class*="knob"]');
303
+ expect(knob).toHaveClass('knobChecked');
304
+ });
305
+
306
+ it('does not apply knobChecked class when unchecked', () => {
307
+ const { container } = render(
308
+ <Checkbox kind="switch" checked={false}>
309
+ Switch
310
+ </Checkbox>,
311
+ );
312
+ const knob = container.querySelector('[class*="knob"]');
313
+ expect(knob).not.toHaveClass('knobChecked');
314
+ });
315
+ });
316
+ });
317
+
318
+ // ─── Hide Label ──────────────────────────────────────────────────
319
+
320
+ describe('hideLabel', () => {
321
+ it('visually hides the label when hideLabel is true', () => {
322
+ const { container } = render(
323
+ <Checkbox kind="default" checked={false} hideLabel>
324
+ Hidden label
325
+ </Checkbox>,
326
+ );
327
+ // VisuallyHidden uses Ariakit's VisuallyHidden which renders content off-screen
328
+ // The text should still be in the DOM for accessibility
329
+ expect(screen.getByText('Hidden label')).toBeInTheDocument();
330
+ });
331
+
332
+ it('shows label when hideLabel is false', () => {
333
+ render(
334
+ <Checkbox kind="default" checked={false} hideLabel={false}>
335
+ Visible label
336
+ </Checkbox>,
337
+ );
338
+ expect(screen.getByText('Visible label')).toBeInTheDocument();
339
+ });
340
+
341
+ it('hides the label for switch kind', () => {
342
+ render(
343
+ <Checkbox kind="switch" checked={false} hideLabel>
344
+ Hidden switch label
345
+ </Checkbox>,
346
+ );
347
+ // Still accessible in DOM
348
+ expect(screen.getByText('Hidden switch label')).toBeInTheDocument();
349
+ });
350
+ });
351
+
352
+ // ─── className forwarding ────────────────────────────────────────
353
+
354
+ describe('className forwarding', () => {
355
+ it('forwards className to the label container', () => {
356
+ const { container } = render(
357
+ <Checkbox checked={false} className="custom-class">
358
+ Label
359
+ </Checkbox>,
360
+ );
361
+ const label = container.querySelector('label');
362
+ expect(label).toHaveClass('custom-class');
363
+ expect(label).toHaveClass('container');
364
+ });
365
+
366
+ it('preserves container class when custom className is added', () => {
367
+ const { container } = render(
368
+ <Checkbox checked={false} className="my-class">
369
+ Label
370
+ </Checkbox>,
371
+ );
372
+ const label = container.querySelector('label');
373
+ expect(label).toHaveClass('container');
374
+ expect(label).toHaveClass('my-class');
375
+ });
376
+ });
377
+
378
+ // ─── Checked state styling ───────────────────────────────────────
379
+
380
+ describe('checked state styling', () => {
381
+ it('applies checked class to container when checked', () => {
382
+ const { container } = render(<Checkbox checked={true}>Checked</Checkbox>);
383
+ const label = container.querySelector('label');
384
+ expect(label).toHaveClass('checked');
385
+ });
386
+
387
+ it('does not apply checked class to container when unchecked', () => {
388
+ const { container } = render(<Checkbox checked={false}>Unchecked</Checkbox>);
389
+ const label = container.querySelector('label');
390
+ expect(label).not.toHaveClass('checked');
391
+ });
392
+ });
393
+
394
+ // ─── Accessibility ───────────────────────────────────────────────
395
+
396
+ describe('accessibility', () => {
397
+ it('has proper role="checkbox" for default kind', () => {
398
+ render(<Checkbox checked={false}>Accessible</Checkbox>);
399
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
400
+ });
401
+
402
+ it('has proper role="switch" for switch kind', () => {
403
+ render(
404
+ <Checkbox kind="switch" checked={false}>
405
+ Switch
406
+ </Checkbox>,
407
+ );
408
+ expect(screen.getByRole('switch')).toBeInTheDocument();
409
+ });
410
+
411
+ it('has aria-checked false when unchecked', () => {
412
+ render(<Checkbox checked={false}>Label</Checkbox>);
413
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-checked', 'false');
414
+ });
415
+
416
+ it('has aria-checked true when checked', () => {
417
+ render(<Checkbox checked={true}>Label</Checkbox>);
418
+ expect(screen.getByRole('checkbox')).toHaveAttribute('aria-checked', 'true');
419
+ });
420
+
421
+ it('sets aria-details when children is a string', () => {
422
+ render(<Checkbox checked={false}>String label</Checkbox>);
423
+ const checkbox = screen.getByRole('checkbox');
424
+ expect(checkbox).toHaveAttribute('aria-details', 'String label');
425
+ });
426
+
427
+ it('does not set aria-details when children is not a string', () => {
428
+ render(
429
+ <Checkbox checked={false}>
430
+ <span>Non-string label</span>
431
+ </Checkbox>,
432
+ );
433
+ const checkbox = screen.getByRole('checkbox');
434
+ expect(checkbox).not.toHaveAttribute('aria-details');
435
+ });
436
+
437
+ it('associates label with checkbox via htmlFor and id', () => {
438
+ const { container } = render(<Checkbox checked={false}>Label</Checkbox>);
439
+ const label = container.querySelector('label');
440
+ const checkbox = screen.getByRole('checkbox');
441
+ expect(label).toHaveAttribute('for');
442
+ expect(checkbox).toHaveAttribute('id', label?.getAttribute('for'));
443
+ });
444
+
445
+ it('associates label with switch via htmlFor and id', () => {
446
+ const { container } = render(
447
+ <Checkbox kind="switch" checked={false}>
448
+ Switch label
449
+ </Checkbox>,
450
+ );
451
+ const label = container.querySelector('label');
452
+ const switchEl = screen.getByRole('switch');
453
+ expect(label).toHaveAttribute('for');
454
+ expect(switchEl).toHaveAttribute('id', label?.getAttribute('for'));
455
+ });
456
+ });
457
+
458
+ // ─── Spread props ────────────────────────────────────────────────
459
+
460
+ describe('spread props', () => {
461
+ it('forwards additional HTML attributes to the label', () => {
462
+ const { container } = render(
463
+ <Checkbox checked={false} data-testid="checkbox-label" title="My checkbox">
464
+ Label
465
+ </Checkbox>,
466
+ );
467
+ const label = container.querySelector('label');
468
+ expect(label).toHaveAttribute('data-testid', 'checkbox-label');
469
+ expect(label).toHaveAttribute('title', 'My checkbox');
470
+ });
471
+
472
+ it('forwards style prop to the label', () => {
473
+ const { container } = render(
474
+ <Checkbox checked={false} style={{ marginTop: '10px' }}>
475
+ Label
476
+ </Checkbox>,
477
+ );
478
+ const label = container.querySelector('label');
479
+ expect(label).toHaveStyle({ marginTop: '10px' });
480
+ });
481
+ });
482
+
483
+ // ─── Edge cases ──────────────────────────────────────────────────
484
+
485
+ describe('edge cases', () => {
486
+ it('handles multiple rapid toggles', async () => {
487
+ const handleChange = vi.fn();
488
+ const { user } = render(<ControlledCheckbox onChange={handleChange}>Rapid</ControlledCheckbox>);
489
+
490
+ const checkbox = screen.getByRole('checkbox');
491
+ await user.click(checkbox);
492
+ await user.click(checkbox);
493
+ await user.click(checkbox);
494
+
495
+ expect(checkbox).toBeChecked();
496
+ });
497
+
498
+ it('renders with no onChange handler without crashing', () => {
499
+ expect(() => {
500
+ render(<Checkbox checked={false}>No handler</Checkbox>);
501
+ }).not.toThrow();
502
+ });
503
+
504
+ it('renders with checked=true and no onChange without crashing', () => {
505
+ expect(() => {
506
+ render(<Checkbox checked={true}>Static checked</Checkbox>);
507
+ }).not.toThrow();
508
+ });
509
+
510
+ it('handles empty string children', () => {
511
+ render(<Checkbox checked={false}>{''}</Checkbox>);
512
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
513
+ });
514
+
515
+ it('handles number children', () => {
516
+ render(<Checkbox checked={false}>{42}</Checkbox>);
517
+ expect(screen.getByText('42')).toBeInTheDocument();
518
+ });
519
+
520
+ it('applies both disabled and checked classes simultaneously', () => {
521
+ const { container } = render(
522
+ <Checkbox checked={true} disabled>
523
+ Both
524
+ </Checkbox>,
525
+ );
526
+ const label = container.querySelector('label');
527
+ expect(label).toHaveClass('disabled');
528
+ expect(label).toHaveClass('checked');
529
+ });
530
+ });
531
+ });