@rozie-ui/popover-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/Popover.css +28 -0
- package/dist/index.cjs +307 -0
- package/dist/index.d.cts +66 -0
- package/dist/index.d.mts +66 -0
- package/dist/index.mjs +302 -0
- package/package.json +82 -0
- package/rozie-manifest.json +112 -0
- package/src/Popover.css +28 -0
- package/src/Popover.d.ts +62 -0
- package/src/Popover.tsx +349 -0
- package/src/index.ts +5 -0
- package/src/internal/middleware.ts +54 -0
- package/src/themes/base.css +24 -0
- package/src/themes/bootstrap.css +19 -0
- package/src/themes/material.css +19 -0
- package/src/themes/shadcn.css +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 One Learning Community LTD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @rozie-ui/popover-react
|
|
2
|
+
|
|
3
|
+
Idiomatic **react** `Popover` — a headless floating primitive for tooltips and popovers, wrapping [`@floating-ui/dom`](https://floating-ui.com) for collision-aware positioning (offset / flip / shift / arrow) with live `autoUpdate` tracking. You bring the anchor (the `anchor` slot) and the floating content (the default slot); Popover owns placement, the open/close gesture (`trigger`: click / hover / focus), dismissal (Escape + click-outside), the WAI-ARIA wiring (tooltip vs dialog), and a two-way `open` model — compiled from one [Rozie](https://github.com/One-Learning-Community/rozie.js) source. Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit `src/` by hand.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @rozie-ui/popover-react @floating-ui/dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Peer dependencies: `react + react-dom + @floating-ui/dom`. Install them alongside this package.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { useState } from 'react';
|
|
17
|
+
import { Popover } from '@rozie-ui/popover-react';
|
|
18
|
+
import '@floating-ui/dom'; // peer engine — installed alongside this package
|
|
19
|
+
|
|
20
|
+
export function Demo() {
|
|
21
|
+
const [open, setOpen] = useState(false);
|
|
22
|
+
return (
|
|
23
|
+
<Popover
|
|
24
|
+
open={open}
|
|
25
|
+
onOpenChange={setOpen}
|
|
26
|
+
trigger="click"
|
|
27
|
+
placement="bottom"
|
|
28
|
+
offset={8}
|
|
29
|
+
arrow
|
|
30
|
+
onChange={(next) => console.log('open:', next)}
|
|
31
|
+
anchor={({ toggle }) => <button onClick={toggle}>Menu</button>}
|
|
32
|
+
>
|
|
33
|
+
<div>Floating content</div>
|
|
34
|
+
</Popover>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Theming
|
|
40
|
+
|
|
41
|
+
Every visual value is a `--rozie-popover-*` CSS custom property (background, border, radius, shadow, padding, z-index, max-width, arrow size) — override any of them at any ancestor scope to match your design system.
|
|
42
|
+
|
|
43
|
+
## Props
|
|
44
|
+
|
|
45
|
+
| Name | Type | Default | Two-way (model) | Required | Description |
|
|
46
|
+
| --- | --- | --- | :---: | :---: | --- |
|
|
47
|
+
| `open` | `Boolean` | `false` | ✓ | | Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default. |
|
|
48
|
+
| `placement` | `String` | `"bottom"` | | | Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime. |
|
|
49
|
+
| `trigger` | `String` | `"click"` | | | How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog). |
|
|
50
|
+
| `offset` | `Number` | `8` | | | Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime. |
|
|
51
|
+
| `disableFlip` | `Boolean` | `false` | | | Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless. |
|
|
52
|
+
| `disableShift` | `Boolean` | `false` | | | Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor. |
|
|
53
|
+
| `arrow` | `Boolean` | `false` | | | Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties. |
|
|
54
|
+
| `disabled` | `Boolean` | `false` | | | Disable the control entirely: the trigger no longer opens the content and any open content is suppressed. |
|
|
55
|
+
| `modal` | `Boolean` | `false` | | | Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored). |
|
|
56
|
+
| `strategy` | `String` | `"absolute"` | | | Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime. |
|
|
57
|
+
|
|
58
|
+
## Events
|
|
59
|
+
|
|
60
|
+
| Event | Description |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `change` | Fired whenever the open state changes — a click/hover/focus trigger gesture, an Escape or click-outside dismissal, or a programmatic `show`/`hide`/`toggle`. Payload is the new `open` boolean. The two-way `open` model is updated alongside it. |
|
|
63
|
+
|
|
64
|
+
## Imperative handle
|
|
65
|
+
|
|
66
|
+
Beyond props, the component exposes imperative methods (declared once in the Rozie source via `$expose`). Grab a handle with the native ref mechanism and call them directly:
|
|
67
|
+
|
|
68
|
+
| Method | Description |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `show` | Open the floating content (no-op when `disabled`). Emits `change` and updates the `open` model. |
|
|
71
|
+
| `hide` | Close the floating content. Emits `change` and updates the `open` model. |
|
|
72
|
+
| `toggle` | Flip the open state (no-op when `disabled`). Emits `change` and updates the `open` model. |
|
|
73
|
+
| `reposition` | Recompute the floating position immediately (the Floating UI `computePosition` pass). Useful after content size changes that `autoUpdate` does not observe. |
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { useRef } from 'react';
|
|
77
|
+
import { Popover, type PopoverHandle } from '@rozie-ui/popover-react';
|
|
78
|
+
|
|
79
|
+
const pop = useRef<PopoverHandle>(null);
|
|
80
|
+
// <Popover ref={pop} ... />
|
|
81
|
+
pop.current?.show();
|
|
82
|
+
pop.current?.hide();
|
|
83
|
+
pop.current?.toggle();
|
|
84
|
+
pop.current?.reposition();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Slots
|
|
88
|
+
|
|
89
|
+
| Slot | Params |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| anchor | open, toggle, show, hide |
|
|
92
|
+
| (default) | |
|
package/dist/Popover.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
.rozie-popover[data-rozie-s-c6cf02ea] {
|
|
2
|
+
display: contents;
|
|
3
|
+
}
|
|
4
|
+
.rozie-popover-anchor[data-rozie-s-c6cf02ea] {
|
|
5
|
+
display: inline-block;
|
|
6
|
+
}
|
|
7
|
+
.rozie-popover-floating[data-rozie-s-c6cf02ea] {
|
|
8
|
+
position: absolute;
|
|
9
|
+
left: 0;
|
|
10
|
+
top: 0;
|
|
11
|
+
z-index: var(--rozie-popover-z, 1000);
|
|
12
|
+
width: max-content;
|
|
13
|
+
max-width: var(--rozie-popover-max-width, calc(100vw - 16px));
|
|
14
|
+
background: var(--rozie-popover-bg, #fff);
|
|
15
|
+
color: var(--rozie-popover-color, inherit);
|
|
16
|
+
border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
|
|
17
|
+
border-radius: var(--rozie-popover-radius, 8px);
|
|
18
|
+
box-shadow: var(--rozie-popover-shadow, 0 8px 24px rgba(0, 0, 0, 0.12));
|
|
19
|
+
padding: var(--rozie-popover-padding, 8px 12px);
|
|
20
|
+
}
|
|
21
|
+
.rozie-popover-arrow[data-rozie-s-c6cf02ea] {
|
|
22
|
+
position: absolute;
|
|
23
|
+
width: var(--rozie-popover-arrow-size, 8px);
|
|
24
|
+
height: var(--rozie-popover-arrow-size, 8px);
|
|
25
|
+
background: var(--rozie-popover-bg, #fff);
|
|
26
|
+
border: var(--rozie-popover-border, 1px solid rgba(0, 0, 0, 0.12));
|
|
27
|
+
transform: rotate(45deg);
|
|
28
|
+
}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
let react = require("react");
|
|
6
|
+
let _rozie_runtime_react = require("@rozie/runtime-react");
|
|
7
|
+
require("./Popover.css");
|
|
8
|
+
let _floating_ui_dom = require("@floating-ui/dom");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/internal/middleware.ts
|
|
11
|
+
/**
|
|
12
|
+
* Build the ordered middleware array for `computePosition`. Pure — no engine
|
|
13
|
+
* import, no DOM read beyond the passed-in arrow element.
|
|
14
|
+
*/
|
|
15
|
+
function buildMiddleware(factories, config) {
|
|
16
|
+
const mw = [factories.offset(config.offset)];
|
|
17
|
+
if (!config.disableFlip) mw.push(factories.flip());
|
|
18
|
+
if (!config.disableShift) mw.push(factories.shift());
|
|
19
|
+
if (config.arrow && config.arrowEl) mw.push(factories.arrow({ element: config.arrowEl }));
|
|
20
|
+
return mw;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/Popover.tsx
|
|
24
|
+
const Popover = (0, react.forwardRef)(function Popover(_props, ref) {
|
|
25
|
+
const props = {
|
|
26
|
+
..._props,
|
|
27
|
+
placement: _props.placement ?? "bottom",
|
|
28
|
+
trigger: _props.trigger ?? "click",
|
|
29
|
+
offset: _props.offset ?? 8,
|
|
30
|
+
disableFlip: _props.disableFlip ?? false,
|
|
31
|
+
disableShift: _props.disableShift ?? false,
|
|
32
|
+
arrow: _props.arrow ?? false,
|
|
33
|
+
disabled: _props.disabled ?? false,
|
|
34
|
+
modal: _props.modal ?? false,
|
|
35
|
+
strategy: _props.strategy ?? "absolute"
|
|
36
|
+
};
|
|
37
|
+
const attrs = (() => {
|
|
38
|
+
const { open, placement, trigger, offset, disableFlip, disableShift, arrow, disabled, modal, strategy, defaultValue, onOpenChange, defaultOpen, ...rest } = _props;
|
|
39
|
+
return rest;
|
|
40
|
+
})();
|
|
41
|
+
const anchorNode = (0, react.useRef)(null);
|
|
42
|
+
const floatingNode = (0, react.useRef)(null);
|
|
43
|
+
const arrowNode = (0, react.useRef)(null);
|
|
44
|
+
const stopAutoUpdate = (0, react.useRef)(null);
|
|
45
|
+
const lastFocusedEl = (0, react.useRef)(null);
|
|
46
|
+
const [open, setOpen] = (0, _rozie_runtime_react.useControllableState)({
|
|
47
|
+
value: props.open,
|
|
48
|
+
defaultValue: props.defaultOpen ?? false,
|
|
49
|
+
onValueChange: props.onOpenChange
|
|
50
|
+
});
|
|
51
|
+
const _openRef = (0, react.useRef)(open);
|
|
52
|
+
_openRef.current = open;
|
|
53
|
+
const anchorEl = (0, react.useRef)(null);
|
|
54
|
+
const floatingEl = (0, react.useRef)(null);
|
|
55
|
+
const arrowEl = (0, react.useRef)(null);
|
|
56
|
+
const _watch0First = (0, react.useRef)(true);
|
|
57
|
+
const _watch1First = (0, react.useRef)(true);
|
|
58
|
+
const _watch2First = (0, react.useRef)(true);
|
|
59
|
+
const _watch3First = (0, react.useRef)(true);
|
|
60
|
+
const _watch4First = (0, react.useRef)(true);
|
|
61
|
+
const _watch5First = (0, react.useRef)(true);
|
|
62
|
+
function deepActiveElement() {
|
|
63
|
+
let el = document.activeElement;
|
|
64
|
+
while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
|
|
65
|
+
return el;
|
|
66
|
+
}
|
|
67
|
+
function requestOpen(next) {
|
|
68
|
+
if (open === next) return;
|
|
69
|
+
if (next && props.trigger === "click") lastFocusedEl.current = deepActiveElement();
|
|
70
|
+
setOpen(next);
|
|
71
|
+
props.onChange && props.onChange(next);
|
|
72
|
+
if (!next && props.trigger === "click" && lastFocusedEl.current && lastFocusedEl.current.isConnected && typeof lastFocusedEl.current.focus === "function") lastFocusedEl.current.focus();
|
|
73
|
+
if (!next) lastFocusedEl.current = null;
|
|
74
|
+
}
|
|
75
|
+
function applyPosition(x, y, middlewareData) {
|
|
76
|
+
if (!floatingNode.current) return;
|
|
77
|
+
floatingNode.current.style.left = x + "px";
|
|
78
|
+
floatingNode.current.style.top = y + "px";
|
|
79
|
+
if (arrowNode.current && middlewareData && middlewareData.arrow) {
|
|
80
|
+
const ax = middlewareData.arrow.x;
|
|
81
|
+
const ay = middlewareData.arrow.y;
|
|
82
|
+
arrowNode.current.style.left = ax == null ? "" : ax + "px";
|
|
83
|
+
arrowNode.current.style.top = ay == null ? "" : ay + "px";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function position() {
|
|
87
|
+
if (!anchorNode.current || !floatingNode.current) return;
|
|
88
|
+
const middleware = buildMiddleware({
|
|
89
|
+
offset: _floating_ui_dom.offset,
|
|
90
|
+
flip: _floating_ui_dom.flip,
|
|
91
|
+
shift: _floating_ui_dom.shift,
|
|
92
|
+
arrow: _floating_ui_dom.arrow
|
|
93
|
+
}, {
|
|
94
|
+
offset: props.offset,
|
|
95
|
+
disableFlip: props.disableFlip,
|
|
96
|
+
disableShift: props.disableShift,
|
|
97
|
+
arrow: props.arrow,
|
|
98
|
+
arrowEl: arrowNode.current
|
|
99
|
+
});
|
|
100
|
+
if (props.strategy === "fixed") floatingNode.current.style.position = "fixed";
|
|
101
|
+
else floatingNode.current.style.position = "";
|
|
102
|
+
let opts = null;
|
|
103
|
+
opts = {
|
|
104
|
+
placement: props.placement,
|
|
105
|
+
strategy: props.strategy,
|
|
106
|
+
middleware
|
|
107
|
+
};
|
|
108
|
+
(0, _floating_ui_dom.computePosition)(anchorNode.current, floatingNode.current, opts).then((result) => {
|
|
109
|
+
applyPosition(result.x, result.y, result.middlewareData);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const startTracking = (0, react.useCallback)(() => {
|
|
113
|
+
if (!anchorNode.current || !floatingNode.current) return;
|
|
114
|
+
if (stopAutoUpdate.current) {
|
|
115
|
+
stopAutoUpdate.current();
|
|
116
|
+
stopAutoUpdate.current = null;
|
|
117
|
+
}
|
|
118
|
+
stopAutoUpdate.current = (0, _floating_ui_dom.autoUpdate)(anchorNode.current, floatingNode.current, position);
|
|
119
|
+
}, [position]);
|
|
120
|
+
const stopTracking = (0, react.useCallback)(() => {
|
|
121
|
+
if (stopAutoUpdate.current) {
|
|
122
|
+
stopAutoUpdate.current();
|
|
123
|
+
stopAutoUpdate.current = null;
|
|
124
|
+
}
|
|
125
|
+
}, []);
|
|
126
|
+
const onAnchorClick = (0, react.useCallback)(() => {
|
|
127
|
+
if (props.disabled) return;
|
|
128
|
+
requestOpen(!open);
|
|
129
|
+
}, [
|
|
130
|
+
open,
|
|
131
|
+
props.disabled,
|
|
132
|
+
requestOpen
|
|
133
|
+
]);
|
|
134
|
+
const onAnchorPointerEnter = (0, react.useCallback)(() => {
|
|
135
|
+
if (props.disabled) return;
|
|
136
|
+
requestOpen(true);
|
|
137
|
+
}, [props.disabled, requestOpen]);
|
|
138
|
+
const onAnchorPointerLeave = (0, react.useCallback)(() => {
|
|
139
|
+
if (props.disabled) return;
|
|
140
|
+
requestOpen(false);
|
|
141
|
+
}, [props.disabled, requestOpen]);
|
|
142
|
+
const onAnchorFocus = (0, react.useCallback)(() => {
|
|
143
|
+
if (props.disabled) return;
|
|
144
|
+
requestOpen(true);
|
|
145
|
+
}, [props.disabled, requestOpen]);
|
|
146
|
+
const onAnchorBlur = (0, react.useCallback)(() => {
|
|
147
|
+
if (props.disabled) return;
|
|
148
|
+
requestOpen(false);
|
|
149
|
+
}, [props.disabled, requestOpen]);
|
|
150
|
+
const dismiss = (0, react.useCallback)(() => {
|
|
151
|
+
requestOpen(false);
|
|
152
|
+
}, [requestOpen]);
|
|
153
|
+
function isTooltip() {
|
|
154
|
+
return props.trigger === "hover" || props.trigger === "focus";
|
|
155
|
+
}
|
|
156
|
+
function floatingRole() {
|
|
157
|
+
return isTooltip() ? "tooltip" : props.modal ? "dialog" : void 0;
|
|
158
|
+
}
|
|
159
|
+
function show() {
|
|
160
|
+
if (!props.disabled) requestOpen(true);
|
|
161
|
+
}
|
|
162
|
+
function hide() {
|
|
163
|
+
requestOpen(false);
|
|
164
|
+
}
|
|
165
|
+
function toggle() {
|
|
166
|
+
if (!props.disabled) requestOpen(!open);
|
|
167
|
+
}
|
|
168
|
+
function reposition() {
|
|
169
|
+
position();
|
|
170
|
+
}
|
|
171
|
+
(0, react.useEffect)(() => {
|
|
172
|
+
anchorNode.current = anchorEl.current;
|
|
173
|
+
if (_openRef.current && !props.disabled) {
|
|
174
|
+
floatingNode.current = floatingEl.current;
|
|
175
|
+
arrowNode.current = arrowEl.current;
|
|
176
|
+
startTracking();
|
|
177
|
+
}
|
|
178
|
+
return () => {
|
|
179
|
+
stopTracking();
|
|
180
|
+
};
|
|
181
|
+
}, []);
|
|
182
|
+
(0, react.useEffect)(() => {
|
|
183
|
+
if (_watch0First.current) {
|
|
184
|
+
_watch0First.current = false;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (open && !props.disabled) queueMicrotask(() => {
|
|
188
|
+
if (!open || props.disabled) return;
|
|
189
|
+
floatingNode.current = floatingEl.current;
|
|
190
|
+
arrowNode.current = arrowEl.current;
|
|
191
|
+
startTracking();
|
|
192
|
+
});
|
|
193
|
+
else stopTracking();
|
|
194
|
+
}, [open]);
|
|
195
|
+
(0, react.useEffect)(() => {
|
|
196
|
+
if (_watch1First.current) {
|
|
197
|
+
_watch1First.current = false;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (open) position();
|
|
201
|
+
}, [props.placement]);
|
|
202
|
+
(0, react.useEffect)(() => {
|
|
203
|
+
if (_watch2First.current) {
|
|
204
|
+
_watch2First.current = false;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (open) position();
|
|
208
|
+
}, [props.offset]);
|
|
209
|
+
(0, react.useEffect)(() => {
|
|
210
|
+
if (_watch3First.current) {
|
|
211
|
+
_watch3First.current = false;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (open) position();
|
|
215
|
+
}, [props.disableFlip]);
|
|
216
|
+
(0, react.useEffect)(() => {
|
|
217
|
+
if (_watch4First.current) {
|
|
218
|
+
_watch4First.current = false;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (open) position();
|
|
222
|
+
}, [props.disableShift]);
|
|
223
|
+
(0, react.useEffect)(() => {
|
|
224
|
+
if (_watch5First.current) {
|
|
225
|
+
_watch5First.current = false;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (open) position();
|
|
229
|
+
}, [props.strategy]);
|
|
230
|
+
(0, react.useEffect)(() => {
|
|
231
|
+
if (!open) return;
|
|
232
|
+
const _rozieHandler = ($event) => {
|
|
233
|
+
if ($event.key !== "Escape") return;
|
|
234
|
+
dismiss($event);
|
|
235
|
+
};
|
|
236
|
+
document.addEventListener("keydown", _rozieHandler);
|
|
237
|
+
return () => document.removeEventListener("keydown", _rozieHandler);
|
|
238
|
+
}, [dismiss, open]);
|
|
239
|
+
(0, _rozie_runtime_react.useOutsideClick)([anchorEl, floatingEl], dismiss, () => !!open);
|
|
240
|
+
const _rozieExposeRef = (0, react.useRef)({
|
|
241
|
+
show,
|
|
242
|
+
hide,
|
|
243
|
+
toggle,
|
|
244
|
+
reposition
|
|
245
|
+
});
|
|
246
|
+
_rozieExposeRef.current = {
|
|
247
|
+
show,
|
|
248
|
+
hide,
|
|
249
|
+
toggle,
|
|
250
|
+
reposition
|
|
251
|
+
};
|
|
252
|
+
(0, react.useImperativeHandle)(ref, () => ({
|
|
253
|
+
show: (...args) => _rozieExposeRef.current.show(...args),
|
|
254
|
+
hide: (...args) => _rozieExposeRef.current.hide(...args),
|
|
255
|
+
toggle: (...args) => _rozieExposeRef.current.toggle(...args),
|
|
256
|
+
reposition: (...args) => _rozieExposeRef.current.reposition(...args)
|
|
257
|
+
}), []);
|
|
258
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
259
|
+
...attrs,
|
|
260
|
+
className: (0, _rozie_runtime_react.clsx)("rozie-popover", attrs.className),
|
|
261
|
+
"data-rozie-s-c6cf02ea": "",
|
|
262
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
263
|
+
className: "rozie-popover-anchor",
|
|
264
|
+
ref: anchorEl,
|
|
265
|
+
"aria-haspopup": "dialog",
|
|
266
|
+
"aria-expanded": !!open,
|
|
267
|
+
"aria-describedby": (0, _rozie_runtime_react.rozieAttr)(isTooltip() && open ? "rozie-popover-floating" : void 0),
|
|
268
|
+
onClick: ($event) => {
|
|
269
|
+
props.trigger === "click" && onAnchorClick();
|
|
270
|
+
},
|
|
271
|
+
onPointerEnter: ($event) => {
|
|
272
|
+
props.trigger === "hover" && onAnchorPointerEnter();
|
|
273
|
+
},
|
|
274
|
+
onPointerLeave: ($event) => {
|
|
275
|
+
props.trigger === "hover" && onAnchorPointerLeave();
|
|
276
|
+
},
|
|
277
|
+
onFocus: ($event) => {
|
|
278
|
+
props.trigger === "focus" && onAnchorFocus();
|
|
279
|
+
},
|
|
280
|
+
onBlur: ($event) => {
|
|
281
|
+
props.trigger === "focus" && onAnchorBlur();
|
|
282
|
+
},
|
|
283
|
+
"data-rozie-s-c6cf02ea": "",
|
|
284
|
+
children: (props.renderAnchor ?? props.slots?.["anchor"])?.({
|
|
285
|
+
open,
|
|
286
|
+
toggle,
|
|
287
|
+
show,
|
|
288
|
+
hide
|
|
289
|
+
})
|
|
290
|
+
}), !!(open && !props.disabled) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
291
|
+
className: "rozie-popover-floating",
|
|
292
|
+
ref: floatingEl,
|
|
293
|
+
id: "rozie-popover-floating",
|
|
294
|
+
role: (0, _rozie_runtime_react.rozieAttr)(floatingRole()),
|
|
295
|
+
"aria-modal": !!(floatingRole() === "dialog"),
|
|
296
|
+
"data-rozie-s-c6cf02ea": "",
|
|
297
|
+
children: [!!props.arrow && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
298
|
+
className: "rozie-popover-arrow",
|
|
299
|
+
ref: arrowEl,
|
|
300
|
+
"data-rozie-s-c6cf02ea": ""
|
|
301
|
+
}), typeof (props.children ?? props.slots?.[""]) === "function" ? (props.children ?? props.slots?.[""])() : props.children ?? props.slots?.[""]]
|
|
302
|
+
})]
|
|
303
|
+
}) });
|
|
304
|
+
});
|
|
305
|
+
//#endregion
|
|
306
|
+
exports.Popover = Popover;
|
|
307
|
+
exports.default = Popover;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/Popover.d.ts
|
|
5
|
+
interface PopoverProps {
|
|
6
|
+
/**
|
|
7
|
+
* Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default.
|
|
8
|
+
*/
|
|
9
|
+
open?: boolean;
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
onOpenChange?: (next: boolean) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime.
|
|
14
|
+
*/
|
|
15
|
+
placement?: string;
|
|
16
|
+
/**
|
|
17
|
+
* How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog).
|
|
18
|
+
*/
|
|
19
|
+
trigger?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime.
|
|
22
|
+
*/
|
|
23
|
+
offset?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless.
|
|
26
|
+
*/
|
|
27
|
+
disableFlip?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor.
|
|
30
|
+
*/
|
|
31
|
+
disableShift?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties.
|
|
34
|
+
*/
|
|
35
|
+
arrow?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Disable the control entirely: the trigger no longer opens the content and any open content is suppressed.
|
|
38
|
+
*/
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored).
|
|
42
|
+
*/
|
|
43
|
+
modal?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime.
|
|
46
|
+
*/
|
|
47
|
+
strategy?: string;
|
|
48
|
+
onChange?: (...args: unknown[]) => void;
|
|
49
|
+
renderAnchor?: (params: {
|
|
50
|
+
open: boolean;
|
|
51
|
+
toggle: () => void;
|
|
52
|
+
show: () => void;
|
|
53
|
+
hide: () => void;
|
|
54
|
+
}) => ReactNode;
|
|
55
|
+
children?: ReactNode;
|
|
56
|
+
slots?: Record<string, () => ReactNode>;
|
|
57
|
+
}
|
|
58
|
+
interface PopoverHandle {
|
|
59
|
+
show: (...args: any[]) => any;
|
|
60
|
+
hide: (...args: any[]) => any;
|
|
61
|
+
toggle: (...args: any[]) => any;
|
|
62
|
+
reposition: (...args: any[]) => any;
|
|
63
|
+
}
|
|
64
|
+
declare const Popover: React.ForwardRefExoticComponent<PopoverProps & React.RefAttributes<PopoverHandle>>;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { Popover, Popover as default, type PopoverHandle };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as _$react from "react";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/Popover.d.ts
|
|
4
|
+
interface AnchorCtx {
|
|
5
|
+
open: any;
|
|
6
|
+
toggle: any;
|
|
7
|
+
show: any;
|
|
8
|
+
hide: any;
|
|
9
|
+
}
|
|
10
|
+
interface PopoverProps {
|
|
11
|
+
/**
|
|
12
|
+
* Whether the floating content is open. The sole `model: true` prop — two-way bind it (`r-model:open` / `v-model:open` / `bind:open` / `[(open)]`) and Popover writes the new state back whenever the trigger or a dismissal toggles it. Left unbound it falls back to an uncontrolled default.
|
|
13
|
+
*/
|
|
14
|
+
open?: boolean;
|
|
15
|
+
defaultOpen?: boolean;
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Floating UI placement of the content relative to the anchor — one of `top`/`right`/`bottom`/`left`, each optionally suffixed `-start`/`-end` (e.g. `bottom-start`). With `disableFlip` off, the content may flip to the opposite side when it would overflow the viewport. Reconciled at runtime.
|
|
19
|
+
*/
|
|
20
|
+
placement?: string;
|
|
21
|
+
/**
|
|
22
|
+
* How the anchor opens the content: `'click'` toggles on click, `'hover'` opens on pointer-enter and closes on pointer-leave (tooltip-style), `'focus'` opens on focus and closes on blur. Drives both the gesture handlers and the ARIA role (`'hover'`/`'focus'` → tooltip, `'click'` → popover dialog).
|
|
23
|
+
*/
|
|
24
|
+
trigger?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Distance in pixels between the anchor and the floating content (the Floating UI `offset` middleware). Reconciled at runtime.
|
|
27
|
+
*/
|
|
28
|
+
offset?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Disable the Floating UI `flip` middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport; set this to keep it pinned to `placement` regardless.
|
|
31
|
+
*/
|
|
32
|
+
disableFlip?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Disable the Floating UI `shift` middleware. By default the content shifts along its axis to stay within the viewport; set this to keep it strictly aligned to the anchor.
|
|
35
|
+
*/
|
|
36
|
+
disableShift?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Opt in to a positioned arrow element. When set, Popover renders an arrow `<div>` and runs the Floating UI `arrow` middleware against it so it points at the anchor. Style it via the `--rozie-popover-*` arrow CSS custom properties.
|
|
39
|
+
*/
|
|
40
|
+
arrow?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Disable the control entirely: the trigger no longer opens the content and any open content is suppressed.
|
|
43
|
+
*/
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Opt in to modal dialog semantics for a `click` popover. **Off by default:** a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a `role="menu"`) and carries NO `aria-modal`. Set `modal` for a genuinely modal dialog popover: the panel then gets `role="dialog"` + `aria-modal="true"`. **Note:** Popover ships no focus trap (it stays a minimal headless primitive); if you set `modal`, provide your own focus containment so the `aria-modal` claim holds. Ignored for `hover`/`focus` triggers (always tooltip-flavored).
|
|
47
|
+
*/
|
|
48
|
+
modal?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime.
|
|
51
|
+
*/
|
|
52
|
+
strategy?: string;
|
|
53
|
+
onChange?: (...args: any[]) => void;
|
|
54
|
+
renderAnchor?: (ctx: AnchorCtx) => ReactNode;
|
|
55
|
+
children?: ReactNode;
|
|
56
|
+
slots?: Record<string, () => _$react.ReactNode>;
|
|
57
|
+
}
|
|
58
|
+
interface PopoverHandle {
|
|
59
|
+
show: (...args: any[]) => any;
|
|
60
|
+
hide: (...args: any[]) => any;
|
|
61
|
+
toggle: (...args: any[]) => any;
|
|
62
|
+
reposition: (...args: any[]) => any;
|
|
63
|
+
}
|
|
64
|
+
declare const Popover: _$react.ForwardRefExoticComponent<PopoverProps & _$react.RefAttributes<PopoverHandle>>;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { Popover, Popover as default, type PopoverHandle };
|