@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 @@
1
+ ._purpur-popover__trigger_10ko3_1{position:relative;width:fit-content}._purpur-popover__trigger-highlight_10ko3_5{position:absolute;inset:-2px;border-radius:var(--purpur-border-radius-sm);pointer-events:none}._purpur-popover__trigger-highlight--state1_10ko3_14{border:var(--purpur-border-radius-xs) solid var(--purpur-color-border-interactive-expressive);box-shadow:0 0 0 4px #990ae380}@media(prefers-reduced-motion:no-preference){._purpur-popover__trigger-highlight--state1_10ko3_14{animation:_highlightState1_10ko3_1 var(--purpur-motion-duration-1500) ease-out 3;animation-fill-mode:forwards}}@media(prefers-reduced-motion:reduce){._purpur-popover__trigger-highlight--state1_10ko3_14{opacity:0}}._purpur-popover__trigger-highlight--state1_10ko3_14._purpur-popover__trigger-highlight--negative_10ko3_29{border-color:var(--purpur-color-purple-200);box-shadow:0 0 0 4px #e4b6fb80}._purpur-popover__trigger-highlight--state2_10ko3_33{border:var(--purpur-border-radius-xs) solid var(--purpur-color-border-interactive-expressive);opacity:1}@media(prefers-reduced-motion:reduce){._purpur-popover__trigger-highlight--state2_10ko3_33{opacity:1}}._purpur-popover__trigger-highlight--state2_10ko3_33._purpur-popover__trigger-highlight--negative_10ko3_29{border-color:var(--purpur-color-purple-200)}._purpur-popover__inner_10ko3_45{display:flex;flex-direction:column;align-items:flex-start;gap:var(--purpur-spacing-100);align-self:stretch}._purpur-popover__content_10ko3_52{display:flex;flex-direction:column;align-items:flex-start;gap:var(--purpur-spacing-300);width:288px;padding:var(--purpur-spacing-200);border-radius:var(--purpur-border-radius-md);box-shadow:var(--purpur-shadow-lg);z-index:var(--popover-z-index)}@media(prefers-reduced-motion:no-preference){._purpur-popover__content_10ko3_52[data-state=open]{animation:_fadeIn_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=open][data-side=top]{animation:_fadeIn_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideFromTop_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=open][data-side=bottom]{animation:_fadeIn_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideFromBottom_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=open][data-side=left]{animation:_fadeIn_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideFromLeft_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=open][data-side=right]{animation:_fadeIn_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideFromRight_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=closed]{animation:_fadeOut_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=closed][data-side=top]{animation:_fadeOut_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideToTop_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=closed][data-side=bottom]{animation:_fadeOut_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideToBottom_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=closed][data-side=left]{animation:_fadeOut_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideToLeft_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}._purpur-popover__content_10ko3_52[data-state=closed][data-side=right]{animation:_fadeOut_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards,_slideToRight_10ko3_1 var(--purpur-motion-duration-200) var(--purpur-motion-easing-ease-in-out) forwards}}@media(min-width:600px){._purpur-popover__content_10ko3_52{width:400px}}._purpur-popover__content_10ko3_52{background:var(--purpur-color-background-tone-on-tone-primary)}._purpur-popover__content_10ko3_52 ._purpur-popover__arrow_10ko3_103{fill:var(--purpur-color-background-tone-on-tone-primary)}._purpur-popover__content_10ko3_52 ._purpur-popover__body_10ko3_106,._purpur-popover__content_10ko3_52 ._purpur-popover__icon_10ko3_107,._purpur-popover__content_10ko3_52 ._purpur-popover__steps_10ko3_108,._purpur-popover__content_10ko3_52 ._purpur-popover__title_10ko3_109{color:var(--purpur-color-text-tone-on-tone-primary)}._purpur-popover__content--negative_10ko3_112{background:var(--purpur-color-background-tone-on-tone-secondary)}._purpur-popover__content--negative_10ko3_112 ._purpur-popover__arrow_10ko3_103{fill:var(--purpur-color-background-tone-on-tone-secondary)}._purpur-popover__content--negative_10ko3_112 ._purpur-popover__body_10ko3_106,._purpur-popover__content--negative_10ko3_112 ._purpur-popover__icon_10ko3_107,._purpur-popover__content--negative_10ko3_112 ._purpur-popover__steps_10ko3_108,._purpur-popover__content--negative_10ko3_112 ._purpur-popover__title_10ko3_109{color:var(--purpur-color-text-tone-on-tone-secondary)}._purpur-popover__header-content_10ko3_124{display:flex;align-items:flex-start;gap:var(--purpur-spacing-100);align-self:stretch;padding-right:var(--purpur-spacing-300)}._purpur-popover__header-content_10ko3_124 ._purpur-popover__icon_10ko3_107{display:flex;padding-top:var(--purpur-spacing-25);align-items:center;gap:8px}._purpur-popover__content_10ko3_52 ._purpur-popover__close_10ko3_137{position:absolute;top:6.26px;right:6.26px}._purpur-popover__footer_10ko3_142{display:flex;text-align:center;gap:var(--purpur-spacing-150);align-self:stretch;align-items:center;flex-direction:column-reverse}@media(min-width:600px){._purpur-popover__footer_10ko3_142{flex-direction:row}}._purpur-popover__footer-button-group_10ko3_155{display:flex;justify-content:flex-end;align-items:center;gap:var(--purpur-spacing-200);flex:1 0 0}._purpur-popover__steps_10ko3_108{flex-shrink:0;color:var(--purpur-color-text-tone-on-tone-primary);overflow:hidden;text-overflow:ellipsis}@keyframes _fadeIn_10ko3_1{0%{opacity:0}to{opacity:1}}@keyframes _fadeOut_10ko3_1{0%{opacity:1}to{opacity:0}}@keyframes _slideFromTop_10ko3_1{0%{transform:translateY(-8px)}to{transform:translateY(0)}}@keyframes _slideToTop_10ko3_1{0%{transform:translateY(0)}to{transform:translateY(-8px)}}@keyframes _slideFromBottom_10ko3_1{0%{transform:translateY(8px)}to{transform:translateY(0)}}@keyframes _slideToBottom_10ko3_1{0%{transform:translateY(0)}to{transform:translateY(8px)}}@keyframes _slideFromLeft_10ko3_1{0%{transform:translate(-8px)}to{transform:translate(0)}}@keyframes _slideToLeft_10ko3_1{0%{transform:translate(0)}to{transform:translate(-8px)}}@keyframes _slideFromRight_10ko3_1{0%{transform:translate(8px)}to{transform:translate(0)}}@keyframes _slideToRight_10ko3_1{0%{transform:translate(0)}to{transform:translate(8px)}}@keyframes _highlightState1_10ko3_1{0%{opacity:0}50%{opacity:1}to{opacity:0}}
@@ -0,0 +1,7 @@
1
+ export declare const SCREEN_MEDIA_QUERY: {
2
+ readonly MAX_MD: `(max-width: ${string})`;
3
+ };
4
+ export declare const useScreenSize: () => {
5
+ isMdOrSmaller: boolean;
6
+ };
7
+ //# sourceMappingURL=use-screen-size.hook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-screen-size.hook.d.ts","sourceRoot":"","sources":["../src/use-screen-size.hook.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB;;CAErB,CAAC;AAEX,eAAO,MAAM,aAAa;;CA6BzB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare const useSmoothScroll: (onComplete?: () => void) => {
2
+ scrollToElement: (element: HTMLElement, duration?: number) => void;
3
+ cancelScroll: () => void;
4
+ };
5
+ //# sourceMappingURL=use-smooth-scroll.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-smooth-scroll.d.ts","sourceRoot":"","sources":["../src/use-smooth-scroll.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,IAAI;+BAWzC,WAAW,aAAY,MAAM;;CAgD1C,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare function usePopoverTrigger(ref: React.Ref<HTMLButtonElement>): {
2
+ triggerRef: import('react').RefObject<HTMLButtonElement | null>;
3
+ context: import('./popover-internal-context').PopoverInternalContextValue | null;
4
+ };
5
+ //# sourceMappingURL=usePopoverTrigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePopoverTrigger.d.ts","sourceRoot":"","sources":["../src/usePopoverTrigger.ts"],"names":[],"mappings":"AAMA,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC;;;EAoDlE"}
@@ -0,0 +1,7 @@
1
+ export declare function usePopoverWalkthrough(step: number): {
2
+ actuallyOpen: boolean;
3
+ onScrollStart: () => void;
4
+ onScrollComplete: () => void;
5
+ handleOpenChange: (open: boolean, consumerOnOpenChange?: (open: boolean) => void) => void;
6
+ };
7
+ //# sourceMappingURL=usePopoverWalkthrough.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePopoverWalkthrough.d.ts","sourceRoot":"","sources":["../src/usePopoverWalkthrough.ts"],"names":[],"mappings":"AAIA,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM;;;;6BA6DvC,OAAO,yBAAyB,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI;EAmBjE"}
@@ -0,0 +1,2 @@
1
+ import purpurCommon from "@purpurds/component-rig/eslint.config.mjs";
2
+ export default purpurCommon;
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@purpurds/popover",
3
+ "version": "0.0.1",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/popover.cjs.js",
6
+ "types": "./dist/popover.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/popover.cjs.js",
10
+ "types": "./dist/popover.d.ts",
11
+ "default": "./dist/popover.es.js"
12
+ },
13
+ "./styles": "./dist/styles.css",
14
+ "./metadata": "./dist/metadata.js"
15
+ },
16
+ "source": "src/popover.tsx",
17
+ "dependencies": {
18
+ "@radix-ui/react-popover": "^1.0.7",
19
+ "@radix-ui/react-slot": "^1.0.2",
20
+ "classnames": "~2.5.1",
21
+ "@purpurds/button": "8.11.0",
22
+ "@purpurds/icon": "8.11.0",
23
+ "@purpurds/heading": "8.11.0",
24
+ "@purpurds/link": "8.11.0",
25
+ "@purpurds/paragraph": "8.11.0",
26
+ "@purpurds/tokens": "8.11.0",
27
+ "@purpurds/common-types": "8.11.0"
28
+ },
29
+ "devDependencies": {
30
+ "@rushstack/eslint-patch": "~1.10.0",
31
+ "@storybook/react": "^10.0.8",
32
+ "@testing-library/dom": "~10.4.1",
33
+ "@testing-library/jest-dom": "~6.9.1",
34
+ "@testing-library/react": "~16.3.0",
35
+ "@testing-library/user-event": "~14.5.1",
36
+ "@types/node": "22.17",
37
+ "@types/react-dom": "^19.2.3",
38
+ "@types/react": "^19.2.6",
39
+ "eslint": "9.39.1",
40
+ "jsdom": "~27.2.0",
41
+ "lint-staged": "16.2.6",
42
+ "prettier": "~2.8.8",
43
+ "react": "^19.2.1",
44
+ "react-dom": "^19.2.1",
45
+ "storybook": "^10.0.8",
46
+ "typescript": "^5.9.3",
47
+ "vite": "^7.2.2",
48
+ "vitest": "^4.0.10",
49
+ "vitest-axe": "~0.1.0",
50
+ "vitest-canvas-mock": "~0.3.3",
51
+ "@types/jest-axe": "~3.5.9",
52
+ "@purpurds/component-rig": "1.0.0"
53
+ },
54
+ "peerDependencies": {
55
+ "@types/react": "^18 || ^19",
56
+ "@types/react-dom": "^18 || ^19",
57
+ "react": "^18 || ^19",
58
+ "react-dom": "^18 || ^19"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@types/react": {
62
+ "optional": true
63
+ },
64
+ "@types/react-dom": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "scripts": {
69
+ "build:dev": "vite",
70
+ "build:watch": "vite build --watch",
71
+ "build": "vite build",
72
+ "ci:build": "rushx build",
73
+ "coverage": "vitest run --coverage",
74
+ "lint:fix": "eslint . --fix",
75
+ "lint": "lint-staged --no-stash 2>&1",
76
+ "sbdev": "rush sbdev",
77
+ "test:unit": "vitest run --passWithNoTests",
78
+ "test:watch": "vitest --watch",
79
+ "test": "rushx test:unit",
80
+ "typecheck": "tsc -p ./tsconfig.json"
81
+ }
82
+ }
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,63 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { axe } from "vitest-axe";
5
+
6
+ import { PopoverBack } from "./popover-back";
7
+ import { PopoverFlow } from "./popover-flow";
8
+ import { PopoverInternalContext } from "./popover-internal-context";
9
+
10
+ describe("PopoverBack", () => {
11
+ it("renders children and handles click", () => {
12
+ const onClick = vi.fn();
13
+ render(
14
+ <PopoverFlow
15
+ separatorText="of"
16
+ stepText="Step"
17
+ backLabel="Back"
18
+ nextLabel="Next"
19
+ finishLabel="Finish"
20
+ initialStep={2}
21
+ >
22
+ <PopoverInternalContext.Provider
23
+ value={{
24
+ isOpen: false,
25
+ walkthroughStep: 0,
26
+ negative: false,
27
+ }}
28
+ >
29
+ <PopoverBack onClick={onClick}>Back</PopoverBack>
30
+ </PopoverInternalContext.Provider>
31
+ </PopoverFlow>
32
+ );
33
+ fireEvent.click(screen.getByText("Back"));
34
+ expect(onClick).toHaveBeenCalled();
35
+ });
36
+
37
+ it("should be accessible", async () => {
38
+ const { container } = render(
39
+ <PopoverFlow
40
+ separatorText="of"
41
+ stepText="Step"
42
+ backLabel="Back"
43
+ nextLabel="Next"
44
+ finishLabel="Finish"
45
+ initialStep={2}
46
+ >
47
+ <PopoverInternalContext.Provider
48
+ value={{
49
+ isOpen: false,
50
+ walkthroughStep: 0,
51
+ negative: false,
52
+ }}
53
+ >
54
+ <PopoverBack>Back</PopoverBack>
55
+ </PopoverInternalContext.Provider>
56
+ </PopoverFlow>
57
+ );
58
+ const results = await axe(container);
59
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
60
+ // @ts-ignore
61
+ expect(results).toHaveNoViolations();
62
+ });
63
+ });
@@ -0,0 +1,40 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { Button } from "@purpurds/button";
3
+
4
+ import { usePopoverFlow } from "./popover-flow";
5
+ import { usePopoverNegative } from "./popover-internal-context";
6
+ import { useScreenSize } from "./use-screen-size.hook";
7
+
8
+ export const PopoverBack = ({
9
+ children,
10
+ onClick,
11
+ }: {
12
+ children: ReactNode;
13
+ onClick?: () => void;
14
+ }) => {
15
+ const { back } = usePopoverFlow();
16
+ const { negative: isNegative } = usePopoverNegative();
17
+ const { isMdOrSmaller } = useScreenSize();
18
+
19
+ // Invert negative: if popover is negative (light), buttons should be normal (dark)
20
+ const negative = isNegative ? false : true;
21
+
22
+ const handleClick = () => {
23
+ onClick?.();
24
+ back();
25
+ };
26
+
27
+ return (
28
+ <Button
29
+ size="sm"
30
+ variant="secondary"
31
+ onClick={handleClick}
32
+ negative={negative}
33
+ fullWidth={isMdOrSmaller}
34
+ >
35
+ {children}
36
+ </Button>
37
+ );
38
+ };
39
+
40
+ PopoverBack.displayName = "PopoverBack";
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import * as RadixPopover from "@radix-ui/react-popover";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { axe } from "vitest-axe";
6
+
7
+ import { PopoverButton } from "./popover-button";
8
+ import { PopoverInternalContext } from "./popover-internal-context";
9
+
10
+ describe("PopoverButton", () => {
11
+ it("renders children and handles click", () => {
12
+ const onClick = vi.fn();
13
+ render(
14
+ <RadixPopover.Root>
15
+ <PopoverInternalContext.Provider
16
+ value={{
17
+ isOpen: false,
18
+ walkthroughStep: 0,
19
+ negative: false,
20
+ }}
21
+ >
22
+ <PopoverButton onClick={onClick} dismiss={false}>
23
+ Button
24
+ </PopoverButton>
25
+ </PopoverInternalContext.Provider>
26
+ </RadixPopover.Root>
27
+ );
28
+ fireEvent.click(screen.getByText("Button"));
29
+ expect(onClick).toHaveBeenCalled();
30
+ });
31
+
32
+ it("should be accessible", async () => {
33
+ const { container } = render(
34
+ <RadixPopover.Root>
35
+ <PopoverInternalContext.Provider
36
+ value={{
37
+ isOpen: false,
38
+ walkthroughStep: 0,
39
+ negative: false,
40
+ }}
41
+ >
42
+ <PopoverButton dismiss={false}>Button</PopoverButton>
43
+ </PopoverInternalContext.Provider>
44
+ </RadixPopover.Root>
45
+ );
46
+ const results = await axe(container);
47
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
48
+ // @ts-ignore
49
+ expect(results).toHaveNoViolations();
50
+ });
51
+ });
@@ -0,0 +1,84 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { Button, type ButtonProps } from "@purpurds/button";
3
+ import * as RadixPopover from "@radix-ui/react-popover";
4
+
5
+ import { useOptionalPopoverFlow } from "./popover-flow";
6
+ import { usePopoverNegative } from "./popover-internal-context";
7
+ import { useScreenSize } from "./use-screen-size.hook";
8
+
9
+ export type PopoverButtonProps = RadixPopover.PopoverCloseProps & {
10
+ /**
11
+ * Button label or content.
12
+ */
13
+ children: ReactNode;
14
+ /**
15
+ * Whether to use the child element as the button instead of wrapping in a Button component.
16
+ * @default false
17
+ */
18
+ asChild?: boolean;
19
+ /**
20
+ * Click handler called when the button is clicked.
21
+ */
22
+ onClick?: () => void;
23
+ /**
24
+ * Visual variant of the button.
25
+ * @default "primary"
26
+ */
27
+ variant?: ButtonProps["variant"];
28
+ /**
29
+ * Whether clicking the button should dismiss/close the popover.
30
+ * @default true
31
+ */
32
+ dismiss?: boolean;
33
+ /**
34
+ * Whether the button should take full width. If not specified, defaults to true on mobile (md or smaller).
35
+ */
36
+ fullWidth?: boolean;
37
+ };
38
+
39
+ export const PopoverButton = ({
40
+ children,
41
+ asChild,
42
+ onClick,
43
+ variant = "primary",
44
+ dismiss = true,
45
+ fullWidth,
46
+ }: PopoverButtonProps) => {
47
+ const flow = useOptionalPopoverFlow();
48
+ const { isMdOrSmaller } = useScreenSize();
49
+ const { negative: isNegative } = usePopoverNegative();
50
+ // Invert negative: if popover is negative (light), buttons should be normal (dark)
51
+ const negative = isNegative ? false : true;
52
+
53
+ const handleClick = () => {
54
+ onClick?.();
55
+ if (dismiss) {
56
+ flow?.dismiss();
57
+ }
58
+ };
59
+
60
+ const isFullWidth = fullWidth ?? isMdOrSmaller;
61
+
62
+ const content = asChild ? (
63
+ children
64
+ ) : (
65
+ <Button
66
+ variant={variant}
67
+ size="sm"
68
+ fullWidth={isFullWidth}
69
+ negative={negative}
70
+ onClick={handleClick}
71
+ >
72
+ {children}
73
+ </Button>
74
+ );
75
+
76
+ // Always wrap with RadixPopover.Close for proper dismiss behavior
77
+ if (dismiss) {
78
+ return <RadixPopover.Close asChild>{content}</RadixPopover.Close>;
79
+ }
80
+
81
+ return content;
82
+ };
83
+
84
+ PopoverButton.displayName = "PopoverButton";