@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.
@@ -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.2.0",
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": "27.0.0",
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
  }