@arun-dev/headless 4.2.0 → 4.4.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/dist/dialog/index.cjs +333 -0
- package/dist/dialog/index.d.cts +117 -0
- package/dist/dialog/index.d.ts +117 -0
- package/dist/dialog/index.js +192 -0
- package/dist/popover/index.cjs +317 -0
- package/dist/popover/index.d.cts +98 -0
- package/dist/popover/index.d.ts +98 -0
- package/dist/popover/index.js +176 -0
- package/package.json +26 -3
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
|
|
3
|
+
|
|
4
|
+
type PopoverRootProps = {
|
|
5
|
+
/**
|
|
6
|
+
* Controlled state. Provide `onOpenChange` alongside it.
|
|
7
|
+
*
|
|
8
|
+
* Never `undefined` once mounted: the mode is latched at mount, as for Switch.
|
|
9
|
+
*/
|
|
10
|
+
open?: boolean;
|
|
11
|
+
/** Initial state when uncontrolled. Read once, at mount. */
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Called on every request to open or close — Trigger, Close, Esc, or a click outside.
|
|
15
|
+
* A controlled popover moves only if the parent accepts it.
|
|
16
|
+
*/
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Holds a popover's state and shares it with its parts. Renders no element of its own.
|
|
22
|
+
*
|
|
23
|
+
* The popup is a native `popover`, placed by CSS anchor positioning (decision 12): the
|
|
24
|
+
* platform supplies the top layer, light dismiss, Esc, the Tab order from the trigger and
|
|
25
|
+
* focus return; the browser keeps it attached. This component adds state, one setter every
|
|
26
|
+
* close goes through, and the anchor wiring.
|
|
27
|
+
*/
|
|
28
|
+
declare function PopoverRoot({ open: openProp, defaultOpen, onOpenChange, children, }: PopoverRootProps): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
type PopoverTriggerOwnProps = {
|
|
31
|
+
/** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
|
|
32
|
+
render?: ReactElement;
|
|
33
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
34
|
+
ref?: Ref<HTMLElement>;
|
|
35
|
+
};
|
|
36
|
+
type PopoverTriggerProps = PopoverTriggerOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof PopoverTriggerOwnProps>;
|
|
37
|
+
/**
|
|
38
|
+
* Opens and closes the popover, and is what it is anchored to: it carries the
|
|
39
|
+
* `anchor-name` the popup positions against. A native `<button>`, so focus and activation
|
|
40
|
+
* are the platform's.
|
|
41
|
+
*
|
|
42
|
+
* It becomes the popup's invoker when it opens, so a click on it while open is not a
|
|
43
|
+
* click outside — it toggles the popover closed rather than light-dismissing and reopening.
|
|
44
|
+
*/
|
|
45
|
+
declare function PopoverTrigger({ className, children, render, ...rest }: PopoverTriggerProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
46
|
+
|
|
47
|
+
type PopoverSide = 'top' | 'bottom' | 'left' | 'right';
|
|
48
|
+
type PopoverAlign = 'start' | 'center' | 'end';
|
|
49
|
+
type PopoverPopupOwnProps = {
|
|
50
|
+
/** Which side of the trigger to open on. Flips to the opposite side when there is no room. */
|
|
51
|
+
side?: PopoverSide;
|
|
52
|
+
/** Where along that side: flush with the trigger's start or end edge, or centred on it. */
|
|
53
|
+
align?: PopoverAlign;
|
|
54
|
+
/** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
|
|
55
|
+
render?: ReactElement;
|
|
56
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
57
|
+
ref?: Ref<HTMLElement>;
|
|
58
|
+
};
|
|
59
|
+
type PopoverPopupProps = PopoverPopupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof PopoverPopupOwnProps | 'popover'>;
|
|
60
|
+
/**
|
|
61
|
+
* The popup: a native `popover="auto"`, shown with `showPopover({ source })` whenever the
|
|
62
|
+
* Root is open and hidden when it is not. State drives the element, never the reverse.
|
|
63
|
+
*
|
|
64
|
+
* Placement is CSS anchor positioning, set inline: `position-anchor` names the trigger,
|
|
65
|
+
* `position-area` comes from `side` and `align`, and `position-try-fallbacks` flips it
|
|
66
|
+
* when there is no room (decision 12). `data-side` and `data-align` repeat the request —
|
|
67
|
+
* not where a fallback moved it, which script cannot observe.
|
|
68
|
+
*
|
|
69
|
+
* Light dismiss and Esc close the element before anyone is asked; the `toggle` event
|
|
70
|
+
* reports it through the Root, and the element reopens if the state stays open.
|
|
71
|
+
*
|
|
72
|
+
* `role="dialog"`, non-modal. Name it with `aria-label` or `aria-labelledby`.
|
|
73
|
+
*/
|
|
74
|
+
declare function PopoverPopup({ side, align, className, children, render, ...rest }: PopoverPopupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
75
|
+
|
|
76
|
+
type PopoverCloseOwnProps = {
|
|
77
|
+
/** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
|
|
78
|
+
render?: ReactElement;
|
|
79
|
+
/** Ref to the rendered element. Merged with any ref on the `render` element. */
|
|
80
|
+
ref?: Ref<HTMLElement>;
|
|
81
|
+
};
|
|
82
|
+
type PopoverCloseProps = PopoverCloseOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof PopoverCloseOwnProps>;
|
|
83
|
+
/**
|
|
84
|
+
* Closes the popover. Esc and a click outside already do; this is the visible way, for
|
|
85
|
+
* pointer users and for screen reader users who would not guess either.
|
|
86
|
+
*/
|
|
87
|
+
declare function PopoverClose({ className, children, render, ...rest }: PopoverCloseProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
|
|
88
|
+
|
|
89
|
+
declare namespace index_parts {
|
|
90
|
+
export { PopoverClose as Close, PopoverPopup as Popup, PopoverRoot as Root, PopoverTrigger as Trigger };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The state Popover.Root shares with its parts, and projects as `data-*` attributes. */
|
|
94
|
+
type PopoverState = {
|
|
95
|
+
open: boolean;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export { index_parts as Popover, type PopoverAlign, type PopoverCloseProps, type PopoverPopupProps, type PopoverRootProps, type PopoverSide, type PopoverState, type PopoverTriggerProps };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useControlled
|
|
3
|
+
} from "../chunk-UEIB3I3T.js";
|
|
4
|
+
import {
|
|
5
|
+
__export,
|
|
6
|
+
useRender
|
|
7
|
+
} from "../chunk-OQTWWZ4B.js";
|
|
8
|
+
|
|
9
|
+
// src/popover/index.parts.ts
|
|
10
|
+
var index_parts_exports = {};
|
|
11
|
+
__export(index_parts_exports, {
|
|
12
|
+
Close: () => PopoverClose,
|
|
13
|
+
Popup: () => PopoverPopup,
|
|
14
|
+
Root: () => PopoverRoot,
|
|
15
|
+
Trigger: () => PopoverTrigger
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// src/popover/PopoverRoot.tsx
|
|
19
|
+
import { useCallback, useId, useMemo, useRef } from "react";
|
|
20
|
+
|
|
21
|
+
// src/popover/PopoverRootContext.ts
|
|
22
|
+
import { createContext, useContext } from "react";
|
|
23
|
+
var PopoverRootContext = createContext(null);
|
|
24
|
+
function usePopoverRootContext(part) {
|
|
25
|
+
const context = useContext(PopoverRootContext);
|
|
26
|
+
if (context === null) {
|
|
27
|
+
throw new Error(`<Popover.${part}> must be rendered inside <Popover.Root>.`);
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/popover/PopoverRoot.tsx
|
|
33
|
+
import { jsx } from "react/jsx-runtime";
|
|
34
|
+
function toAnchorName(id) {
|
|
35
|
+
return `--hl-anchor-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
36
|
+
}
|
|
37
|
+
function PopoverRoot({
|
|
38
|
+
open: openProp,
|
|
39
|
+
defaultOpen,
|
|
40
|
+
onOpenChange,
|
|
41
|
+
children
|
|
42
|
+
}) {
|
|
43
|
+
const [open, setOpenState] = useControlled({
|
|
44
|
+
controlled: openProp,
|
|
45
|
+
default: defaultOpen ?? false,
|
|
46
|
+
name: "Popover.Root",
|
|
47
|
+
state: "open"
|
|
48
|
+
});
|
|
49
|
+
const setOpen = useCallback(
|
|
50
|
+
(next) => {
|
|
51
|
+
if (next === open) return;
|
|
52
|
+
setOpenState(next);
|
|
53
|
+
onOpenChange?.(next);
|
|
54
|
+
},
|
|
55
|
+
[open, setOpenState, onOpenChange]
|
|
56
|
+
);
|
|
57
|
+
const id = useId();
|
|
58
|
+
const triggerRef = useRef(null);
|
|
59
|
+
const context = useMemo(
|
|
60
|
+
() => ({ open, setOpen, popupId: id, anchorName: toAnchorName(id), triggerRef }),
|
|
61
|
+
[open, setOpen, id]
|
|
62
|
+
);
|
|
63
|
+
return /* @__PURE__ */ jsx(PopoverRootContext.Provider, { value: context, children });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/popover/popoverDataAttributes.ts
|
|
67
|
+
function popoverDataAttributes({ open }) {
|
|
68
|
+
return {
|
|
69
|
+
"data-open": open ? "" : void 0,
|
|
70
|
+
"data-closed": open ? void 0 : ""
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/popover/PopoverTrigger.tsx
|
|
75
|
+
function PopoverTrigger({ className, children, render, ...rest }) {
|
|
76
|
+
const { open, setOpen, popupId, anchorName, triggerRef } = usePopoverRootContext("Trigger");
|
|
77
|
+
return useRender({
|
|
78
|
+
render,
|
|
79
|
+
defaultTagName: "button",
|
|
80
|
+
props: {
|
|
81
|
+
type: "button",
|
|
82
|
+
"aria-haspopup": "dialog",
|
|
83
|
+
"aria-expanded": open,
|
|
84
|
+
"aria-controls": popupId,
|
|
85
|
+
...popoverDataAttributes({ open }),
|
|
86
|
+
style: { anchorName },
|
|
87
|
+
className,
|
|
88
|
+
children,
|
|
89
|
+
ref: triggerRef,
|
|
90
|
+
onClick() {
|
|
91
|
+
setOpen(!open);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
consumerProps: rest
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/popover/PopoverPopup.tsx
|
|
99
|
+
import { useEffect, useReducer, useRef as useRef2 } from "react";
|
|
100
|
+
var POSITION_AREA = {
|
|
101
|
+
bottom: { start: "bottom span-right", center: "bottom", end: "bottom span-left" },
|
|
102
|
+
top: { start: "top span-right", center: "top", end: "top span-left" },
|
|
103
|
+
right: { start: "right span-bottom", center: "right", end: "right span-top" },
|
|
104
|
+
left: { start: "left span-bottom", center: "left", end: "left span-top" }
|
|
105
|
+
};
|
|
106
|
+
function tryFallbacks(side) {
|
|
107
|
+
return side === "top" || side === "bottom" ? "flip-block" : "flip-inline";
|
|
108
|
+
}
|
|
109
|
+
function PopoverPopup({
|
|
110
|
+
side = "bottom",
|
|
111
|
+
align = "center",
|
|
112
|
+
className,
|
|
113
|
+
children,
|
|
114
|
+
render,
|
|
115
|
+
...rest
|
|
116
|
+
}) {
|
|
117
|
+
const { open, setOpen, popupId, anchorName, triggerRef } = usePopoverRootContext("Popup");
|
|
118
|
+
const elementRef = useRef2(null);
|
|
119
|
+
const [, resync] = useReducer((n) => n + 1, 0);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const popup = elementRef.current;
|
|
122
|
+
if (!popup || typeof popup.showPopover !== "function") return;
|
|
123
|
+
const shown = popup.matches(":popover-open");
|
|
124
|
+
if (open && !shown) {
|
|
125
|
+
popup.showPopover({ source: triggerRef.current ?? void 0 });
|
|
126
|
+
} else if (!open && shown) popup.hidePopover();
|
|
127
|
+
});
|
|
128
|
+
return useRender({
|
|
129
|
+
render,
|
|
130
|
+
defaultTagName: "div",
|
|
131
|
+
props: {
|
|
132
|
+
id: popupId,
|
|
133
|
+
popover: "auto",
|
|
134
|
+
role: "dialog",
|
|
135
|
+
"data-side": side,
|
|
136
|
+
"data-align": align,
|
|
137
|
+
...popoverDataAttributes({ open }),
|
|
138
|
+
style: {
|
|
139
|
+
positionAnchor: anchorName,
|
|
140
|
+
positionArea: POSITION_AREA[side][align],
|
|
141
|
+
positionTryFallbacks: tryFallbacks(side)
|
|
142
|
+
},
|
|
143
|
+
className,
|
|
144
|
+
children,
|
|
145
|
+
ref: elementRef,
|
|
146
|
+
onToggle(event) {
|
|
147
|
+
if (event.target !== event.currentTarget) return;
|
|
148
|
+
if (event.nativeEvent.newState !== "closed") return;
|
|
149
|
+
setOpen(false);
|
|
150
|
+
resync();
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
consumerProps: rest
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/popover/PopoverClose.tsx
|
|
158
|
+
function PopoverClose({ className, children, render, ...rest }) {
|
|
159
|
+
const { setOpen } = usePopoverRootContext("Close");
|
|
160
|
+
return useRender({
|
|
161
|
+
render,
|
|
162
|
+
defaultTagName: "button",
|
|
163
|
+
props: {
|
|
164
|
+
type: "button",
|
|
165
|
+
className,
|
|
166
|
+
children,
|
|
167
|
+
onClick() {
|
|
168
|
+
setOpen(false);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
consumerProps: rest
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export {
|
|
175
|
+
index_parts_exports as Popover
|
|
176
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arun-dev/headless",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,6 +54,26 @@
|
|
|
54
54
|
"default": "./dist/checkbox/index.cjs"
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
+
"./dialog": {
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./dist/dialog/index.d.ts",
|
|
60
|
+
"default": "./dist/dialog/index.js"
|
|
61
|
+
},
|
|
62
|
+
"require": {
|
|
63
|
+
"types": "./dist/dialog/index.d.cts",
|
|
64
|
+
"default": "./dist/dialog/index.cjs"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"./popover": {
|
|
68
|
+
"import": {
|
|
69
|
+
"types": "./dist/popover/index.d.ts",
|
|
70
|
+
"default": "./dist/popover/index.js"
|
|
71
|
+
},
|
|
72
|
+
"require": {
|
|
73
|
+
"types": "./dist/popover/index.d.cts",
|
|
74
|
+
"default": "./dist/popover/index.cjs"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
57
77
|
"./radio-group": {
|
|
58
78
|
"import": {
|
|
59
79
|
"types": "./dist/radio-group/index.d.ts",
|
|
@@ -93,8 +113,10 @@
|
|
|
93
113
|
"@testing-library/react": "16.3.0",
|
|
94
114
|
"@types/react": "19.2.18",
|
|
95
115
|
"@types/react-dom": "19.2.5",
|
|
116
|
+
"@vitest/browser-playwright": "4.1.11",
|
|
96
117
|
"eslint": "9.39.4",
|
|
97
|
-
"jsdom": "
|
|
118
|
+
"jsdom": "30.0.1",
|
|
119
|
+
"playwright": "1.63.0",
|
|
98
120
|
"react": "19.2.8",
|
|
99
121
|
"react-dom": "19.2.8",
|
|
100
122
|
"tsup": "8.5.1",
|
|
@@ -109,6 +131,7 @@
|
|
|
109
131
|
"lint": "eslint .",
|
|
110
132
|
"lint:fix": "eslint . --fix",
|
|
111
133
|
"test": "pnpm run test:unit",
|
|
112
|
-
"test:unit": "vitest run --passWithNoTests --config vitest.unit.config.ts"
|
|
134
|
+
"test:unit": "vitest run --passWithNoTests --config vitest.unit.config.ts",
|
|
135
|
+
"test:browser": "vitest run --config vitest.browser.config.ts"
|
|
113
136
|
}
|
|
114
137
|
}
|