@primitiv-ui/web 0.1.55 → 0.1.56
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 +36 -3
- package/dist/cdn/tooltip.js +1 -0
- package/dist/custom-elements.json +357 -0
- package/dist/define/index.d.ts +1 -0
- package/dist/define/index.js +1 -0
- package/dist/define/tooltip.d.ts +1 -0
- package/dist/define/tooltip.js +11 -0
- package/dist/frameworks/events.d.ts +4 -0
- package/dist/frameworks/react.d.ts +18 -2
- package/dist/frameworks/svelte.d.ts +15 -1
- package/dist/frameworks/vue.d.ts +15 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/primitiv-web.js +1 -1
- package/dist/tags.d.ts +6 -0
- package/dist/tooltip/TooltipController.d.ts +66 -0
- package/dist/tooltip/TooltipController.js +115 -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 +222 -0
- package/dist/tooltip/index.d.ts +3 -0
- package/dist/tooltip/index.js +3 -0
- package/package.json +10 -2
|
@@ -0,0 +1,222 @@
|
|
|
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
|
+
};
|
|
97
|
+
}
|
|
98
|
+
#disconnect() {
|
|
99
|
+
// An open tooltip that leaves would hold its group's skip window open for
|
|
100
|
+
// good, opening every later tooltip at once: close the window instead.
|
|
101
|
+
if (this.open)
|
|
102
|
+
this.#host().tooltipGroup().closed();
|
|
103
|
+
this.#bindings?.abort();
|
|
104
|
+
this.#bindings = null;
|
|
105
|
+
this.#trigger = null;
|
|
106
|
+
this.#content = null;
|
|
107
|
+
// Leaving the document hides a popover with no event: show it again on the next connect.
|
|
108
|
+
this.#shown = null;
|
|
109
|
+
this.#tooltip.disconnect();
|
|
110
|
+
}
|
|
111
|
+
/** Listen on `trigger` and `content`, rebinding only when either changed. */
|
|
112
|
+
#bind(trigger, content) {
|
|
113
|
+
// Stryker disable next-line ConditionalExpression: equivalent — rebinding the same elements swaps one listener set for an identical one.
|
|
114
|
+
if (trigger === this.#trigger && content === this.#content)
|
|
115
|
+
return;
|
|
116
|
+
this.#bindings?.abort();
|
|
117
|
+
this.#bindings = new AbortController();
|
|
118
|
+
this.#trigger = trigger;
|
|
119
|
+
this.#content = content;
|
|
120
|
+
const options = { signal: this.#bindings.signal };
|
|
121
|
+
const tooltip = this.#tooltip;
|
|
122
|
+
trigger?.addEventListener("pointerenter", () => tooltip.hoverStart(), options);
|
|
123
|
+
trigger?.addEventListener("pointerleave", () => (this.hasAttribute("disable-hoverable-content") ? tooltip.closeNow() : tooltip.hoverEnd()), options);
|
|
124
|
+
trigger?.addEventListener("focus", () => tooltip.openNow(), options);
|
|
125
|
+
trigger?.addEventListener("blur", () => tooltip.closeNow(), options);
|
|
126
|
+
content?.addEventListener("pointerenter", () => tooltip.holdOpen(), options);
|
|
127
|
+
content?.addEventListener("pointerleave", () => tooltip.closeNow(), options);
|
|
128
|
+
}
|
|
129
|
+
reconcile() {
|
|
130
|
+
const open = this.open;
|
|
131
|
+
const state = open ? "open" : "closed";
|
|
132
|
+
write(this, "data-state", state);
|
|
133
|
+
const [triggerPart] = this.parts(TRIGGER);
|
|
134
|
+
const [content = null] = this.parts(CONTENT);
|
|
135
|
+
const trigger = triggerPart ? decoratable(triggerPart) : null;
|
|
136
|
+
if (content) {
|
|
137
|
+
if (!content.id)
|
|
138
|
+
content.id = `${this.rootId()}-content`;
|
|
139
|
+
write(content, "role", "tooltip");
|
|
140
|
+
if (!content.hasAttribute("popover"))
|
|
141
|
+
content.setAttribute("popover", "manual");
|
|
142
|
+
write(content, "data-state", state);
|
|
143
|
+
}
|
|
144
|
+
if (trigger) {
|
|
145
|
+
write(trigger, "aria-describedby", content ? content.id : null);
|
|
146
|
+
write(trigger, "data-state", state);
|
|
147
|
+
}
|
|
148
|
+
this.#bind(trigger, content);
|
|
149
|
+
this.#show(open ? content : null);
|
|
150
|
+
this.#tooltip.sync();
|
|
151
|
+
}
|
|
152
|
+
/** Show `content` (anchored to the trigger), or hide what is showing when it is `null`. */
|
|
153
|
+
#show(content) {
|
|
154
|
+
if (content === this.#shown)
|
|
155
|
+
return;
|
|
156
|
+
// Stryker disable next-line ConditionalExpression: equivalent — hiding nothing throws inside the try below, which swallows it.
|
|
157
|
+
if (this.#shown) {
|
|
158
|
+
try {
|
|
159
|
+
this.#shown.hidePopover();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Already hidden, or no longer a popover.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.#shown = content;
|
|
166
|
+
if (!content)
|
|
167
|
+
return;
|
|
168
|
+
const showable = content;
|
|
169
|
+
if (this.#trigger)
|
|
170
|
+
showable.showPopover({ source: this.#trigger });
|
|
171
|
+
else
|
|
172
|
+
showable.showPopover();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** The one `<button>` a trigger decorates, or `null`, reported once, without exactly one element child. */
|
|
176
|
+
function decoratable(trigger) {
|
|
177
|
+
if (trigger.childElementCount === 1)
|
|
178
|
+
return trigger.firstElementChild;
|
|
179
|
+
reportOnce(trigger, `<${TRIGGER}> needs exactly one element child (a <button>) to decorate.`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `<primitiv-tooltip-trigger>` — an interactive part. It decorates exactly one
|
|
184
|
+
* child the consumer writes, a `<button>`, which opens the tooltip on hover and
|
|
185
|
+
* focus and is described by it (`aria-describedby`), so the rendered DOM
|
|
186
|
+
* matches React's (RFC 0033 D24).
|
|
187
|
+
*
|
|
188
|
+
* @customElement primitiv-tooltip-trigger
|
|
189
|
+
*/
|
|
190
|
+
export class PrimitivTooltipTriggerElement extends ElementBase {
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `<primitiv-tooltip-content>` — the tooltip itself. Structural: the root
|
|
194
|
+
* gives it an id, `role="tooltip"`, `popover="manual"` (write it in your markup
|
|
195
|
+
* too, so it is hidden before the script loads) and `data-state`.
|
|
196
|
+
*
|
|
197
|
+
* @customElement primitiv-tooltip-content
|
|
198
|
+
*/
|
|
199
|
+
export class PrimitivTooltipContentElement extends ElementBase {
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* `<primitiv-tooltip-arrow>` — an optional, decorative pointer, inside the
|
|
203
|
+
* content. The element writes nothing on it; style it as you like.
|
|
204
|
+
*
|
|
205
|
+
* @customElement primitiv-tooltip-arrow
|
|
206
|
+
*/
|
|
207
|
+
export class PrimitivTooltipArrowElement extends ElementBase {
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Define the five Tooltip elements on `registry` (the global `customElements`
|
|
211
|
+
* by default). Safe to call more than once, and a no-op where there is no
|
|
212
|
+
* registry (a server render).
|
|
213
|
+
*/
|
|
214
|
+
export function defineTooltip(registry = globalThis.customElements) {
|
|
215
|
+
defineElements(registry, [
|
|
216
|
+
[ROOT, PrimitivTooltipElement],
|
|
217
|
+
[PROVIDER, PrimitivTooltipProviderElement],
|
|
218
|
+
[TRIGGER, PrimitivTooltipTriggerElement],
|
|
219
|
+
[CONTENT, PrimitivTooltipContentElement],
|
|
220
|
+
[ARROW, PrimitivTooltipArrowElement],
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
@@ -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.56",
|
|
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",
|
|
@@ -30,6 +30,10 @@
|
|
|
30
30
|
"types": "./dist/tabs/index.d.ts",
|
|
31
31
|
"default": "./dist/tabs/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./tooltip": {
|
|
34
|
+
"types": "./dist/tooltip/index.d.ts",
|
|
35
|
+
"default": "./dist/tooltip/index.js"
|
|
36
|
+
},
|
|
33
37
|
"./define": {
|
|
34
38
|
"types": "./dist/define/index.d.ts",
|
|
35
39
|
"default": "./dist/define/index.js"
|
|
@@ -54,6 +58,10 @@
|
|
|
54
58
|
"types": "./dist/define/tabs.d.ts",
|
|
55
59
|
"default": "./dist/define/tabs.js"
|
|
56
60
|
},
|
|
61
|
+
"./define/tooltip": {
|
|
62
|
+
"types": "./dist/define/tooltip.d.ts",
|
|
63
|
+
"default": "./dist/define/tooltip.js"
|
|
64
|
+
},
|
|
57
65
|
"./tags": {
|
|
58
66
|
"types": "./dist/tags.d.ts",
|
|
59
67
|
"default": "./dist/tags.js"
|
|
@@ -84,7 +92,7 @@
|
|
|
84
92
|
"directory": "packages/web"
|
|
85
93
|
},
|
|
86
94
|
"dependencies": {
|
|
87
|
-
"@primitiv-ui/core": "0.1.
|
|
95
|
+
"@primitiv-ui/core": "0.1.56"
|
|
88
96
|
},
|
|
89
97
|
"devDependencies": {
|
|
90
98
|
"@custom-elements-manifest/analyzer": "0.11.0",
|