@primitiv-ui/web 0.1.55 → 0.1.57

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.
Files changed (39) hide show
  1. package/README.md +70 -3
  2. package/dist/cdn/chunks/chunk-P2K4DPA4.js +1 -0
  3. package/dist/cdn/dropdown.js +1 -0
  4. package/dist/cdn/popover.js +1 -1
  5. package/dist/cdn/tooltip.js +1 -0
  6. package/dist/custom-elements.json +1647 -381
  7. package/dist/define/dropdown.d.ts +1 -0
  8. package/dist/define/dropdown.js +12 -0
  9. package/dist/define/index.d.ts +2 -0
  10. package/dist/define/index.js +2 -0
  11. package/dist/define/tooltip.d.ts +1 -0
  12. package/dist/define/tooltip.js +11 -0
  13. package/dist/dropdown/DropdownController.d.ts +63 -0
  14. package/dist/dropdown/DropdownController.js +130 -0
  15. package/dist/dropdown/DropdownSubController.d.ts +34 -0
  16. package/dist/dropdown/DropdownSubController.js +78 -0
  17. package/dist/dropdown/elements.d.ts +174 -0
  18. package/dist/dropdown/elements.js +501 -0
  19. package/dist/dropdown/index.d.ts +3 -0
  20. package/dist/dropdown/index.js +3 -0
  21. package/dist/frameworks/events.d.ts +22 -0
  22. package/dist/frameworks/react.d.ts +62 -2
  23. package/dist/frameworks/svelte.d.ts +59 -1
  24. package/dist/frameworks/vue.d.ts +64 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +2 -0
  27. package/dist/popover/PopoverController.d.ts +6 -0
  28. package/dist/popover/PopoverController.js +41 -1
  29. package/dist/primitiv-web.js +1 -1
  30. package/dist/tags.d.ts +21 -0
  31. package/dist/tooltip/TooltipController.d.ts +74 -0
  32. package/dist/tooltip/TooltipController.js +149 -0
  33. package/dist/tooltip/TooltipGroup.d.ts +23 -0
  34. package/dist/tooltip/TooltipGroup.js +44 -0
  35. package/dist/tooltip/elements.d.ts +83 -0
  36. package/dist/tooltip/elements.js +223 -0
  37. package/dist/tooltip/index.d.ts +3 -0
  38. package/dist/tooltip/index.js +3 -0
  39. package/package.json +18 -2
@@ -0,0 +1,149 @@
1
+ import { panelPlacement, TOOLTIP_GRACE_MS, tooltipOpenDelay } from "@primitiv-ui/core";
2
+ import { write } from "../internal/element.js";
3
+ /**
4
+ * The behaviour of a tooltip: hover intent and dismissal. Its host wires the
5
+ * trigger's and the content's own events to the intents below, so the React
6
+ * adapter keeps composing them with its consumers' handlers as it always has;
7
+ * the controller owns the timers, and the document-wide Escape and outside
8
+ * press while open (WAI-ARIA tooltip pattern).
9
+ *
10
+ * - {@link TooltipController.hoverStart | `hoverStart`} — open after the
11
+ * delay, or at once while the group is in its skip-delay window.
12
+ * - {@link TooltipController.hoverEnd | `hoverEnd`} — close after a short
13
+ * grace period, long enough for the pointer to reach hoverable content.
14
+ * - {@link TooltipController.holdOpen | `holdOpen`} — the pointer reached the
15
+ * content: cancel that grace close.
16
+ * - {@link TooltipController.openNow | `openNow`} / {@link TooltipController.closeNow | `closeNow`} —
17
+ * focus and blur, Escape on the trigger, the pointer leaving the content.
18
+ * - {@link TooltipController.sync | `sync`} — listen for Escape and outside
19
+ * presses while open, and mark the tooltip's `data-side` and `data-align`
20
+ * (where it landed against its anchor, after any flip) as it opens and on
21
+ * every scroll and resize; a host calls it whenever the state may have
22
+ * changed, after showing the tooltip.
23
+ */
24
+ export class TooltipController {
25
+ #host;
26
+ #opening = null;
27
+ #grace = null;
28
+ // The document listeners, added and removed by hand rather than through an
29
+ // AbortSignal, so a host can see the very handler removed. Adding the same
30
+ // function twice is a no-op, so syncing open again never stacks them.
31
+ #onKeydown = (event) => this.#keydown(event);
32
+ #onPointerdown = (event) => this.#pointerdown(event);
33
+ /** The scroll and resize listeners that re-measure the placement, while open. */
34
+ #following = null;
35
+ constructor(host) {
36
+ this.#host = host;
37
+ }
38
+ /** Open now, cancelling a pending hover open. */
39
+ openNow() {
40
+ this.#cancelOpening();
41
+ this.#host.change(true);
42
+ this.#host.tooltipGroup().opened();
43
+ }
44
+ /** Open after the delay, or at once while the group is in its skip-delay window. */
45
+ hoverStart() {
46
+ const delay = tooltipOpenDelay({ groupOpen: this.#host.tooltipGroup().isOpen(), delay: this.#host.delay() });
47
+ if (delay === 0) {
48
+ this.openNow();
49
+ return;
50
+ }
51
+ this.#opening = setTimeout(() => {
52
+ this.openNow();
53
+ this.#opening = null;
54
+ }, delay);
55
+ }
56
+ /** Close now, cancelling a pending grace close. */
57
+ closeNow() {
58
+ this.holdOpen();
59
+ this.#host.change(false);
60
+ this.#host.tooltipGroup().closed();
61
+ }
62
+ /** Close after the grace period, cancelling a pending hover open. */
63
+ hoverEnd() {
64
+ this.#cancelOpening();
65
+ this.#grace = setTimeout(() => {
66
+ this.#host.change(false);
67
+ this.#host.tooltipGroup().closed();
68
+ this.#grace = null;
69
+ }, TOOLTIP_GRACE_MS);
70
+ }
71
+ /** The pointer reached the content: cancel a pending grace close. */
72
+ holdOpen() {
73
+ // Stryker disable next-line ConditionalExpression: equivalent — clearTimeout(null) is a spec'd no-op, so forcing this guard true is harmless when no grace close is pending.
74
+ if (this.#grace !== null) {
75
+ clearTimeout(this.#grace);
76
+ this.#grace = null;
77
+ }
78
+ }
79
+ /** Listen for Escape and outside presses while open, and only then. */
80
+ sync() {
81
+ const { open } = this.#host.state();
82
+ if (open) {
83
+ document.addEventListener("keydown", this.#onKeydown);
84
+ document.addEventListener("pointerdown", this.#onPointerdown);
85
+ if (!this.#following)
86
+ this.#follow();
87
+ }
88
+ else {
89
+ this.#stopListening();
90
+ }
91
+ }
92
+ /** Measure the placement now, and again on every scroll and resize until it closes. */
93
+ #follow() {
94
+ this.#following = new AbortController();
95
+ const options = {
96
+ // A scroll inside an overflow pane does not bubble to the window.
97
+ capture: true,
98
+ // Stryker disable next-line BooleanLiteral: equivalent — passive only spares the scroll from waiting on the listener.
99
+ passive: true,
100
+ signal: this.#following.signal,
101
+ };
102
+ window.addEventListener("scroll", () => this.#place(), options);
103
+ window.addEventListener("resize", () => this.#place(), options);
104
+ this.#place();
105
+ }
106
+ /** Mark the side and alignment the tooltip landed on, against its anchor. */
107
+ #place() {
108
+ const content = this.#host.content();
109
+ const anchor = this.#host.anchor?.();
110
+ if (!content || !anchor)
111
+ return;
112
+ const { side, align } = panelPlacement(content.getBoundingClientRect(), anchor.getBoundingClientRect());
113
+ write(content, "data-side", side);
114
+ write(content, "data-align", align);
115
+ }
116
+ #stopListening() {
117
+ document.removeEventListener("keydown", this.#onKeydown);
118
+ document.removeEventListener("pointerdown", this.#onPointerdown);
119
+ this.#following?.abort();
120
+ this.#following = null;
121
+ }
122
+ /** Cancel every timer and stop listening, so nothing outlives the host. */
123
+ disconnect() {
124
+ this.#cancelOpening();
125
+ this.holdOpen();
126
+ this.#stopListening();
127
+ }
128
+ #cancelOpening() {
129
+ // Stryker disable next-line ConditionalExpression: equivalent — clearTimeout(null) is a spec'd no-op, so forcing this guard true is harmless when no hover open is pending.
130
+ if (this.#opening !== null) {
131
+ clearTimeout(this.#opening);
132
+ this.#opening = null;
133
+ }
134
+ }
135
+ #keydown(event) {
136
+ if (event.key !== "Escape")
137
+ return;
138
+ this.#host.escapeKeyDown?.(event);
139
+ if (!event.defaultPrevented)
140
+ this.closeNow();
141
+ }
142
+ #pointerdown(event) {
143
+ if (this.#host.content()?.contains(event.target))
144
+ return;
145
+ this.#host.pointerDownOutside?.(event);
146
+ if (!event.defaultPrevented)
147
+ this.closeNow();
148
+ }
149
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A set of tooltips that share a skip-delay window (React's `Tooltip.Provider`):
3
+ * once one has opened, the next to be hovered opens at once rather than
4
+ * waiting its delay, until the group has been quiet for the skip delay. The
5
+ * React Provider and the custom elements each hold one.
6
+ */
7
+ export declare class TooltipGroup {
8
+ #private;
9
+ /** `skipDelay` is read each time a tooltip closes, so a changed value applies. */
10
+ constructor(skipDelay: () => number);
11
+ /** Whether a tooltip in the group is open, or closed within the skip delay. */
12
+ isOpen(): boolean;
13
+ /** A tooltip in the group opened: the window is open, and any pending close is off. */
14
+ opened(): void;
15
+ /**
16
+ * A tooltip in the group closed: the window stays open for the skip delay.
17
+ * A second close restarts it, cancelling the first timer rather than
18
+ * orphaning it — two closes in a row are routine (Escape, then a blur).
19
+ */
20
+ closed(): void;
21
+ /** Cancel a pending close, so no timer outlives the group's host. */
22
+ dispose(): void;
23
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A set of tooltips that share a skip-delay window (React's `Tooltip.Provider`):
3
+ * once one has opened, the next to be hovered opens at once rather than
4
+ * waiting its delay, until the group has been quiet for the skip delay. The
5
+ * React Provider and the custom elements each hold one.
6
+ */
7
+ export class TooltipGroup {
8
+ #skipDelay;
9
+ #open = false;
10
+ #closing = null;
11
+ /** `skipDelay` is read each time a tooltip closes, so a changed value applies. */
12
+ constructor(skipDelay) {
13
+ this.#skipDelay = skipDelay;
14
+ }
15
+ /** Whether a tooltip in the group is open, or closed within the skip delay. */
16
+ isOpen() {
17
+ return this.#open;
18
+ }
19
+ /** A tooltip in the group opened: the window is open, and any pending close is off. */
20
+ opened() {
21
+ // Stryker disable next-line ConditionalExpression: equivalent — clearTimeout(null) is a spec'd no-op, so forcing this guard true is harmless when no close is pending.
22
+ if (this.#closing !== null) {
23
+ clearTimeout(this.#closing);
24
+ this.#closing = null;
25
+ }
26
+ this.#open = true;
27
+ }
28
+ /**
29
+ * A tooltip in the group closed: the window stays open for the skip delay.
30
+ * A second close restarts it, cancelling the first timer rather than
31
+ * orphaning it — two closes in a row are routine (Escape, then a blur).
32
+ */
33
+ closed() {
34
+ clearTimeout(this.#closing ?? undefined);
35
+ this.#closing = setTimeout(() => {
36
+ this.#open = false;
37
+ this.#closing = null;
38
+ }, this.#skipDelay());
39
+ }
40
+ /** Cancel a pending close, so no timer outlives the group's host. */
41
+ dispose() {
42
+ clearTimeout(this.#closing ?? undefined);
43
+ }
44
+ }
@@ -0,0 +1,83 @@
1
+ import { ElementBase, type ElementRegistry } from "../internal/element.js";
2
+ import { PrimitivRootElement } from "../internal/RootElement.js";
3
+ import { TooltipGroup } from "./TooltipGroup.js";
4
+ export type { ElementRegistry };
5
+ /**
6
+ * `<primitiv-tooltip-provider>` — optional. The tooltips inside it share one
7
+ * skip-delay window and its default delay, as inside React's
8
+ * `Tooltip.Provider`; tooltips outside any provider share a document-wide one.
9
+ *
10
+ * @customElement primitiv-tooltip-provider
11
+ * @attr {number} delay-duration - How long, in milliseconds, a pointer rests on a trigger before its tooltip opens. Default 700. A tooltip's own `delay-duration` overrides it.
12
+ * @attr {number} skip-delay-duration - How long, in milliseconds, after one tooltip closes that hovering another opens it at once. Default 300.
13
+ */
14
+ export declare class PrimitivTooltipProviderElement extends ElementBase {
15
+ #private;
16
+ /** The skip-delay window this provider's tooltips share, created on first use. */
17
+ get group(): TooltipGroup;
18
+ }
19
+ /**
20
+ * `<primitiv-tooltip>` — the state owner of a tooltip: a short description of
21
+ * its trigger that appears on hover after a delay, and at once on focus. It
22
+ * closes when the pointer leaves (after a moment, so it can be reached),
23
+ * on blur, on Escape and on a press outside. Moving from one trigger to the
24
+ * next opens each next tooltip at once (the skip delay). The tooltip is shown
25
+ * as a `popover="manual"` in the top layer, with the trigger as its source,
26
+ * which makes the trigger its implicit anchor for CSS anchor positioning.
27
+ *
28
+ * The root finds its own parts and reconciles their ARIA from its reflected
29
+ * state whenever a part arrives, leaves, or changes an attribute the root
30
+ * reads (the lifecycle is `PrimitivRootElement`'s).
31
+ *
32
+ * @customElement primitiv-tooltip
33
+ * @summary The state owner of a tooltip, shown on hover and focus.
34
+ * @attr {boolean} open - Whether the tooltip is showing. Reflected: it changes as the user hovers, focuses and dismisses it. Set it (or the `open` property) to show or hide it from script.
35
+ * @attr {number} delay-duration - How long, in milliseconds, a pointer rests on the trigger before the tooltip opens. Defaults to the provider's, or 700. Focus always opens it at once.
36
+ * @attr {boolean} disable-hoverable-content - Close as soon as the pointer leaves the trigger, rather than giving it a moment to reach the tooltip.
37
+ * @fires {CustomEvent<{ open: boolean }>} primitiv-tooltip-change - Bubbles, cancelable, fired before a user-driven change is applied — hover, focus, blur, Escape or a press outside — with the open state afterwards. Call `preventDefault()` to veto it.
38
+ */
39
+ export declare class PrimitivTooltipElement extends PrimitivRootElement {
40
+ #private;
41
+ protected readonly controller: {
42
+ connect(): void;
43
+ disconnect(): void;
44
+ };
45
+ protected readonly reconcileOn: readonly string[];
46
+ /** Whether the tooltip is showing, reflected as the boolean `open` attribute. */
47
+ get open(): boolean;
48
+ set open(next: boolean);
49
+ protected reconcile(): void;
50
+ }
51
+ /**
52
+ * `<primitiv-tooltip-trigger>` — an interactive part. It decorates exactly one
53
+ * child the consumer writes, a `<button>`, which opens the tooltip on hover and
54
+ * focus and is described by it (`aria-describedby`), so the rendered DOM
55
+ * matches React's (RFC 0033 D24).
56
+ *
57
+ * @customElement primitiv-tooltip-trigger
58
+ */
59
+ export declare class PrimitivTooltipTriggerElement extends ElementBase {
60
+ }
61
+ /**
62
+ * `<primitiv-tooltip-content>` — the tooltip itself. Structural: the root
63
+ * gives it an id, `role="tooltip"`, `popover="manual"` (write it in your markup
64
+ * too, so it is hidden before the script loads) and `data-state`.
65
+ *
66
+ * @customElement primitiv-tooltip-content
67
+ */
68
+ export declare class PrimitivTooltipContentElement extends ElementBase {
69
+ }
70
+ /**
71
+ * `<primitiv-tooltip-arrow>` — an optional, decorative pointer, inside the
72
+ * content. The element writes nothing on it; style it as you like.
73
+ *
74
+ * @customElement primitiv-tooltip-arrow
75
+ */
76
+ export declare class PrimitivTooltipArrowElement extends ElementBase {
77
+ }
78
+ /**
79
+ * Define the five Tooltip elements on `registry` (the global `customElements`
80
+ * by default). Safe to call more than once, and a no-op where there is no
81
+ * registry (a server render).
82
+ */
83
+ export declare function defineTooltip(registry?: ElementRegistry | undefined): void;
@@ -0,0 +1,223 @@
1
+ import { defineElements, ElementBase, reportOnce, write } from "../internal/element.js";
2
+ import { PrimitivRootElement } from "../internal/RootElement.js";
3
+ import { TooltipController } from "./TooltipController.js";
4
+ import { TooltipGroup } from "./TooltipGroup.js";
5
+ // Stryker disable StringLiteral: static — evaluated once at import, so runtime mutant switching never reaches them; the names are pinned by the defineTooltip registry test instead.
6
+ const ROOT = "primitiv-tooltip";
7
+ const PROVIDER = "primitiv-tooltip-provider";
8
+ const TRIGGER = "primitiv-tooltip-trigger";
9
+ const CONTENT = "primitiv-tooltip-content";
10
+ const ARROW = "primitiv-tooltip-arrow";
11
+ // Stryker restore StringLiteral
12
+ /** React's `Tooltip.Provider` defaults, for a tooltip and a provider that set none. */
13
+ const DEFAULT_DELAY = 700;
14
+ const DEFAULT_SKIP_DELAY = 300;
15
+ /** A numeric attribute in milliseconds, or `fallback` when it is absent or not a number. */
16
+ function milliseconds(element, name, fallback) {
17
+ const value = Number(element.getAttribute(name) ?? Number.NaN);
18
+ return Number.isNaN(value) ? fallback : value;
19
+ }
20
+ /** The skip-delay window every tooltip outside a provider shares. */
21
+ // Stryker disable next-line ArrowFunction: static — created once at import, so runtime mutant switching never reaches it; the 300ms window is pinned by the skip-delay interaction tests.
22
+ const documentGroup = new TooltipGroup(() => DEFAULT_SKIP_DELAY);
23
+ /**
24
+ * `<primitiv-tooltip-provider>` — optional. The tooltips inside it share one
25
+ * skip-delay window and its default delay, as inside React's
26
+ * `Tooltip.Provider`; tooltips outside any provider share a document-wide one.
27
+ *
28
+ * @customElement primitiv-tooltip-provider
29
+ * @attr {number} delay-duration - How long, in milliseconds, a pointer rests on a trigger before its tooltip opens. Default 700. A tooltip's own `delay-duration` overrides it.
30
+ * @attr {number} skip-delay-duration - How long, in milliseconds, after one tooltip closes that hovering another opens it at once. Default 300.
31
+ */
32
+ export class PrimitivTooltipProviderElement extends ElementBase {
33
+ #group;
34
+ /** The skip-delay window this provider's tooltips share, created on first use. */
35
+ get group() {
36
+ return (this.#group ??= new TooltipGroup(() => milliseconds(this, "skip-delay-duration", DEFAULT_SKIP_DELAY)));
37
+ }
38
+ }
39
+ /**
40
+ * `<primitiv-tooltip>` — the state owner of a tooltip: a short description of
41
+ * its trigger that appears on hover after a delay, and at once on focus. It
42
+ * closes when the pointer leaves (after a moment, so it can be reached),
43
+ * on blur, on Escape and on a press outside. Moving from one trigger to the
44
+ * next opens each next tooltip at once (the skip delay). The tooltip is shown
45
+ * as a `popover="manual"` in the top layer, with the trigger as its source,
46
+ * which makes the trigger its implicit anchor for CSS anchor positioning.
47
+ *
48
+ * The root finds its own parts and reconciles their ARIA from its reflected
49
+ * state whenever a part arrives, leaves, or changes an attribute the root
50
+ * reads (the lifecycle is `PrimitivRootElement`'s).
51
+ *
52
+ * @customElement primitiv-tooltip
53
+ * @summary The state owner of a tooltip, shown on hover and focus.
54
+ * @attr {boolean} open - Whether the tooltip is showing. Reflected: it changes as the user hovers, focuses and dismisses it. Set it (or the `open` property) to show or hide it from script.
55
+ * @attr {number} delay-duration - How long, in milliseconds, a pointer rests on the trigger before the tooltip opens. Defaults to the provider's, or 700. Focus always opens it at once.
56
+ * @attr {boolean} disable-hoverable-content - Close as soon as the pointer leaves the trigger, rather than giving it a moment to reach the tooltip.
57
+ * @fires {CustomEvent<{ open: boolean }>} primitiv-tooltip-change - Bubbles, cancelable, fired before a user-driven change is applied — hover, focus, blur, Escape or a press outside — with the open state afterwards. Call `preventDefault()` to veto it.
58
+ */
59
+ export class PrimitivTooltipElement extends PrimitivRootElement {
60
+ /** The decorated trigger button and the tooltip, as of the last reconcile. */
61
+ #trigger = null;
62
+ #content = null;
63
+ /** The tooltip this root has shown, while it shows. */
64
+ #shown = null;
65
+ /** The listeners on the trigger and the tooltip, rebuilt when either changes. */
66
+ #bindings = null;
67
+ #tooltip = new TooltipController(this.#host());
68
+ controller = {
69
+ connect: () => { },
70
+ disconnect: () => this.#disconnect(),
71
+ };
72
+ reconcileOn = ["open"];
73
+ /** Whether the tooltip is showing, reflected as the boolean `open` attribute. */
74
+ get open() {
75
+ return this.hasAttribute("open");
76
+ }
77
+ set open(next) {
78
+ this.toggleAttribute("open", next);
79
+ }
80
+ /** The controller's view of this element: state lives in the reflected attribute. */
81
+ #host() {
82
+ const provider = () => this.closest(PROVIDER);
83
+ return {
84
+ state: () => ({ open: this.open }),
85
+ change: (open) => {
86
+ // The controller reports every close it is asked for; announce a real change only.
87
+ if (open !== this.open && this.announce("primitiv-tooltip-change", { open }))
88
+ this.open = open;
89
+ },
90
+ delay: () => {
91
+ const fallback = provider();
92
+ return milliseconds(this, "delay-duration", fallback ? milliseconds(fallback, "delay-duration", DEFAULT_DELAY) : DEFAULT_DELAY);
93
+ },
94
+ tooltipGroup: () => provider()?.group ?? documentGroup,
95
+ content: () => this.#content,
96
+ anchor: () => this.#trigger,
97
+ };
98
+ }
99
+ #disconnect() {
100
+ // An open tooltip that leaves would hold its group's skip window open for
101
+ // good, opening every later tooltip at once: close the window instead.
102
+ if (this.open)
103
+ this.#host().tooltipGroup().closed();
104
+ this.#bindings?.abort();
105
+ this.#bindings = null;
106
+ this.#trigger = null;
107
+ this.#content = null;
108
+ // Leaving the document hides a popover with no event: show it again on the next connect.
109
+ this.#shown = null;
110
+ this.#tooltip.disconnect();
111
+ }
112
+ /** Listen on `trigger` and `content`, rebinding only when either changed. */
113
+ #bind(trigger, content) {
114
+ // Stryker disable next-line ConditionalExpression: equivalent — rebinding the same elements swaps one listener set for an identical one.
115
+ if (trigger === this.#trigger && content === this.#content)
116
+ return;
117
+ this.#bindings?.abort();
118
+ this.#bindings = new AbortController();
119
+ this.#trigger = trigger;
120
+ this.#content = content;
121
+ const options = { signal: this.#bindings.signal };
122
+ const tooltip = this.#tooltip;
123
+ trigger?.addEventListener("pointerenter", () => tooltip.hoverStart(), options);
124
+ trigger?.addEventListener("pointerleave", () => (this.hasAttribute("disable-hoverable-content") ? tooltip.closeNow() : tooltip.hoverEnd()), options);
125
+ trigger?.addEventListener("focus", () => tooltip.openNow(), options);
126
+ trigger?.addEventListener("blur", () => tooltip.closeNow(), options);
127
+ content?.addEventListener("pointerenter", () => tooltip.holdOpen(), options);
128
+ content?.addEventListener("pointerleave", () => tooltip.closeNow(), options);
129
+ }
130
+ reconcile() {
131
+ const open = this.open;
132
+ const state = open ? "open" : "closed";
133
+ write(this, "data-state", state);
134
+ const [triggerPart] = this.parts(TRIGGER);
135
+ const [content = null] = this.parts(CONTENT);
136
+ const trigger = triggerPart ? decoratable(triggerPart) : null;
137
+ if (content) {
138
+ if (!content.id)
139
+ content.id = `${this.rootId()}-content`;
140
+ write(content, "role", "tooltip");
141
+ if (!content.hasAttribute("popover"))
142
+ content.setAttribute("popover", "manual");
143
+ write(content, "data-state", state);
144
+ }
145
+ if (trigger) {
146
+ write(trigger, "aria-describedby", content ? content.id : null);
147
+ write(trigger, "data-state", state);
148
+ }
149
+ this.#bind(trigger, content);
150
+ this.#show(open ? content : null);
151
+ this.#tooltip.sync();
152
+ }
153
+ /** Show `content` (anchored to the trigger), or hide what is showing when it is `null`. */
154
+ #show(content) {
155
+ if (content === this.#shown)
156
+ return;
157
+ // Stryker disable next-line ConditionalExpression: equivalent — hiding nothing throws inside the try below, which swallows it.
158
+ if (this.#shown) {
159
+ try {
160
+ this.#shown.hidePopover();
161
+ }
162
+ catch {
163
+ // Already hidden, or no longer a popover.
164
+ }
165
+ }
166
+ this.#shown = content;
167
+ if (!content)
168
+ return;
169
+ const showable = content;
170
+ if (this.#trigger)
171
+ showable.showPopover({ source: this.#trigger });
172
+ else
173
+ showable.showPopover();
174
+ }
175
+ }
176
+ /** The one `<button>` a trigger decorates, or `null`, reported once, without exactly one element child. */
177
+ function decoratable(trigger) {
178
+ if (trigger.childElementCount === 1)
179
+ return trigger.firstElementChild;
180
+ reportOnce(trigger, `<${TRIGGER}> needs exactly one element child (a <button>) to decorate.`);
181
+ return null;
182
+ }
183
+ /**
184
+ * `<primitiv-tooltip-trigger>` — an interactive part. It decorates exactly one
185
+ * child the consumer writes, a `<button>`, which opens the tooltip on hover and
186
+ * focus and is described by it (`aria-describedby`), so the rendered DOM
187
+ * matches React's (RFC 0033 D24).
188
+ *
189
+ * @customElement primitiv-tooltip-trigger
190
+ */
191
+ export class PrimitivTooltipTriggerElement extends ElementBase {
192
+ }
193
+ /**
194
+ * `<primitiv-tooltip-content>` — the tooltip itself. Structural: the root
195
+ * gives it an id, `role="tooltip"`, `popover="manual"` (write it in your markup
196
+ * too, so it is hidden before the script loads) and `data-state`.
197
+ *
198
+ * @customElement primitiv-tooltip-content
199
+ */
200
+ export class PrimitivTooltipContentElement extends ElementBase {
201
+ }
202
+ /**
203
+ * `<primitiv-tooltip-arrow>` — an optional, decorative pointer, inside the
204
+ * content. The element writes nothing on it; style it as you like.
205
+ *
206
+ * @customElement primitiv-tooltip-arrow
207
+ */
208
+ export class PrimitivTooltipArrowElement extends ElementBase {
209
+ }
210
+ /**
211
+ * Define the five Tooltip elements on `registry` (the global `customElements`
212
+ * by default). Safe to call more than once, and a no-op where there is no
213
+ * registry (a server render).
214
+ */
215
+ export function defineTooltip(registry = globalThis.customElements) {
216
+ defineElements(registry, [
217
+ [ROOT, PrimitivTooltipElement],
218
+ [PROVIDER, PrimitivTooltipProviderElement],
219
+ [TRIGGER, PrimitivTooltipTriggerElement],
220
+ [CONTENT, PrimitivTooltipContentElement],
221
+ [ARROW, PrimitivTooltipArrowElement],
222
+ ]);
223
+ }
@@ -0,0 +1,3 @@
1
+ export { TooltipController, type TooltipControllerHost } from "./TooltipController.js";
2
+ export { TooltipGroup } from "./TooltipGroup.js";
3
+ export { defineTooltip, PrimitivTooltipArrowElement, PrimitivTooltipContentElement, PrimitivTooltipElement, PrimitivTooltipProviderElement, PrimitivTooltipTriggerElement, } from "./elements.js";
@@ -0,0 +1,3 @@
1
+ export { TooltipController } from "./TooltipController.js";
2
+ export { TooltipGroup } from "./TooltipGroup.js";
3
+ export { defineTooltip, PrimitivTooltipArrowElement, PrimitivTooltipContentElement, PrimitivTooltipElement, PrimitivTooltipProviderElement, PrimitivTooltipTriggerElement, } from "./elements.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitiv-ui/web",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "Primitiv's headless components as standard custom elements: light DOM, no framework, no runtime dependencies. Works in plain HTML, server-rendered stacks, React, Vue, Angular and anything else that renders HTML.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,6 +18,10 @@
18
18
  "types": "./dist/collapsible/index.d.ts",
19
19
  "default": "./dist/collapsible/index.js"
20
20
  },
21
+ "./dropdown": {
22
+ "types": "./dist/dropdown/index.d.ts",
23
+ "default": "./dist/dropdown/index.js"
24
+ },
21
25
  "./modal": {
22
26
  "types": "./dist/modal/index.d.ts",
23
27
  "default": "./dist/modal/index.js"
@@ -30,6 +34,10 @@
30
34
  "types": "./dist/tabs/index.d.ts",
31
35
  "default": "./dist/tabs/index.js"
32
36
  },
37
+ "./tooltip": {
38
+ "types": "./dist/tooltip/index.d.ts",
39
+ "default": "./dist/tooltip/index.js"
40
+ },
33
41
  "./define": {
34
42
  "types": "./dist/define/index.d.ts",
35
43
  "default": "./dist/define/index.js"
@@ -42,6 +50,10 @@
42
50
  "types": "./dist/define/collapsible.d.ts",
43
51
  "default": "./dist/define/collapsible.js"
44
52
  },
53
+ "./define/dropdown": {
54
+ "types": "./dist/define/dropdown.d.ts",
55
+ "default": "./dist/define/dropdown.js"
56
+ },
45
57
  "./define/modal": {
46
58
  "types": "./dist/define/modal.d.ts",
47
59
  "default": "./dist/define/modal.js"
@@ -54,6 +66,10 @@
54
66
  "types": "./dist/define/tabs.d.ts",
55
67
  "default": "./dist/define/tabs.js"
56
68
  },
69
+ "./define/tooltip": {
70
+ "types": "./dist/define/tooltip.d.ts",
71
+ "default": "./dist/define/tooltip.js"
72
+ },
57
73
  "./tags": {
58
74
  "types": "./dist/tags.d.ts",
59
75
  "default": "./dist/tags.js"
@@ -84,7 +100,7 @@
84
100
  "directory": "packages/web"
85
101
  },
86
102
  "dependencies": {
87
- "@primitiv-ui/core": "0.1.55"
103
+ "@primitiv-ui/core": "0.1.57"
88
104
  },
89
105
  "devDependencies": {
90
106
  "@custom-elements-manifest/analyzer": "0.11.0",