@arun-dev/headless 4.4.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,12 @@
1
+ /**
2
+ * CSS anchor positioning for popups placed beside a trigger — Popover and Tooltip
3
+ * (decision 12). Internal: shared between components, not exported (decision 9).
4
+ *
5
+ * The trigger carries a generated `anchor-name`; the popup names it in `position-anchor`
6
+ * and is placed by `position-area`, flipping across the axis it opens along when there is
7
+ * no room. All inline, so the browser keeps it attached with nothing measured.
8
+ */
9
+ type AnchorSide = 'top' | 'bottom' | 'left' | 'right';
10
+ type AnchorAlign = 'start' | 'center' | 'end';
11
+
12
+ export type { AnchorSide as A, AnchorAlign as a };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * CSS anchor positioning for popups placed beside a trigger — Popover and Tooltip
3
+ * (decision 12). Internal: shared between components, not exported (decision 9).
4
+ *
5
+ * The trigger carries a generated `anchor-name`; the popup names it in `position-anchor`
6
+ * and is placed by `position-area`, flipping across the axis it opens along when there is
7
+ * no room. All inline, so the browser keeps it attached with nothing measured.
8
+ */
9
+ type AnchorSide = 'top' | 'bottom' | 'left' | 'right';
10
+ type AnchorAlign = 'start' | 'center' | 'end';
11
+
12
+ export type { AnchorSide as A, AnchorAlign as a };
@@ -0,0 +1,27 @@
1
+ // src/core/anchoring.ts
2
+ function anchorNameFor(id) {
3
+ return `--hl-anchor-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
4
+ }
5
+ var POSITION_AREA = {
6
+ bottom: { start: "bottom span-right", center: "bottom", end: "bottom span-left" },
7
+ top: { start: "top span-right", center: "top", end: "top span-left" },
8
+ right: { start: "right span-bottom", center: "right", end: "right span-top" },
9
+ left: { start: "left span-bottom", center: "left", end: "left span-top" }
10
+ };
11
+ function anchoredPopupStyle(anchorName, side, align) {
12
+ return {
13
+ positionAnchor: anchorName,
14
+ positionArea: POSITION_AREA[side][align],
15
+ // Flip across the axis it opens on; the other axis is left alone.
16
+ positionTryFallbacks: side === "top" || side === "bottom" ? "flip-block" : "flip-inline"
17
+ };
18
+ }
19
+ function anchoredDataAttributes(side, align) {
20
+ return { "data-side": side, "data-align": align };
21
+ }
22
+
23
+ export {
24
+ anchorNameFor,
25
+ anchoredPopupStyle,
26
+ anchoredDataAttributes
27
+ };
@@ -36,6 +36,28 @@ __export(index_parts_exports, {
36
36
  // src/popover/PopoverRoot.tsx
37
37
  var import_react3 = require("react");
38
38
 
39
+ // src/core/anchoring.ts
40
+ function anchorNameFor(id) {
41
+ return `--hl-anchor-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
42
+ }
43
+ var POSITION_AREA = {
44
+ bottom: { start: "bottom span-right", center: "bottom", end: "bottom span-left" },
45
+ top: { start: "top span-right", center: "top", end: "top span-left" },
46
+ right: { start: "right span-bottom", center: "right", end: "right span-top" },
47
+ left: { start: "left span-bottom", center: "left", end: "left span-top" }
48
+ };
49
+ function anchoredPopupStyle(anchorName, side, align) {
50
+ return {
51
+ positionAnchor: anchorName,
52
+ positionArea: POSITION_AREA[side][align],
53
+ // Flip across the axis it opens on; the other axis is left alone.
54
+ positionTryFallbacks: side === "top" || side === "bottom" ? "flip-block" : "flip-inline"
55
+ };
56
+ }
57
+ function anchoredDataAttributes(side, align) {
58
+ return { "data-side": side, "data-align": align };
59
+ }
60
+
39
61
  // src/core/useControlled.ts
40
62
  var import_react = require("react");
41
63
  function useControlled({
@@ -82,9 +104,6 @@ function usePopoverRootContext(part) {
82
104
 
83
105
  // src/popover/PopoverRoot.tsx
84
106
  var import_jsx_runtime = require("react/jsx-runtime");
85
- function toAnchorName(id) {
86
- return `--hl-anchor-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
87
- }
88
107
  function PopoverRoot({
89
108
  open: openProp,
90
109
  defaultOpen,
@@ -108,7 +127,7 @@ function PopoverRoot({
108
127
  const id = (0, import_react3.useId)();
109
128
  const triggerRef = (0, import_react3.useRef)(null);
110
129
  const context = (0, import_react3.useMemo)(
111
- () => ({ open, setOpen, popupId: id, anchorName: toAnchorName(id), triggerRef }),
130
+ () => ({ open, setOpen, popupId: id, anchorName: anchorNameFor(id), triggerRef }),
112
131
  [open, setOpen, id]
113
132
  );
114
133
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PopoverRootContext.Provider, { value: context, children });
@@ -237,15 +256,6 @@ function PopoverTrigger({ className, children, render, ...rest }) {
237
256
 
238
257
  // src/popover/PopoverPopup.tsx
239
258
  var import_react5 = require("react");
240
- var POSITION_AREA = {
241
- bottom: { start: "bottom span-right", center: "bottom", end: "bottom span-left" },
242
- top: { start: "top span-right", center: "top", end: "top span-left" },
243
- right: { start: "right span-bottom", center: "right", end: "right span-top" },
244
- left: { start: "left span-bottom", center: "left", end: "left span-top" }
245
- };
246
- function tryFallbacks(side) {
247
- return side === "top" || side === "bottom" ? "flip-block" : "flip-inline";
248
- }
249
259
  function PopoverPopup({
250
260
  side = "bottom",
251
261
  align = "center",
@@ -272,14 +282,9 @@ function PopoverPopup({
272
282
  id: popupId,
273
283
  popover: "auto",
274
284
  role: "dialog",
275
- "data-side": side,
276
- "data-align": align,
285
+ ...anchoredDataAttributes(side, align),
277
286
  ...popoverDataAttributes({ open }),
278
- style: {
279
- positionAnchor: anchorName,
280
- positionArea: POSITION_AREA[side][align],
281
- positionTryFallbacks: tryFallbacks(side)
282
- },
287
+ style: anchoredPopupStyle(anchorName, side, align),
283
288
  className,
284
289
  children,
285
290
  ref: elementRef,
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
+ import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.cjs';
3
4
 
4
5
  type PopoverRootProps = {
5
6
  /**
@@ -44,8 +45,8 @@ type PopoverTriggerProps = PopoverTriggerOwnProps & Omit<ComponentPropsWithRef<'
44
45
  */
45
46
  declare function PopoverTrigger({ className, children, render, ...rest }: PopoverTriggerProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
46
47
 
47
- type PopoverSide = 'top' | 'bottom' | 'left' | 'right';
48
- type PopoverAlign = 'start' | 'center' | 'end';
48
+ type PopoverSide = AnchorSide;
49
+ type PopoverAlign = AnchorAlign;
49
50
  type PopoverPopupOwnProps = {
50
51
  /** Which side of the trigger to open on. Flips to the opposite side when there is no room. */
51
52
  side?: PopoverSide;
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement, Ref, ComponentPropsWithRef } from 'react';
3
+ import { A as AnchorSide, a as AnchorAlign } from '../anchoring-Btxi0QwC.js';
3
4
 
4
5
  type PopoverRootProps = {
5
6
  /**
@@ -44,8 +45,8 @@ type PopoverTriggerProps = PopoverTriggerOwnProps & Omit<ComponentPropsWithRef<'
44
45
  */
45
46
  declare function PopoverTrigger({ className, children, render, ...rest }: PopoverTriggerProps): ReactElement<unknown, string | react.JSXElementConstructor<any>>;
46
47
 
47
- type PopoverSide = 'top' | 'bottom' | 'left' | 'right';
48
- type PopoverAlign = 'start' | 'center' | 'end';
48
+ type PopoverSide = AnchorSide;
49
+ type PopoverAlign = AnchorAlign;
49
50
  type PopoverPopupOwnProps = {
50
51
  /** Which side of the trigger to open on. Flips to the opposite side when there is no room. */
51
52
  side?: PopoverSide;
@@ -1,3 +1,8 @@
1
+ import {
2
+ anchorNameFor,
3
+ anchoredDataAttributes,
4
+ anchoredPopupStyle
5
+ } from "../chunk-HCOCPQXS.js";
1
6
  import {
2
7
  useControlled
3
8
  } from "../chunk-UEIB3I3T.js";
@@ -31,9 +36,6 @@ function usePopoverRootContext(part) {
31
36
 
32
37
  // src/popover/PopoverRoot.tsx
33
38
  import { jsx } from "react/jsx-runtime";
34
- function toAnchorName(id) {
35
- return `--hl-anchor-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
36
- }
37
39
  function PopoverRoot({
38
40
  open: openProp,
39
41
  defaultOpen,
@@ -57,7 +59,7 @@ function PopoverRoot({
57
59
  const id = useId();
58
60
  const triggerRef = useRef(null);
59
61
  const context = useMemo(
60
- () => ({ open, setOpen, popupId: id, anchorName: toAnchorName(id), triggerRef }),
62
+ () => ({ open, setOpen, popupId: id, anchorName: anchorNameFor(id), triggerRef }),
61
63
  [open, setOpen, id]
62
64
  );
63
65
  return /* @__PURE__ */ jsx(PopoverRootContext.Provider, { value: context, children });
@@ -97,15 +99,6 @@ function PopoverTrigger({ className, children, render, ...rest }) {
97
99
 
98
100
  // src/popover/PopoverPopup.tsx
99
101
  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
102
  function PopoverPopup({
110
103
  side = "bottom",
111
104
  align = "center",
@@ -132,14 +125,9 @@ function PopoverPopup({
132
125
  id: popupId,
133
126
  popover: "auto",
134
127
  role: "dialog",
135
- "data-side": side,
136
- "data-align": align,
128
+ ...anchoredDataAttributes(side, align),
137
129
  ...popoverDataAttributes({ open }),
138
- style: {
139
- positionAnchor: anchorName,
140
- positionArea: POSITION_AREA[side][align],
141
- positionTryFallbacks: tryFallbacks(side)
142
- },
130
+ style: anchoredPopupStyle(anchorName, side, align),
143
131
  className,
144
132
  children,
145
133
  ref: elementRef,
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/tooltip/index.ts
21
+ var tooltip_exports = {};
22
+ __export(tooltip_exports, {
23
+ Tooltip: () => index_parts_exports
24
+ });
25
+ module.exports = __toCommonJS(tooltip_exports);
26
+
27
+ // src/tooltip/index.parts.ts
28
+ var index_parts_exports = {};
29
+ __export(index_parts_exports, {
30
+ Popup: () => TooltipPopup,
31
+ Root: () => TooltipRoot,
32
+ Trigger: () => TooltipTrigger
33
+ });
34
+
35
+ // src/tooltip/TooltipRoot.tsx
36
+ var import_react3 = require("react");
37
+
38
+ // src/core/anchoring.ts
39
+ function anchorNameFor(id) {
40
+ return `--hl-anchor-${id.replace(/[^a-zA-Z0-9_-]/g, "")}`;
41
+ }
42
+ var POSITION_AREA = {
43
+ bottom: { start: "bottom span-right", center: "bottom", end: "bottom span-left" },
44
+ top: { start: "top span-right", center: "top", end: "top span-left" },
45
+ right: { start: "right span-bottom", center: "right", end: "right span-top" },
46
+ left: { start: "left span-bottom", center: "left", end: "left span-top" }
47
+ };
48
+ function anchoredPopupStyle(anchorName, side, align) {
49
+ return {
50
+ positionAnchor: anchorName,
51
+ positionArea: POSITION_AREA[side][align],
52
+ // Flip across the axis it opens on; the other axis is left alone.
53
+ positionTryFallbacks: side === "top" || side === "bottom" ? "flip-block" : "flip-inline"
54
+ };
55
+ }
56
+ function anchoredDataAttributes(side, align) {
57
+ return { "data-side": side, "data-align": align };
58
+ }
59
+
60
+ // src/core/useControlled.ts
61
+ var import_react = require("react");
62
+ function useControlled({
63
+ controlled,
64
+ default: defaultValue,
65
+ name = "Component",
66
+ state = "value"
67
+ }) {
68
+ const { current: isControlled } = (0, import_react.useRef)(controlled !== void 0);
69
+ const [uncontrolled, setUncontrolled] = (0, import_react.useState)(defaultValue);
70
+ const defaultRef = (0, import_react.useRef)(defaultValue);
71
+ const value = isControlled ? controlled : uncontrolled;
72
+ if (process.env.NODE_ENV !== "production") {
73
+ if (isControlled !== (controlled !== void 0)) {
74
+ console.error(
75
+ `${name}: cannot switch between controlled and uncontrolled \`${state}\`. Decide which one this component is for the whole of its life.`
76
+ );
77
+ }
78
+ if (!isControlled && defaultRef.current !== defaultValue) {
79
+ console.error(
80
+ `${name}: cannot change the default \`${state}\` after mount. It is only read once, so later changes are silently ignored.`
81
+ );
82
+ }
83
+ }
84
+ const setValue = (0, import_react.useCallback)(
85
+ (next) => {
86
+ if (!isControlled) setUncontrolled(next);
87
+ },
88
+ [isControlled]
89
+ );
90
+ return [value, setValue];
91
+ }
92
+
93
+ // src/tooltip/TooltipRootContext.ts
94
+ var import_react2 = require("react");
95
+ var TooltipRootContext = (0, import_react2.createContext)(null);
96
+ function useTooltipRootContext(part) {
97
+ const context = (0, import_react2.useContext)(TooltipRootContext);
98
+ if (context === null) {
99
+ throw new Error(`<Tooltip.${part}> must be rendered inside <Tooltip.Root>.`);
100
+ }
101
+ return context;
102
+ }
103
+
104
+ // src/tooltip/TooltipRoot.tsx
105
+ var import_jsx_runtime = require("react/jsx-runtime");
106
+ var WARM_WINDOW = 300;
107
+ var openCount = 0;
108
+ var lastClosedAt = Number.NEGATIVE_INFINITY;
109
+ function isWarm() {
110
+ return openCount > 0 || Date.now() - lastClosedAt < WARM_WINDOW;
111
+ }
112
+ function TooltipRoot({
113
+ open: openProp,
114
+ defaultOpen,
115
+ onOpenChange,
116
+ delay = 600,
117
+ closeDelay = 100,
118
+ children
119
+ }) {
120
+ const [open, setOpenState] = useControlled({
121
+ controlled: openProp,
122
+ default: defaultOpen ?? false,
123
+ name: "Tooltip.Root",
124
+ state: "open"
125
+ });
126
+ const timer = (0, import_react3.useRef)(void 0);
127
+ const cancelPending = (0, import_react3.useCallback)(() => {
128
+ clearTimeout(timer.current);
129
+ timer.current = void 0;
130
+ }, []);
131
+ const setOpen = (0, import_react3.useCallback)(
132
+ (next) => {
133
+ cancelPending();
134
+ if (next === open) return;
135
+ setOpenState(next);
136
+ onOpenChange?.(next);
137
+ },
138
+ [open, setOpenState, onOpenChange, cancelPending]
139
+ );
140
+ const openAfterDelay = (0, import_react3.useCallback)(() => {
141
+ cancelPending();
142
+ if (open) return;
143
+ if (isWarm()) setOpen(true);
144
+ else timer.current = setTimeout(() => setOpen(true), delay);
145
+ }, [open, delay, setOpen, cancelPending]);
146
+ const closeAfterGrace = (0, import_react3.useCallback)(() => {
147
+ cancelPending();
148
+ if (!open) return;
149
+ timer.current = setTimeout(() => setOpen(false), closeDelay);
150
+ }, [open, closeDelay, setOpen, cancelPending]);
151
+ (0, import_react3.useEffect)(() => cancelPending, [cancelPending]);
152
+ (0, import_react3.useEffect)(() => {
153
+ if (!open) return;
154
+ openCount += 1;
155
+ return () => {
156
+ openCount -= 1;
157
+ lastClosedAt = Date.now();
158
+ };
159
+ }, [open]);
160
+ const id = (0, import_react3.useId)();
161
+ const context = (0, import_react3.useMemo)(
162
+ () => ({
163
+ open,
164
+ setOpen,
165
+ openAfterDelay,
166
+ closeAfterGrace,
167
+ cancelPending,
168
+ popupId: id,
169
+ anchorName: anchorNameFor(id)
170
+ }),
171
+ [open, setOpen, openAfterDelay, closeAfterGrace, cancelPending, id]
172
+ );
173
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TooltipRootContext.Provider, { value: context, children });
174
+ }
175
+
176
+ // src/core/useRender.ts
177
+ var import_react4 = require("react");
178
+
179
+ // src/core/mergeProps.ts
180
+ function isEventHandler(key) {
181
+ return key.length > 2 && key.startsWith("on") && key[2] === key[2]?.toUpperCase();
182
+ }
183
+ function mergeRefs(...refs) {
184
+ return (node) => {
185
+ const cleanups = refs.map((ref) => {
186
+ if (typeof ref === "function") {
187
+ const result = ref(node);
188
+ return typeof result === "function" ? result : () => ref(null);
189
+ }
190
+ if (ref && typeof ref === "object") {
191
+ ref.current = node;
192
+ return () => {
193
+ ref.current = null;
194
+ };
195
+ }
196
+ return void 0;
197
+ });
198
+ return () => {
199
+ cleanups.forEach((cleanup) => {
200
+ if (typeof cleanup === "function") cleanup();
201
+ });
202
+ };
203
+ };
204
+ }
205
+ function isSyntheticEvent(event) {
206
+ return typeof event === "object" && event !== null && "nativeEvent" in event;
207
+ }
208
+ function makeEventPreventable(event) {
209
+ if (Object.prototype.hasOwnProperty.call(event, "preventComponentHandler")) return;
210
+ const target = event;
211
+ target.preventComponentHandler = () => {
212
+ target.componentHandlerPrevented = true;
213
+ };
214
+ }
215
+ function isComponentHandlerPrevented(event) {
216
+ return typeof event === "object" && event !== null && event.componentHandlerPrevented === true;
217
+ }
218
+ function chainHandlers(earlier, value) {
219
+ return (...args) => {
220
+ const event = args[0];
221
+ if (isSyntheticEvent(event)) makeEventPreventable(event);
222
+ value(...args);
223
+ if (isComponentHandlerPrevented(event)) return;
224
+ earlier(...args);
225
+ };
226
+ }
227
+ function mergeProps(...objects) {
228
+ const merged = {};
229
+ for (const props of objects) {
230
+ if (!props) continue;
231
+ for (const key of Object.keys(props)) {
232
+ const value = props[key];
233
+ if (value === void 0) continue;
234
+ if (isEventHandler(key) && typeof value === "function") {
235
+ const earlier = merged[key];
236
+ merged[key] = typeof earlier === "function" ? chainHandlers(earlier, value) : value;
237
+ } else if (key === "className") {
238
+ merged.className = merged.className ? `${String(merged.className)} ${String(value)}` : value;
239
+ } else if (key === "style") {
240
+ merged.style = { ...merged.style, ...value };
241
+ } else if (key === "ref") {
242
+ merged.ref = merged.ref ? mergeRefs(merged.ref, value) : value;
243
+ } else {
244
+ merged[key] = value;
245
+ }
246
+ }
247
+ }
248
+ return merged;
249
+ }
250
+
251
+ // src/core/useRender.ts
252
+ function useRender({
253
+ render,
254
+ defaultTagName,
255
+ props,
256
+ consumerProps
257
+ }) {
258
+ const merged = mergeProps(props, consumerProps);
259
+ if ((0, import_react4.isValidElement)(render)) {
260
+ return (0, import_react4.cloneElement)(render, mergeProps(merged, render.props));
261
+ }
262
+ return (0, import_react4.createElement)(defaultTagName, merged);
263
+ }
264
+
265
+ // src/tooltip/tooltipDataAttributes.ts
266
+ function tooltipDataAttributes({ open }) {
267
+ return {
268
+ "data-open": open ? "" : void 0,
269
+ "data-closed": open ? void 0 : ""
270
+ };
271
+ }
272
+
273
+ // src/tooltip/TooltipTrigger.tsx
274
+ function TooltipTrigger({
275
+ className,
276
+ children,
277
+ render,
278
+ "aria-describedby": describedBy,
279
+ ...rest
280
+ }) {
281
+ const { open, setOpen, openAfterDelay, closeAfterGrace, popupId, anchorName } = useTooltipRootContext("Trigger");
282
+ return useRender({
283
+ render,
284
+ defaultTagName: "button",
285
+ props: {
286
+ type: "button",
287
+ "aria-describedby": describedBy ? `${describedBy} ${popupId}` : popupId,
288
+ ...tooltipDataAttributes({ open }),
289
+ style: { anchorName },
290
+ className,
291
+ children,
292
+ onPointerEnter(event) {
293
+ if (event.pointerType !== "touch") openAfterDelay();
294
+ },
295
+ onPointerLeave(event) {
296
+ if (event.pointerType !== "touch") closeAfterGrace();
297
+ },
298
+ onPointerDown() {
299
+ setOpen(false);
300
+ },
301
+ onFocus(event) {
302
+ if (event.currentTarget.matches(":focus-visible")) setOpen(true);
303
+ },
304
+ onBlur() {
305
+ setOpen(false);
306
+ }
307
+ },
308
+ consumerProps: rest
309
+ });
310
+ }
311
+
312
+ // src/tooltip/TooltipPopup.tsx
313
+ var import_react5 = require("react");
314
+ function TooltipPopup({
315
+ side = "top",
316
+ align = "center",
317
+ className,
318
+ children,
319
+ render,
320
+ ...rest
321
+ }) {
322
+ const { open, setOpen, cancelPending, closeAfterGrace, popupId, anchorName } = useTooltipRootContext("Popup");
323
+ const elementRef = (0, import_react5.useRef)(null);
324
+ (0, import_react5.useEffect)(() => {
325
+ const popup = elementRef.current;
326
+ if (!popup || typeof popup.showPopover !== "function") return;
327
+ const shown = popup.matches(":popover-open");
328
+ if (open && !shown) popup.showPopover();
329
+ else if (!open && shown) popup.hidePopover();
330
+ });
331
+ (0, import_react5.useEffect)(() => {
332
+ if (!open) return;
333
+ function onKeyDown(event) {
334
+ if (event.key !== "Escape") return;
335
+ event.preventDefault();
336
+ event.stopPropagation();
337
+ setOpen(false);
338
+ }
339
+ document.addEventListener("keydown", onKeyDown, true);
340
+ return () => document.removeEventListener("keydown", onKeyDown, true);
341
+ }, [open, setOpen]);
342
+ return useRender({
343
+ render,
344
+ defaultTagName: "div",
345
+ props: {
346
+ id: popupId,
347
+ popover: "manual",
348
+ role: "tooltip",
349
+ ...anchoredDataAttributes(side, align),
350
+ ...tooltipDataAttributes({ open }),
351
+ style: anchoredPopupStyle(anchorName, side, align),
352
+ className,
353
+ children,
354
+ ref: elementRef,
355
+ onPointerEnter: cancelPending,
356
+ onPointerLeave: closeAfterGrace
357
+ },
358
+ consumerProps: rest
359
+ });
360
+ }
361
+ // Annotate the CommonJS export names for ESM import in node:
362
+ 0 && (module.exports = {
363
+ Tooltip
364
+ });
@@ -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.cjs';
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,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.4.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",
@@ -93,6 +93,16 @@
93
93
  "types": "./dist/switch/index.d.cts",
94
94
  "default": "./dist/switch/index.cjs"
95
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
+ }
96
106
  }
97
107
  },
98
108
  "publishConfig": {