@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.
- package/README.md +70 -3
- package/dist/cdn/chunks/chunk-P2K4DPA4.js +1 -0
- package/dist/cdn/dropdown.js +1 -0
- package/dist/cdn/popover.js +1 -1
- package/dist/cdn/tooltip.js +1 -0
- package/dist/custom-elements.json +1647 -381
- package/dist/define/dropdown.d.ts +1 -0
- package/dist/define/dropdown.js +12 -0
- package/dist/define/index.d.ts +2 -0
- package/dist/define/index.js +2 -0
- package/dist/define/tooltip.d.ts +1 -0
- package/dist/define/tooltip.js +11 -0
- package/dist/dropdown/DropdownController.d.ts +63 -0
- package/dist/dropdown/DropdownController.js +130 -0
- package/dist/dropdown/DropdownSubController.d.ts +34 -0
- package/dist/dropdown/DropdownSubController.js +78 -0
- package/dist/dropdown/elements.d.ts +174 -0
- package/dist/dropdown/elements.js +501 -0
- package/dist/dropdown/index.d.ts +3 -0
- package/dist/dropdown/index.js +3 -0
- package/dist/frameworks/events.d.ts +22 -0
- package/dist/frameworks/react.d.ts +62 -2
- package/dist/frameworks/svelte.d.ts +59 -1
- package/dist/frameworks/vue.d.ts +64 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/popover/PopoverController.d.ts +6 -0
- package/dist/popover/PopoverController.js +41 -1
- package/dist/primitiv-web.js +1 -1
- package/dist/tags.d.ts +21 -0
- package/dist/tooltip/TooltipController.d.ts +74 -0
- package/dist/tooltip/TooltipController.js +149 -0
- package/dist/tooltip/TooltipGroup.d.ts +23 -0
- package/dist/tooltip/TooltipGroup.js +44 -0
- package/dist/tooltip/elements.d.ts +83 -0
- package/dist/tooltip/elements.js +223 -0
- package/dist/tooltip/index.d.ts +3 -0
- package/dist/tooltip/index.js +3 -0
- 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.
|
|
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.
|
|
103
|
+
"@primitiv-ui/core": "0.1.57"
|
|
88
104
|
},
|
|
89
105
|
"devDependencies": {
|
|
90
106
|
"@custom-elements-manifest/analyzer": "0.11.0",
|