@tpzdsp/next-toolkit 1.13.0 → 1.14.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/package.json +13 -1
- package/src/components/ButtonLink/ButtonLink.stories.tsx +72 -0
- package/src/components/ButtonLink/ButtonLink.test.tsx +154 -0
- package/src/components/ButtonLink/ButtonLink.tsx +33 -0
- package/src/components/InfoBox/InfoBox.stories.tsx +31 -28
- package/src/components/InfoBox/InfoBox.test.tsx +8 -60
- package/src/components/InfoBox/InfoBox.tsx +60 -69
- package/src/components/LinkButton/LinkButton.stories.tsx +74 -0
- package/src/components/LinkButton/LinkButton.test.tsx +177 -0
- package/src/components/LinkButton/LinkButton.tsx +80 -0
- package/src/components/index.ts +5 -8
- package/src/components/link/ExternalLink.test.tsx +104 -0
- package/src/components/link/ExternalLink.tsx +1 -0
- package/src/map/MapComponent.tsx +7 -12
- package/src/components/InfoBox/hooks/index.ts +0 -3
- package/src/components/InfoBox/hooks/useInfoBoxPosition.test.ts +0 -187
- package/src/components/InfoBox/hooks/useInfoBoxPosition.ts +0 -69
- package/src/components/InfoBox/hooks/useInfoBoxState.test.ts +0 -168
- package/src/components/InfoBox/hooks/useInfoBoxState.ts +0 -71
- package/src/components/InfoBox/hooks/usePortalMount.test.ts +0 -62
- package/src/components/InfoBox/hooks/usePortalMount.ts +0 -15
- package/src/components/InfoBox/utils/focusTrapConfig.test.ts +0 -310
- package/src/components/InfoBox/utils/focusTrapConfig.ts +0 -59
- package/src/components/InfoBox/utils/index.ts +0 -2
- package/src/components/InfoBox/utils/positionUtils.test.ts +0 -170
- package/src/components/InfoBox/utils/positionUtils.ts +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpzdsp/next-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A reusable React component library for Next.js applications",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">= 24.12.0",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"import": "./src/components/select/index.ts",
|
|
58
58
|
"require": "./src/components/select/index.ts"
|
|
59
59
|
},
|
|
60
|
+
"./components/info-box": {
|
|
61
|
+
"types": "./src/components/InfoBox/InfoBox.tsx",
|
|
62
|
+
"import": "./src/components/InfoBox/InfoBox.tsx",
|
|
63
|
+
"require": "./src/components/InfoBox/InfoBox.tsx"
|
|
64
|
+
},
|
|
60
65
|
"./http": {
|
|
61
66
|
"types": "./src/http/index.ts",
|
|
62
67
|
"import": "./src/http/index.ts",
|
|
@@ -200,6 +205,7 @@
|
|
|
200
205
|
},
|
|
201
206
|
"peerDependencies": {
|
|
202
207
|
"@better-fetch/fetch": "^1.1.21",
|
|
208
|
+
"@floating-ui/react": "^0.27.17",
|
|
203
209
|
"@tanstack/react-query": "^5.90.19",
|
|
204
210
|
"@testing-library/react": "^16.0.0",
|
|
205
211
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -227,6 +233,9 @@
|
|
|
227
233
|
"@better-fetch/fetch": {
|
|
228
234
|
"optional": true
|
|
229
235
|
},
|
|
236
|
+
"@floating-ui/react": {
|
|
237
|
+
"optional": true
|
|
238
|
+
},
|
|
230
239
|
"@turf/turf": {
|
|
231
240
|
"optional": true
|
|
232
241
|
},
|
|
@@ -272,5 +281,8 @@
|
|
|
272
281
|
},
|
|
273
282
|
"release": {
|
|
274
283
|
"extends": "./release.config.js"
|
|
284
|
+
},
|
|
285
|
+
"dependencies": {
|
|
286
|
+
"@floating-ui/react": "^0.27.17"
|
|
275
287
|
}
|
|
276
288
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { ButtonLink } from './ButtonLink';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/ButtonLink',
|
|
7
|
+
component: ButtonLink,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
type: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['button', 'submit', 'reset'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof ButtonLink>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
children: 'Click me',
|
|
26
|
+
type: 'button',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const WithClickHandler: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
children: 'Show alert',
|
|
33
|
+
onClick: () => alert('Button clicked!'),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Submit: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
children: 'Submit form',
|
|
40
|
+
type: 'submit',
|
|
41
|
+
},
|
|
42
|
+
render: (args) => (
|
|
43
|
+
<form
|
|
44
|
+
onSubmit={(e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
alert('Form submitted!');
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<ButtonLink {...args} />
|
|
50
|
+
</form>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Disabled: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
children: 'Disabled button link',
|
|
57
|
+
disabled: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithCustomStyling: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
children: 'Custom styled',
|
|
64
|
+
className: 'font-bold text-xl',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const LongText: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
children: 'This is a longer button link that demonstrates how text wraps',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ButtonLink } from './ButtonLink';
|
|
2
|
+
import { render, screen, userEvent } from '../../test/renderers';
|
|
3
|
+
|
|
4
|
+
describe('ButtonLink', () => {
|
|
5
|
+
describe('rendering', () => {
|
|
6
|
+
it('should render with children', () => {
|
|
7
|
+
render(<ButtonLink>Click me</ButtonLink>);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should render as a button element', () => {
|
|
13
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
14
|
+
|
|
15
|
+
const button = screen.getByRole('button');
|
|
16
|
+
|
|
17
|
+
expect(button.tagName).toBe('BUTTON');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have link styling classes', () => {
|
|
21
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
22
|
+
|
|
23
|
+
const button = screen.getByRole('button');
|
|
24
|
+
|
|
25
|
+
expect(button).toHaveClass('text-link', 'underline', 'cursor-pointer');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should merge custom className', () => {
|
|
29
|
+
render(<ButtonLink className="custom-class">Button</ButtonLink>);
|
|
30
|
+
|
|
31
|
+
const button = screen.getByRole('button');
|
|
32
|
+
|
|
33
|
+
expect(button).toHaveClass('custom-class');
|
|
34
|
+
expect(button).toHaveClass('text-link'); // Still has base classes
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('button types', () => {
|
|
39
|
+
it('should default to type="button"', () => {
|
|
40
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
41
|
+
|
|
42
|
+
const button = screen.getByRole('button');
|
|
43
|
+
|
|
44
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should accept type="submit"', () => {
|
|
48
|
+
render(<ButtonLink type="submit">Submit</ButtonLink>);
|
|
49
|
+
|
|
50
|
+
const button = screen.getByRole('button');
|
|
51
|
+
|
|
52
|
+
expect(button).toHaveAttribute('type', 'submit');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should accept type="reset"', () => {
|
|
56
|
+
render(<ButtonLink type="reset">Reset</ButtonLink>);
|
|
57
|
+
|
|
58
|
+
const button = screen.getByRole('button');
|
|
59
|
+
|
|
60
|
+
expect(button).toHaveAttribute('type', 'reset');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('interactions', () => {
|
|
65
|
+
it('should handle click events', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
const onClick = vi.fn();
|
|
68
|
+
|
|
69
|
+
render(<ButtonLink onClick={onClick}>Click me</ButtonLink>);
|
|
70
|
+
|
|
71
|
+
await user.click(screen.getByRole('button'));
|
|
72
|
+
|
|
73
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should not trigger onClick when disabled', async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
const onClick = vi.fn();
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<ButtonLink onClick={onClick} disabled>
|
|
82
|
+
Disabled
|
|
83
|
+
</ButtonLink>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await user.click(screen.getByRole('button'));
|
|
87
|
+
|
|
88
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should submit form when type="submit"', () => {
|
|
92
|
+
const onSubmit = vi.fn((e) => e.preventDefault());
|
|
93
|
+
|
|
94
|
+
render(
|
|
95
|
+
<form onSubmit={onSubmit}>
|
|
96
|
+
<ButtonLink type="submit">Submit</ButtonLink>
|
|
97
|
+
</form>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const button = screen.getByRole('button');
|
|
101
|
+
|
|
102
|
+
button.click();
|
|
103
|
+
|
|
104
|
+
expect(onSubmit).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('disabled state', () => {
|
|
109
|
+
it('should have disabled attribute when disabled', () => {
|
|
110
|
+
render(<ButtonLink disabled>Disabled</ButtonLink>);
|
|
111
|
+
|
|
112
|
+
const button = screen.getByRole('button');
|
|
113
|
+
|
|
114
|
+
expect(button).toBeDisabled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should have reduced opacity when disabled', () => {
|
|
118
|
+
render(<ButtonLink disabled>Disabled</ButtonLink>);
|
|
119
|
+
|
|
120
|
+
const button = screen.getByRole('button');
|
|
121
|
+
|
|
122
|
+
expect(button).toHaveClass('disabled:opacity-50');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should have not-allowed cursor when disabled', () => {
|
|
126
|
+
render(<ButtonLink disabled>Disabled</ButtonLink>);
|
|
127
|
+
|
|
128
|
+
const button = screen.getByRole('button');
|
|
129
|
+
|
|
130
|
+
expect(button).toHaveClass('disabled:cursor-not-allowed');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('accessibility', () => {
|
|
135
|
+
it('should have button role', () => {
|
|
136
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
137
|
+
|
|
138
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should accept aria attributes', () => {
|
|
142
|
+
render(
|
|
143
|
+
<ButtonLink aria-label="Custom label" aria-describedby="description">
|
|
144
|
+
Button
|
|
145
|
+
</ButtonLink>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const button = screen.getByRole('button', { name: 'Custom label' });
|
|
149
|
+
|
|
150
|
+
expect(button).toBeInTheDocument();
|
|
151
|
+
expect(button).toHaveAttribute('aria-describedby', 'description');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtendProps } from '../../types/utils';
|
|
2
|
+
import { cn } from '../../utils';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
type?: 'submit' | 'reset' | 'button';
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ButtonLinkProps = ExtendProps<'button', Props>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A button that looks like a link.
|
|
13
|
+
* Use when you need button functionality (onClick, form submission)
|
|
14
|
+
* but want the visual appearance of a link.
|
|
15
|
+
*/
|
|
16
|
+
export const ButtonLink = ({ type = 'button', className, children, ...props }: ButtonLinkProps) => {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
type={type}
|
|
20
|
+
className={cn(
|
|
21
|
+
`cursor-pointer text-link hover:decoration-[max(3px,_.1875rem,_.12em)] hover:text-link-hover
|
|
22
|
+
visited:text-link-visited active:text-black focus:decoration-[max(3px,_.1875rem,_.12em)]
|
|
23
|
+
decoration-[max(1px,_.0625rem)] underline-offset-[0.1578em] underline outline-none
|
|
24
|
+
focus:text-focus-text focus:bg-focus inline-block bg-transparent border-0 p-0
|
|
25
|
+
disabled:opacity-50 disabled:cursor-not-allowed`,
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</button>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
InfoBox,
|
|
5
|
-
POSITION_TOP_LEFT,
|
|
6
|
-
POSITION_TOP_RIGHT,
|
|
7
|
-
POSITION_BOTTOM_LEFT,
|
|
8
|
-
POSITION_BOTTOM_RIGHT,
|
|
9
|
-
} from './InfoBox';
|
|
3
|
+
import { InfoBox } from './InfoBox';
|
|
10
4
|
|
|
11
5
|
const meta = {
|
|
12
6
|
title: 'Components/InfoBox',
|
|
@@ -16,7 +10,7 @@ const meta = {
|
|
|
16
10
|
docs: {
|
|
17
11
|
description: {
|
|
18
12
|
component:
|
|
19
|
-
'An accessible info box component that displays contextual information in a popover triggered by an info icon button.
|
|
13
|
+
'An accessible info box component that displays contextual information in a popover triggered by an info icon button. Uses Floating UI for intelligent auto-positioning that adapts to viewport constraints.',
|
|
20
14
|
},
|
|
21
15
|
},
|
|
22
16
|
},
|
|
@@ -39,16 +33,24 @@ const meta = {
|
|
|
39
33
|
control: 'text',
|
|
40
34
|
description: 'Accessible label for the trigger button',
|
|
41
35
|
},
|
|
42
|
-
|
|
36
|
+
placement: {
|
|
43
37
|
control: 'select',
|
|
44
38
|
options: [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
'top',
|
|
40
|
+
'top-start',
|
|
41
|
+
'top-end',
|
|
42
|
+
'bottom',
|
|
43
|
+
'bottom-start',
|
|
44
|
+
'bottom-end',
|
|
45
|
+
'left',
|
|
46
|
+
'left-start',
|
|
47
|
+
'left-end',
|
|
48
|
+
'right',
|
|
49
|
+
'right-start',
|
|
50
|
+
'right-end',
|
|
50
51
|
],
|
|
51
|
-
description:
|
|
52
|
+
description:
|
|
53
|
+
'Preferred placement (Floating UI will auto-adjust if there is not enough space)',
|
|
52
54
|
},
|
|
53
55
|
children: {
|
|
54
56
|
control: false,
|
|
@@ -163,7 +165,8 @@ export const AllPositions: Story = {
|
|
|
163
165
|
layout: 'fullscreen',
|
|
164
166
|
docs: {
|
|
165
167
|
description: {
|
|
166
|
-
story:
|
|
168
|
+
story:
|
|
169
|
+
'Demonstrates various placement options using Floating UI. The content will automatically adjust if there is not enough space.',
|
|
167
170
|
},
|
|
168
171
|
},
|
|
169
172
|
},
|
|
@@ -171,40 +174,40 @@ export const AllPositions: Story = {
|
|
|
171
174
|
<div className="p-8 grid grid-cols-2 gap-8 h-screen">
|
|
172
175
|
<div className="flex items-start justify-start">
|
|
173
176
|
<div className="flex items-center gap-2">
|
|
174
|
-
<span className="text-sm text-gray-600">bottom-
|
|
177
|
+
<span className="text-sm text-gray-600">bottom-start:</span>
|
|
175
178
|
|
|
176
|
-
<InfoBox title="Position Demo"
|
|
177
|
-
<p>
|
|
179
|
+
<InfoBox title="Position Demo" placement="bottom-start">
|
|
180
|
+
<p>Prefers bottom-start placement.</p>
|
|
178
181
|
</InfoBox>
|
|
179
182
|
</div>
|
|
180
183
|
</div>
|
|
181
184
|
|
|
182
185
|
<div className="flex items-start justify-end">
|
|
183
186
|
<div className="flex items-center gap-2">
|
|
184
|
-
<span className="text-sm text-gray-600">bottom-
|
|
187
|
+
<span className="text-sm text-gray-600">bottom-end:</span>
|
|
185
188
|
|
|
186
|
-
<InfoBox title="Position Demo"
|
|
187
|
-
<p>
|
|
189
|
+
<InfoBox title="Position Demo" placement="bottom-end">
|
|
190
|
+
<p>Prefers bottom-end placement.</p>
|
|
188
191
|
</InfoBox>
|
|
189
192
|
</div>
|
|
190
193
|
</div>
|
|
191
194
|
|
|
192
195
|
<div className="flex items-end justify-start">
|
|
193
196
|
<div className="flex items-center gap-2">
|
|
194
|
-
<span className="text-sm text-gray-600">top-
|
|
197
|
+
<span className="text-sm text-gray-600">top-start:</span>
|
|
195
198
|
|
|
196
|
-
<InfoBox title="Position Demo"
|
|
197
|
-
<p>
|
|
199
|
+
<InfoBox title="Position Demo" placement="top-start">
|
|
200
|
+
<p>Prefers top-start placement.</p>
|
|
198
201
|
</InfoBox>
|
|
199
202
|
</div>
|
|
200
203
|
</div>
|
|
201
204
|
|
|
202
205
|
<div className="flex items-end justify-end">
|
|
203
206
|
<div className="flex items-center gap-2">
|
|
204
|
-
<span className="text-sm text-gray-600">top-
|
|
207
|
+
<span className="text-sm text-gray-600">top-end:</span>
|
|
205
208
|
|
|
206
|
-
<InfoBox title="Position Demo"
|
|
207
|
-
<p>
|
|
209
|
+
<InfoBox title="Position Demo" placement="top-end">
|
|
210
|
+
<p>Prefers top-end placement.</p>
|
|
208
211
|
</InfoBox>
|
|
209
212
|
</div>
|
|
210
213
|
</div>
|
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import { InfoBox
|
|
1
|
+
import { InfoBox } from './InfoBox';
|
|
2
2
|
import { render, screen, userEvent, waitFor } from '../../test/renderers';
|
|
3
3
|
|
|
4
4
|
const TEST_CONTENT = 'Test info content';
|
|
5
5
|
const TEST_TITLE = 'Test Title';
|
|
6
6
|
const ARIA_EXPANDED = 'aria-expanded';
|
|
7
7
|
|
|
8
|
-
// Mock focus-trap-react to simplify testing
|
|
9
|
-
vi.mock('focus-trap-react', () => ({
|
|
10
|
-
FocusTrap: ({ children }: { children: React.ReactNode }) => (
|
|
11
|
-
<div data-testid="focus-trap">{children}</div>
|
|
12
|
-
),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
8
|
describe('InfoBox', () => {
|
|
16
9
|
describe('rendering', () => {
|
|
17
10
|
it('should render the trigger button with info icon', () => {
|
|
@@ -131,27 +124,6 @@ describe('InfoBox', () => {
|
|
|
131
124
|
expect(button).toHaveAttribute(ARIA_EXPANDED, 'true');
|
|
132
125
|
});
|
|
133
126
|
|
|
134
|
-
it('should have aria-controls linking trigger to content', async () => {
|
|
135
|
-
const user = userEvent.setup();
|
|
136
|
-
|
|
137
|
-
render(
|
|
138
|
-
<InfoBox>
|
|
139
|
-
<p>{TEST_CONTENT}</p>
|
|
140
|
-
</InfoBox>,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
const button = screen.getByRole('button', { name: /show information/i });
|
|
144
|
-
const controlsId = button.getAttribute('aria-controls');
|
|
145
|
-
|
|
146
|
-
expect(controlsId).toBeTruthy();
|
|
147
|
-
|
|
148
|
-
await user.click(button);
|
|
149
|
-
|
|
150
|
-
const dialog = screen.getByRole('dialog');
|
|
151
|
-
|
|
152
|
-
expect(dialog).toHaveAttribute('id', controlsId);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
127
|
it('should have aria-haspopup="dialog" on trigger', () => {
|
|
156
128
|
render(
|
|
157
129
|
<InfoBox>
|
|
@@ -164,7 +136,7 @@ describe('InfoBox', () => {
|
|
|
164
136
|
expect(button).toHaveAttribute('aria-haspopup', 'dialog');
|
|
165
137
|
});
|
|
166
138
|
|
|
167
|
-
it('should
|
|
139
|
+
it('should have dialog role', async () => {
|
|
168
140
|
const user = userEvent.setup();
|
|
169
141
|
|
|
170
142
|
render(
|
|
@@ -177,8 +149,8 @@ describe('InfoBox', () => {
|
|
|
177
149
|
|
|
178
150
|
const dialog = screen.getByRole('dialog');
|
|
179
151
|
|
|
180
|
-
//
|
|
181
|
-
expect(dialog
|
|
152
|
+
// Floating UI uses div with role="dialog"
|
|
153
|
+
expect(dialog).toBeInTheDocument();
|
|
182
154
|
});
|
|
183
155
|
|
|
184
156
|
it('should have aria-labelledby when title is provided', async () => {
|
|
@@ -267,34 +239,11 @@ describe('InfoBox', () => {
|
|
|
267
239
|
});
|
|
268
240
|
|
|
269
241
|
describe('positioning', () => {
|
|
270
|
-
|
|
271
|
-
// Mock window dimensions
|
|
272
|
-
Object.defineProperty(globalThis, 'innerWidth', { value: 1000, writable: true });
|
|
273
|
-
Object.defineProperty(globalThis, 'innerHeight', { value: 800, writable: true });
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('should use forced position when position prop is provided', async () => {
|
|
277
|
-
const user = userEvent.setup();
|
|
278
|
-
|
|
279
|
-
render(
|
|
280
|
-
<InfoBox position={POSITION_TOP_LEFT}>
|
|
281
|
-
<p>{TEST_CONTENT}</p>
|
|
282
|
-
</InfoBox>,
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
await user.click(screen.getByRole('button', { name: /show information/i }));
|
|
286
|
-
|
|
287
|
-
const dialog = screen.getByRole('dialog');
|
|
288
|
-
|
|
289
|
-
// top-left position applies both X and Y transforms
|
|
290
|
-
expect(dialog).toHaveClass('-translate-x-full', '-translate-y-full');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('should apply correct transform for bottom-right position', async () => {
|
|
242
|
+
it('should accept placement prop', async () => {
|
|
294
243
|
const user = userEvent.setup();
|
|
295
244
|
|
|
296
245
|
render(
|
|
297
|
-
<InfoBox
|
|
246
|
+
<InfoBox placement="top-start">
|
|
298
247
|
<p>{TEST_CONTENT}</p>
|
|
299
248
|
</InfoBox>,
|
|
300
249
|
);
|
|
@@ -303,9 +252,8 @@ describe('InfoBox', () => {
|
|
|
303
252
|
|
|
304
253
|
const dialog = screen.getByRole('dialog');
|
|
305
254
|
|
|
306
|
-
//
|
|
307
|
-
expect(dialog).
|
|
308
|
-
expect(dialog).not.toHaveClass('-translate-y-full');
|
|
255
|
+
// Floating UI handles positioning automatically
|
|
256
|
+
expect(dialog).toBeInTheDocument();
|
|
309
257
|
});
|
|
310
258
|
});
|
|
311
259
|
|