@edux-design/popovers 1.0.6 → 1.1.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.
- package/CHANGELOG.md +17 -0
- package/README.md +14 -0
- package/dist/index.d.mts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +166 -20
- package/dist/index.mjs +165 -20
- package/docs/confirmation.md +118 -0
- package/package.json +10 -16
- package/src/demos/Popover.stories.jsx +0 -248
- package/src/elements/Popover.jsx +0 -156
- package/src/elements/Popover.test.jsx +0 -63
- package/src/index.js +0 -8
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
import React, { forwardRef } from "react";
|
|
3
3
|
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
4
4
|
import { cx } from "@edux-design/utils";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
var POPOVER_Z_INDEX = 2147483646;
|
|
7
|
+
var POPOVER_POSITIONING_CLASSES = {
|
|
8
|
+
top: "-translate-y-(--motion-distance-xs)",
|
|
9
|
+
right: "translate-x-(--motion-distance-xs)",
|
|
10
|
+
bottom: "translate-y-(--motion-distance-xs)",
|
|
11
|
+
left: "-translate-x-(--motion-distance-xs)"
|
|
12
|
+
};
|
|
6
13
|
var Popover = PopoverPrimitive.Root;
|
|
7
14
|
var PopoverTrigger = PopoverPrimitive.Trigger;
|
|
8
15
|
var PopoverAnchor = PopoverPrimitive.Anchor;
|
|
@@ -24,14 +31,8 @@ var PopoverContent = forwardRef(
|
|
|
24
31
|
contentProps = {},
|
|
25
32
|
...restProps
|
|
26
33
|
}, ref) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
top: "translate-y-[-4px]",
|
|
30
|
-
right: "translate-x-[4px]",
|
|
31
|
-
bottom: "translate-y-[4px]",
|
|
32
|
-
left: "translate-x-[-4px]"
|
|
33
|
-
}[side];
|
|
34
|
-
return /* @__PURE__ */ React.createElement(PopoverPortal, { container, ...portalProps }, /* @__PURE__ */ React.createElement(
|
|
34
|
+
const positioning = POPOVER_POSITIONING_CLASSES[side];
|
|
35
|
+
return /* @__PURE__ */ jsx(PopoverPortal, { container, ...portalProps, children: /* @__PURE__ */ jsxs(
|
|
35
36
|
PopoverPrimitive.Content,
|
|
36
37
|
{
|
|
37
38
|
ref,
|
|
@@ -44,23 +45,167 @@ var PopoverContent = forwardRef(
|
|
|
44
45
|
className: cx(positioning, className, "relative"),
|
|
45
46
|
style: { zIndex: POPOVER_Z_INDEX, ...style },
|
|
46
47
|
...contentProps,
|
|
47
|
-
...restProps
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
...restProps,
|
|
49
|
+
children: [
|
|
50
|
+
children,
|
|
51
|
+
showArrow && /* @__PURE__ */ jsx(
|
|
52
|
+
PopoverPrimitive.Arrow,
|
|
53
|
+
{
|
|
54
|
+
className: "fill-fg-base",
|
|
55
|
+
width: 12,
|
|
56
|
+
height: 8
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
) });
|
|
59
62
|
}
|
|
60
63
|
);
|
|
61
64
|
PopoverContent.displayName = "PopoverContent";
|
|
62
65
|
var PopoverClose = PopoverPrimitive.Close;
|
|
66
|
+
|
|
67
|
+
// src/elements/ConfirmationPopover.tsx
|
|
68
|
+
import { forwardRef as forwardRef2, useId, useRef, useState } from "react";
|
|
69
|
+
import { Button } from "@edux-design/buttons";
|
|
70
|
+
import { cx as cx2 } from "@edux-design/utils";
|
|
71
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
72
|
+
var ConfirmationPopover = forwardRef2(function ConfirmationPopover2({
|
|
73
|
+
open,
|
|
74
|
+
defaultOpen = false,
|
|
75
|
+
onOpenChange,
|
|
76
|
+
title,
|
|
77
|
+
description,
|
|
78
|
+
children,
|
|
79
|
+
trigger,
|
|
80
|
+
onConfirm,
|
|
81
|
+
onCancel,
|
|
82
|
+
confirmLabel = "Confirm",
|
|
83
|
+
cancelLabel = "Cancel",
|
|
84
|
+
pendingLabel = "Working\u2026",
|
|
85
|
+
pending = false,
|
|
86
|
+
error,
|
|
87
|
+
confirmDisabled = false,
|
|
88
|
+
cancelDisabled = false,
|
|
89
|
+
dismissible = true,
|
|
90
|
+
returnFocusRef,
|
|
91
|
+
isDanger = false,
|
|
92
|
+
allowDismissWhilePending = false,
|
|
93
|
+
modal = true,
|
|
94
|
+
anchor,
|
|
95
|
+
contentProps = {},
|
|
96
|
+
portalProps,
|
|
97
|
+
confirmButtonProps = {},
|
|
98
|
+
cancelButtonProps = {}
|
|
99
|
+
}, ref) {
|
|
100
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
101
|
+
const resolvedOpen = open ?? uncontrolledOpen;
|
|
102
|
+
const cancelRef = useRef(null);
|
|
103
|
+
const titleId = useId();
|
|
104
|
+
const descriptionId = useId();
|
|
105
|
+
const locked = !dismissible || pending && !allowDismissWhilePending;
|
|
106
|
+
const {
|
|
107
|
+
onClick: onCancelClick,
|
|
108
|
+
ref: actionRef,
|
|
109
|
+
...cancelProps
|
|
110
|
+
} = cancelButtonProps;
|
|
111
|
+
const { onClick: onConfirmClick, ...confirmProps } = confirmButtonProps;
|
|
112
|
+
const requestOpen = (next) => {
|
|
113
|
+
if (!next && locked) return;
|
|
114
|
+
if (open === void 0) setUncontrolledOpen(next);
|
|
115
|
+
onOpenChange?.(next);
|
|
116
|
+
};
|
|
117
|
+
return /* @__PURE__ */ jsxs2(Popover, { open: resolvedOpen, onOpenChange: requestOpen, modal, children: [
|
|
118
|
+
anchor && /* @__PURE__ */ jsx2(PopoverAnchor, { asChild: true, children: anchor }),
|
|
119
|
+
/* @__PURE__ */ jsx2(PopoverTrigger, { asChild: true, children: trigger }),
|
|
120
|
+
/* @__PURE__ */ jsxs2(
|
|
121
|
+
PopoverContent,
|
|
122
|
+
{
|
|
123
|
+
side: "bottom",
|
|
124
|
+
align: "end",
|
|
125
|
+
...contentProps,
|
|
126
|
+
ref,
|
|
127
|
+
portalProps,
|
|
128
|
+
"aria-labelledby": contentProps["aria-labelledby"] ?? titleId,
|
|
129
|
+
"aria-describedby": contentProps["aria-describedby"] ?? (description ? descriptionId : void 0),
|
|
130
|
+
"aria-busy": pending || void 0,
|
|
131
|
+
className: cx2(
|
|
132
|
+
"w-[min(320px,calc(100vw-32px))] max-h-[var(--radix-popover-content-available-height)] overflow-auto rounded-md border-2 border-border-subtle bg-bg-base p-16 text-sm text-fg-base shadow-lg motion-reduce:animate-none",
|
|
133
|
+
contentProps.className
|
|
134
|
+
),
|
|
135
|
+
onOpenAutoFocus: (event) => {
|
|
136
|
+
contentProps.onOpenAutoFocus?.(event);
|
|
137
|
+
if (!event.defaultPrevented) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
if (!locked && !cancelDisabled) cancelRef.current?.focus();
|
|
140
|
+
else if (event.target instanceof HTMLElement) event.target.focus();
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
onCloseAutoFocus: (event) => {
|
|
144
|
+
contentProps.onCloseAutoFocus?.(event);
|
|
145
|
+
if (!event.defaultPrevented && returnFocusRef?.current?.isConnected) {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
returnFocusRef.current.focus();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
onEscapeKeyDown: (event) => {
|
|
151
|
+
contentProps.onEscapeKeyDown?.(event);
|
|
152
|
+
if (locked) event.preventDefault();
|
|
153
|
+
},
|
|
154
|
+
onInteractOutside: (event) => {
|
|
155
|
+
contentProps.onInteractOutside?.(event);
|
|
156
|
+
if (locked) event.preventDefault();
|
|
157
|
+
},
|
|
158
|
+
children: [
|
|
159
|
+
/* @__PURE__ */ jsx2("h2", { id: titleId, className: "text-base font-semibold text-fg-base", children: title }),
|
|
160
|
+
description && /* @__PURE__ */ jsx2("p", { id: descriptionId, className: "mt-8 text-sm text-fg-subtle", children: description }),
|
|
161
|
+
children && /* @__PURE__ */ jsx2("div", { className: "mt-12", children }),
|
|
162
|
+
error && /* @__PURE__ */ jsx2("div", { role: "alert", className: "mt-8 text-fg-danger-emphasis", children: error }),
|
|
163
|
+
pending && /* @__PURE__ */ jsx2("p", { role: "status", className: "sr-only", children: pendingLabel }),
|
|
164
|
+
/* @__PURE__ */ jsxs2("div", { className: "mt-16 flex flex-wrap justify-end gap-8", children: [
|
|
165
|
+
/* @__PURE__ */ jsx2(
|
|
166
|
+
Button,
|
|
167
|
+
{
|
|
168
|
+
...cancelProps,
|
|
169
|
+
ref: (node) => {
|
|
170
|
+
cancelRef.current = node;
|
|
171
|
+
if (typeof actionRef === "function") actionRef(node);
|
|
172
|
+
else if (actionRef) actionRef.current = node;
|
|
173
|
+
},
|
|
174
|
+
type: "button",
|
|
175
|
+
size: "small",
|
|
176
|
+
isSecondary: true,
|
|
177
|
+
disabled: locked || cancelDisabled,
|
|
178
|
+
onClick: (event) => {
|
|
179
|
+
onCancelClick?.(event);
|
|
180
|
+
if (!event.defaultPrevented) onCancel?.(event);
|
|
181
|
+
if (!event.defaultPrevented) requestOpen(false);
|
|
182
|
+
},
|
|
183
|
+
children: cancelLabel
|
|
184
|
+
}
|
|
185
|
+
),
|
|
186
|
+
/* @__PURE__ */ jsx2(
|
|
187
|
+
Button,
|
|
188
|
+
{
|
|
189
|
+
...confirmProps,
|
|
190
|
+
type: "button",
|
|
191
|
+
size: "small",
|
|
192
|
+
isDanger,
|
|
193
|
+
disabled: pending || confirmDisabled,
|
|
194
|
+
onClick: (event) => {
|
|
195
|
+
onConfirmClick?.(event);
|
|
196
|
+
if (!event.defaultPrevented) onConfirm(event);
|
|
197
|
+
},
|
|
198
|
+
children: pending ? pendingLabel : confirmLabel
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
] })
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
] });
|
|
206
|
+
});
|
|
63
207
|
export {
|
|
208
|
+
ConfirmationPopover,
|
|
64
209
|
Popover,
|
|
65
210
|
PopoverAnchor,
|
|
66
211
|
PopoverClose,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# ConfirmationPopover
|
|
2
|
+
|
|
3
|
+
Import `ConfirmationPopover` and `ConfirmationPopoverProps` from `@edux-design/popovers`.
|
|
4
|
+
It composes the existing Popover root/trigger/anchor/content so it retains portalling,
|
|
5
|
+
placement, offsets, collision handling and public ref/event behaviour.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
<ConfirmationPopover
|
|
9
|
+
open={activeConfirmation === row.id}
|
|
10
|
+
onOpenChange={(next) => setActiveConfirmation(next ? row.id : null)}
|
|
11
|
+
title="Delete this question?"
|
|
12
|
+
description="The question will be removed from this group."
|
|
13
|
+
trigger={
|
|
14
|
+
<IconButton
|
|
15
|
+
aria-label="Delete question"
|
|
16
|
+
icon={<Trash />}
|
|
17
|
+
isBare
|
|
18
|
+
isDanger
|
|
19
|
+
size="small"
|
|
20
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
21
|
+
/>
|
|
22
|
+
}
|
|
23
|
+
confirmLabel="Delete question"
|
|
24
|
+
isDanger
|
|
25
|
+
onConfirm={handleDelete}
|
|
26
|
+
confirmButtonProps={{
|
|
27
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
28
|
+
onClick: (event) => event.stopPropagation(),
|
|
29
|
+
}}
|
|
30
|
+
cancelButtonProps={{
|
|
31
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
32
|
+
onClick: (event) => event.stopPropagation(),
|
|
33
|
+
}}
|
|
34
|
+
contentProps={{ side: "bottom", align: "end", collisionPadding: 16 }}
|
|
35
|
+
/>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`trigger` is required: pass one ref-forwarding element; `PopoverTrigger asChild`
|
|
39
|
+
preserves its original handlers/ref. `anchor` optionally supplies another
|
|
40
|
+
ref-forwarding positioning element. `modal` defaults to `true`, containing keyboard
|
|
41
|
+
focus and blocking outside pointer interaction using the underlying Radix root.
|
|
42
|
+
Set `modal={false}` only when a non-modal contextual workflow is intended; it permits
|
|
43
|
+
focus to leave. Outside focus dismisses that non-modal presentation unless locked
|
|
44
|
+
or vetoed. Idle Escape and Cancel close in both modes. The trigger normally receives
|
|
45
|
+
focus after modal closure; `returnFocusRef` and preventable `onCloseAutoFocus` cover
|
|
46
|
+
removed rows and explicit host editor focus.
|
|
47
|
+
|
|
48
|
+
Use `contentProps` for `side`, `align`, offsets, collision settings and native event
|
|
49
|
+
hooks; defaults are bottom/end with the existing Popover gap. `portalProps` forwards
|
|
50
|
+
the existing Radix Portal options. The responsive surface is at most 320px wide,
|
|
51
|
+
leaves 16px at viewport edges, and scrolls within available Popover height.
|
|
52
|
+
The generic composition adds no editor selection suppression or row drag policy;
|
|
53
|
+
pass native trigger/action handlers when an adapter needs those behaviours.
|
|
54
|
+
|
|
55
|
+
Both confirmations leave mutation policy in the host. They never await, interpret,
|
|
56
|
+
catch, or auto-close after `onConfirm`: synchronous commands, promises, false results,
|
|
57
|
+
blocking document errors and retriable service errors can keep their own lifecycle.
|
|
58
|
+
An async handler must catch its own rejection and update its own `pending`/`error`.
|
|
59
|
+
Nothing schedules asynchronous component state after the callback.
|
|
60
|
+
|
|
61
|
+
`open` is authoritative when supplied. Omit it and use `defaultOpen` for an
|
|
62
|
+
uncontrolled presentation; `onOpenChange(next)` reports user requests in either mode.
|
|
63
|
+
Confirm does not close even in uncontrolled mode: use controlled state when success
|
|
64
|
+
must close the presentation. `onCancel(event)` only reports the Cancel button;
|
|
65
|
+
Escape and outside dismissal report through `onOpenChange`, without pretending to
|
|
66
|
+
be a Cancel activation.
|
|
67
|
+
|
|
68
|
+
Actions use Edux `Button`, default to `type="button"`, and wrap on narrow surfaces.
|
|
69
|
+
The default confirm action is neutral. Set `isDanger` for destructive actions.
|
|
70
|
+
`confirmDisabled` and `cancelDisabled` independently disable their respective
|
|
71
|
+
buttons. An error renders a `role="alert"`; an error alone does not disable retry.
|
|
72
|
+
For a stale editor/document error, the host also sets `confirmDisabled`.
|
|
73
|
+
|
|
74
|
+
While `pending`, both actions are disabled, the surface has `aria-busy`, and
|
|
75
|
+
`pendingLabel` (default “Working…”) replaces the confirm text and is exposed as a
|
|
76
|
+
status. Escape, outside pointer/focus dismissal, Cancel, and trigger toggle requests
|
|
77
|
+
cannot close the presentation. `allowDismissWhilePending` explicitly unlocks user
|
|
78
|
+
cancellation while confirm remains disabled; use it only when the host permits that
|
|
79
|
+
policy. `dismissible={false}` locks user dismissal regardless of pending. A controlled
|
|
80
|
+
host can always set `open={false}`, for example after confirmed success. There is no
|
|
81
|
+
implicit header X action to bypass these rules.
|
|
82
|
+
|
|
83
|
+
Safe initial focus goes to Cancel. If Cancel is unavailable, focus stays on the
|
|
84
|
+
surface rather than choosing the destructive action. Native `contentProps`
|
|
85
|
+
`onOpenAutoFocus`/`onCloseAutoFocus` can call `event.preventDefault()` to replace the
|
|
86
|
+
focus policy; these hooks run first. `returnFocusRef` supplies an explicit return
|
|
87
|
+
target when the originating row might disappear. For an editor command that places
|
|
88
|
+
its own selection/focus, prevent close autofocus so automatic restoration cannot
|
|
89
|
+
undo that command. A missing or disconnected return target is not focused.
|
|
90
|
+
|
|
91
|
+
`contentProps` forwards native attributes, style/className, keyboard/pointer events,
|
|
92
|
+
and Radix focus/dismissal hooks. The surface ref is an `HTMLDivElement`.
|
|
93
|
+
`confirmButtonProps`/`cancelButtonProps` forward native button attributes, refs and
|
|
94
|
+
mouse/keyboard handlers. Their click handlers run before `onConfirm`/`onCancel`;
|
|
95
|
+
`preventDefault()` vetoes the corresponding action, and `stopPropagation()` remains
|
|
96
|
+
available for editable/sortable row adapters. Forced action type, disabled state and
|
|
97
|
+
pending guards take precedence. Caller dismissal hooks run first, then the internal
|
|
98
|
+
lock always prevents dismissal while locked; a handler cannot accidentally undo it.
|
|
99
|
+
|
|
100
|
+
Each surface is a labelled `dialog`, with a meaningful description when provided.
|
|
101
|
+
Confirmation is a decision, so it uses ordinary buttons rather than menu navigation.
|
|
102
|
+
It does not automatically claim urgent `alertdialog` semantics. A caller may set
|
|
103
|
+
`contentProps.role="alertdialog"` when that semantic is appropriate. Error messages
|
|
104
|
+
have alert semantics separately. IDs remain unique across instances.
|
|
105
|
+
|
|
106
|
+
Use current semantic surface/foreground/border/action/danger tokens. There are no
|
|
107
|
+
new tokens or package-owned palettes, and theme switching does not change geometry.
|
|
108
|
+
Pass `portalProps.container` when a brand/mode scope is below the document root;
|
|
109
|
+
put `brand--blue`/`brand--teal` and `theme--light`/`theme--dark` on that same scope.
|
|
110
|
+
Default portals inherit the application root theme. Native reduced-motion rules
|
|
111
|
+
apply; the compositions also disable their surface animation for reduced motion.
|
|
112
|
+
The primitives keep collision/viewport bounds, content scrolls, and footer actions
|
|
113
|
+
wrap. Domain checks, permissions, editor resolver registration, persistence, source
|
|
114
|
+
warnings, server mutation and cancellation remain local.
|
|
115
|
+
|
|
116
|
+
Storybook: `Popovers/ConfirmationPopover` includes the shared confirmation cases and
|
|
117
|
+
an editable/sortable-style row proving action propagation stays under host control.
|
|
118
|
+
Component tests use real Radix interaction. Public type/runtime fixtures live under `test/`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edux-design/popovers",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -13,25 +13,19 @@
|
|
|
13
13
|
"require": "./dist/index.js"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"lint": "eslint . --max-warnings 0",
|
|
18
|
-
"build": "tsup src/index.js --format esm,cjs --external react,react-dom,@radix-ui/react-popover",
|
|
19
|
-
"dev": "tsup src/index.js --format esm,cjs --external react,react-dom,@radix-ui/react-popover",
|
|
20
|
-
"generate:component": "turbo gen react-component",
|
|
21
|
-
"check-types": "tsc --noEmit"
|
|
22
|
-
},
|
|
23
16
|
"peerDependencies": {
|
|
24
17
|
"react": ">=18",
|
|
25
18
|
"react-dom": ">=18",
|
|
26
19
|
"@edux-design/utils": "*",
|
|
27
|
-
"@radix-ui/react-popover": "1.1.
|
|
20
|
+
"@radix-ui/react-popover": "1.1.19"
|
|
28
21
|
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"docs",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@edux-design/buttons": "*"
|
|
36
30
|
}
|
|
37
31
|
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
Popover,
|
|
4
|
-
PopoverTrigger,
|
|
5
|
-
PopoverContent,
|
|
6
|
-
PopoverClose,
|
|
7
|
-
} from "./../index";
|
|
8
|
-
import { Button } from "@edux-design/buttons";
|
|
9
|
-
import { cx } from "@edux-design/utils";
|
|
10
|
-
|
|
11
|
-
const meta = {
|
|
12
|
-
title: "Components/Popover",
|
|
13
|
-
component: Popover,
|
|
14
|
-
parameters: {
|
|
15
|
-
layout: "centered",
|
|
16
|
-
docs: {
|
|
17
|
-
description: {
|
|
18
|
-
component: `
|
|
19
|
-
Radix-based **Popover** component.
|
|
20
|
-
|
|
21
|
-
Use for contextual overlays triggered by a click, such as menus, mini-editors, or form helpers.
|
|
22
|
-
|
|
23
|
-
Built with [@radix-ui/react-popover](https://www.radix-ui.com/primitives/docs/components/popover).
|
|
24
|
-
`,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
argTypes: {
|
|
29
|
-
side: {
|
|
30
|
-
control: "select",
|
|
31
|
-
options: ["top", "right", "bottom", "left"],
|
|
32
|
-
description: "Placement of the popover relative to its trigger.",
|
|
33
|
-
table: { defaultValue: { summary: "bottom" } },
|
|
34
|
-
},
|
|
35
|
-
align: {
|
|
36
|
-
control: "select",
|
|
37
|
-
options: ["start", "center", "end"],
|
|
38
|
-
description: "Alignment of the popover along the cross axis.",
|
|
39
|
-
table: { defaultValue: { summary: "center" } },
|
|
40
|
-
},
|
|
41
|
-
sideOffset: {
|
|
42
|
-
control: { type: "number", min: 0, step: 1 },
|
|
43
|
-
description: "Gap between the trigger and popover.",
|
|
44
|
-
table: { defaultValue: { summary: 8 } },
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
export default meta;
|
|
49
|
-
/**
|
|
50
|
-
* ───────────────────────────────────────────────
|
|
51
|
-
* Basic Popover
|
|
52
|
-
* ───────────────────────────────────────────────
|
|
53
|
-
*/
|
|
54
|
-
export const Basic = {
|
|
55
|
-
render: (args) => (
|
|
56
|
-
<Popover>
|
|
57
|
-
<PopoverTrigger asChild>
|
|
58
|
-
<Button variant="primary">Open Popover</Button>
|
|
59
|
-
</PopoverTrigger>
|
|
60
|
-
|
|
61
|
-
<PopoverContent {...args}>
|
|
62
|
-
<div className="space-y-4 w-[200px]">
|
|
63
|
-
<p>This is a simple popover content.</p>
|
|
64
|
-
<PopoverClose asChild>
|
|
65
|
-
<Button variant="secondary" size="sm">
|
|
66
|
-
Close
|
|
67
|
-
</Button>
|
|
68
|
-
</PopoverClose>
|
|
69
|
-
</div>
|
|
70
|
-
</PopoverContent>
|
|
71
|
-
</Popover>
|
|
72
|
-
),
|
|
73
|
-
args: {
|
|
74
|
-
side: "bottom",
|
|
75
|
-
align: "center",
|
|
76
|
-
sideOffset: 8,
|
|
77
|
-
},
|
|
78
|
-
parameters: {
|
|
79
|
-
docs: {
|
|
80
|
-
description: {
|
|
81
|
-
story: "A basic popover triggered by a button click.",
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* ───────────────────────────────────────────────
|
|
89
|
-
* Custom Content
|
|
90
|
-
* ───────────────────────────────────────────────
|
|
91
|
-
*/
|
|
92
|
-
export const CustomContent = {
|
|
93
|
-
render: (args) => (
|
|
94
|
-
<Popover>
|
|
95
|
-
<PopoverTrigger asChild>
|
|
96
|
-
<Button variant="primary">Show Custom Popover</Button>
|
|
97
|
-
</PopoverTrigger>
|
|
98
|
-
<PopoverContent
|
|
99
|
-
{...args}
|
|
100
|
-
className={cx(
|
|
101
|
-
"rounded-2xl border border-fg-subtle bg-fg-base p-16 w-[240px]",
|
|
102
|
-
"shadow-lg"
|
|
103
|
-
)}
|
|
104
|
-
>
|
|
105
|
-
<h3 className="font-semibold text-base mb-4">Popover title</h3>
|
|
106
|
-
<p className="text-sm text-fg-subtle">
|
|
107
|
-
You can render **anything** inside here — forms, menus, etc.
|
|
108
|
-
</p>
|
|
109
|
-
<div className="mt-8 flex justify-end">
|
|
110
|
-
<PopoverClose asChild>
|
|
111
|
-
<Button variant="bare" size="sm">
|
|
112
|
-
Dismiss
|
|
113
|
-
</Button>
|
|
114
|
-
</PopoverClose>
|
|
115
|
-
</div>
|
|
116
|
-
</PopoverContent>
|
|
117
|
-
</Popover>
|
|
118
|
-
),
|
|
119
|
-
args: {
|
|
120
|
-
side: "top",
|
|
121
|
-
},
|
|
122
|
-
parameters: {
|
|
123
|
-
docs: {
|
|
124
|
-
description: {
|
|
125
|
-
story:
|
|
126
|
-
"Demonstrates custom content styling, with title, text, and custom buttons.",
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* ───────────────────────────────────────────────
|
|
134
|
-
* Controlled Popover
|
|
135
|
-
* ───────────────────────────────────────────────
|
|
136
|
-
*/
|
|
137
|
-
export const Controlled = {
|
|
138
|
-
render: (args) => {
|
|
139
|
-
const [open, setOpen] = useState(false);
|
|
140
|
-
return (
|
|
141
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
142
|
-
<PopoverTrigger asChild>
|
|
143
|
-
<Button variant="primary">
|
|
144
|
-
{open ? "Close Popover" : "Open Controlled Popover"}
|
|
145
|
-
</Button>
|
|
146
|
-
</PopoverTrigger>
|
|
147
|
-
<PopoverContent {...args}>
|
|
148
|
-
<div className="w-[220px] text-sm">
|
|
149
|
-
<p>This popover is **controlled** externally using React state.</p>
|
|
150
|
-
<div className="mt-6 flex justify-end">
|
|
151
|
-
<Button variant="bare" size="sm" onClick={() => setOpen(false)}>
|
|
152
|
-
Close
|
|
153
|
-
</Button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</PopoverContent>
|
|
157
|
-
</Popover>
|
|
158
|
-
);
|
|
159
|
-
},
|
|
160
|
-
parameters: {
|
|
161
|
-
docs: {
|
|
162
|
-
description: {
|
|
163
|
-
story:
|
|
164
|
-
"Shows how to control the popover’s open state externally via React state.",
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* ───────────────────────────────────────────────
|
|
172
|
-
* All Sides Example
|
|
173
|
-
* ───────────────────────────────────────────────
|
|
174
|
-
*/
|
|
175
|
-
export const Sides = {
|
|
176
|
-
render: () => (
|
|
177
|
-
<div className="grid grid-cols-2 gap-16">
|
|
178
|
-
{["top", "right", "bottom", "left"].map((side) => (
|
|
179
|
-
<Popover key={side}>
|
|
180
|
-
<PopoverTrigger asChild>
|
|
181
|
-
<Button variant="secondary">Side: {side}</Button>
|
|
182
|
-
</PopoverTrigger>
|
|
183
|
-
<PopoverContent side={side}>
|
|
184
|
-
<p className="w-[140px] text-center">
|
|
185
|
-
Popover on <strong>{side}</strong>
|
|
186
|
-
</p>
|
|
187
|
-
</PopoverContent>
|
|
188
|
-
</Popover>
|
|
189
|
-
))}
|
|
190
|
-
</div>
|
|
191
|
-
),
|
|
192
|
-
parameters: {
|
|
193
|
-
docs: {
|
|
194
|
-
description: {
|
|
195
|
-
story: "Showcases popovers appearing on all sides of the trigger.",
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* ───────────────────────────────────────────────
|
|
203
|
-
* No Auto Focus Example
|
|
204
|
-
* ───────────────────────────────────────────────
|
|
205
|
-
*/
|
|
206
|
-
export const NoAutoFocus = {
|
|
207
|
-
render: (args) => (
|
|
208
|
-
<Popover>
|
|
209
|
-
<PopoverTrigger asChild>
|
|
210
|
-
<Button variant="primary">Open Non-Focusing Popover</Button>
|
|
211
|
-
</PopoverTrigger>
|
|
212
|
-
|
|
213
|
-
<PopoverContent
|
|
214
|
-
{...args}
|
|
215
|
-
onOpenAutoFocus={(event) => {
|
|
216
|
-
// Prevent Radix from automatically moving focus into the popover
|
|
217
|
-
event.preventDefault();
|
|
218
|
-
}}
|
|
219
|
-
trapFocus={false}
|
|
220
|
-
disableOutsidePointerEvents={false}
|
|
221
|
-
className="w-[220px]"
|
|
222
|
-
>
|
|
223
|
-
<p className="text-sm mb-4">
|
|
224
|
-
This popover <strong>does not steal focus</strong>. The trigger (or
|
|
225
|
-
other input) remains focused.
|
|
226
|
-
</p>
|
|
227
|
-
<PopoverClose asChild>
|
|
228
|
-
<Button variant="secondary" size="sm">
|
|
229
|
-
Close
|
|
230
|
-
</Button>
|
|
231
|
-
</PopoverClose>
|
|
232
|
-
</PopoverContent>
|
|
233
|
-
</Popover>
|
|
234
|
-
),
|
|
235
|
-
args: {
|
|
236
|
-
side: "bottom",
|
|
237
|
-
align: "center",
|
|
238
|
-
sideOffset: 8,
|
|
239
|
-
},
|
|
240
|
-
parameters: {
|
|
241
|
-
docs: {
|
|
242
|
-
description: {
|
|
243
|
-
story:
|
|
244
|
-
"Demonstrates disabling Radix’s default auto-focus behaviour using `onOpenAutoFocus={(e) => e.preventDefault()}`. The editor or trigger element retains focus when the popover opens.",
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
};
|