@regardio/react 1.2.0 → 1.3.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/README.md +17 -16
- package/package.json +17 -38
- package/src/background-slideshow/background-slideshow.test.tsx +146 -0
- package/src/carousel/carousel.test.tsx +86 -0
- package/src/countdown/countdown.test.tsx +46 -0
- package/src/grid/grid.test.tsx +94 -0
- package/src/markdown-container/markdown-container.test.tsx +124 -0
- package/src/password-input/password-input.test.tsx +53 -0
- package/src/protected-email/protected-email.test.tsx +82 -0
- package/dist/generic-error/index.d.mts +0 -47
- package/dist/generic-error/index.mjs +0 -56
- package/dist/hooks/use-current-route-data.d.mts +0 -8
- package/dist/hooks/use-current-route-data.mjs +0 -19
- package/dist/hooks/use-matches-data.d.mts +0 -10
- package/dist/hooks/use-matches-data.mjs +0 -20
- package/dist/hooks/use-user.d.mts +0 -53
- package/dist/hooks/use-user.mjs +0 -32
- package/dist/link/index.d.mts +0 -71
- package/dist/link/index.mjs +0 -127
- package/src/generic-error/generic-error.stories.tsx +0 -45
- package/src/generic-error/generic-error.tsx +0 -105
- package/src/generic-error/index.ts +0 -2
- package/src/hooks/use-current-route-data.ts +0 -20
- package/src/hooks/use-matches-data.ts +0 -21
- package/src/hooks/use-user.tsx +0 -73
- package/src/link/index.ts +0 -2
- package/src/link/link.stories.tsx +0 -109
- package/src/link/link.test.tsx +0 -169
- package/src/link/link.tsx +0 -218
package/README.md
CHANGED
|
@@ -6,11 +6,11 @@ Components that have carried real Regardio work, gathered here so other projects
|
|
|
6
6
|
|
|
7
7
|
## Pre-release notice
|
|
8
8
|
|
|
9
|
-
This package is
|
|
9
|
+
This package is work-in-progress. The components run in production across Regardio's own projects, but the API may still move between minor versions. A few habits that help:
|
|
10
10
|
|
|
11
11
|
- Pin to exact versions in `package.json`
|
|
12
12
|
- Read the changelog before upgrading
|
|
13
|
-
- Expect the occasional breaking change
|
|
13
|
+
- Expect the occasional breaking change as we move along.
|
|
14
14
|
|
|
15
15
|
## What it's for
|
|
16
16
|
|
|
@@ -31,7 +31,6 @@ pnpm add @regardio/react
|
|
|
31
31
|
|
|
32
32
|
- `react` >= 19.0.0
|
|
33
33
|
- `react-dom` >= 19.0.0
|
|
34
|
-
- `react-router` >= 7.0.0 (for routing components)
|
|
35
34
|
- `tailwindcss` >= 4.0.0 (for styling)
|
|
36
35
|
|
|
37
36
|
## Usage
|
|
@@ -39,8 +38,8 @@ pnpm add @regardio/react
|
|
|
39
38
|
Import components, hooks, and utilities directly from their paths so tree-shaking does its work:
|
|
40
39
|
|
|
41
40
|
```tsx
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
41
|
+
import { Button } from '@regardio/react/button';
|
|
42
|
+
import { Heading } from '@regardio/react/heading';
|
|
44
43
|
import { useNonce } from '@regardio/react/hooks/use-nonce';
|
|
45
44
|
```
|
|
46
45
|
|
|
@@ -60,46 +59,48 @@ import '@regardio/react/tailwind.css';
|
|
|
60
59
|
|-----------|-------------|
|
|
61
60
|
| `BackgroundSlideshow` | Animated background image carousel with crossfade transitions |
|
|
62
61
|
| `BlurryGradient` | SVG-based decorative gradient backgrounds |
|
|
63
|
-
| `
|
|
62
|
+
| `Button` | Button component with variant-based styling |
|
|
64
63
|
| `Carousel` | Embla-powered carousel with navigation controls |
|
|
64
|
+
| `Checkbox` | Checkbox input component |
|
|
65
|
+
| `CheckboxGroup` | Group of checkbox inputs |
|
|
65
66
|
| `Countdown` | Dynamic countdown timer display |
|
|
66
|
-
| `
|
|
67
|
+
| `Field` | Form field wrapper component |
|
|
68
|
+
| `Fieldset` | Fieldset component for form grouping |
|
|
69
|
+
| `Form` | Form component with validation support |
|
|
70
|
+
| `Grid` | Grid layout component |
|
|
67
71
|
| `Heading` | Semantic headings (h1–h6) with consistent styling |
|
|
68
72
|
| `Highlight` | Text highlighting with customisable styles |
|
|
69
73
|
| `IconButton` | Button shaped for icon-only content |
|
|
70
74
|
| `If` | Conditional rendering utility component |
|
|
71
75
|
| `Iframe` | Responsive iframe with sensible defaults |
|
|
72
|
-
| `
|
|
73
|
-
| `LeafletMap` | Leaflet map integration |
|
|
74
|
-
| `Link` | React Router link with external URL detection |
|
|
76
|
+
| `Input` | Text input component |
|
|
75
77
|
| `List` | Compound list component with Root and Item |
|
|
76
|
-
| `MaptilerMap` | MapTiler SDK integration |
|
|
77
78
|
| `MarkdownContainer` | MDX/Markdown renderer with typography processing |
|
|
78
79
|
| `PasswordInput` | Password field with visibility toggle |
|
|
79
80
|
| `Picture` | Responsive images with srcset generation |
|
|
80
81
|
| `ProtectedEmail` | Email obfuscation for spam protection |
|
|
82
|
+
| `Radio` | Radio input component |
|
|
83
|
+
| `RadioGroup` | Group of radio inputs |
|
|
84
|
+
| `Slider` | Slider input component |
|
|
85
|
+
| `Switch` | Toggle switch component |
|
|
81
86
|
| `Text` | Typography component with variants |
|
|
87
|
+
| `Toggle` | Toggle button component |
|
|
82
88
|
|
|
83
89
|
### Hooks
|
|
84
90
|
|
|
85
91
|
| Hook | Description |
|
|
86
92
|
|------|-------------|
|
|
87
|
-
| `useCurrentRouteData` | Access current route loader data |
|
|
88
93
|
| `useFocusSearch` | Focus management for search inputs |
|
|
89
|
-
| `useMatchesData` | Access matched route data |
|
|
90
94
|
| `useMediaQuery` | Reactive media query matching |
|
|
91
95
|
| `useMobile` | Mobile device detection |
|
|
92
96
|
| `useNonce` | CSP nonce access for inline styles |
|
|
93
97
|
| `useOrientation` | Device orientation detection |
|
|
94
|
-
| `useUser` | User context from Supabase auth |
|
|
95
98
|
|
|
96
99
|
### Utilities
|
|
97
100
|
|
|
98
101
|
| Utility | Description |
|
|
99
102
|
|---------|-------------|
|
|
100
103
|
| `author` | Author and contributor data formatting |
|
|
101
|
-
| `isRouteActive` | Route matching helpers |
|
|
102
|
-
| `locale` | Locale detection and formatting |
|
|
103
104
|
| `text` | Typography processing (quotes, special characters) |
|
|
104
105
|
|
|
105
106
|
> **Note:** For Tailwind utilities like `cn`, `tv`, and `twMerge`, reach for `@regardio/tailwind/utils`.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "@regardio/react",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Regardio React UI components",
|
|
7
7
|
"keywords": [
|
|
@@ -67,10 +67,6 @@
|
|
|
67
67
|
"import": "./dist/form/index.mjs",
|
|
68
68
|
"types": "./dist/form/index.d.mts"
|
|
69
69
|
},
|
|
70
|
-
"./generic-error": {
|
|
71
|
-
"import": "./dist/generic-error/index.mjs",
|
|
72
|
-
"types": "./dist/generic-error/index.d.mts"
|
|
73
|
-
},
|
|
74
70
|
"./grid": {
|
|
75
71
|
"import": "./dist/grid/index.mjs",
|
|
76
72
|
"types": "./dist/grid/index.d.mts"
|
|
@@ -83,18 +79,10 @@
|
|
|
83
79
|
"import": "./dist/highlight/index.mjs",
|
|
84
80
|
"types": "./dist/highlight/index.d.mts"
|
|
85
81
|
},
|
|
86
|
-
"./hooks/use-current-route-data": {
|
|
87
|
-
"import": "./dist/hooks/use-current-route-data.mjs",
|
|
88
|
-
"types": "./dist/hooks/use-current-route-data.d.mts"
|
|
89
|
-
},
|
|
90
82
|
"./hooks/use-focus-search": {
|
|
91
83
|
"import": "./dist/hooks/use-focus-search.mjs",
|
|
92
84
|
"types": "./dist/hooks/use-focus-search.d.mts"
|
|
93
85
|
},
|
|
94
|
-
"./hooks/use-matches-data": {
|
|
95
|
-
"import": "./dist/hooks/use-matches-data.mjs",
|
|
96
|
-
"types": "./dist/hooks/use-matches-data.d.mts"
|
|
97
|
-
},
|
|
98
86
|
"./hooks/use-media-query": {
|
|
99
87
|
"import": "./dist/hooks/use-media-query.mjs",
|
|
100
88
|
"types": "./dist/hooks/use-media-query.d.mts"
|
|
@@ -111,10 +99,6 @@
|
|
|
111
99
|
"import": "./dist/hooks/use-orientation.mjs",
|
|
112
100
|
"types": "./dist/hooks/use-orientation.d.mts"
|
|
113
101
|
},
|
|
114
|
-
"./hooks/use-user": {
|
|
115
|
-
"import": "./dist/hooks/use-user.mjs",
|
|
116
|
-
"types": "./dist/hooks/use-user.d.mts"
|
|
117
|
-
},
|
|
118
102
|
"./icon-button": {
|
|
119
103
|
"import": "./dist/icon-button/index.mjs",
|
|
120
104
|
"types": "./dist/icon-button/index.d.mts"
|
|
@@ -131,10 +115,6 @@
|
|
|
131
115
|
"import": "./dist/input/index.mjs",
|
|
132
116
|
"types": "./dist/input/index.d.mts"
|
|
133
117
|
},
|
|
134
|
-
"./link": {
|
|
135
|
-
"import": "./dist/link/index.mjs",
|
|
136
|
-
"types": "./dist/link/index.d.mts"
|
|
137
|
-
},
|
|
138
118
|
"./list": {
|
|
139
119
|
"import": "./dist/list/index.mjs",
|
|
140
120
|
"types": "./dist/list/index.d.mts"
|
|
@@ -198,12 +178,11 @@
|
|
|
198
178
|
"dependencies": {
|
|
199
179
|
"@base-ui/react": "1.4.1",
|
|
200
180
|
"@mdx-js/react": "3.1.1",
|
|
201
|
-
"@supabase/supabase-js": "2.105.3",
|
|
202
181
|
"embla-carousel": "8.6.0",
|
|
203
182
|
"embla-carousel-react": "8.6.0",
|
|
204
|
-
"markdown-to-jsx": "9.
|
|
205
|
-
"@regardio/js": "1.2.
|
|
206
|
-
"@regardio/tailwind": "1.2.
|
|
183
|
+
"markdown-to-jsx": "9.8.0",
|
|
184
|
+
"@regardio/js": "1.2.2",
|
|
185
|
+
"@regardio/tailwind": "1.2.2"
|
|
207
186
|
},
|
|
208
187
|
"devDependencies": {
|
|
209
188
|
"@storybook/addon-a11y": "10.3.6",
|
|
@@ -211,32 +190,32 @@
|
|
|
211
190
|
"@storybook/addon-vitest": "10.3.6",
|
|
212
191
|
"@storybook/react": "10.3.6",
|
|
213
192
|
"@storybook/react-vite": "10.3.6",
|
|
214
|
-
"@tailwindcss/vite": "4.
|
|
193
|
+
"@tailwindcss/vite": "4.3.0",
|
|
215
194
|
"@testing-library/jest-dom": "6.9.1",
|
|
216
195
|
"@testing-library/react": "16.3.2",
|
|
196
|
+
"@testing-library/user-event": "14.6.1",
|
|
217
197
|
"@total-typescript/ts-reset": "0.6.1",
|
|
218
198
|
"@types/leaflet": "1.9.21",
|
|
219
|
-
"@types/node": "25.
|
|
199
|
+
"@types/node": "25.7.0",
|
|
220
200
|
"@types/react": "19.2.14",
|
|
221
201
|
"@types/react-dom": "19.2.3",
|
|
222
202
|
"@vitejs/plugin-react": "6.0.1",
|
|
223
|
-
"@vitest/browser-playwright": "4.1.
|
|
224
|
-
"@vitest/coverage-v8": "4.1.
|
|
225
|
-
"@vitest/ui": "4.1.
|
|
203
|
+
"@vitest/browser-playwright": "4.1.6",
|
|
204
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
205
|
+
"@vitest/ui": "4.1.6",
|
|
226
206
|
"jsdom": "29.1.1",
|
|
227
|
-
"playwright": "1.
|
|
207
|
+
"playwright": "1.60.0",
|
|
228
208
|
"storybook": "10.3.6",
|
|
229
|
-
"tailwindcss": "4.
|
|
209
|
+
"tailwindcss": "4.3.0",
|
|
230
210
|
"tsdown": "0.22.0",
|
|
231
211
|
"typescript": "6.0.3",
|
|
232
|
-
"vite": "8.0.
|
|
233
|
-
"vitest": "4.1.
|
|
234
|
-
"@regardio/dev": "2.
|
|
212
|
+
"vite": "8.0.12",
|
|
213
|
+
"vitest": "4.1.6",
|
|
214
|
+
"@regardio/dev": "2.7.0"
|
|
235
215
|
},
|
|
236
216
|
"peerDependencies": {
|
|
237
217
|
"react": ">=19",
|
|
238
|
-
"react-dom": ">=19"
|
|
239
|
-
"react-router": ">=7"
|
|
218
|
+
"react-dom": ">=19"
|
|
240
219
|
},
|
|
241
220
|
"scripts": {
|
|
242
221
|
"build": "tsdown",
|
|
@@ -250,7 +229,7 @@
|
|
|
250
229
|
"lint:biome": "biome check .",
|
|
251
230
|
"lint:md": "markdownlint-cli2 --config ../../.markdownlint-cli2.jsonc",
|
|
252
231
|
"lint:pkg": "sort-package-json --check",
|
|
253
|
-
"report": "vitest run --coverage",
|
|
232
|
+
"report": "vitest run --coverage --project unit",
|
|
254
233
|
"storybook": "storybook dev -p 6006",
|
|
255
234
|
"storybook:build": "storybook build -o storybook-static",
|
|
256
235
|
"test": "run-p test:*",
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { BackgroundSlideshow, type ImageData } from './background-slideshow';
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
cleanup();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const makeImage = (id: string): ImageData => ({
|
|
10
|
+
at: { en: `Alt for ${id}` },
|
|
11
|
+
fn: `${id}.jpg`,
|
|
12
|
+
hu: 0,
|
|
13
|
+
id,
|
|
14
|
+
po: false,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('BackgroundSlideshow', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.useFakeTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders an empty div when no images are provided', () => {
|
|
27
|
+
const { container } = render(
|
|
28
|
+
<BackgroundSlideshow
|
|
29
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
30
|
+
images={[]}
|
|
31
|
+
locale="en"
|
|
32
|
+
/>,
|
|
33
|
+
);
|
|
34
|
+
expect(container.firstChild).toBeEmptyDOMElement();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders the first image immediately', () => {
|
|
38
|
+
const images = [makeImage('img-1'), makeImage('img-2')];
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<BackgroundSlideshow
|
|
41
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
42
|
+
images={images}
|
|
43
|
+
locale="en"
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
const imgs = container.querySelectorAll('img');
|
|
47
|
+
expect(imgs[0]).toHaveAttribute('alt', 'Alt for img-1');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not start a slideshow with a single image', () => {
|
|
51
|
+
const images = [makeImage('img-1')];
|
|
52
|
+
const { container } = render(
|
|
53
|
+
<BackgroundSlideshow
|
|
54
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
55
|
+
images={images}
|
|
56
|
+
locale="en"
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
act(() => {
|
|
60
|
+
vi.advanceTimersByTime(20_000);
|
|
61
|
+
});
|
|
62
|
+
expect(container.querySelectorAll('img')).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('starts the slideshow after the interval when slideshow=true', () => {
|
|
66
|
+
const images = [makeImage('img-1'), makeImage('img-2')];
|
|
67
|
+
const { container } = render(
|
|
68
|
+
<BackgroundSlideshow
|
|
69
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
70
|
+
images={images}
|
|
71
|
+
locale="en"
|
|
72
|
+
slideshow
|
|
73
|
+
slideshowInterval={1000}
|
|
74
|
+
transitionDuration={500}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
act(() => {
|
|
78
|
+
vi.advanceTimersByTime(2000);
|
|
79
|
+
});
|
|
80
|
+
expect(container.querySelectorAll('img').length).toBeGreaterThanOrEqual(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not start slideshow when slideshow=false', () => {
|
|
84
|
+
const images = [makeImage('img-1'), makeImage('img-2')];
|
|
85
|
+
const { container } = render(
|
|
86
|
+
<BackgroundSlideshow
|
|
87
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
88
|
+
images={images}
|
|
89
|
+
locale="en"
|
|
90
|
+
slideshow={false}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
act(() => {
|
|
94
|
+
vi.advanceTimersByTime(20_000);
|
|
95
|
+
});
|
|
96
|
+
expect(container.querySelectorAll('img')).toHaveLength(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('applies a filter function', () => {
|
|
100
|
+
const images = [makeImage('img-1'), makeImage('img-2'), makeImage('img-3')];
|
|
101
|
+
const { container } = render(
|
|
102
|
+
<BackgroundSlideshow
|
|
103
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
104
|
+
filter={(img) => img.id === 'img-2'}
|
|
105
|
+
images={images}
|
|
106
|
+
locale="en"
|
|
107
|
+
slideshow={false}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
expect(container.querySelector('img')).toHaveAttribute('alt', 'Alt for img-2');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('falls back to all images when filter excludes everything', () => {
|
|
114
|
+
const images = [makeImage('img-1'), makeImage('img-2')];
|
|
115
|
+
const { container } = render(
|
|
116
|
+
<BackgroundSlideshow
|
|
117
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
118
|
+
filter={() => false}
|
|
119
|
+
images={images}
|
|
120
|
+
locale="en"
|
|
121
|
+
slideshow={false}
|
|
122
|
+
/>,
|
|
123
|
+
);
|
|
124
|
+
expect(container.querySelectorAll('img')[0]).toHaveAttribute('alt', 'Alt for img-1');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('clears timers on unmount', () => {
|
|
128
|
+
const clearTimeoutSpy = vi.spyOn(window, 'clearTimeout');
|
|
129
|
+
const images = [makeImage('img-1'), makeImage('img-2')];
|
|
130
|
+
const { unmount } = render(
|
|
131
|
+
<BackgroundSlideshow
|
|
132
|
+
baseUrl="https://cdn.example.com/{id}/{fn}"
|
|
133
|
+
images={images}
|
|
134
|
+
locale="en"
|
|
135
|
+
slideshow
|
|
136
|
+
slideshowInterval={1000}
|
|
137
|
+
transitionDuration={500}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
act(() => {
|
|
141
|
+
vi.advanceTimersByTime(1500);
|
|
142
|
+
});
|
|
143
|
+
unmount();
|
|
144
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { CarouselRoot, useCarousel } from './carousel-root';
|
|
4
|
+
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
7
|
+
value: vi.fn().mockImplementation((query: string) => ({
|
|
8
|
+
addEventListener: vi.fn(),
|
|
9
|
+
addListener: vi.fn(),
|
|
10
|
+
dispatchEvent: vi.fn(),
|
|
11
|
+
matches: false,
|
|
12
|
+
media: query,
|
|
13
|
+
onchange: null,
|
|
14
|
+
removeEventListener: vi.fn(),
|
|
15
|
+
removeListener: vi.fn(),
|
|
16
|
+
})),
|
|
17
|
+
writable: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
globalThis.IntersectionObserver = vi.fn().mockImplementation(function MockIntersectionObserver() {
|
|
21
|
+
return { disconnect: vi.fn(), observe: vi.fn(), unobserve: vi.fn() };
|
|
22
|
+
}) as unknown as typeof IntersectionObserver;
|
|
23
|
+
|
|
24
|
+
globalThis.ResizeObserver = vi.fn().mockImplementation(function MockResizeObserver() {
|
|
25
|
+
return { disconnect: vi.fn(), observe: vi.fn(), unobserve: vi.fn() };
|
|
26
|
+
}) as unknown as typeof ResizeObserver;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
cleanup();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('CarouselRoot', () => {
|
|
34
|
+
it('renders children', () => {
|
|
35
|
+
const { getByTestId } = render(
|
|
36
|
+
<CarouselRoot>
|
|
37
|
+
<div data-testid="child">slide</div>
|
|
38
|
+
</CarouselRoot>,
|
|
39
|
+
);
|
|
40
|
+
expect(getByTestId('child')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders as a section with carousel role description', () => {
|
|
44
|
+
const { getByRole } = render(<CarouselRoot>content</CarouselRoot>);
|
|
45
|
+
const section = getByRole('region', { name: 'Carousel' });
|
|
46
|
+
expect(section).toBeInTheDocument();
|
|
47
|
+
expect(section).toHaveAttribute('aria-roledescription', 'carousel');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('applies className', () => {
|
|
51
|
+
const { getByRole } = render(<CarouselRoot className="custom-carousel">content</CarouselRoot>);
|
|
52
|
+
expect(getByRole('region')).toHaveClass('custom-carousel');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles ArrowLeft keydown', () => {
|
|
56
|
+
const { getByRole } = render(<CarouselRoot>content</CarouselRoot>);
|
|
57
|
+
fireEvent.keyDown(getByRole('region'), { key: 'ArrowLeft' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles ArrowRight keydown', () => {
|
|
61
|
+
const { getByRole } = render(<CarouselRoot>content</CarouselRoot>);
|
|
62
|
+
fireEvent.keyDown(getByRole('region'), { key: 'ArrowRight' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('calls setApi when provided', () => {
|
|
66
|
+
const setApi = (api: unknown) => api;
|
|
67
|
+
render(<CarouselRoot setApi={setApi}>content</CarouselRoot>);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders with vertical orientation', () => {
|
|
71
|
+
const { getByRole } = render(<CarouselRoot orientation="vertical">content</CarouselRoot>);
|
|
72
|
+
expect(getByRole('region')).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('useCarousel', () => {
|
|
77
|
+
it('throws when used outside CarouselRoot', () => {
|
|
78
|
+
const ThrowingComponent = () => {
|
|
79
|
+
useCarousel();
|
|
80
|
+
return null;
|
|
81
|
+
};
|
|
82
|
+
expect(() => render(<ThrowingComponent />)).toThrow(
|
|
83
|
+
'useCarousel must be used within a <Carousel.Root />',
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { Countdown } from './countdown';
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
cleanup();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe('Countdown', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.useRealTimers();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renders initial mounted state showing padded zero', () => {
|
|
19
|
+
render(<Countdown />);
|
|
20
|
+
const span = document.querySelector('span');
|
|
21
|
+
expect(span?.textContent).toBe('00');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('shows padded value after one tick', () => {
|
|
25
|
+
render(<Countdown />);
|
|
26
|
+
act(() => {
|
|
27
|
+
vi.advanceTimersByTime(1000);
|
|
28
|
+
});
|
|
29
|
+
expect(screen.getByText('01')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('shows unpadded value once counter reaches 10', () => {
|
|
33
|
+
render(<Countdown />);
|
|
34
|
+
act(() => {
|
|
35
|
+
vi.advanceTimersByTime(10_000);
|
|
36
|
+
});
|
|
37
|
+
expect(screen.getByText('10')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('clears the interval on unmount', () => {
|
|
41
|
+
const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval');
|
|
42
|
+
const { unmount } = render(<Countdown />);
|
|
43
|
+
unmount();
|
|
44
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { cleanup, render } from '@testing-library/react';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { GridItem } from './grid-item';
|
|
4
|
+
import { GridRoot, useGrid } from './grid-root';
|
|
5
|
+
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('GridRoot', () => {
|
|
11
|
+
it('renders children', () => {
|
|
12
|
+
const { getByTestId } = render(
|
|
13
|
+
<GridRoot>
|
|
14
|
+
<div data-testid="child">content</div>
|
|
15
|
+
</GridRoot>,
|
|
16
|
+
);
|
|
17
|
+
expect(getByTestId('child')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('applies className', () => {
|
|
21
|
+
const { getByTestId } = render(
|
|
22
|
+
<GridRoot
|
|
23
|
+
className="custom-grid"
|
|
24
|
+
data-testid="grid"
|
|
25
|
+
>
|
|
26
|
+
content
|
|
27
|
+
</GridRoot>,
|
|
28
|
+
);
|
|
29
|
+
expect(getByTestId('grid')).toHaveClass('custom-grid');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('applies flow variant', () => {
|
|
33
|
+
const { getByTestId } = render(
|
|
34
|
+
<GridRoot
|
|
35
|
+
data-testid="grid"
|
|
36
|
+
flow="row"
|
|
37
|
+
>
|
|
38
|
+
content
|
|
39
|
+
</GridRoot>,
|
|
40
|
+
);
|
|
41
|
+
expect(getByTestId('grid')).toHaveClass('grid-auto-flow-row');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('applies align variant', () => {
|
|
45
|
+
const { getByTestId } = render(
|
|
46
|
+
<GridRoot
|
|
47
|
+
align="center"
|
|
48
|
+
data-testid="grid"
|
|
49
|
+
>
|
|
50
|
+
content
|
|
51
|
+
</GridRoot>,
|
|
52
|
+
);
|
|
53
|
+
expect(getByTestId('grid')).toHaveClass('content-center');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('useGrid', () => {
|
|
58
|
+
it('throws when used outside GridRoot', () => {
|
|
59
|
+
const ThrowingComponent = () => {
|
|
60
|
+
useGrid();
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
expect(() => render(<ThrowingComponent />)).toThrow(
|
|
64
|
+
'useGrid must be used within a <Grid.Root />',
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('GridItem', () => {
|
|
70
|
+
it('renders children inside GridRoot', () => {
|
|
71
|
+
const { getByTestId } = render(
|
|
72
|
+
<GridRoot>
|
|
73
|
+
<GridItem>
|
|
74
|
+
<span data-testid="item-content">item</span>
|
|
75
|
+
</GridItem>
|
|
76
|
+
</GridRoot>,
|
|
77
|
+
);
|
|
78
|
+
expect(getByTestId('item-content')).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('applies span class', () => {
|
|
82
|
+
const { getByTestId } = render(
|
|
83
|
+
<GridRoot>
|
|
84
|
+
<GridItem
|
|
85
|
+
data-testid="item"
|
|
86
|
+
span={6}
|
|
87
|
+
>
|
|
88
|
+
item
|
|
89
|
+
</GridItem>
|
|
90
|
+
</GridRoot>,
|
|
91
|
+
);
|
|
92
|
+
expect(getByTestId('item')).toHaveClass('col-span-6');
|
|
93
|
+
});
|
|
94
|
+
});
|