@dashforge/tw 0.3.0-beta → 0.5.0-beta
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/CHANGELOG.md +246 -0
- package/PARITY.md +483 -0
- package/PERFORMANCE.md +169 -0
- package/dist/index.esm.js +1055 -2
- package/dist/src/components/Accordion/Accordion.d.ts +21 -0
- package/dist/src/components/Accordion/Accordion.d.ts.map +1 -0
- package/dist/src/components/Accordion/accordion.types.d.ts +74 -0
- package/dist/src/components/Accordion/accordion.types.d.ts.map +1 -0
- package/dist/src/components/Accordion/accordion.variants.d.ts +58 -0
- package/dist/src/components/Accordion/accordion.variants.d.ts.map +1 -0
- package/dist/src/components/Dialog/Dialog.d.ts +17 -0
- package/dist/src/components/Dialog/Dialog.d.ts.map +1 -0
- package/dist/src/components/Dialog/dialog.types.d.ts +75 -0
- package/dist/src/components/Dialog/dialog.types.d.ts.map +1 -0
- package/dist/src/components/Dialog/dialog.variants.d.ts +80 -0
- package/dist/src/components/Dialog/dialog.variants.d.ts.map +1 -0
- package/dist/src/components/Pagination/Pagination.d.ts +32 -0
- package/dist/src/components/Pagination/Pagination.d.ts.map +1 -0
- package/dist/src/components/Pagination/pagination.helpers.d.ts +23 -0
- package/dist/src/components/Pagination/pagination.helpers.d.ts.map +1 -0
- package/dist/src/components/Pagination/pagination.types.d.ts +120 -0
- package/dist/src/components/Pagination/pagination.types.d.ts.map +1 -0
- package/dist/src/components/Pagination/pagination.variants.d.ts +150 -0
- package/dist/src/components/Pagination/pagination.variants.d.ts.map +1 -0
- package/dist/src/components/Popover/Popover.d.ts +15 -0
- package/dist/src/components/Popover/Popover.d.ts.map +1 -0
- package/dist/src/components/Popover/popover.types.d.ts +45 -0
- package/dist/src/components/Popover/popover.types.d.ts.map +1 -0
- package/dist/src/components/Popover/popover.variants.d.ts +34 -0
- package/dist/src/components/Popover/popover.variants.d.ts.map +1 -0
- package/dist/src/components/Skeleton/Skeleton.d.ts +23 -0
- package/dist/src/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/dist/src/components/Skeleton/skeleton.types.d.ts +56 -0
- package/dist/src/components/Skeleton/skeleton.types.d.ts.map +1 -0
- package/dist/src/components/Skeleton/skeleton.variants.d.ts +110 -0
- package/dist/src/components/Skeleton/skeleton.variants.d.ts.map +1 -0
- package/dist/src/components/Tabs/Tabs.d.ts +15 -0
- package/dist/src/components/Tabs/Tabs.d.ts.map +1 -0
- package/dist/src/components/Tabs/tabs.types.d.ts +55 -0
- package/dist/src/components/Tabs/tabs.types.d.ts.map +1 -0
- package/dist/src/components/Tabs/tabs.variants.d.ts +82 -0
- package/dist/src/components/Tabs/tabs.variants.d.ts.map +1 -0
- package/dist/src/components/Tooltip/Tooltip.d.ts +29 -0
- package/dist/src/components/Tooltip/Tooltip.d.ts.map +1 -0
- package/dist/src/components/Tooltip/tooltip.types.d.ts +47 -0
- package/dist/src/components/Tooltip/tooltip.types.d.ts.map +1 -0
- package/dist/src/components/Tooltip/tooltip.variants.d.ts +34 -0
- package/dist/src/components/Tooltip/tooltip.variants.d.ts.map +1 -0
- package/dist/src/index.d.ts +22 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +8 -3
- package/src/components/Accordion/Accordion.test.tsx +95 -0
- package/src/components/Accordion/Accordion.tsx +97 -0
- package/src/components/Accordion/accordion.types.ts +66 -0
- package/src/components/Accordion/accordion.variants.ts +30 -0
- package/src/components/Dialog/Dialog.test.tsx +96 -0
- package/src/components/Dialog/Dialog.tsx +93 -0
- package/src/components/Dialog/dialog.types.ts +62 -0
- package/src/components/Dialog/dialog.variants.ts +62 -0
- package/src/components/Pagination/Pagination.test.tsx +314 -0
- package/src/components/Pagination/Pagination.tsx +275 -0
- package/src/components/Pagination/pagination.helpers.ts +102 -0
- package/src/components/Pagination/pagination.types.ts +117 -0
- package/src/components/Pagination/pagination.variants.ts +111 -0
- package/src/components/Popover/Popover.test.tsx +72 -0
- package/src/components/Popover/Popover.tsx +55 -0
- package/src/components/Popover/popover.types.ts +42 -0
- package/src/components/Popover/popover.variants.ts +18 -0
- package/src/components/Skeleton/Skeleton.test.tsx +127 -0
- package/src/components/Skeleton/Skeleton.tsx +49 -0
- package/src/components/Skeleton/skeleton.types.ts +59 -0
- package/src/components/Skeleton/skeleton.variants.ts +86 -0
- package/src/components/Tabs/Tabs.test.tsx +74 -0
- package/src/components/Tabs/Tabs.tsx +66 -0
- package/src/components/Tabs/tabs.types.ts +49 -0
- package/src/components/Tabs/tabs.variants.ts +58 -0
- package/src/components/Tooltip/Tooltip.test.tsx +73 -0
- package/src/components/Tooltip/Tooltip.tsx +70 -0
- package/src/components/Tooltip/tooltip.types.ts +44 -0
- package/src/components/Tooltip/tooltip.variants.ts +18 -0
- package/src/index.ts +57 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { DialogVariants } from './dialog.variants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-slot className overrides for `<Dialog>`. Mirrors the
|
|
6
|
+
* canonical TW slotProps shape: `{ slot?: { className?: string } }`.
|
|
7
|
+
*/
|
|
8
|
+
export interface DialogSlotProps {
|
|
9
|
+
overlay?: { className?: string };
|
|
10
|
+
content?: { className?: string };
|
|
11
|
+
title?: { className?: string };
|
|
12
|
+
description?: { className?: string };
|
|
13
|
+
closeButton?: { className?: string };
|
|
14
|
+
body?: { className?: string };
|
|
15
|
+
actions?: { className?: string };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Props for the declarative `<Dialog>` component.
|
|
20
|
+
*
|
|
21
|
+
* Built on `@radix-ui/react-dialog`. Use this when:
|
|
22
|
+
* - You want full control over the dialog's open state (controlled
|
|
23
|
+
* from a parent component).
|
|
24
|
+
* - You want to mount a custom-styled dialog body / trigger that
|
|
25
|
+
* composes with the rest of your component tree.
|
|
26
|
+
* - You don't need an imperative `confirm()`-style return value
|
|
27
|
+
* (use `useDialog()` or `useConfirm()` for those).
|
|
28
|
+
*
|
|
29
|
+
* Three size variants (`sm` / `md` / `lg`) with sensible defaults.
|
|
30
|
+
* Backdrop + content animations respect `prefers-reduced-motion`.
|
|
31
|
+
*/
|
|
32
|
+
export interface DialogProps extends Pick<DialogVariants, 'size'> {
|
|
33
|
+
/** Controlled open state. */
|
|
34
|
+
open: boolean;
|
|
35
|
+
/** Called when the dialog requests to change open state. */
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
/** Dialog body — usually a `<DialogContent>` subtree. */
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
/** Dialog title (screen-reader announced via `aria-labelledby`). */
|
|
40
|
+
title?: ReactNode;
|
|
41
|
+
/** Dialog description (announced via `aria-describedby`). */
|
|
42
|
+
description?: ReactNode;
|
|
43
|
+
/**
|
|
44
|
+
* Show the `×` close button in the top-right corner.
|
|
45
|
+
* @default true
|
|
46
|
+
*/
|
|
47
|
+
showCloseButton?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Disable closing on backdrop click.
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
disableBackdropClose?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Disable closing on Escape key.
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
57
|
+
disableEscapeClose?: boolean;
|
|
58
|
+
/** Root className shortcut on the content element. */
|
|
59
|
+
sx?: string;
|
|
60
|
+
/** Per-slot overrides. */
|
|
61
|
+
slotProps?: DialogSlotProps;
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<Dialog>` variant recipe.
|
|
5
|
+
*
|
|
6
|
+
* Slots:
|
|
7
|
+
* - `overlay` — backdrop (Radix's `DialogOverlay`)
|
|
8
|
+
* - `content` — the dialog box itself
|
|
9
|
+
* - `title` — heading at the top
|
|
10
|
+
* - `description` — secondary text under the title
|
|
11
|
+
* - `closeButton` — top-right `×` button
|
|
12
|
+
* - `body` — main scrollable area
|
|
13
|
+
* - `actions` — footer action bar
|
|
14
|
+
*
|
|
15
|
+
* Three sizes (`sm` / `md` / `lg`) drive the content max-width.
|
|
16
|
+
* Motion is gated by `prefers-reduced-motion` so the slide-in
|
|
17
|
+
* animation collapses to an instant appear on users that opt out.
|
|
18
|
+
*/
|
|
19
|
+
export const dialogVariants = tv({
|
|
20
|
+
slots: {
|
|
21
|
+
overlay: [
|
|
22
|
+
'fixed inset-0 z-50 bg-neutral-950/60 backdrop-blur-sm',
|
|
23
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
|
24
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0',
|
|
25
|
+
'motion-reduce:transition-none motion-reduce:duration-0',
|
|
26
|
+
],
|
|
27
|
+
content: [
|
|
28
|
+
'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2',
|
|
29
|
+
'w-full bg-white dark:bg-neutral-900 rounded-lg shadow-xl',
|
|
30
|
+
'border border-neutral-200 dark:border-neutral-800',
|
|
31
|
+
'p-6 flex flex-col gap-4 max-h-[90vh]',
|
|
32
|
+
'focus:outline-none',
|
|
33
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
34
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
35
|
+
'motion-reduce:transition-none motion-reduce:duration-0',
|
|
36
|
+
],
|
|
37
|
+
title: 'text-lg font-semibold leading-tight text-neutral-900 dark:text-neutral-50',
|
|
38
|
+
description: 'text-sm text-neutral-600 dark:text-neutral-400',
|
|
39
|
+
closeButton: [
|
|
40
|
+
'absolute top-4 right-4 inline-flex items-center justify-center',
|
|
41
|
+
'h-8 w-8 rounded-md text-neutral-500 hover:text-neutral-900',
|
|
42
|
+
'dark:text-neutral-400 dark:hover:text-neutral-50',
|
|
43
|
+
'hover:bg-neutral-100 dark:hover:bg-neutral-800',
|
|
44
|
+
'focus:outline-none focus:ring-2 focus:ring-primary-500',
|
|
45
|
+
'transition-colors',
|
|
46
|
+
],
|
|
47
|
+
body: 'flex-1 overflow-y-auto',
|
|
48
|
+
actions: 'flex justify-end gap-2 pt-2',
|
|
49
|
+
},
|
|
50
|
+
variants: {
|
|
51
|
+
size: {
|
|
52
|
+
sm: { content: 'max-w-sm' },
|
|
53
|
+
md: { content: 'max-w-md' },
|
|
54
|
+
lg: { content: 'max-w-2xl' },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: {
|
|
58
|
+
size: 'md',
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type DialogVariants = VariantProps<typeof dialogVariants>;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
4
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
|
5
|
+
import { Pagination } from './Pagination.js';
|
|
6
|
+
import { computePageRange } from './pagination.helpers.js';
|
|
7
|
+
|
|
8
|
+
void React;
|
|
9
|
+
afterEach(() => cleanup());
|
|
10
|
+
|
|
11
|
+
describe('computePageRange — helper', () => {
|
|
12
|
+
it('returns empty for totalPages=0', () => {
|
|
13
|
+
expect(computePageRange(1, 0, 1, 1)).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('returns [1] for totalPages=1', () => {
|
|
17
|
+
expect(computePageRange(1, 1, 1, 1)).toEqual([1]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('lists all pages when totalPages <= window size', () => {
|
|
21
|
+
expect(computePageRange(1, 5, 1, 1)).toEqual([1, 2, 3, 4, 5]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('inserts ellipsis on the right when current is near start', () => {
|
|
25
|
+
const r = computePageRange(2, 20, 1, 1);
|
|
26
|
+
expect(r).toContain(1);
|
|
27
|
+
expect(r).toContain(2);
|
|
28
|
+
expect(r).toContain(3);
|
|
29
|
+
expect(r).toContain('ellipsis');
|
|
30
|
+
expect(r).toContain(20);
|
|
31
|
+
expect(r).not.toContain(10);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('inserts ellipsis on both sides when current is in the middle', () => {
|
|
35
|
+
const r = computePageRange(10, 20, 1, 1);
|
|
36
|
+
expect(r.filter((x) => x === 'ellipsis').length).toBeGreaterThanOrEqual(1);
|
|
37
|
+
expect(r).toContain(1);
|
|
38
|
+
expect(r).toContain(9);
|
|
39
|
+
expect(r).toContain(10);
|
|
40
|
+
expect(r).toContain(11);
|
|
41
|
+
expect(r).toContain(20);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('inserts ellipsis on the left when current is near end', () => {
|
|
45
|
+
const r = computePageRange(19, 20, 1, 1);
|
|
46
|
+
expect(r).toContain(1);
|
|
47
|
+
expect(r).toContain('ellipsis');
|
|
48
|
+
expect(r).toContain(18);
|
|
49
|
+
expect(r).toContain(19);
|
|
50
|
+
expect(r).toContain(20);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('respects siblingCount=2', () => {
|
|
54
|
+
const r = computePageRange(10, 20, 2, 1);
|
|
55
|
+
expect(r).toContain(8);
|
|
56
|
+
expect(r).toContain(9);
|
|
57
|
+
expect(r).toContain(10);
|
|
58
|
+
expect(r).toContain(11);
|
|
59
|
+
expect(r).toContain(12);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('respects boundaryCount=2', () => {
|
|
63
|
+
const r = computePageRange(10, 20, 1, 2);
|
|
64
|
+
expect(r).toContain(1);
|
|
65
|
+
expect(r).toContain(2);
|
|
66
|
+
expect(r).toContain(19);
|
|
67
|
+
expect(r).toContain(20);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('Pagination — rendering (default variant)', () => {
|
|
72
|
+
it('renders nav landmark with aria-label', () => {
|
|
73
|
+
render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
|
|
74
|
+
expect(screen.getByRole('navigation', { name: 'Pagination' })).not.toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('renders the "Showing X-Y of Z" summary', () => {
|
|
78
|
+
render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
|
|
79
|
+
expect(screen.getByText(/Showing 1-10 of 50/)).not.toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders correct summary on last page (partial fill)', () => {
|
|
83
|
+
render(<Pagination page={5} pageSize={10} totalCount={47} onPageChange={() => {}} />);
|
|
84
|
+
expect(screen.getByText(/Showing 41-47 of 47/)).not.toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('marks active page with aria-current=page', () => {
|
|
88
|
+
render(<Pagination page={3} pageSize={10} totalCount={50} onPageChange={() => {}} />);
|
|
89
|
+
const active = screen.getByRole('button', { name: 'Page 3' });
|
|
90
|
+
expect(active.getAttribute('aria-current')).toBe('page');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('disables Prev + First on page 1', () => {
|
|
94
|
+
render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
|
|
95
|
+
expect((screen.getByRole('button', { name: 'Previous' }) as HTMLButtonElement).disabled).toBe(true);
|
|
96
|
+
expect((screen.getByRole('button', { name: 'First' }) as HTMLButtonElement).disabled).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('disables Next + Last on last page', () => {
|
|
100
|
+
render(<Pagination page={5} pageSize={10} totalCount={50} onPageChange={() => {}} />);
|
|
101
|
+
expect((screen.getByRole('button', { name: 'Next' }) as HTMLButtonElement).disabled).toBe(true);
|
|
102
|
+
expect((screen.getByRole('button', { name: 'Last' }) as HTMLButtonElement).disabled).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('Pagination — interactions', () => {
|
|
107
|
+
it('emits onPageChange when a page button is clicked', () => {
|
|
108
|
+
let last = 0;
|
|
109
|
+
render(
|
|
110
|
+
<Pagination
|
|
111
|
+
page={1}
|
|
112
|
+
pageSize={10}
|
|
113
|
+
totalCount={50}
|
|
114
|
+
onPageChange={(p) => (last = p)}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
fireEvent.click(screen.getByRole('button', { name: 'Page 3' }));
|
|
118
|
+
expect(last).toBe(3);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('Prev decrements page', () => {
|
|
122
|
+
let last = 0;
|
|
123
|
+
render(
|
|
124
|
+
<Pagination
|
|
125
|
+
page={3}
|
|
126
|
+
pageSize={10}
|
|
127
|
+
totalCount={50}
|
|
128
|
+
onPageChange={(p) => (last = p)}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
|
|
132
|
+
expect(last).toBe(2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('Next increments page', () => {
|
|
136
|
+
let last = 0;
|
|
137
|
+
render(
|
|
138
|
+
<Pagination
|
|
139
|
+
page={3}
|
|
140
|
+
pageSize={10}
|
|
141
|
+
totalCount={50}
|
|
142
|
+
onPageChange={(p) => (last = p)}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
146
|
+
expect(last).toBe(4);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('First/Last jump to extremes', () => {
|
|
150
|
+
let last = 0;
|
|
151
|
+
const { rerender } = render(
|
|
152
|
+
<Pagination
|
|
153
|
+
page={3}
|
|
154
|
+
pageSize={10}
|
|
155
|
+
totalCount={50}
|
|
156
|
+
onPageChange={(p) => (last = p)}
|
|
157
|
+
/>,
|
|
158
|
+
);
|
|
159
|
+
fireEvent.click(screen.getByRole('button', { name: 'First' }));
|
|
160
|
+
expect(last).toBe(1);
|
|
161
|
+
rerender(
|
|
162
|
+
<Pagination
|
|
163
|
+
page={3}
|
|
164
|
+
pageSize={10}
|
|
165
|
+
totalCount={50}
|
|
166
|
+
onPageChange={(p) => (last = p)}
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
fireEvent.click(screen.getByRole('button', { name: 'Last' }));
|
|
170
|
+
expect(last).toBe(5);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('emits onPageSizeChange when selector changes', () => {
|
|
174
|
+
let lastSize = 0;
|
|
175
|
+
render(
|
|
176
|
+
<Pagination
|
|
177
|
+
page={1}
|
|
178
|
+
pageSize={10}
|
|
179
|
+
totalCount={100}
|
|
180
|
+
onPageChange={() => {}}
|
|
181
|
+
onPageSizeChange={(s) => (lastSize = s)}
|
|
182
|
+
/>,
|
|
183
|
+
);
|
|
184
|
+
const selector = screen.getByLabelText('per page') as HTMLSelectElement;
|
|
185
|
+
fireEvent.change(selector, { target: { value: '50' } });
|
|
186
|
+
expect(lastSize).toBe(50);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('hides page-size selector if onPageSizeChange not provided', () => {
|
|
190
|
+
render(<Pagination page={1} pageSize={10} totalCount={50} onPageChange={() => {}} />);
|
|
191
|
+
expect(screen.queryByLabelText('per page')).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('Pagination — variants', () => {
|
|
196
|
+
it('compact variant hides summary + selector', () => {
|
|
197
|
+
render(
|
|
198
|
+
<Pagination
|
|
199
|
+
variant="compact"
|
|
200
|
+
page={1}
|
|
201
|
+
pageSize={10}
|
|
202
|
+
totalCount={50}
|
|
203
|
+
onPageChange={() => {}}
|
|
204
|
+
onPageSizeChange={() => {}}
|
|
205
|
+
/>,
|
|
206
|
+
);
|
|
207
|
+
expect(screen.queryByText(/Showing/)).toBeNull();
|
|
208
|
+
expect(screen.queryByLabelText('per page')).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('minimal variant shows "Page X of Y" + only Prev/Next', () => {
|
|
212
|
+
render(
|
|
213
|
+
<Pagination
|
|
214
|
+
variant="minimal"
|
|
215
|
+
page={3}
|
|
216
|
+
pageSize={10}
|
|
217
|
+
totalCount={50}
|
|
218
|
+
onPageChange={() => {}}
|
|
219
|
+
/>,
|
|
220
|
+
);
|
|
221
|
+
expect(screen.getByText(/Page 3 of 5/)).not.toBeNull();
|
|
222
|
+
// Page-number buttons should NOT exist
|
|
223
|
+
expect(screen.queryByRole('button', { name: 'Page 1' })).toBeNull();
|
|
224
|
+
// Prev / Next remain
|
|
225
|
+
expect(screen.queryByRole('button', { name: 'Previous' })).not.toBeNull();
|
|
226
|
+
expect(screen.queryByRole('button', { name: 'Next' })).not.toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Pagination — i18n', () => {
|
|
231
|
+
it('honors custom labels', () => {
|
|
232
|
+
render(
|
|
233
|
+
<Pagination
|
|
234
|
+
page={1}
|
|
235
|
+
pageSize={10}
|
|
236
|
+
totalCount={50}
|
|
237
|
+
onPageChange={() => {}}
|
|
238
|
+
labels={{ showing: 'Mostro', of: 'di', prev: 'Indietro', next: 'Avanti' }}
|
|
239
|
+
/>,
|
|
240
|
+
);
|
|
241
|
+
expect(screen.getByText(/Mostro 1-10 di 50/)).not.toBeNull();
|
|
242
|
+
expect(screen.queryByRole('button', { name: 'Indietro' })).not.toBeNull();
|
|
243
|
+
expect(screen.queryByRole('button', { name: 'Avanti' })).not.toBeNull();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('Pagination — disabled', () => {
|
|
248
|
+
it('disables all buttons when disabled=true', () => {
|
|
249
|
+
render(
|
|
250
|
+
<Pagination
|
|
251
|
+
page={3}
|
|
252
|
+
pageSize={10}
|
|
253
|
+
totalCount={50}
|
|
254
|
+
onPageChange={() => {}}
|
|
255
|
+
disabled
|
|
256
|
+
/>,
|
|
257
|
+
);
|
|
258
|
+
expect((screen.getByRole('button', { name: 'Previous' }) as HTMLButtonElement).disabled).toBe(true);
|
|
259
|
+
expect((screen.getByRole('button', { name: 'Next' }) as HTMLButtonElement).disabled).toBe(true);
|
|
260
|
+
expect((screen.getByRole('button', { name: 'Page 2' }) as HTMLButtonElement).disabled).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('Pagination — sx + slotProps', () => {
|
|
265
|
+
it('applies sx to root', () => {
|
|
266
|
+
const { container } = render(
|
|
267
|
+
<Pagination
|
|
268
|
+
page={1}
|
|
269
|
+
pageSize={10}
|
|
270
|
+
totalCount={50}
|
|
271
|
+
onPageChange={() => {}}
|
|
272
|
+
sx="my-custom-root"
|
|
273
|
+
/>,
|
|
274
|
+
);
|
|
275
|
+
expect(container.querySelector('nav')!.className).toContain('my-custom-root');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('applies slotProps.pageButton.className', () => {
|
|
279
|
+
render(
|
|
280
|
+
<Pagination
|
|
281
|
+
page={1}
|
|
282
|
+
pageSize={10}
|
|
283
|
+
totalCount={50}
|
|
284
|
+
onPageChange={() => {}}
|
|
285
|
+
slotProps={{ pageButton: { className: 'slot-page' } }}
|
|
286
|
+
/>,
|
|
287
|
+
);
|
|
288
|
+
expect(screen.getByRole('button', { name: 'Page 2' }).className).toContain('slot-page');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('Pagination — edge cases', () => {
|
|
293
|
+
it('renders correctly with totalCount=0', () => {
|
|
294
|
+
render(<Pagination page={1} pageSize={10} totalCount={0} onPageChange={() => {}} />);
|
|
295
|
+
expect(screen.getByText(/Showing 0-0 of 0/)).not.toBeNull();
|
|
296
|
+
expect((screen.getByRole('button', { name: 'Previous' }) as HTMLButtonElement).disabled).toBe(true);
|
|
297
|
+
expect((screen.getByRole('button', { name: 'Next' }) as HTMLButtonElement).disabled).toBe(true);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('clamps page > totalPages internally without crashing', () => {
|
|
301
|
+
let last = 0;
|
|
302
|
+
render(
|
|
303
|
+
<Pagination
|
|
304
|
+
page={99}
|
|
305
|
+
pageSize={10}
|
|
306
|
+
totalCount={50}
|
|
307
|
+
onPageChange={(p) => (last = p)}
|
|
308
|
+
/>,
|
|
309
|
+
);
|
|
310
|
+
// Component computes safePage=5 (last valid). User clicks Prev → goes to 4.
|
|
311
|
+
fireEvent.click(screen.getByRole('button', { name: 'Previous' }));
|
|
312
|
+
expect(last).toBe(4);
|
|
313
|
+
});
|
|
314
|
+
});
|