@m3e/tooltip 1.0.0-rc.1 → 1.0.0-rc.2

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.
@@ -1,374 +0,0 @@
1
- import { css, CSSResultGroup, html, LitElement, PropertyValues, unsafeCSS } from "lit";
2
- import { customElement, property, query } from "lit/decorators.js";
3
-
4
- import { M3eAriaDescriber } from "@m3e/core/a11y";
5
- import { M3ePlatform } from "@m3e/core/platform";
6
- import { positionAnchor } from "@m3e/core/anchoring";
7
-
8
- import {
9
- AttachInternals,
10
- DesignToken,
11
- HoverController,
12
- HtmlFor,
13
- isDisabledMixin,
14
- LongPressController,
15
- Role,
16
- } from "@m3e/core";
17
-
18
- import { TooltipPosition } from "./TooltipPosition";
19
- import { TooltipTouchGestures } from "./TooltipTouchGestures";
20
-
21
- /** The space, in pixels, between the tooltip and anchor. */
22
- const TOOLTIP_OFFSET = 4;
23
-
24
- /** The default time, in milliseconds, before hiding a tooltip. */
25
- const TOOLTIP_HIDE_DELAY = 200;
26
-
27
- /**
28
- * @summary
29
- * Adds additional context to a button or other UI element.
30
- *
31
- * @description
32
- * The `m3e-tooltip` component provides contextual information in response to user interaction, enhancing comprehension
33
- * and reducing ambiguity. Tooltips are positioned relative to a target element and support configurable delays for
34
- * show and hide behavior. The component is designed to reinforce accessibility and usability, especially in dense or
35
- * icon-driven interfaces. Use the `for` attribute to designate the element for which to provide a tooltip.
36
- *
37
- * @example
38
- * The following example illustrates connecting a tooltip to a button using the `for` attribute.
39
- * ```html
40
- * <m3e-icon-button id="button" aria-label="Back">
41
- * <m3e-icon name="arrow_back"></m3e-icon>
42
- * </m3e-icon-button>
43
- * <m3e-tooltip for="button">Go Back</m3e-tooltip>
44
- * ```
45
- *
46
- * @tag m3e-tooltip
47
- *
48
- * @slot - Renders the content of the tooltip.
49
- *
50
- * @attr disabled - Whether the element is disabled.
51
- * @attr for - The query selector used to specify the element related to this element.
52
- * @attr hide-delay - The amount of time, in milliseconds, before hiding the tooltip.
53
- * @attr position - The position of the tooltip.
54
- * @attr show-delay - The amount of time, in milliseconds, before showing the tooltip.
55
- *
56
- * @cssprop --m3e-tooltip-padding - Internal spacing of the tooltip container.
57
- * @cssprop --m3e-tooltip-min-width - Minimum width of the tooltip.
58
- * @cssprop --m3e-tooltip-max-width - Maximum width of the tooltip.
59
- * @cssprop --m3e-tooltip-min-height - Minimum height of the tooltip container.
60
- * @cssprop --m3e-tooltip-max-height - Maximum height of the tooltip.
61
- * @cssprop --m3e-tooltip-shape - Border radius of the tooltip container.
62
- * @cssprop --m3e-tooltip-container-color - Background color of the tooltip.
63
- * @cssprop --m3e-tooltip-supporting-text-color - Text color of supporting text.
64
- * @cssprop --m3e-tooltip-supporting-text-font-size - Font size of supporting text.
65
- * @cssprop --m3e-tooltip-supporting-text-font-weight - Font weight of supporting text.
66
- * @cssprop --m3e-tooltip-supporting-text-line-height - Line height of supporting text.
67
- * @cssprop --m3e-tooltip-supporting-text-tracking - Letter spacing of supporting text.
68
- */
69
- @customElement("m3e-tooltip")
70
- export class M3eTooltipElement extends HtmlFor(AttachInternals(Role(LitElement, "none"))) {
71
- /** The styles of the element. */
72
- static override styles: CSSResultGroup = css`
73
- :host {
74
- display: contents;
75
- }
76
- .base {
77
- position: absolute;
78
- pointer-events: none;
79
- margin: unset;
80
- border: unset;
81
- word-break: normal;
82
- overflow-wrap: anywhere;
83
- padding: var(--m3e-tooltip-padding, 0.25rem 0.5rem);
84
- min-width: var(--m3e-tooltip-min-width, 2.5rem);
85
- max-width: var(--m3e-tooltip-max-width, 12.5rem);
86
- min-height: var(--m3e-tooltip-min-height, 1.5rem);
87
- max-height: var(--m3e-tooltip-max-height, 40vh);
88
- box-sizing: border-box;
89
- overflow: hidden;
90
- text-align: center;
91
- border-radius: var(--m3e-tooltip-shape, ${DesignToken.shape.corner.extraSmall});
92
- background-color: var(--m3e-tooltip-container-color, ${DesignToken.color.inverseSurface});
93
- color: var(--m3e-tooltip-supporting-text-color, ${DesignToken.color.inverseOnSurface});
94
- font-size: var(--m3e-tooltip-supporting-text-font-size, ${DesignToken.typescale.standard.body.small.fontSize});
95
- font-weight: var(
96
- --m3e-tooltip-supporting-text-font-weight,
97
- ${DesignToken.typescale.standard.body.small.fontWeight}
98
- );
99
- line-height: var(
100
- --m3e-tooltip-supporting-text-line-height,
101
- ${DesignToken.typescale.standard.body.small.lineHeight}
102
- );
103
- letter-spacing: var(
104
- --m3e-tooltip-supporting-text-tracking,
105
- ${DesignToken.typescale.standard.body.small.tracking}
106
- );
107
- visibility: hidden;
108
- opacity: 0;
109
- transform: scale(0.8);
110
- transition: ${unsafeCSS(
111
- `opacity ${DesignToken.motion.duration.short3} ${DesignToken.motion.easing.standard},
112
- transform ${DesignToken.motion.duration.short3} ${DesignToken.motion.easing.standard},
113
- overlay ${DesignToken.motion.duration.short3} ${DesignToken.motion.easing.standard} allow-discrete,
114
- visibility ${DesignToken.motion.duration.short3} ${DesignToken.motion.easing.standard} allow-discrete`
115
- )};
116
- }
117
- :host(.-multiline) .base {
118
- text-align: start;
119
- }
120
- .base::backdrop {
121
- background-color: transparent;
122
- }
123
- .base:not(:popover-open) {
124
- visibility: hidden;
125
- opacity: 0;
126
- transform: scale(0.8);
127
- }
128
- .base:popover-open {
129
- visibility: visible;
130
- opacity: 1;
131
- transform: scale(1);
132
- }
133
- @starting-style {
134
- .base:popover-open {
135
- opacity: 0;
136
- transform: scale(0.8);
137
- }
138
- }
139
- @media (prefers-reduced-motion) {
140
- .base {
141
- transition: none;
142
- }
143
- }
144
- @media (forced-colors: active) {
145
- .base {
146
- background-color: Canvas;
147
- color: CanvasText;
148
- box-sizing: border-box;
149
- border: 1px solid CanvasText;
150
- }
151
- }
152
- `;
153
-
154
- /** @private */ private static readonly __openTooltips = new Array<M3eTooltipElement>();
155
-
156
- /** @private */ @query(".base") private readonly _base!: HTMLElement;
157
- /** @private */ #message?: string | null;
158
- /** @private */ #for: HTMLElement | null = null;
159
- /** @private */ #anchorCleanup?: () => void;
160
-
161
- /** @private */ readonly #clickHandler = () => this.hide();
162
-
163
- /** @private */
164
- readonly #hoverController = new HoverController(this, {
165
- target: null,
166
- endDelay: TOOLTIP_HIDE_DELAY,
167
- callback: (hovering) => {
168
- if (hovering) {
169
- this.show();
170
- } else {
171
- this.hide();
172
- }
173
- },
174
- });
175
-
176
- /** @private */
177
- readonly #longPressController = new LongPressController(this, {
178
- target: null,
179
- callback: (pressed) => {
180
- if (pressed) {
181
- this.show();
182
- } else {
183
- this.hide();
184
- }
185
- },
186
- });
187
-
188
- /**
189
- * Whether the element is disabled.
190
- * @default false
191
- */
192
- @property({ type: Boolean, reflect: true }) disabled = false;
193
-
194
- /**
195
- * The position of the tooltip.
196
- * @default "below"
197
- */
198
- @property() position: TooltipPosition = "below";
199
-
200
- /**
201
- * The amount of time, in milliseconds, before showing the tooltip.
202
- * @default 0
203
- */
204
- @property({ attribute: "show-delay", type: Number }) get showDelay(): number {
205
- return this.#hoverController.startDelay;
206
- }
207
- set showDelay(value: number) {
208
- this.#hoverController.startDelay = value;
209
- }
210
-
211
- /**
212
- * The amount of time, in milliseconds, before hiding the tooltip.
213
- * @default 200
214
- */
215
- @property({ attribute: "hide-delay", type: Number }) get hideDelay(): number {
216
- return this.#hoverController.endDelay;
217
- }
218
- set hideDelay(value: number) {
219
- this.#hoverController.endDelay = value;
220
- }
221
-
222
- /**
223
- * The mode in which to handle touch gestures.
224
- * @default "auto"
225
- */
226
- @property({ attribute: "touch-gestures" }) touchGestures: TooltipTouchGestures = "auto";
227
-
228
- /** @inheritdoc */
229
- override attach(control: HTMLElement): void {
230
- super.attach(control);
231
-
232
- if (this.#message) {
233
- M3eAriaDescriber.describe(control, this.#message);
234
- }
235
-
236
- if (M3ePlatform.iOS || M3ePlatform.Android) {
237
- this.#longPressController.observe(control);
238
- this.#disableNativeGesturesIfNecessary();
239
- } else {
240
- this.#hoverController.observe(control);
241
- }
242
-
243
- control.addEventListener("click", this.#clickHandler);
244
- }
245
-
246
- /** @inheritdoc */
247
- override detach(): void {
248
- if (this.control) {
249
- if (this.#message) {
250
- M3eAriaDescriber.removeDescription(this.control, this.#message);
251
- }
252
-
253
- this.#hoverController.unobserve(this.control);
254
- this.#longPressController.observe(this.control);
255
- this.control.removeEventListener("click", this.#clickHandler);
256
- this.hide();
257
- }
258
- super.detach();
259
- }
260
-
261
- /** @inheritdoc */
262
- override connectedCallback(): void {
263
- super.connectedCallback();
264
- this.ariaHidden = "true";
265
- }
266
-
267
- /** @inheritdoc */
268
- protected override update(changedProperties: PropertyValues<this>): void {
269
- super.update(changedProperties);
270
-
271
- if (changedProperties.has("disabled") && this.disabled) {
272
- this.hide();
273
- }
274
- }
275
-
276
- /** @inheritdoc */
277
- protected override render(): unknown {
278
- return html`<div class="base" popover="manual" @toggle="${this.#handleToggle}">
279
- <slot @slotchange="${this.#handleSlotChange}"></slot>
280
- </div>`;
281
- }
282
-
283
- /**
284
- * Manually shows the tooltip.
285
- * @returns {Promise<void>} A `Promise` that resolves when the tooltip is shown.
286
- */
287
- async show(): Promise<void> {
288
- if (!this.control || this.disabled || (isDisabledMixin(this.control) && this.control.disabled)) return;
289
-
290
- M3eTooltipElement.__openTooltips.filter((x) => x !== this).forEach((x) => x.hide());
291
-
292
- this._base.showPopover();
293
- this.#anchorCleanup = await positionAnchor(this._base, this.control, {
294
- position:
295
- this.position === "above"
296
- ? "top"
297
- : this.position === "below"
298
- ? "bottom"
299
- : this.position === "before"
300
- ? "left"
301
- : "right",
302
- inline: true,
303
- flip: true,
304
- shift: true,
305
- offset: TOOLTIP_OFFSET,
306
- });
307
-
308
- if (!M3eTooltipElement.__openTooltips.includes(this)) {
309
- M3eTooltipElement.__openTooltips.push(this);
310
- }
311
- }
312
-
313
- /** Manually hides the tooltip. */
314
- hide(): void {
315
- this._base.hidePopover();
316
- this.#anchorCleanup?.();
317
- this.#anchorCleanup = undefined;
318
- this.#hoverController.clearDelays();
319
- }
320
-
321
- /** @private */
322
- #handleSlotChange(): void {
323
- if (this.isConnected && this.control) {
324
- if (this.#message) {
325
- M3eAriaDescriber.removeDescription(this.control, this.#message);
326
- }
327
-
328
- this.#message = this.textContent;
329
-
330
- if (this.#message) {
331
- M3eAriaDescriber.describe(this.control, this.#message);
332
- }
333
- }
334
- }
335
-
336
- /** @private */
337
- #handleToggle(e: ToggleEvent): void {
338
- if (e.newState === "open") {
339
- const multiline = this._base.getBoundingClientRect().height > parseFloat(getComputedStyle(this._base).minHeight);
340
- this.classList.toggle("-multiline", multiline);
341
- }
342
- }
343
-
344
- /** @private */
345
- #disableNativeGesturesIfNecessary() {
346
- if (this.touchGestures !== "off" && this.#for) {
347
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
348
- const style: any = this.#for.style;
349
-
350
- // If gestures are set to `auto`, we don't disable text selection on inputs and
351
- // textareas, because it prevents the user from typing into them on iOS Safari.
352
-
353
- if (this.touchGestures === "on" || (this.#for.nodeName !== "INPUT" && this.#for.nodeName !== "TEXTAREA")) {
354
- style.userSelect = style.msUserSelect = style.webkitUserSelect = style.MozUserSelect = "none";
355
- }
356
-
357
- // If we have `auto` gestures and the element uses native HTML dragging,
358
- // we don't set `-webkit-user-drag` because it prevents the native behavior.
359
-
360
- if (this.touchGestures === "on" || !this.#for.draggable) {
361
- style.webkitUserDrag = "none";
362
- }
363
-
364
- style.touchAction = "none";
365
- style.webkitTapHighlightColor = "transparent";
366
- }
367
- }
368
- }
369
-
370
- declare global {
371
- interface HTMLElementTagNameMap {
372
- "m3e-tooltip": M3eTooltipElement;
373
- }
374
- }
@@ -1,2 +0,0 @@
1
- /** Specifies the possible positions for a tooltip. */
2
- export type TooltipPosition = "above" | "below" | "before" | "after";
@@ -1,2 +0,0 @@
1
- /** Specifies the possible modes in which a tooltip should handle touch gestures. */
2
- export type TooltipTouchGestures = "auto" | "on" | "off";
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from "./TooltipElement";
2
- export * from "./TooltipPosition";
3
- export * from "./TooltipTouchGestures";
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "rootDir": "./src",
5
- "outDir": "./dist/src"
6
- },
7
- "include": ["src/**/*.ts", "**/*.mjs", "**/*.js"],
8
- "exclude": []
9
- }