@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.
Files changed (105) hide show
  1. package/libs/chunk-25KCUE3R.cjs +17 -0
  2. package/libs/chunk-25KCUE3R.cjs.map +1 -0
  3. package/libs/chunk-34NWHFHP.js +10 -0
  4. package/libs/chunk-34NWHFHP.js.map +1 -0
  5. package/libs/{chunk-SQ44OCJ2.js → chunk-6NMLU5FA.js} +2 -2
  6. package/libs/{chunk-GVVCXXKI.cjs → chunk-6YVR4TDM.cjs} +3 -3
  7. package/libs/chunk-DSQ2TUCR.js +7 -0
  8. package/libs/chunk-DSQ2TUCR.js.map +1 -0
  9. package/libs/{chunk-H6A2CUWA.js → chunk-VQTCTLFN.js} +2 -2
  10. package/libs/chunk-ZJ4RUKI2.cjs +14 -0
  11. package/libs/chunk-ZJ4RUKI2.cjs.map +1 -0
  12. package/libs/{chunk-H4JRUNKU.cjs → chunk-ZOPHCNFD.cjs} +3 -3
  13. package/libs/components/button.cjs +3 -3
  14. package/libs/components/button.d.cts +34 -1
  15. package/libs/components/button.d.ts +34 -1
  16. package/libs/components/button.js +1 -1
  17. package/libs/components/buttons/button.css +1 -1
  18. package/libs/components/buttons/button.css.map +1 -1
  19. package/libs/components/buttons/button.min.css +2 -2
  20. package/libs/components/buttons/icon-button.css +1 -0
  21. package/libs/components/buttons/icon-button.css.map +1 -0
  22. package/libs/components/buttons/icon-button.min.css +3 -0
  23. package/libs/components/dialog/dialog.cjs +4 -4
  24. package/libs/components/dialog/dialog.js +2 -2
  25. package/libs/components/icons/icon.d.cts +1 -1
  26. package/libs/components/icons/icon.d.ts +1 -1
  27. package/libs/components/link/link.css +1 -1
  28. package/libs/components/link/link.min.css +1 -1
  29. package/libs/components/modal.cjs +3 -3
  30. package/libs/components/modal.js +2 -2
  31. package/libs/components/popover/popover.cjs +3 -8
  32. package/libs/components/popover/popover.css +1 -0
  33. package/libs/components/popover/popover.css.map +1 -0
  34. package/libs/components/popover/popover.d.cts +54 -26
  35. package/libs/components/popover/popover.d.ts +54 -26
  36. package/libs/components/popover/popover.js +1 -2
  37. package/libs/components/popover/popover.min.css +3 -0
  38. package/libs/hooks.cjs +3 -6
  39. package/libs/hooks.cjs.map +1 -1
  40. package/libs/hooks.d.cts +30 -10
  41. package/libs/hooks.d.ts +30 -10
  42. package/libs/hooks.js +5 -1
  43. package/libs/hooks.js.map +1 -1
  44. package/libs/{icons-48788561.d.ts → icons-2c09535c.d.ts} +32 -32
  45. package/libs/icons.d.cts +1 -1
  46. package/libs/icons.d.ts +1 -1
  47. package/libs/index.cjs +35 -35
  48. package/libs/index.cjs.map +1 -1
  49. package/libs/index.css +1 -1
  50. package/libs/index.css.map +1 -1
  51. package/libs/index.d.cts +64 -5
  52. package/libs/index.d.ts +64 -5
  53. package/libs/index.js +9 -10
  54. package/libs/index.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/components/buttons/README.mdx +107 -11
  57. package/src/components/buttons/STYLES.mdx +182 -47
  58. package/src/components/buttons/button.scss +93 -16
  59. package/src/components/buttons/button.stories.tsx +149 -0
  60. package/src/components/buttons/button.test.tsx +12 -0
  61. package/src/components/buttons/button.tsx +50 -6
  62. package/src/components/buttons/icon-button.scss +45 -0
  63. package/src/components/buttons/icon-button.stories.tsx +200 -0
  64. package/src/components/buttons/icon-button.test.tsx +132 -0
  65. package/src/components/buttons/icon-button.tsx +72 -0
  66. package/src/components/form/select.tsx +55 -51
  67. package/src/components/link/link.scss +2 -2
  68. package/src/components/popover/README.mdx +478 -0
  69. package/src/components/popover/STYLES.mdx +389 -0
  70. package/src/components/popover/index.ts +3 -0
  71. package/src/components/popover/popover.scss +249 -0
  72. package/src/components/popover/popover.stories.tsx +315 -15
  73. package/src/components/popover/popover.test.tsx +249 -37
  74. package/src/components/popover/popover.tsx +165 -62
  75. package/src/hooks/popover/popover.tsx +26 -10
  76. package/src/hooks/popover/use-popover.tsx +30 -10
  77. package/src/hooks.ts +5 -0
  78. package/src/index.scss +1 -0
  79. package/src/index.ts +1 -0
  80. package/src/styles/buttons/button.css +78 -16
  81. package/src/styles/buttons/button.css.map +1 -1
  82. package/src/styles/buttons/icon-button.css +32 -0
  83. package/src/styles/buttons/icon-button.css.map +1 -0
  84. package/src/styles/index.css +268 -18
  85. package/src/styles/index.css.map +1 -1
  86. package/src/styles/link/link.css +2 -2
  87. package/src/styles/popover/popover.css +190 -0
  88. package/src/styles/popover/popover.css.map +1 -0
  89. package/src/types/popover.d.ts +64 -0
  90. package/libs/chunk-4I5MF54P.js +0 -8
  91. package/libs/chunk-4I5MF54P.js.map +0 -1
  92. package/libs/chunk-GCGKYLDG.js +0 -7
  93. package/libs/chunk-GCGKYLDG.js.map +0 -1
  94. package/libs/chunk-NZVSXRTB.cjs +0 -16
  95. package/libs/chunk-NZVSXRTB.cjs.map +0 -1
  96. package/libs/chunk-PDD4N5P5.cjs +0 -10
  97. package/libs/chunk-PDD4N5P5.cjs.map +0 -1
  98. package/libs/chunk-S7NIA6PI.cjs +0 -17
  99. package/libs/chunk-S7NIA6PI.cjs.map +0 -1
  100. package/libs/chunk-X2RDXWH5.js +0 -10
  101. package/libs/chunk-X2RDXWH5.js.map +0 -1
  102. /package/libs/{chunk-SQ44OCJ2.js.map → chunk-6NMLU5FA.js.map} +0 -0
  103. /package/libs/{chunk-GVVCXXKI.cjs.map → chunk-6YVR4TDM.cjs.map} +0 -0
  104. /package/libs/{chunk-H6A2CUWA.js.map → chunk-VQTCTLFN.js.map} +0 -0
  105. /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/Basic Popover",
10
+ title: "FP.React Components/Popover",
8
11
  component: Popover,
9
- tags: ["experimental"],
10
- args: {
11
- children: "Hi, I am a popover.",
12
- popoverTrigger: "Hover here",
13
- styles: Popover.styles,
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
- export const PopoverComponent: Story = {
21
- args: {},
22
- play: async ({ canvasElement }) => {
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
- expect(await canvas.queryByText("Hover here")).toBeInTheDocument();
25
- userEvent.hover(canvas.getByText("Hover here"));
26
- expect(await canvas.findByText("Hi, I am a popover.")).toBeInTheDocument();
27
- await userEvent.unhover(canvas.getByText("Hover here"));
28
- expect(canvas.queryByText("Hi, I am a popover.")).not.toBeInTheDocument();
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, fireEvent } from '@testing-library/react'
3
- import Popover from './popover'
4
- import userEvent from '@testing-library/user-event'
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 the popover content when the trigger element is hovered', async () => {
8
- render(
9
- <Popover popoverTrigger={<button>Hover here</button>}>
10
- Popover content
11
- </Popover>,
12
- )
13
-
14
- const triggerElement = screen.getByRole('button', {
15
- name: /hover here/i,
16
- })
17
- userEvent.hover(triggerElement)
18
- expect(await screen.findByText('Popover content')).toBeInTheDocument()
19
- })
20
-
21
- it('hides the popover content when the trigger element is no longer hovered', () => {
22
- render(
23
- <Popover popoverTrigger={<button>Hover here</button>}>
24
- Popover content
25
- </Popover>,
26
- )
27
-
28
- const triggerElement = screen.getByRole('button', {
29
- name: /hover here/i,
30
- })
31
- fireEvent.pointerEnter(triggerElement)
32
-
33
- const popoverContent = screen.getByText('Popover content')
34
- expect(popoverContent).toBeInTheDocument()
35
-
36
- fireEvent.pointerLeave(triggerElement)
37
- expect(popoverContent).not.toBeInTheDocument()
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
+ });