@arun-dev/headless 4.3.0 → 4.5.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,92 @@
1
+ import * as react from 'react';
2
+ import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
+ import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.js';
4
+
5
+ type TooltipRootProps = {
6
+ /**
7
+ * Controlled state. Provide `onOpenChange` alongside it.
8
+ *
9
+ * Never `undefined` once mounted: the mode is latched at mount, as for Switch.
10
+ */
11
+ open?: boolean;
12
+ /** Initial state when uncontrolled. Read once, at mount. */
13
+ defaultOpen?: boolean;
14
+ /** Called on every open and close, after any delay has run. */
15
+ onOpenChange?: (open: boolean) => void;
16
+ /** Milliseconds a pointer rests on the trigger before it opens. Focus opens at once. */
17
+ delay?: number;
18
+ /**
19
+ * Milliseconds between the pointer leaving and the tooltip closing — time to move onto
20
+ * the tooltip, which then stays open while hovered (WCAG 1.4.13).
21
+ */
22
+ closeDelay?: number;
23
+ children?: ReactNode;
24
+ };
25
+ /**
26
+ * Holds a tooltip's state and timers, and shares them with its parts. Renders no element.
27
+ *
28
+ * The popup is a native `popover="manual"`, anchored like Popover (decision 12). `hint`
29
+ * would be the natural fit, but Safari lacks it; `manual` behaves the same everywhere and,
30
+ * like `hint`, does not close an open Popover when it shows. What `manual` leaves out —
31
+ * opening on hover and focus, closing on leave, blur and Esc — is the timing below.
32
+ */
33
+ declare function TooltipRoot({ open: openProp, defaultOpen, onOpenChange, delay, closeDelay, children, }: TooltipRootProps): react.JSX.Element;
34
+
35
+ type TooltipTriggerOwnProps = {
36
+ /** Element to render instead of the default `<button>`. Props and ref are merged onto it. */
37
+ render?: ReactElement;
38
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
39
+ ref?: Ref<HTMLElement>;
40
+ };
41
+ type TooltipTriggerProps = TooltipTriggerOwnProps & Omit<ComponentPropsWithRef<'button'>, keyof TooltipTriggerOwnProps>;
42
+ /**
43
+ * The element the tooltip describes and is anchored to. A native `<button>` by default;
44
+ * render another focusable element when the tooltip belongs to one — a Popover.Trigger,
45
+ * say, or a link.
46
+ *
47
+ * - A mouse or pen resting on it opens the tooltip after the Root's `delay`; leaving
48
+ * closes it after `closeDelay`. Touch never opens it: there is no hover to end it.
49
+ * - Keyboard focus opens it at once; blur closes it. Focus from a click does not open it —
50
+ * only `:focus-visible` does.
51
+ * - Pressing it closes the tooltip, since the press is what the user came to do.
52
+ *
53
+ * `aria-describedby` points at the tooltip at all times — the description is read from the
54
+ * hidden element too — and is joined with any of yours.
55
+ */
56
+ declare function TooltipTrigger({ className, children, render, 'aria-describedby': describedBy, ...rest }: TooltipTriggerProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
57
+
58
+ type TooltipSide = AnchorSide;
59
+ type TooltipAlign = AnchorAlign;
60
+ type TooltipPopupOwnProps = {
61
+ /** Which side of the trigger to show on. Flips to the opposite side when there is no room. */
62
+ side?: TooltipSide;
63
+ /** Where along that side: flush with the trigger's start or end edge, or centred on it. */
64
+ align?: TooltipAlign;
65
+ /** Element to render instead of the default `<div>`. Props and ref are merged onto it. */
66
+ render?: ReactElement;
67
+ /** Ref to the rendered element. Merged with any ref on the `render` element. */
68
+ ref?: Ref<HTMLElement>;
69
+ };
70
+ type TooltipPopupProps = TooltipPopupOwnProps & Omit<ComponentPropsWithRef<'div'>, keyof TooltipPopupOwnProps | 'popover'>;
71
+ /**
72
+ * The tooltip: a native `popover="manual"` with `role="tooltip"`, anchored to the trigger
73
+ * as Popover is (decision 12). State drives the element, never the reverse.
74
+ *
75
+ * It stays open while the pointer is on it, so its text can be read and selected, and
76
+ * closes on Esc — which closes only the tooltip, not a Dialog or Popover beneath it.
77
+ *
78
+ * Text only, by the ARIA pattern: it describes the trigger and is never focused, so it must
79
+ * not hold anything interactive. Use a Popover for that.
80
+ */
81
+ declare function TooltipPopup({ side, align, className, children, render, ...rest }: TooltipPopupProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
82
+
83
+ declare namespace index_parts {
84
+ export { TooltipPopup as Popup, TooltipRoot as Root, TooltipTrigger as Trigger };
85
+ }
86
+
87
+ /** The state Tooltip.Root shares with its parts, and projects as `data-*` attributes. */
88
+ type TooltipState = {
89
+ open: boolean;
90
+ };
91
+
92
+ export { index_parts as Tooltip, type TooltipAlign, type TooltipPopupProps, type TooltipRootProps, type TooltipSide, type TooltipState, type TooltipTriggerProps };
@@ -0,0 +1,206 @@
1
+ import {
2
+ anchorNameFor,
3
+ anchoredDataAttributes,
4
+ anchoredPopupStyle
5
+ } from "../chunk-HCOCPQXS.js";
6
+ import {
7
+ useControlled
8
+ } from "../chunk-UEIB3I3T.js";
9
+ import {
10
+ __export,
11
+ useRender
12
+ } from "../chunk-OQTWWZ4B.js";
13
+
14
+ // src/tooltip/index.parts.ts
15
+ var index_parts_exports = {};
16
+ __export(index_parts_exports, {
17
+ Popup: () => TooltipPopup,
18
+ Root: () => TooltipRoot,
19
+ Trigger: () => TooltipTrigger
20
+ });
21
+
22
+ // src/tooltip/TooltipRoot.tsx
23
+ import { useCallback, useEffect, useId, useMemo, useRef } from "react";
24
+
25
+ // src/tooltip/TooltipRootContext.ts
26
+ import { createContext, useContext } from "react";
27
+ var TooltipRootContext = createContext(null);
28
+ function useTooltipRootContext(part) {
29
+ const context = useContext(TooltipRootContext);
30
+ if (context === null) {
31
+ throw new Error(`<Tooltip.${part}> must be rendered inside <Tooltip.Root>.`);
32
+ }
33
+ return context;
34
+ }
35
+
36
+ // src/tooltip/TooltipRoot.tsx
37
+ import { jsx } from "react/jsx-runtime";
38
+ var WARM_WINDOW = 300;
39
+ var openCount = 0;
40
+ var lastClosedAt = Number.NEGATIVE_INFINITY;
41
+ function isWarm() {
42
+ return openCount > 0 || Date.now() - lastClosedAt < WARM_WINDOW;
43
+ }
44
+ function TooltipRoot({
45
+ open: openProp,
46
+ defaultOpen,
47
+ onOpenChange,
48
+ delay = 600,
49
+ closeDelay = 100,
50
+ children
51
+ }) {
52
+ const [open, setOpenState] = useControlled({
53
+ controlled: openProp,
54
+ default: defaultOpen ?? false,
55
+ name: "Tooltip.Root",
56
+ state: "open"
57
+ });
58
+ const timer = useRef(void 0);
59
+ const cancelPending = useCallback(() => {
60
+ clearTimeout(timer.current);
61
+ timer.current = void 0;
62
+ }, []);
63
+ const setOpen = useCallback(
64
+ (next) => {
65
+ cancelPending();
66
+ if (next === open) return;
67
+ setOpenState(next);
68
+ onOpenChange?.(next);
69
+ },
70
+ [open, setOpenState, onOpenChange, cancelPending]
71
+ );
72
+ const openAfterDelay = useCallback(() => {
73
+ cancelPending();
74
+ if (open) return;
75
+ if (isWarm()) setOpen(true);
76
+ else timer.current = setTimeout(() => setOpen(true), delay);
77
+ }, [open, delay, setOpen, cancelPending]);
78
+ const closeAfterGrace = useCallback(() => {
79
+ cancelPending();
80
+ if (!open) return;
81
+ timer.current = setTimeout(() => setOpen(false), closeDelay);
82
+ }, [open, closeDelay, setOpen, cancelPending]);
83
+ useEffect(() => cancelPending, [cancelPending]);
84
+ useEffect(() => {
85
+ if (!open) return;
86
+ openCount += 1;
87
+ return () => {
88
+ openCount -= 1;
89
+ lastClosedAt = Date.now();
90
+ };
91
+ }, [open]);
92
+ const id = useId();
93
+ const context = useMemo(
94
+ () => ({
95
+ open,
96
+ setOpen,
97
+ openAfterDelay,
98
+ closeAfterGrace,
99
+ cancelPending,
100
+ popupId: id,
101
+ anchorName: anchorNameFor(id)
102
+ }),
103
+ [open, setOpen, openAfterDelay, closeAfterGrace, cancelPending, id]
104
+ );
105
+ return /* @__PURE__ */ jsx(TooltipRootContext.Provider, { value: context, children });
106
+ }
107
+
108
+ // src/tooltip/tooltipDataAttributes.ts
109
+ function tooltipDataAttributes({ open }) {
110
+ return {
111
+ "data-open": open ? "" : void 0,
112
+ "data-closed": open ? void 0 : ""
113
+ };
114
+ }
115
+
116
+ // src/tooltip/TooltipTrigger.tsx
117
+ function TooltipTrigger({
118
+ className,
119
+ children,
120
+ render,
121
+ "aria-describedby": describedBy,
122
+ ...rest
123
+ }) {
124
+ const { open, setOpen, openAfterDelay, closeAfterGrace, popupId, anchorName } = useTooltipRootContext("Trigger");
125
+ return useRender({
126
+ render,
127
+ defaultTagName: "button",
128
+ props: {
129
+ type: "button",
130
+ "aria-describedby": describedBy ? `${describedBy} ${popupId}` : popupId,
131
+ ...tooltipDataAttributes({ open }),
132
+ style: { anchorName },
133
+ className,
134
+ children,
135
+ onPointerEnter(event) {
136
+ if (event.pointerType !== "touch") openAfterDelay();
137
+ },
138
+ onPointerLeave(event) {
139
+ if (event.pointerType !== "touch") closeAfterGrace();
140
+ },
141
+ onPointerDown() {
142
+ setOpen(false);
143
+ },
144
+ onFocus(event) {
145
+ if (event.currentTarget.matches(":focus-visible")) setOpen(true);
146
+ },
147
+ onBlur() {
148
+ setOpen(false);
149
+ }
150
+ },
151
+ consumerProps: rest
152
+ });
153
+ }
154
+
155
+ // src/tooltip/TooltipPopup.tsx
156
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
157
+ function TooltipPopup({
158
+ side = "top",
159
+ align = "center",
160
+ className,
161
+ children,
162
+ render,
163
+ ...rest
164
+ }) {
165
+ const { open, setOpen, cancelPending, closeAfterGrace, popupId, anchorName } = useTooltipRootContext("Popup");
166
+ const elementRef = useRef2(null);
167
+ useEffect2(() => {
168
+ const popup = elementRef.current;
169
+ if (!popup || typeof popup.showPopover !== "function") return;
170
+ const shown = popup.matches(":popover-open");
171
+ if (open && !shown) popup.showPopover();
172
+ else if (!open && shown) popup.hidePopover();
173
+ });
174
+ useEffect2(() => {
175
+ if (!open) return;
176
+ function onKeyDown(event) {
177
+ if (event.key !== "Escape") return;
178
+ event.preventDefault();
179
+ event.stopPropagation();
180
+ setOpen(false);
181
+ }
182
+ document.addEventListener("keydown", onKeyDown, true);
183
+ return () => document.removeEventListener("keydown", onKeyDown, true);
184
+ }, [open, setOpen]);
185
+ return useRender({
186
+ render,
187
+ defaultTagName: "div",
188
+ props: {
189
+ id: popupId,
190
+ popover: "manual",
191
+ role: "tooltip",
192
+ ...anchoredDataAttributes(side, align),
193
+ ...tooltipDataAttributes({ open }),
194
+ style: anchoredPopupStyle(anchorName, side, align),
195
+ className,
196
+ children,
197
+ ref: elementRef,
198
+ onPointerEnter: cancelPending,
199
+ onPointerLeave: closeAfterGrace
200
+ },
201
+ consumerProps: rest
202
+ });
203
+ }
204
+ export {
205
+ index_parts_exports as Tooltip
206
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "4.3.0",
3
+ "version": "4.5.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",
@@ -64,6 +64,16 @@
64
64
  "default": "./dist/dialog/index.cjs"
65
65
  }
66
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
+ },
67
77
  "./radio-group": {
68
78
  "import": {
69
79
  "types": "./dist/radio-group/index.d.ts",
@@ -83,6 +93,16 @@
83
93
  "types": "./dist/switch/index.d.cts",
84
94
  "default": "./dist/switch/index.cjs"
85
95
  }
96
+ },
97
+ "./tooltip": {
98
+ "import": {
99
+ "types": "./dist/tooltip/index.d.ts",
100
+ "default": "./dist/tooltip/index.js"
101
+ },
102
+ "require": {
103
+ "types": "./dist/tooltip/index.d.cts",
104
+ "default": "./dist/tooltip/index.cjs"
105
+ }
86
106
  }
87
107
  },
88
108
  "publishConfig": {
@@ -105,7 +125,7 @@
105
125
  "@types/react-dom": "19.2.5",
106
126
  "@vitest/browser-playwright": "4.1.11",
107
127
  "eslint": "9.39.4",
108
- "jsdom": "27.0.0",
128
+ "jsdom": "30.0.1",
109
129
  "playwright": "1.63.0",
110
130
  "react": "19.2.8",
111
131
  "react-dom": "19.2.8",