@fpkit/acss 6.2.0 → 6.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/libs/chunk-25KCUE3R.cjs +17 -0
- package/libs/chunk-25KCUE3R.cjs.map +1 -0
- package/libs/chunk-34NWHFHP.js +10 -0
- package/libs/chunk-34NWHFHP.js.map +1 -0
- package/libs/{chunk-SQ44OCJ2.js → chunk-6NMLU5FA.js} +2 -2
- package/libs/{chunk-GVVCXXKI.cjs → chunk-6YVR4TDM.cjs} +3 -3
- package/libs/chunk-DSQ2TUCR.js +7 -0
- package/libs/chunk-DSQ2TUCR.js.map +1 -0
- package/libs/{chunk-H6A2CUWA.js → chunk-VQTCTLFN.js} +2 -2
- package/libs/chunk-ZJ4RUKI2.cjs +14 -0
- package/libs/chunk-ZJ4RUKI2.cjs.map +1 -0
- package/libs/{chunk-H4JRUNKU.cjs → chunk-ZOPHCNFD.cjs} +3 -3
- package/libs/components/button.cjs +3 -3
- package/libs/components/button.d.cts +34 -1
- package/libs/components/button.d.ts +34 -1
- package/libs/components/button.js +1 -1
- package/libs/components/buttons/button.css +1 -1
- package/libs/components/buttons/button.css.map +1 -1
- package/libs/components/buttons/button.min.css +2 -2
- package/libs/components/buttons/icon-button.css +1 -0
- package/libs/components/buttons/icon-button.css.map +1 -0
- package/libs/components/buttons/icon-button.min.css +3 -0
- package/libs/components/dialog/dialog.cjs +4 -4
- package/libs/components/dialog/dialog.js +2 -2
- package/libs/components/icons/icon.d.cts +1 -1
- package/libs/components/icons/icon.d.ts +1 -1
- package/libs/components/link/link.css +1 -1
- package/libs/components/link/link.min.css +1 -1
- package/libs/components/modal.cjs +3 -3
- package/libs/components/modal.js +2 -2
- package/libs/components/popover/popover.cjs +3 -8
- package/libs/components/popover/popover.css +1 -0
- package/libs/components/popover/popover.css.map +1 -0
- package/libs/components/popover/popover.d.cts +54 -26
- package/libs/components/popover/popover.d.ts +54 -26
- package/libs/components/popover/popover.js +1 -2
- package/libs/components/popover/popover.min.css +3 -0
- package/libs/hooks.cjs +3 -6
- package/libs/hooks.cjs.map +1 -1
- package/libs/hooks.d.cts +30 -10
- package/libs/hooks.d.ts +30 -10
- package/libs/hooks.js +5 -1
- package/libs/hooks.js.map +1 -1
- package/libs/{icons-48788561.d.ts → icons-2c09535c.d.ts} +32 -32
- package/libs/icons.d.cts +1 -1
- package/libs/icons.d.ts +1 -1
- package/libs/index.cjs +35 -35
- package/libs/index.cjs.map +1 -1
- package/libs/index.css +1 -1
- package/libs/index.css.map +1 -1
- package/libs/index.d.cts +64 -5
- package/libs/index.d.ts +64 -5
- package/libs/index.js +9 -10
- package/libs/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/buttons/README.mdx +107 -11
- package/src/components/buttons/STYLES.mdx +182 -47
- package/src/components/buttons/button.scss +93 -16
- package/src/components/buttons/button.stories.tsx +149 -0
- package/src/components/buttons/button.test.tsx +12 -0
- package/src/components/buttons/button.tsx +50 -6
- package/src/components/buttons/icon-button.scss +45 -0
- package/src/components/buttons/icon-button.stories.tsx +200 -0
- package/src/components/buttons/icon-button.test.tsx +132 -0
- package/src/components/buttons/icon-button.tsx +72 -0
- package/src/components/form/select.tsx +55 -51
- package/src/components/link/link.scss +2 -2
- package/src/components/popover/README.mdx +478 -0
- package/src/components/popover/STYLES.mdx +389 -0
- package/src/components/popover/index.ts +3 -0
- package/src/components/popover/popover.scss +249 -0
- package/src/components/popover/popover.stories.tsx +315 -15
- package/src/components/popover/popover.test.tsx +249 -37
- package/src/components/popover/popover.tsx +165 -62
- package/src/hooks/popover/popover.tsx +26 -10
- package/src/hooks/popover/use-popover.tsx +30 -10
- package/src/hooks.ts +5 -0
- package/src/index.scss +1 -0
- package/src/index.ts +1 -0
- package/src/styles/buttons/button.css +78 -16
- package/src/styles/buttons/button.css.map +1 -1
- package/src/styles/buttons/icon-button.css +32 -0
- package/src/styles/buttons/icon-button.css.map +1 -0
- package/src/styles/index.css +268 -18
- package/src/styles/index.css.map +1 -1
- package/src/styles/link/link.css +2 -2
- package/src/styles/popover/popover.css +190 -0
- package/src/styles/popover/popover.css.map +1 -0
- package/src/types/popover.d.ts +64 -0
- package/libs/chunk-4I5MF54P.js +0 -8
- package/libs/chunk-4I5MF54P.js.map +0 -1
- package/libs/chunk-GCGKYLDG.js +0 -7
- package/libs/chunk-GCGKYLDG.js.map +0 -1
- package/libs/chunk-NZVSXRTB.cjs +0 -16
- package/libs/chunk-NZVSXRTB.cjs.map +0 -1
- package/libs/chunk-PDD4N5P5.cjs +0 -10
- package/libs/chunk-PDD4N5P5.cjs.map +0 -1
- package/libs/chunk-S7NIA6PI.cjs +0 -17
- package/libs/chunk-S7NIA6PI.cjs.map +0 -1
- package/libs/chunk-X2RDXWH5.js +0 -10
- package/libs/chunk-X2RDXWH5.js.map +0 -1
- /package/libs/{chunk-SQ44OCJ2.js.map → chunk-6NMLU5FA.js.map} +0 -0
- /package/libs/{chunk-GVVCXXKI.cjs.map → chunk-6YVR4TDM.cjs.map} +0 -0
- /package/libs/{chunk-H6A2CUWA.js.map → chunk-VQTCTLFN.js.map} +0 -0
- /package/libs/{chunk-H4JRUNKU.cjs.map → chunk-ZOPHCNFD.cjs.map} +0 -0
|
@@ -1,30 +1,330 @@
|
|
|
1
1
|
import { StoryObj, Meta } from "@storybook/react-vite";
|
|
2
2
|
import { within, expect, userEvent } from "storybook/test";
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
|
|
4
|
-
import Popover from "./popover";
|
|
5
|
+
import { Popover } from "./popover";
|
|
6
|
+
import type {} from "../../types/popover";
|
|
7
|
+
import "./popover.scss";
|
|
5
8
|
|
|
6
9
|
const meta: Meta<typeof Popover> = {
|
|
7
|
-
title: "FP.React Components/
|
|
10
|
+
title: "FP.React Components/Popover",
|
|
8
11
|
component: Popover,
|
|
9
|
-
tags: ["
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
tags: ["stable"],
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: "centered",
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
"Native HTML Popover API component with automatic top-layer rendering, light dismiss, and accessibility features. Requires Chrome 125+, Edge 125+, or Safari 17.4+.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
mode: {
|
|
24
|
+
control: "select",
|
|
25
|
+
options: ["auto", "manual"],
|
|
26
|
+
description: "Popover dismiss behavior",
|
|
27
|
+
},
|
|
28
|
+
placement: {
|
|
29
|
+
control: "select",
|
|
30
|
+
options: ["top", "bottom", "left", "right"],
|
|
31
|
+
description: "Preferred placement position",
|
|
32
|
+
},
|
|
33
|
+
showArrow: {
|
|
34
|
+
control: "boolean",
|
|
35
|
+
description: "Show positioning arrow",
|
|
36
|
+
},
|
|
37
|
+
showCloseButton: {
|
|
38
|
+
control: "boolean",
|
|
39
|
+
description: "Show close button",
|
|
40
|
+
},
|
|
14
41
|
},
|
|
15
42
|
} as Meta;
|
|
16
43
|
|
|
17
44
|
export default meta;
|
|
18
45
|
type Story = StoryObj<typeof Popover>;
|
|
19
46
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Default auto-dismiss popover with bottom placement
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
id: "default-popover",
|
|
53
|
+
triggerLabel: "Open Popover",
|
|
54
|
+
children: (
|
|
55
|
+
<>
|
|
56
|
+
<h3 style={{ margin: "0 0 0.5rem 0", fontSize: "1.125rem" }}>
|
|
57
|
+
Popover Title
|
|
58
|
+
</h3>
|
|
59
|
+
<p style={{ margin: 0, fontSize: "0.875rem" }}>
|
|
60
|
+
This popover dismisses automatically when you click outside or press
|
|
61
|
+
Escape.
|
|
62
|
+
</p>
|
|
63
|
+
</>
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
play: async ({ canvasElement, step }) => {
|
|
67
|
+
const canvas = within(canvasElement);
|
|
68
|
+
|
|
69
|
+
await step("Render trigger button", async () => {
|
|
70
|
+
const trigger = canvas.getByRole("button", { name: "Open Popover" });
|
|
71
|
+
expect(trigger).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await step("Click trigger opens popover", async () => {
|
|
75
|
+
const trigger = canvas.getByRole("button", { name: "Open Popover" });
|
|
76
|
+
await userEvent.click(trigger);
|
|
77
|
+
expect(canvas.getByText("Popover Title")).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await step("Escape key closes popover (auto mode)", async () => {
|
|
81
|
+
await userEvent.keyboard("{Escape}");
|
|
82
|
+
// Wait for animation
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
84
|
+
expect(canvas.queryByText("Popover Title")).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Manual mode requires explicit close action
|
|
91
|
+
*/
|
|
92
|
+
export const ManualMode: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
id: "manual-popover",
|
|
95
|
+
triggerLabel: "Open Manual Popover",
|
|
96
|
+
mode: "manual",
|
|
97
|
+
children: (
|
|
98
|
+
<>
|
|
99
|
+
<h3 style={{ margin: "0 0 0.5rem 0", fontSize: "1.125rem" }}>
|
|
100
|
+
Manual Popover
|
|
101
|
+
</h3>
|
|
102
|
+
<p style={{ margin: 0, fontSize: "0.875rem" }}>
|
|
103
|
+
This popover requires clicking the close button or trigger to dismiss.
|
|
104
|
+
It includes a backdrop overlay.
|
|
105
|
+
</p>
|
|
106
|
+
</>
|
|
107
|
+
),
|
|
108
|
+
},
|
|
109
|
+
play: async ({ canvasElement, step }) => {
|
|
23
110
|
const canvas = within(canvasElement);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
111
|
+
|
|
112
|
+
await step("Click trigger opens popover", async () => {
|
|
113
|
+
const trigger = canvas.getByRole("button", {
|
|
114
|
+
name: "Open Manual Popover",
|
|
115
|
+
});
|
|
116
|
+
await userEvent.click(trigger);
|
|
117
|
+
expect(canvas.getByText("Manual Popover")).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await step("Close button dismisses popover", async () => {
|
|
121
|
+
const closeButton = canvas.getByRole("button", { name: "Close" });
|
|
122
|
+
await userEvent.click(closeButton);
|
|
123
|
+
// Wait for animation
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
125
|
+
expect(canvas.queryByText("Manual Popover")).not.toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Popover with top placement
|
|
132
|
+
*/
|
|
133
|
+
export const TopPlacement: Story = {
|
|
134
|
+
args: {
|
|
135
|
+
id: "top-popover",
|
|
136
|
+
triggerLabel: "Open Above",
|
|
137
|
+
placement: "top",
|
|
138
|
+
children: (
|
|
139
|
+
<p style={{ margin: 0 }}>This popover appears above the trigger</p>
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Popover with left placement
|
|
146
|
+
*/
|
|
147
|
+
export const LeftPlacement: Story = {
|
|
148
|
+
args: {
|
|
149
|
+
id: "left-popover",
|
|
150
|
+
triggerLabel: "Open Left",
|
|
151
|
+
placement: "left",
|
|
152
|
+
children: <p style={{ margin: 0 }}>This popover appears to the left</p>,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Popover with right placement
|
|
158
|
+
*/
|
|
159
|
+
export const RightPlacement: Story = {
|
|
160
|
+
args: {
|
|
161
|
+
id: "right-popover",
|
|
162
|
+
triggerLabel: "Open Right",
|
|
163
|
+
placement: "right",
|
|
164
|
+
children: <p style={{ margin: 0 }}>This popover appears to the right</p>,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Custom trigger element
|
|
170
|
+
*/
|
|
171
|
+
export const CustomTrigger: Story = {
|
|
172
|
+
args: {
|
|
173
|
+
id: "custom-trigger-popover",
|
|
174
|
+
trigger: (
|
|
175
|
+
<button style={{ padding: "0.5rem 1rem", borderRadius: "2rem" }}>
|
|
176
|
+
🎨 Custom
|
|
177
|
+
</button>
|
|
178
|
+
),
|
|
179
|
+
children: (
|
|
180
|
+
<>
|
|
181
|
+
<h4 style={{ margin: "0 0 0.5rem 0" }}>Custom Trigger</h4>
|
|
182
|
+
<p style={{ margin: 0, fontSize: "0.875rem" }}>
|
|
183
|
+
You can use any React element as trigger
|
|
184
|
+
</p>
|
|
185
|
+
</>
|
|
186
|
+
),
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Popover without arrow
|
|
192
|
+
*/
|
|
193
|
+
export const NoArrow: Story = {
|
|
194
|
+
args: {
|
|
195
|
+
id: "no-arrow-popover",
|
|
196
|
+
triggerLabel: "No Arrow",
|
|
197
|
+
showArrow: false,
|
|
198
|
+
children: <p style={{ margin: 0 }}>This popover has no arrow indicator</p>,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Popover with custom styling via CSS variables
|
|
204
|
+
*/
|
|
205
|
+
export const CustomStyling: Story = {
|
|
206
|
+
args: {
|
|
207
|
+
id: "custom-styled-popover",
|
|
208
|
+
triggerLabel: "Custom Style",
|
|
209
|
+
styles: {
|
|
210
|
+
"--popover-bg": "#1a1a2e",
|
|
211
|
+
"--popover-border": "0.125rem solid #16213e",
|
|
212
|
+
"--popover-border-radius": "0.75rem",
|
|
213
|
+
"--popover-padding": "1.5rem",
|
|
214
|
+
"--popover-shadow": "0 0.5rem 1rem rgba(0, 0, 0, 0.3)",
|
|
215
|
+
color: "#eee",
|
|
216
|
+
} as React.CSSProperties,
|
|
217
|
+
children: (
|
|
218
|
+
<>
|
|
219
|
+
<h3 style={{ margin: "0 0 0.5rem 0", color: "#0f3" }}>Dark Theme</h3>
|
|
220
|
+
<p style={{ margin: 0, fontSize: "0.875rem" }}>
|
|
221
|
+
Customize appearance using CSS custom properties
|
|
222
|
+
</p>
|
|
223
|
+
</>
|
|
224
|
+
),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Controlled popover with external state
|
|
230
|
+
*/
|
|
231
|
+
const ControlledExample = () => {
|
|
232
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "1rem" }}>
|
|
236
|
+
<Popover
|
|
237
|
+
id="controlled-popover"
|
|
238
|
+
triggerLabel="Toggle Popover"
|
|
239
|
+
isOpen={isOpen}
|
|
240
|
+
onToggle={setIsOpen}
|
|
241
|
+
>
|
|
242
|
+
<div>
|
|
243
|
+
<h4 style={{ margin: "0 0 0.5rem 0" }}>Controlled Popover</h4>
|
|
244
|
+
<p style={{ margin: "0 0 0.5rem 0", fontSize: "0.875rem" }}>
|
|
245
|
+
State is managed externally
|
|
246
|
+
</p>
|
|
247
|
+
<button
|
|
248
|
+
onClick={() => setIsOpen(false)}
|
|
249
|
+
style={{ padding: "0.25rem 0.5rem", fontSize: "0.75rem" }}
|
|
250
|
+
>
|
|
251
|
+
Close via State
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</Popover>
|
|
255
|
+
<div style={{ textAlign: "center" }}>
|
|
256
|
+
<p style={{ margin: "0 0 0.5rem 0" }}>Current state: {isOpen ? "Open" : "Closed"}</p>
|
|
257
|
+
<button onClick={() => setIsOpen(!isOpen)}>External Toggle</button>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const Controlled: Story = {
|
|
264
|
+
render: () => <ControlledExample />,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Popover with form content
|
|
269
|
+
*/
|
|
270
|
+
export const WithForm: Story = {
|
|
271
|
+
args: {
|
|
272
|
+
id: "form-popover",
|
|
273
|
+
triggerLabel: "Show Form",
|
|
274
|
+
mode: "manual",
|
|
275
|
+
children: (
|
|
276
|
+
<form
|
|
277
|
+
style={{
|
|
278
|
+
display: "flex",
|
|
279
|
+
flexDirection: "column",
|
|
280
|
+
gap: "0.75rem",
|
|
281
|
+
minWidth: "15rem",
|
|
282
|
+
}}
|
|
283
|
+
onSubmit={(e) => {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
alert("Form submitted!");
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<h4 style={{ margin: 0 }}>Contact Form</h4>
|
|
289
|
+
<input
|
|
290
|
+
type="text"
|
|
291
|
+
placeholder="Name"
|
|
292
|
+
style={{
|
|
293
|
+
padding: "0.5rem",
|
|
294
|
+
border: "0.0625rem solid #ccc",
|
|
295
|
+
borderRadius: "0.25rem",
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
<input
|
|
299
|
+
type="email"
|
|
300
|
+
placeholder="Email"
|
|
301
|
+
style={{
|
|
302
|
+
padding: "0.5rem",
|
|
303
|
+
border: "0.0625rem solid #ccc",
|
|
304
|
+
borderRadius: "0.25rem",
|
|
305
|
+
}}
|
|
306
|
+
/>
|
|
307
|
+
<textarea
|
|
308
|
+
placeholder="Message"
|
|
309
|
+
rows={3}
|
|
310
|
+
style={{
|
|
311
|
+
padding: "0.5rem",
|
|
312
|
+
border: "0.0625rem solid #ccc",
|
|
313
|
+
borderRadius: "0.25rem",
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
<button
|
|
317
|
+
type="submit"
|
|
318
|
+
style={{
|
|
319
|
+
padding: "0.5rem",
|
|
320
|
+
background: "#0066cc",
|
|
321
|
+
color: "#fff",
|
|
322
|
+
border: "none",
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
Submit
|
|
326
|
+
</button>
|
|
327
|
+
</form>
|
|
328
|
+
),
|
|
29
329
|
},
|
|
30
330
|
};
|
|
@@ -1,39 +1,251 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { render, screen,
|
|
3
|
-
import
|
|
4
|
-
import
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import { Popover } from './popover';
|
|
5
|
+
import type {} from '../../types/popover';
|
|
6
|
+
|
|
7
|
+
// Mock showPopover and hidePopover methods
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
|
|
10
|
+
this.setAttribute('data-popover-open', 'true');
|
|
11
|
+
}) as unknown as () => void;
|
|
12
|
+
|
|
13
|
+
HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
|
|
14
|
+
this.removeAttribute('data-popover-open');
|
|
15
|
+
}) as unknown as () => void;
|
|
16
|
+
|
|
17
|
+
HTMLElement.prototype.togglePopover = vi.fn(function (this: HTMLElement) {
|
|
18
|
+
if (this.hasAttribute('data-popover-open')) {
|
|
19
|
+
this.removeAttribute('data-popover-open');
|
|
20
|
+
} else {
|
|
21
|
+
this.setAttribute('data-popover-open', 'true');
|
|
22
|
+
}
|
|
23
|
+
}) as unknown as (force?: boolean) => void;
|
|
24
|
+
});
|
|
5
25
|
|
|
6
26
|
describe('Popover', () => {
|
|
7
|
-
it('renders
|
|
8
|
-
render(
|
|
9
|
-
<Popover
|
|
10
|
-
|
|
11
|
-
</Popover
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
27
|
+
it('renders trigger button with default label', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Popover id="test-popover" triggerLabel="Open Menu">
|
|
30
|
+
<p>Content</p>
|
|
31
|
+
</Popover>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const trigger = screen.getByRole('button', { name: 'Open Menu' });
|
|
35
|
+
expect(trigger).toBeInTheDocument();
|
|
36
|
+
expect(trigger).toHaveAttribute('popovertarget', 'test-popover');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders popover content with correct attributes', () => {
|
|
40
|
+
render(
|
|
41
|
+
<Popover id="test-popover" mode="auto" placement="bottom">
|
|
42
|
+
<p>Popover content</p>
|
|
43
|
+
</Popover>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const popover = screen.getByText('Popover content').closest('[popover]');
|
|
47
|
+
expect(popover).toBeInTheDocument();
|
|
48
|
+
expect(popover).toHaveAttribute('popover', 'auto');
|
|
49
|
+
expect(popover).toHaveAttribute('data-placement', 'bottom');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('uses custom trigger element', () => {
|
|
53
|
+
render(
|
|
54
|
+
<Popover id="test-popover" trigger={<button>Custom Trigger</button>}>
|
|
55
|
+
<p>Content</p>
|
|
56
|
+
</Popover>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const trigger = screen.getByRole('button', { name: 'Custom Trigger' });
|
|
60
|
+
expect(trigger).toBeInTheDocument();
|
|
61
|
+
expect(trigger).toHaveAttribute('popovertarget', 'test-popover');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('shows close button in manual mode by default', () => {
|
|
65
|
+
render(
|
|
66
|
+
<Popover id="test-popover" mode="manual">
|
|
67
|
+
<p>Content</p>
|
|
68
|
+
</Popover>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const closeButton = screen.getByRole('button', { name: 'Close' });
|
|
72
|
+
expect(closeButton).toBeInTheDocument();
|
|
73
|
+
expect(closeButton).toHaveAttribute('popovertargetaction', 'hide');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('hides close button in auto mode by default', () => {
|
|
77
|
+
render(
|
|
78
|
+
<Popover id="test-popover" mode="auto">
|
|
79
|
+
<p>Content</p>
|
|
80
|
+
</Popover>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const closeButton = screen.queryByRole('button', { name: 'Close' });
|
|
84
|
+
expect(closeButton).not.toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('respects showCloseButton prop override', () => {
|
|
88
|
+
render(
|
|
89
|
+
<Popover id="test-popover" mode="auto" showCloseButton={true}>
|
|
90
|
+
<p>Content</p>
|
|
91
|
+
</Popover>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const closeButton = screen.getByRole('button', { name: 'Close' });
|
|
95
|
+
expect(closeButton).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('shows arrow by default', () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<Popover id="test-popover">
|
|
101
|
+
<p>Content</p>
|
|
102
|
+
</Popover>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const arrow = container.querySelector('.fpkit-popover-arrow');
|
|
106
|
+
expect(arrow).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('hides arrow when showArrow is false', () => {
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<Popover id="test-popover" showArrow={false}>
|
|
112
|
+
<p>Content</p>
|
|
113
|
+
</Popover>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const arrow = container.querySelector('.fpkit-popover-arrow');
|
|
117
|
+
expect(arrow).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('applies custom className', () => {
|
|
121
|
+
const { container } = render(
|
|
122
|
+
<Popover id="test-popover" className="custom-class">
|
|
123
|
+
<p>Content</p>
|
|
124
|
+
</Popover>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const popover = container.querySelector('.fpkit-popover.custom-class');
|
|
128
|
+
expect(popover).toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('applies inline styles', () => {
|
|
132
|
+
const customStyles = { '--popover-bg': '#000000' } as React.CSSProperties;
|
|
133
|
+
const { container } = render(
|
|
134
|
+
<Popover id="test-popover" styles={customStyles}>
|
|
135
|
+
<p>Content</p>
|
|
136
|
+
</Popover>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const popover = container.querySelector('.fpkit-popover') as HTMLElement;
|
|
140
|
+
expect(popover.style.getPropertyValue('--popover-bg')).toBe('#000000');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('calls onToggle callback when popover state changes', async () => {
|
|
144
|
+
const handleToggle = vi.fn();
|
|
145
|
+
const { container } = render(
|
|
146
|
+
<Popover id="test-popover" onToggle={handleToggle}>
|
|
147
|
+
<p>Content</p>
|
|
148
|
+
</Popover>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const popover = container.querySelector('[popover]') as HTMLElement;
|
|
152
|
+
|
|
153
|
+
// Simulate toggle event - open
|
|
154
|
+
const toggleEventOpen = Object.assign(new Event('toggle'), {
|
|
155
|
+
newState: 'open' as const,
|
|
156
|
+
oldState: 'closed' as const,
|
|
157
|
+
}) as ToggleEvent;
|
|
158
|
+
popover.dispatchEvent(toggleEventOpen);
|
|
159
|
+
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(handleToggle).toHaveBeenCalledWith(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Simulate close
|
|
165
|
+
const toggleEventClose = Object.assign(new Event('toggle'), {
|
|
166
|
+
newState: 'closed' as const,
|
|
167
|
+
oldState: 'open' as const,
|
|
168
|
+
}) as ToggleEvent;
|
|
169
|
+
popover.dispatchEvent(toggleEventClose);
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(handleToggle).toHaveBeenCalledWith(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('handles controlled state with isOpen prop', async () => {
|
|
177
|
+
const { rerender, container } = render(
|
|
178
|
+
<Popover id="test-popover" isOpen={false}>
|
|
179
|
+
<p>Content</p>
|
|
180
|
+
</Popover>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const popover = container.querySelector('[popover]') as HTMLElement;
|
|
184
|
+
|
|
185
|
+
// Initially closed
|
|
186
|
+
expect(popover.hasAttribute('data-popover-open')).toBe(false);
|
|
187
|
+
|
|
188
|
+
// Open popover
|
|
189
|
+
rerender(
|
|
190
|
+
<Popover id="test-popover" isOpen={true}>
|
|
191
|
+
<p>Content</p>
|
|
192
|
+
</Popover>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
await waitFor(() => {
|
|
196
|
+
expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('generates unique ID when not provided', () => {
|
|
201
|
+
const { container } = render(
|
|
202
|
+
<Popover>
|
|
203
|
+
<p>Content</p>
|
|
204
|
+
</Popover>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const popover = container.querySelector('[popover]');
|
|
208
|
+
const trigger = screen.getByRole('button');
|
|
209
|
+
|
|
210
|
+
expect(popover).toHaveAttribute('id');
|
|
211
|
+
expect(trigger).toHaveAttribute('popovertarget');
|
|
212
|
+
|
|
213
|
+
const popoverId = popover?.getAttribute('id');
|
|
214
|
+
const triggerId = trigger.getAttribute('popovertarget');
|
|
215
|
+
|
|
216
|
+
expect(popoverId).toBe(triggerId);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('uses provided ID', () => {
|
|
220
|
+
render(
|
|
221
|
+
<Popover id="custom-id">
|
|
222
|
+
<p>Content</p>
|
|
223
|
+
</Popover>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const trigger = screen.getByRole('button');
|
|
227
|
+
expect(trigger).toHaveAttribute('popovertarget', 'custom-id');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('custom close button label', () => {
|
|
231
|
+
render(
|
|
232
|
+
<Popover id="test-popover" mode="manual" closeButtonLabel="Dismiss">
|
|
233
|
+
<p>Content</p>
|
|
234
|
+
</Popover>
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const closeButton = screen.getByRole('button', { name: 'Dismiss' });
|
|
238
|
+
expect(closeButton).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('renders arrow with correct placement attribute', () => {
|
|
242
|
+
const { container } = render(
|
|
243
|
+
<Popover id="test-popover" placement="top">
|
|
244
|
+
<p>Content</p>
|
|
245
|
+
</Popover>
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const arrow = container.querySelector('.fpkit-popover-arrow');
|
|
249
|
+
expect(arrow).toHaveAttribute('data-placement', 'top');
|
|
250
|
+
});
|
|
251
|
+
});
|