@purpurds/popover 0.0.1

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 (72) hide show
  1. package/dist/LICENSE.txt +905 -0
  2. package/dist/metadata.js +8 -0
  3. package/dist/popover-back.d.ts +9 -0
  4. package/dist/popover-back.d.ts.map +1 -0
  5. package/dist/popover-button.d.ts +37 -0
  6. package/dist/popover-button.d.ts.map +1 -0
  7. package/dist/popover-content.d.ts +93 -0
  8. package/dist/popover-content.d.ts.map +1 -0
  9. package/dist/popover-flow.d.ts +65 -0
  10. package/dist/popover-flow.d.ts.map +1 -0
  11. package/dist/popover-footer.d.ts +16 -0
  12. package/dist/popover-footer.d.ts.map +1 -0
  13. package/dist/popover-header.d.ts +7 -0
  14. package/dist/popover-header.d.ts.map +1 -0
  15. package/dist/popover-internal-context.d.ts +15 -0
  16. package/dist/popover-internal-context.d.ts.map +1 -0
  17. package/dist/popover-next.d.ts +9 -0
  18. package/dist/popover-next.d.ts.map +1 -0
  19. package/dist/popover-standalone.d.ts +12 -0
  20. package/dist/popover-standalone.d.ts.map +1 -0
  21. package/dist/popover-steps.d.ts +6 -0
  22. package/dist/popover-steps.d.ts.map +1 -0
  23. package/dist/popover-trigger.d.ts +27 -0
  24. package/dist/popover-trigger.d.ts.map +1 -0
  25. package/dist/popover-walkthrough.d.ts +13 -0
  26. package/dist/popover-walkthrough.d.ts.map +1 -0
  27. package/dist/popover.cjs.js +42 -0
  28. package/dist/popover.cjs.js.map +1 -0
  29. package/dist/popover.d.ts +36 -0
  30. package/dist/popover.d.ts.map +1 -0
  31. package/dist/popover.es.js +3849 -0
  32. package/dist/popover.es.js.map +1 -0
  33. package/dist/styles.css +1 -0
  34. package/dist/use-screen-size.hook.d.ts +7 -0
  35. package/dist/use-screen-size.hook.d.ts.map +1 -0
  36. package/dist/use-smooth-scroll.d.ts +5 -0
  37. package/dist/use-smooth-scroll.d.ts.map +1 -0
  38. package/dist/usePopoverTrigger.d.ts +5 -0
  39. package/dist/usePopoverTrigger.d.ts.map +1 -0
  40. package/dist/usePopoverWalkthrough.d.ts +7 -0
  41. package/dist/usePopoverWalkthrough.d.ts.map +1 -0
  42. package/eslint.config.mjs +2 -0
  43. package/package.json +82 -0
  44. package/src/global.d.ts +4 -0
  45. package/src/popover-back.test.tsx +63 -0
  46. package/src/popover-back.tsx +40 -0
  47. package/src/popover-button.test.tsx +51 -0
  48. package/src/popover-button.tsx +84 -0
  49. package/src/popover-content.test.tsx +1122 -0
  50. package/src/popover-content.tsx +277 -0
  51. package/src/popover-flow.tsx +170 -0
  52. package/src/popover-footer.test.tsx +21 -0
  53. package/src/popover-footer.tsx +32 -0
  54. package/src/popover-header.test.tsx +22 -0
  55. package/src/popover-header.tsx +32 -0
  56. package/src/popover-internal-context.tsx +28 -0
  57. package/src/popover-next.test.tsx +61 -0
  58. package/src/popover-next.tsx +40 -0
  59. package/src/popover-standalone.tsx +48 -0
  60. package/src/popover-steps.tsx +32 -0
  61. package/src/popover-trigger.tsx +71 -0
  62. package/src/popover-walkthrough.test.tsx +346 -0
  63. package/src/popover-walkthrough.tsx +45 -0
  64. package/src/popover.module.scss +315 -0
  65. package/src/popover.stories.tsx +1157 -0
  66. package/src/popover.test.tsx +642 -0
  67. package/src/popover.tsx +76 -0
  68. package/src/use-screen-size.hook.ts +39 -0
  69. package/src/use-smooth-scroll.ts +62 -0
  70. package/src/usePopoverTrigger.ts +59 -0
  71. package/src/usePopoverWalkthrough.ts +85 -0
  72. package/vitest.setup.ts +30 -0
@@ -0,0 +1,1157 @@
1
+ import React from "react";
2
+ import { Button } from "@purpurds/button";
3
+ import { Heading } from "@purpurds/heading";
4
+ import { IconHeart } from "@purpurds/icon/heart";
5
+ import { IconInfo } from "@purpurds/icon/info";
6
+ import { Link } from "@purpurds/link";
7
+ import { Paragraph } from "@purpurds/paragraph";
8
+ import { type Meta, type StoryObj } from "@storybook/react";
9
+
10
+ import "@purpurds/link/styles";
11
+ import "@purpurds/icon/styles";
12
+ import "@purpurds/button/styles";
13
+ import "@purpurds/heading/styles";
14
+ import "@purpurds/paragraph/styles";
15
+ import { Popover, type PopoverAction } from "./popover";
16
+
17
+ const meta: Meta<typeof Popover> = {
18
+ title: "Dialogs and Overlays/Popover",
19
+ component: Popover,
20
+ subcomponents: {
21
+ "Popover.Trigger": Popover.Trigger,
22
+ "Popover.Content": Popover.Content,
23
+ "Popover.Footer": Popover.Footer,
24
+ "Popover.Button": Popover.Button,
25
+ "Popover.Flow": Popover.Flow,
26
+ },
27
+ parameters: {
28
+ layout: "centered",
29
+ docs: {
30
+ description: {
31
+ component: `
32
+ ## Popover
33
+
34
+ Popovers help users discover new features and important changes in the UI, enhancing the user experience and driving adoption of high-value features.
35
+
36
+ **Built on [Radix Popover](https://www.radix-ui.com/primitives/docs/components/popover) for accessibility and positioning.**
37
+
38
+ **Usage guidelines:**
39
+ - Prefer single-step popovers for clarity. If a walkthrough is needed, keep it to 3–5 steps maximum and follow the user's natural workflow order.
40
+ - Use popovers for features with high business value or those that create new value for users, not just minor improvements.
41
+ - Consider user segmentation and avoid conflicting or redundant messages. Bundle related highlights when possible.
42
+
43
+ [See Figma design](https://www.figma.com/design/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Components-and-guidelines?node-id=43484-6510&p=f&m=dev)
44
+ `,
45
+ },
46
+ },
47
+ design: [
48
+ {
49
+ name: "Popover",
50
+ type: "figma",
51
+ url: "https://www.figma.com/design/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Components-and-guidelines?node-id=43484-6510&p=f&m=dev",
52
+ },
53
+ ],
54
+ },
55
+ argTypes: {
56
+ open: {
57
+ control: "boolean",
58
+ description: "Controls whether the popover is open",
59
+ },
60
+ },
61
+ };
62
+
63
+ export default meta;
64
+
65
+ interface StandaloneArgs {
66
+ negative: boolean;
67
+ beakPosition: "up" | "right" | "down" | "left" | "none";
68
+ align: "start" | "center" | "end";
69
+ closeIconAriaLabel: string;
70
+ headerTitle: string;
71
+ headerIcon: boolean;
72
+ body: string;
73
+ showFooter: boolean;
74
+ button: boolean;
75
+ buttonText: string;
76
+ highlight: boolean;
77
+ }
78
+
79
+ interface WalkthroughArgs {
80
+ separatorText: string;
81
+ stepText: string;
82
+ backLabel: string;
83
+ nextLabel: string;
84
+ finishLabel: string;
85
+ closeIconAriaLabel: string;
86
+ step1BeakPosition: "up" | "right" | "down" | "left" | "none";
87
+ step2BeakPosition: "up" | "right" | "down" | "left" | "none";
88
+ step3BeakPosition: "up" | "right" | "down" | "left" | "none";
89
+ step4BeakPosition: "up" | "right" | "down" | "left" | "none";
90
+ avoidCollisions: boolean;
91
+ step1Body: string;
92
+ step2Body: string;
93
+ step3Body: string;
94
+ step4Body: string;
95
+ }
96
+
97
+ /**
98
+ * Standalone
99
+ *
100
+ * A popover highlights new features or significant changes in existing ones. Content may link to help articles or telia.se/foretag pages. Triggered by user action or appearing on a new or updated page, it's a single callout with no subsequent steps, designed for onboarding users to new or improved features/flows.
101
+ *
102
+ * Use cases: Introducing a feature, highlighting significant changes, prompting user action.
103
+ *
104
+ * Props:
105
+ * - Built on Radix Popover for accessibility and positioning.
106
+ * - `title` (required): string — header title text.
107
+ * - `icon` (optional): ReactNode — optional header icon.
108
+ * - `body` (required): string — main content.
109
+ * - `beakPosition`, `align`, `showArrow` (optional): Placement and appearance.
110
+ * - `negative`, `highlight` (optional): Visual variants.
111
+ * - `closeIconAriaLabel` (required for accessibility): Label for close icon.
112
+ * - All other [Radix Popover props](https://www.radix-ui.com/primitives/docs/components/popover#root) are supported.
113
+ *
114
+ * Note: Story args like `headerTitle`, `headerIcon`, `buttonText`, etc. are for Storybook controls only and are mapped to the actual component props in the render function.
115
+ */
116
+ export const Standalone: StoryObj = {
117
+ args: {
118
+ negative: false,
119
+ beakPosition: "down",
120
+ align: "center",
121
+ closeIconAriaLabel: "Close",
122
+ headerTitle: "This is a title",
123
+ headerIcon: true,
124
+ body: "Body text lorem ipsum dolor sit amet, consectetur adipiscing elit.",
125
+ showFooter: true,
126
+ button: false,
127
+ buttonText: "Got it",
128
+ highlight: false,
129
+ },
130
+ argTypes: {
131
+ negative: {
132
+ control: "boolean",
133
+ description: "Use negative (light) variant",
134
+ },
135
+ beakPosition: {
136
+ control: "select",
137
+ options: ["up", "right", "down", "left", "none"],
138
+ description:
139
+ "Position of the popover beak/arrow (up, right, down, left). Use none to hide the arrow.",
140
+ },
141
+ align: {
142
+ control: "select",
143
+ options: ["start", "center", "end"],
144
+ description: "Alignment of the popover",
145
+ },
146
+ closeIconAriaLabel: {
147
+ control: "text",
148
+ description: "Accessible label for close icon",
149
+ },
150
+ headerTitle: {
151
+ control: "text",
152
+ description: "Title text (required prop in component, mapped from this control)",
153
+ },
154
+ headerIcon: {
155
+ control: "boolean",
156
+ description: "Show header icon (optional icon prop in component)",
157
+ },
158
+ body: {
159
+ control: "text",
160
+ description: "Body text content",
161
+ },
162
+ showFooter: {
163
+ control: "boolean",
164
+ description: "Show footer with button",
165
+ },
166
+ button: {
167
+ control: "boolean",
168
+ description: "Use button as trigger",
169
+ },
170
+ buttonText: {
171
+ control: "text",
172
+ description: "Footer button text",
173
+ },
174
+ highlight: {
175
+ control: "boolean",
176
+ description: "Highlight the trigger",
177
+ },
178
+ },
179
+ render: (args) => {
180
+ const typedArgs = args as unknown as StandaloneArgs;
181
+ const backgroundColor = typedArgs.negative
182
+ ? "var(--purpur-color-background-tone-on-tone-primary)"
183
+ : "transparent";
184
+
185
+ return (
186
+ <div
187
+ style={{
188
+ padding: "50px",
189
+ backgroundColor,
190
+ minHeight: "450px",
191
+ minWidth: "450px",
192
+ display: "flex",
193
+ placeContent: "center",
194
+ alignItems: "center",
195
+ }}
196
+ >
197
+ <Popover>
198
+ <Popover.Trigger highlight={typedArgs.highlight}>
199
+ {typedArgs.button ? (
200
+ <Button variant="primary">{typedArgs.buttonText}</Button>
201
+ ) : (
202
+ <Button variant="primary" aria-label={typedArgs.buttonText} iconOnly>
203
+ <IconInfo />
204
+ </Button>
205
+ )}
206
+ </Popover.Trigger>
207
+ <Popover.Content
208
+ negative={typedArgs.negative}
209
+ beakPosition={typedArgs.beakPosition}
210
+ align={typedArgs.align}
211
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
212
+ title={typedArgs.headerTitle || "Default Title"}
213
+ {...(typedArgs.headerIcon && { icon: <IconHeart size="sm" /> })}
214
+ body={typedArgs.body}
215
+ onAction={(action: PopoverAction) => console.log("Popover action:", action)}
216
+ >
217
+ {typedArgs.showFooter && (
218
+ <Popover.Footer>
219
+ <Popover.Button onClick={() => console.log("Popover dismissed")}>
220
+ {typedArgs.buttonText}
221
+ </Popover.Button>
222
+ </Popover.Footer>
223
+ )}
224
+ </Popover.Content>
225
+ </Popover>
226
+ </div>
227
+ );
228
+ },
229
+ };
230
+
231
+ /**
232
+ * User Initiated Walkthrough
233
+ *
234
+ * A walkthrough is an interactive tour guiding users through a series of hands-on steps. It effectively introduces new features within a single page, a sequence of steps across multiple pages, or highlights a feature's immediate business value.
235
+ *
236
+ * All popovers in a walkthrough must be wrapped under a single `PopoverFlow` component to ensure correct step management and navigation.
237
+ *
238
+ * Use cases: Onboarding users to a new workflow, providing a tour of new features, or offering contextual assistance during MyBusiness configuration.
239
+ *
240
+ * Props (for the Popover component):
241
+ * - Built on Radix Popover for accessibility and positioning.
242
+ * - `multistep` (required): true
243
+ * - `step` (required): number — the step index for this popover in the walkthrough. Steps must be sequential (e.g., 1, 2, 3, ...).
244
+ * - `initialStep` (optional): number — sets which step is open when the walkthrough mounts. If set to 0, the walkthrough starts closed and no step is shown initially.
245
+ * - `title` (required): string — header title text.
246
+ * - `icon` (optional): ReactNode — optional header icon.
247
+ * - `body` (required): string — main content.
248
+ * - `side`, `align`, `showArrow` (optional): Placement and appearance.
249
+ * - `negative`, `highlight` (optional): Visual variants.
250
+ * - `closeIconAriaLabel` (required for accessibility): Label for close icon.
251
+ * - All other [Radix Popover props](https://www.radix-ui.com/primitives/docs/components/popover#root) are supported.
252
+ *
253
+ * Note: Story args like `separatorText`, `stepText`, `backLabel`, `nextLabel`, `finishLabel`, etc. are for Storybook controls only and are mapped to the actual component props in the render function or via the PopoverFlow provider.
254
+ */
255
+ export const UserInitiatedWalkthrough: StoryObj = {
256
+ args: {
257
+ separatorText: "of",
258
+ stepText: "Step",
259
+ backLabel: "Back",
260
+ nextLabel: "Next",
261
+ finishLabel: "Finish",
262
+ closeIconAriaLabel: "Exit tour",
263
+ openDelay: 0,
264
+ },
265
+ argTypes: {
266
+ separatorText: {
267
+ control: "text",
268
+ description: "Separator text between step numbers",
269
+ },
270
+ stepText: {
271
+ control: "text",
272
+ description: "Label for 'Step'",
273
+ },
274
+ backLabel: {
275
+ control: "text",
276
+ description: "Label for back button",
277
+ },
278
+ nextLabel: {
279
+ control: "text",
280
+ description: "Label for next button",
281
+ },
282
+ finishLabel: {
283
+ control: "text",
284
+ description: "Label for finish button",
285
+ },
286
+ closeIconAriaLabel: {
287
+ control: "text",
288
+ description: "Accessible label for close icon",
289
+ },
290
+ openDelay: {
291
+ control: "number",
292
+ description: "Delay in milliseconds before opening the popover",
293
+ },
294
+ },
295
+ render: function UserInitiatedWalkthroughStory(args) {
296
+ const typedArgs = args as unknown as {
297
+ separatorText: string;
298
+ stepText: string;
299
+ backLabel: string;
300
+ nextLabel: string;
301
+ finishLabel: string;
302
+ closeIconAriaLabel: string;
303
+ openDelay: number;
304
+ };
305
+
306
+ const [walkthroughStarted, setWalkthroughStarted] = React.useState(false);
307
+ const [walkthroughCompleted, setWalkthroughCompleted] = React.useState(false);
308
+ const startButtonRef = React.useRef<HTMLButtonElement>(null);
309
+
310
+ const handleStartWalkthrough = () => {
311
+ setWalkthroughStarted(true);
312
+ setWalkthroughCompleted(false);
313
+ };
314
+
315
+ const handleAction = (action: PopoverAction) => {
316
+ console.log("Walkthrough action:", action);
317
+
318
+ if (action.type === "finish" || action.type === "dismiss") {
319
+ setWalkthroughCompleted(true);
320
+ setWalkthroughStarted(false);
321
+
322
+ // Return focus to the start button
323
+ setTimeout(() => {
324
+ startButtonRef.current?.focus();
325
+ console.log("Focus returned to start button");
326
+ }, 100);
327
+
328
+ if (action.type === "finish") {
329
+ console.log("Walkthrough completed successfully");
330
+ } else {
331
+ console.log("Walkthrough dismissed");
332
+ }
333
+ }
334
+ };
335
+
336
+ return (
337
+ <div
338
+ style={{
339
+ minHeight: "600px",
340
+ position: "relative",
341
+ padding: "40px",
342
+ }}
343
+ >
344
+ {/* Header with Start Button */}
345
+ <div
346
+ style={{
347
+ display: "flex",
348
+ flexDirection: "column",
349
+ alignItems: "center",
350
+ gap: "16px",
351
+ marginBottom: "40px",
352
+ paddingBottom: "20px",
353
+ borderBottom: "1px solid var(--purpur-color-border-secondary)",
354
+ }}
355
+ >
356
+ <Heading tag="h2">User-Initiated Walkthrough Demo</Heading>
357
+ <p style={{ textAlign: "center", maxWidth: "500px" }}>
358
+ This demonstrates a walkthrough that starts when the user clicks the button below. After
359
+ completion or dismissal, focus returns to the start button.
360
+ </p>
361
+ <Button
362
+ ref={startButtonRef}
363
+ variant="primary"
364
+ onClick={handleStartWalkthrough}
365
+ disabled={walkthroughStarted}
366
+ >
367
+ {walkthroughStarted ? "Tour in Progress..." : "Start Product Tour"}
368
+ </Button>
369
+ {walkthroughCompleted && (
370
+ <div
371
+ style={{
372
+ backgroundColor: "var(--purpur-color-background-tone-success)",
373
+ color: "var(--purpur-color-text-on-color)",
374
+ padding: "12px 24px",
375
+ borderRadius: "4px",
376
+ fontSize: "14px",
377
+ }}
378
+ >
379
+ ✅ Tour completed! Focus has returned to the start button.
380
+ </div>
381
+ )}
382
+ </div>
383
+
384
+ {/* Main Content Area - Always Visible */}
385
+ <div
386
+ style={{
387
+ position: "relative",
388
+ minHeight: "400px",
389
+ backgroundColor: "var(--purpur-color-background-surface)",
390
+ borderRadius: "8px",
391
+ padding: "20px",
392
+ }}
393
+ >
394
+ {walkthroughStarted && (
395
+ <Popover.Flow
396
+ separatorText={typedArgs.separatorText}
397
+ stepText={typedArgs.stepText}
398
+ backLabel={typedArgs.backLabel}
399
+ nextLabel={typedArgs.nextLabel}
400
+ finishLabel={typedArgs.finishLabel}
401
+ openDelay={typedArgs.openDelay}
402
+ >
403
+ {/* Step 1 - Top Left */}
404
+ <div style={{ position: "absolute", top: "40px", left: "40px" }}>
405
+ <Popover multistep step={1}>
406
+ <Popover.Trigger>
407
+ <Button variant="primary" iconOnly>
408
+ <IconHeart />
409
+ </Button>
410
+ </Popover.Trigger>
411
+ <Popover.Content
412
+ beakPosition="down"
413
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
414
+ title="Welcome to the Tour"
415
+ icon={<IconHeart size="sm" />}
416
+ body="Let's explore the main features of this application. Click Next to continue."
417
+ onAction={handleAction}
418
+ />
419
+ </Popover>
420
+ </div>
421
+
422
+ {/* Step 2 - Top Right */}
423
+ <div style={{ position: "absolute", top: "40px", right: "40px" }}>
424
+ <Popover multistep step={2}>
425
+ <Popover.Trigger>
426
+ <Button variant="primary">Settings</Button>
427
+ </Popover.Trigger>
428
+ <Popover.Content
429
+ beakPosition="down"
430
+ align="end"
431
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
432
+ title="Customize Your Experience"
433
+ body="Access your settings and preferences here. You can customize themes, notifications, and more."
434
+ onAction={handleAction}
435
+ />
436
+ </Popover>
437
+ </div>
438
+
439
+ {/* Step 3 - Center */}
440
+ <div
441
+ style={{
442
+ position: "absolute",
443
+ top: "50%",
444
+ left: "50%",
445
+ transform: "translate(-50%, -50%)",
446
+ }}
447
+ >
448
+ <div
449
+ style={{
450
+ padding: "40px",
451
+ backgroundColor: "var(--purpur-color-background-base)",
452
+ borderRadius: "8px",
453
+ textAlign: "center",
454
+ boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
455
+ }}
456
+ >
457
+ <Heading tag="h3" style={{ marginBottom: "16px" }}>
458
+ Main Content Area
459
+ </Heading>
460
+ <Popover multistep step={3}>
461
+ <Popover.Trigger>
462
+ <Button variant="primary">Get Started</Button>
463
+ </Popover.Trigger>
464
+ <Popover.Content
465
+ beakPosition="up"
466
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
467
+ title="Ready to Begin!"
468
+ body="This is where you'll spend most of your time. Click Complete to finish the tour and return focus to the start button."
469
+ onAction={handleAction}
470
+ />
471
+ </Popover>
472
+ </div>
473
+ </div>
474
+ </Popover.Flow>
475
+ )}
476
+
477
+ {/* Static elements when walkthrough is not active */}
478
+ {!walkthroughStarted && (
479
+ <>
480
+ <div style={{ position: "absolute", top: "40px", left: "40px" }}>
481
+ <Button variant="primary" iconOnly disabled>
482
+ <IconHeart />
483
+ </Button>
484
+ </div>
485
+ <div style={{ position: "absolute", top: "40px", right: "40px" }}>
486
+ <Button variant="primary" disabled>
487
+ Settings
488
+ </Button>
489
+ </div>
490
+ <div
491
+ style={{
492
+ position: "absolute",
493
+ top: "50%",
494
+ left: "50%",
495
+ transform: "translate(-50%, -50%)",
496
+ padding: "40px",
497
+ backgroundColor: "var(--purpur-color-background-base)",
498
+ borderRadius: "8px",
499
+ textAlign: "center",
500
+ boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
501
+ }}
502
+ >
503
+ <Heading tag="h3" style={{ marginBottom: "16px" }}>
504
+ Main Content Area
505
+ </Heading>
506
+ <Button variant="primary" disabled>
507
+ Get Started
508
+ </Button>
509
+ </div>
510
+ </>
511
+ )}
512
+ </div>
513
+ </div>
514
+ );
515
+ },
516
+ };
517
+
518
+ export const Walkthrough: StoryObj = {
519
+ parameters: {
520
+ docs: { disable: true },
521
+ },
522
+ args: {
523
+ separatorText: "of",
524
+ stepText: "Step",
525
+ backLabel: "Back",
526
+ nextLabel: "Next",
527
+ finishLabel: "Finish",
528
+ closeIconAriaLabel: "Close",
529
+ step1BeakPosition: "down",
530
+ step2BeakPosition: "down",
531
+ step3BeakPosition: "up",
532
+ step4BeakPosition: "down",
533
+ avoidCollisions: false,
534
+ step1Body: "Start here in the top left corner. This is the first step.",
535
+ step2Body: "Continue here in the top right corner. This is the second step.",
536
+ step3Body: "Now we're at the bottom of the page. This is the third step.",
537
+ step4Body: "Finish here in the middle. You are done!",
538
+ },
539
+ argTypes: {
540
+ separatorText: {
541
+ control: "text",
542
+ description: "Separator text between step numbers",
543
+ },
544
+ stepText: {
545
+ control: "text",
546
+ description: "Label for 'Step'",
547
+ },
548
+ backLabel: {
549
+ control: "text",
550
+ description: "Label for back button",
551
+ },
552
+ nextLabel: {
553
+ control: "text",
554
+ description: "Label for next button",
555
+ },
556
+ finishLabel: {
557
+ control: "text",
558
+ description: "Label for finish button",
559
+ },
560
+ closeIconAriaLabel: {
561
+ control: "text",
562
+ description: "Accessible label for close icon",
563
+ },
564
+ step1BeakPosition: {
565
+ control: "select",
566
+ options: ["up", "right", "down", "left", "none"],
567
+ description: "Beak position for step 1",
568
+ },
569
+ step2BeakPosition: {
570
+ control: "select",
571
+ options: ["up", "right", "down", "left", "none"],
572
+ description: "Beak position for step 2",
573
+ },
574
+ step3BeakPosition: {
575
+ control: "select",
576
+ options: ["up", "right", "down", "left", "none"],
577
+ description: "Beak position for step 3",
578
+ },
579
+ step4BeakPosition: {
580
+ control: "select",
581
+ options: ["up", "right", "down", "left", "none"],
582
+ description: "Beak position for step 4",
583
+ },
584
+ avoidCollisions: {
585
+ control: "boolean",
586
+ description: "Avoid collisions with viewport edges",
587
+ },
588
+ step1Body: {
589
+ control: "text",
590
+ description: "Body text for step 1",
591
+ },
592
+ step2Body: {
593
+ control: "text",
594
+ description: "Body text for step 2",
595
+ },
596
+ step3Body: {
597
+ control: "text",
598
+ description: "Body text for step 3",
599
+ },
600
+ step4Body: {
601
+ control: "text",
602
+ description: "Body text for step 4",
603
+ },
604
+ },
605
+ render: function WalkthroughStory(args) {
606
+ const typedArgs = args as unknown as WalkthroughArgs;
607
+ const [key, setKey] = React.useState(0);
608
+ const [showAutoStartMessage, setShowAutoStartMessage] = React.useState(true);
609
+ const [walkthroughCompleted, setWalkthroughCompleted] = React.useState(false);
610
+
611
+ const handleAction = (action: PopoverAction) => {
612
+ console.log("Popover action:", action);
613
+
614
+ // Handle different completion scenarios
615
+ if (action.type === "finish") {
616
+ console.log("Walkthrough completed successfully!");
617
+ setShowAutoStartMessage(false);
618
+ setWalkthroughCompleted(true);
619
+
620
+ // Example: Consumer can decide what to do here
621
+ // - Redirect to a specific page
622
+ // - Focus on a specific element
623
+ // - Save user preference to not show again
624
+ // - Enable certain features
625
+ // window.location.href = '/dashboard';
626
+ // document.querySelector('#main-feature')?.focus();
627
+ } else if (action.type === "dismiss") {
628
+ console.log("Walkthrough dismissed by user");
629
+ setShowAutoStartMessage(false);
630
+
631
+ // Example: Different behavior for dismiss
632
+ // - Maybe show a "resume tour" option
633
+ // - Track analytics that user skipped
634
+ // - Show a less intrusive hint later
635
+ }
636
+ };
637
+
638
+ return (
639
+ <div
640
+ style={{
641
+ width: "100vw",
642
+ height: "250vh",
643
+ position: "relative",
644
+ }}
645
+ >
646
+ {showAutoStartMessage && (
647
+ <div
648
+ style={{
649
+ position: "fixed",
650
+ top: "20px",
651
+ left: "50%",
652
+ transform: "translateX(-50%)",
653
+ backgroundColor: "var(--purpur-color-background-tone-info)",
654
+ color: "var(--purpur-color-text-on-color)",
655
+ padding: "12px 24px",
656
+ borderRadius: "4px",
657
+ zIndex: 1000,
658
+ fontSize: "14px",
659
+ }}
660
+ >
661
+ 🎉 Welcome! We&apos;ve added new features. Let&apos;s take a quick tour.
662
+ </div>
663
+ )}
664
+
665
+ {walkthroughCompleted && (
666
+ <div
667
+ style={{
668
+ position: "fixed",
669
+ top: "20px",
670
+ left: "50%",
671
+ transform: "translateX(-50%)",
672
+ backgroundColor: "var(--purpur-color-background-tone-success)",
673
+ color: "var(--purpur-color-text-on-color)",
674
+ padding: "12px 24px",
675
+ borderRadius: "4px",
676
+ zIndex: 1000,
677
+ fontSize: "14px",
678
+ }}
679
+ >
680
+ ✅ Tour completed! You can now explore the new features.
681
+ </div>
682
+ )}
683
+
684
+ <div
685
+ style={{
686
+ paddingTop: "60px",
687
+ display: "flex",
688
+ justifyContent: "center",
689
+ }}
690
+ >
691
+ <Button
692
+ variant="primary"
693
+ onClick={() => {
694
+ setKey((prev) => prev + 1);
695
+ setShowAutoStartMessage(true);
696
+ setWalkthroughCompleted(false);
697
+ }}
698
+ >
699
+ Restart Walkthrough
700
+ </Button>
701
+ </div>
702
+
703
+ <Popover.Flow
704
+ key={key}
705
+ separatorText={typedArgs.separatorText}
706
+ stepText={typedArgs.stepText}
707
+ backLabel={typedArgs.backLabel}
708
+ nextLabel={typedArgs.nextLabel}
709
+ finishLabel={typedArgs.finishLabel}
710
+ >
711
+ {/* Step 1 - Top Left - New Dashboard Widget */}
712
+ <div style={{ position: "absolute", top: "150px", left: "100px" }}>
713
+ <div
714
+ style={{
715
+ border: "2px dashed var(--purpur-color-border-primary)",
716
+ padding: "20px",
717
+ borderRadius: "8px",
718
+ background: "var(--purpur-color-background-surface)",
719
+ }}
720
+ >
721
+ <Heading tag="h3" style={{ marginBottom: "8px" }}>
722
+ 📊 Analytics Widget
723
+ </Heading>
724
+ <p style={{ color: "var(--purpur-color-text-secondary)" }}>New feature!</p>
725
+ <Popover multistep step={1}>
726
+ <Popover.Trigger>
727
+ <Button variant="primary" size="sm">
728
+ View Analytics
729
+ </Button>
730
+ </Popover.Trigger>
731
+ <Popover.Content
732
+ beakPosition={typedArgs.step1BeakPosition}
733
+ avoidCollisions={typedArgs.avoidCollisions}
734
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
735
+ title="New: Analytics Dashboard"
736
+ body="Track your performance with our new analytics widget. View real-time data and insights."
737
+ onAction={handleAction}
738
+ />
739
+ </Popover>
740
+ </div>
741
+ </div>
742
+
743
+ {/* Step 2 - Top Right - Quick Actions */}
744
+ <div style={{ position: "absolute", top: "150px", right: "100px" }}>
745
+ <div
746
+ style={{
747
+ border: "2px dashed var(--purpur-color-border-primary)",
748
+ padding: "20px",
749
+ borderRadius: "8px",
750
+ background: "var(--purpur-color-background-surface)",
751
+ }}
752
+ >
753
+ <Heading tag="h3" style={{ marginBottom: "8px" }}>
754
+ ⚡ Quick Actions
755
+ </Heading>
756
+ <p style={{ color: "var(--purpur-color-text-secondary)" }}>New feature!</p>
757
+ <Popover multistep step={2}>
758
+ <Popover.Trigger>
759
+ <Button variant="primary" size="sm">
760
+ Quick Actions
761
+ </Button>
762
+ </Popover.Trigger>
763
+ <Popover.Content
764
+ beakPosition={typedArgs.step2BeakPosition}
765
+ align="end"
766
+ avoidCollisions={typedArgs.avoidCollisions}
767
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
768
+ title="New: Quick Actions Menu"
769
+ body="Access frequently used actions with one click. Customize your shortcuts here."
770
+ onAction={handleAction}
771
+ />
772
+ </Popover>
773
+ </div>
774
+ </div>
775
+
776
+ {/* Step 3 - Bottom - Export Feature */}
777
+ <div
778
+ style={{
779
+ position: "absolute",
780
+ bottom: "100px",
781
+ left: "50%",
782
+ transform: "translateX(-50%)",
783
+ }}
784
+ >
785
+ <div
786
+ style={{
787
+ border: "2px dashed var(--purpur-color-border-primary)",
788
+ padding: "20px",
789
+ borderRadius: "8px",
790
+ background: "var(--purpur-color-background-surface)",
791
+ }}
792
+ >
793
+ <Heading tag="h3" style={{ marginBottom: "8px" }}>
794
+ 💾 Export Center
795
+ </Heading>
796
+ <p style={{ color: "var(--purpur-color-text-secondary)" }}>New feature!</p>
797
+ <Popover multistep step={3}>
798
+ <Popover.Trigger>
799
+ <Button variant="primary" size="sm">
800
+ Export Data
801
+ </Button>
802
+ </Popover.Trigger>
803
+ <Popover.Content
804
+ beakPosition={typedArgs.step3BeakPosition}
805
+ avoidCollisions={typedArgs.avoidCollisions}
806
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
807
+ title="New: Advanced Export Options"
808
+ body="Export your data in multiple formats. Schedule automatic exports and more."
809
+ onAction={handleAction}
810
+ />
811
+ </Popover>
812
+ </div>
813
+ </div>
814
+
815
+ {/* Step 4 - Middle - Main CTA */}
816
+ <div
817
+ style={{
818
+ position: "absolute",
819
+ top: "50%",
820
+ left: "50%",
821
+ transform: "translate(-50%, -50%)",
822
+ }}
823
+ >
824
+ <div
825
+ style={{
826
+ backgroundColor: "var(--purpur-color-background-tone-on-tone-primary)",
827
+ padding: "80px 120px",
828
+ borderRadius: "8px",
829
+ display: "flex",
830
+ flexDirection: "column",
831
+ justifyContent: "center",
832
+ alignItems: "center",
833
+ width: "500px",
834
+ height: "250px",
835
+ gap: "20px",
836
+ }}
837
+ >
838
+ <Heading tag="h2" negative style={{ textAlign: "center" }}>
839
+ Ready to explore?
840
+ </Heading>
841
+ <Popover multistep step={4}>
842
+ <Popover.Trigger>
843
+ <Link href="#" variant="standalone" negative>
844
+ Start Using New Features
845
+ </Link>
846
+ </Popover.Trigger>
847
+ <Popover.Content
848
+ negative
849
+ beakPosition={typedArgs.step4BeakPosition}
850
+ avoidCollisions={typedArgs.avoidCollisions}
851
+ closeIconAriaLabel={typedArgs.closeIconAriaLabel}
852
+ title="You're All Set!"
853
+ body="Click here to start using the new features. We'll save your preferences and won't show this tour again."
854
+ onAction={handleAction}
855
+ />
856
+ </Popover>
857
+ <Paragraph negative>
858
+ After completing the tour, focus remains here by default. Consumers can handle the
859
+ &apos;finish&apos; or &apos;dismiss&apos; events to control what happens next.
860
+ </Paragraph>
861
+ </div>
862
+ </div>
863
+ </Popover.Flow>
864
+ </div>
865
+ );
866
+ },
867
+ };
868
+
869
+ export const Variants: StoryObj = {
870
+ argTypes: {
871
+ negative: {
872
+ control: "boolean",
873
+ description: "Use negative (light) variant",
874
+ },
875
+ beakPosition: {
876
+ control: "select",
877
+ options: ["Up", "Right", "Down", "Left", "None"],
878
+ description:
879
+ "Position of the popover beak/arrow (Up, Right, Down, Left). Use None to hide the arrow.",
880
+ },
881
+ align: {
882
+ control: "select",
883
+ options: ["start", "center", "end"],
884
+ description: "Alignment of the popover",
885
+ },
886
+ closeIconAriaLabel: {
887
+ control: "text",
888
+ description: "Accessible label for close icon",
889
+ },
890
+ headerTitle: {
891
+ control: "text",
892
+ description: "Title text (required prop in component, mapped from this control)",
893
+ },
894
+ headerIcon: {
895
+ control: "boolean",
896
+ description: "Show header icon (optional icon prop in component)",
897
+ },
898
+ body: {
899
+ control: "text",
900
+ description: "Body text content",
901
+ },
902
+ showFooter: {
903
+ control: "boolean",
904
+ description: "Show footer with button",
905
+ },
906
+ button: {
907
+ control: "boolean",
908
+ description: "Use button as trigger",
909
+ },
910
+ buttonText: {
911
+ control: "text",
912
+ description: "Footer button text",
913
+ },
914
+ highlight: {
915
+ control: "boolean",
916
+ description: "Highlight the trigger",
917
+ },
918
+ },
919
+ render: () => {
920
+ return (
921
+ <div>
922
+ <div
923
+ style={{
924
+ display: "flex",
925
+ width: "800px",
926
+ height: "500px",
927
+ gap: "200px",
928
+ }}
929
+ >
930
+ <div>
931
+ <Popover>
932
+ <Popover.Trigger>
933
+ <Button variant="primary">Default</Button>
934
+ </Popover.Trigger>
935
+ <Popover.Content
936
+ beakPosition="down"
937
+ closeIconAriaLabel="Close"
938
+ title="Default variant"
939
+ body="This has the default dark background."
940
+ >
941
+ <Popover.Footer>
942
+ <Popover.Button>Got it</Popover.Button>
943
+ </Popover.Footer>
944
+ </Popover.Content>
945
+ </Popover>
946
+ </div>
947
+
948
+ <div
949
+ style={{
950
+ backgroundColor: "var(--purpur-color-background-tone-on-tone-primary)",
951
+ padding: "20px",
952
+ borderRadius: "8px",
953
+ height: "500px",
954
+ width: "500px",
955
+ }}
956
+ >
957
+ <div
958
+ style={{
959
+ width: "fit-content",
960
+ }}
961
+ >
962
+ <Popover>
963
+ <Popover.Trigger negative>
964
+ <Button variant="primary" negative>
965
+ Negative
966
+ </Button>
967
+ </Popover.Trigger>
968
+ <Popover.Content
969
+ negative
970
+ closeIconAriaLabel="Close"
971
+ title="Negative variant"
972
+ body="This has a light background with dark text."
973
+ align="start"
974
+ beakPosition="up"
975
+ >
976
+ <Popover.Footer>
977
+ <Popover.Button>Got it</Popover.Button>
978
+ </Popover.Footer>
979
+ </Popover.Content>
980
+ </Popover>
981
+ </div>
982
+ </div>
983
+ </div>
984
+ </div>
985
+ );
986
+ },
987
+ };
988
+
989
+ export const BeakPlacements: StoryObj = {
990
+ render: () => {
991
+ return (
992
+ <div
993
+ style={{
994
+ padding: "100px",
995
+ display: "flex",
996
+ flexDirection: "column",
997
+ gap: "60px",
998
+ }}
999
+ >
1000
+ {/* Basic Positions */}
1001
+ <div>
1002
+ <Heading tag="h3" style={{ marginBottom: "20px", fontSize: "18px", fontWeight: "600" }}>
1003
+ Basic Positions
1004
+ </Heading>
1005
+ <div
1006
+ style={{
1007
+ display: "flex",
1008
+ gap: "32px",
1009
+ flexWrap: "wrap",
1010
+ }}
1011
+ >
1012
+ {(["up", "down", "left", "right"] as const).map((position) => (
1013
+ <Popover key={position}>
1014
+ <Popover.Trigger>
1015
+ <Button variant="primary">Beak {position}</Button>
1016
+ </Popover.Trigger>
1017
+ <Popover.Content
1018
+ beakPosition={position}
1019
+ closeIconAriaLabel="Close"
1020
+ title={`Beak ${position}`}
1021
+ body={`The popover beak points ${position.toLowerCase()}.`}
1022
+ >
1023
+ <Popover.Footer>
1024
+ <Popover.Button>Got it</Popover.Button>
1025
+ </Popover.Footer>
1026
+ </Popover.Content>
1027
+ </Popover>
1028
+ ))}
1029
+ <Popover>
1030
+ <Popover.Trigger>
1031
+ <Button variant="primary">No Arrow</Button>
1032
+ </Popover.Trigger>
1033
+ <Popover.Content
1034
+ beakPosition="none"
1035
+ closeIconAriaLabel="Close"
1036
+ title="No Arrow"
1037
+ body="The popover has no arrow."
1038
+ >
1039
+ <Popover.Footer>
1040
+ <Popover.Button>Got it</Popover.Button>
1041
+ </Popover.Footer>
1042
+ </Popover.Content>
1043
+ </Popover>
1044
+ </div>
1045
+ </div>
1046
+
1047
+ {/* Custom Alignment */}
1048
+ <div>
1049
+ <Heading tag="h3" style={{ marginBottom: "20px", fontSize: "18px", fontWeight: "600" }}>
1050
+ Custom Alignment (Bottom Position)
1051
+ </Heading>
1052
+ <div
1053
+ style={{
1054
+ display: "flex",
1055
+ gap: "32px",
1056
+ flexWrap: "wrap",
1057
+ }}
1058
+ >
1059
+ {(["start", "center", "end"] as const).map((align) => (
1060
+ <Popover key={align}>
1061
+ <Popover.Trigger>
1062
+ <Button variant="primary">Align {align}</Button>
1063
+ </Popover.Trigger>
1064
+ <Popover.Content
1065
+ beakPosition="down"
1066
+ align={align}
1067
+ closeIconAriaLabel="Close"
1068
+ title={`Aligned ${align}`}
1069
+ body={`The popover is aligned to the ${align} of the content.`}
1070
+ >
1071
+ <Popover.Footer>
1072
+ <Popover.Button>Got it</Popover.Button>
1073
+ </Popover.Footer>
1074
+ </Popover.Content>
1075
+ </Popover>
1076
+ ))}
1077
+ </div>
1078
+ </div>
1079
+
1080
+ {/* Custom Offset */}
1081
+ <div>
1082
+ <Heading tag="h3" style={{ marginBottom: "20px", fontSize: "18px", fontWeight: "600" }}>
1083
+ Custom Offset
1084
+ </Heading>
1085
+ <div
1086
+ style={{
1087
+ display: "flex",
1088
+ flexDirection: "column",
1089
+ gap: "40px",
1090
+ }}
1091
+ >
1092
+ {/* Start aligned with offsets */}
1093
+ <div>
1094
+ <Heading
1095
+ tag="h4"
1096
+ style={{ marginBottom: "12px", fontSize: "14px", fontWeight: "500" }}
1097
+ >
1098
+ Start Aligned (Bottom Position)
1099
+ </Heading>
1100
+ <div style={{ display: "flex", gap: "32px", flexWrap: "wrap" }}>
1101
+ {([0, 35, -35] as const).map((offset) => (
1102
+ <Popover key={`start-${offset}`}>
1103
+ <Popover.Trigger>
1104
+ <Button variant="primary">Offset {offset}px</Button>
1105
+ </Popover.Trigger>
1106
+ <Popover.Content
1107
+ beakPosition="down"
1108
+ align="start"
1109
+ alignOffset={offset}
1110
+ closeIconAriaLabel="Close"
1111
+ title={`Start ${offset}px`}
1112
+ body={`Start aligned with ${offset}px offset from the start.`}
1113
+ >
1114
+ <Popover.Footer>
1115
+ <Popover.Button>Got it</Popover.Button>
1116
+ </Popover.Footer>
1117
+ </Popover.Content>
1118
+ </Popover>
1119
+ ))}
1120
+ </div>
1121
+ </div>
1122
+
1123
+ <div>
1124
+ <Heading
1125
+ tag="h4"
1126
+ style={{ marginBottom: "12px", fontSize: "14px", fontWeight: "500" }}
1127
+ >
1128
+ Start Aligned (Left Position)
1129
+ </Heading>
1130
+ <div style={{ display: "flex", gap: "32px", flexWrap: "wrap" }}>
1131
+ {([0, 15, -25] as const).map((offset) => (
1132
+ <Popover key={`start-${offset}`}>
1133
+ <Popover.Trigger>
1134
+ <Button variant="primary">Offset {offset}px</Button>
1135
+ </Popover.Trigger>
1136
+ <Popover.Content
1137
+ beakPosition="left"
1138
+ align="start"
1139
+ alignOffset={offset}
1140
+ closeIconAriaLabel="Close"
1141
+ title={`Start ${offset}px`}
1142
+ body={`Start aligned with ${offset}px offset from the start.`}
1143
+ >
1144
+ <Popover.Footer>
1145
+ <Popover.Button>Got it</Popover.Button>
1146
+ </Popover.Footer>
1147
+ </Popover.Content>
1148
+ </Popover>
1149
+ ))}
1150
+ </div>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+ </div>
1155
+ );
1156
+ },
1157
+ };