@justeattakeaway/pie-tooltip 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -32
- package/custom-elements.json +27 -2
- package/dist/index.d.ts +18 -58
- package/dist/index.js +223 -214
- package/dist/react.d.ts +18 -58
- package/dist/react.js +1 -1
- package/package.json +4 -4
- package/src/defs.ts +14 -57
- package/src/index.ts +53 -74
- package/src/react.ts +1 -1
- package/src/tooltip.scss +8 -1
package/dist/react.d.ts
CHANGED
|
@@ -6,13 +6,9 @@ import * as React_2 from 'react';
|
|
|
6
6
|
import { TemplateResult } from 'lit';
|
|
7
7
|
|
|
8
8
|
declare type AriaProps = {
|
|
9
|
-
/**
|
|
10
|
-
* The accessible name for the close button. Required whenever `isDismissible` is set.
|
|
11
|
-
*/
|
|
9
|
+
/** The accessible name for the close button. Required whenever `isDismissible` is set. */
|
|
12
10
|
close?: string;
|
|
13
|
-
/**
|
|
14
|
-
* The accessible name for the panel in dialog mode. Only used when `heading` is not provided.
|
|
15
|
-
*/
|
|
11
|
+
/** The accessible name for the panel in dialog mode, when no `heading` is provided. */
|
|
16
12
|
label?: string;
|
|
17
13
|
};
|
|
18
14
|
|
|
@@ -26,10 +22,7 @@ export declare const defaultProps: DefaultProps;
|
|
|
26
22
|
|
|
27
23
|
export declare const headingLevels: readonly ["h2", "h3", "h4", "h5", "h6"];
|
|
28
24
|
|
|
29
|
-
/**
|
|
30
|
-
* The two patterns the panel can present as. Inferred from the `action` slot rather than
|
|
31
|
-
* configured: an empty `action` slot is a tooltip, a filled one is a non-modal dialog.
|
|
32
|
-
*/
|
|
25
|
+
/** The two patterns the panel can present as, inferred from the `action` slot. */
|
|
33
26
|
export declare const modes: readonly ["tooltip", "dialog"];
|
|
34
27
|
|
|
35
28
|
export declare const ON_TOOLTIP_CLOSE_EVENT = "pie-tooltip-close";
|
|
@@ -41,7 +34,7 @@ export declare const PieTooltip: React_2.ForwardRefExoticComponent<React_2.Props
|
|
|
41
34
|
/**
|
|
42
35
|
* @tagname pie-tooltip
|
|
43
36
|
* @event {Event} pie-tooltip-open - When a configured trigger asks for the panel. Set `isOpen` to `true` in response.
|
|
44
|
-
* @event {Event} pie-tooltip-close - When the close button is clicked, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
37
|
+
* @event {Event} pie-tooltip-close - When the close button is clicked, Escape is pressed, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
45
38
|
* @slot content - The descriptive content of the panel. Must not contain focusable elements.
|
|
46
39
|
* @slot action - An optional slot for interactive content such as a `pie-button`. Filling this slot switches the panel to a non-modal dialog.
|
|
47
40
|
*/
|
|
@@ -75,11 +68,14 @@ declare class PieTooltip_2 extends PieElement implements TooltipProps {
|
|
|
75
68
|
private _reanchorFrame;
|
|
76
69
|
private _hoverCloseTimer;
|
|
77
70
|
private _openedByClick;
|
|
71
|
+
private readonly _instanceId;
|
|
72
|
+
private get _headingId();
|
|
78
73
|
static styles: CSSResult;
|
|
79
74
|
private get _mode();
|
|
80
75
|
protected firstUpdated(): void;
|
|
81
76
|
protected updated(changedProperties: PropertyValues<this>): void;
|
|
82
77
|
disconnectedCallback(): void;
|
|
78
|
+
focusPanel(): Promise<boolean>;
|
|
83
79
|
private startTrackingTrigger;
|
|
84
80
|
private stopTrackingTrigger;
|
|
85
81
|
private resolveMode;
|
|
@@ -117,63 +113,27 @@ export declare const sizes: readonly ["default", "fit-to-content", "fill-contain
|
|
|
117
113
|
export declare type TooltipMode = typeof modes[number];
|
|
118
114
|
|
|
119
115
|
export declare interface TooltipProps {
|
|
120
|
-
/**
|
|
121
|
-
* The `id` of the element the panel is anchored to. The trigger lives elsewhere in the
|
|
122
|
-
* DOM and is never slotted into the tooltip.
|
|
123
|
-
*/
|
|
116
|
+
/** The `id` of the element the panel is anchored to. */
|
|
124
117
|
trigger?: string;
|
|
125
|
-
/**
|
|
126
|
-
* When true, the panel is visible. The component never writes to this property: the
|
|
127
|
-
* consumer owns it and updates it in response to the close event.
|
|
128
|
-
*/
|
|
118
|
+
/** When true, the panel is visible. The component never writes to this property. */
|
|
129
119
|
isOpen?: boolean;
|
|
130
|
-
/**
|
|
131
|
-
* The preferred side of the trigger the panel sits on, and its alignment along the cross axis.
|
|
132
|
-
* The component repositions the panel automatically — flipping to the opposite side and/or
|
|
133
|
-
* shifting its alignment — when the preferred position would collide with the viewport or with
|
|
134
|
-
* a clipping scroll container. This behaviour cannot be disabled.
|
|
135
|
-
*/
|
|
120
|
+
/** The preferred side and alignment; the panel repositions itself to avoid collisions. */
|
|
136
121
|
position?: typeof positions[number];
|
|
137
|
-
/**
|
|
138
|
-
* How the panel sizes itself. `default` is a fixed 280px and wraps, `fit-to-content` is as
|
|
139
|
-
* wide as its content, and `fill-container` matches the inline size of the trigger's parent
|
|
140
|
-
* element. Not applied when `type` is `icon`.
|
|
141
|
-
*/
|
|
122
|
+
/** How the panel sizes itself. Not applied when `type` is `icon`. */
|
|
142
123
|
size?: typeof sizes[number];
|
|
143
|
-
/**
|
|
144
|
-
* The colour treatment of the panel. `default` is the dark panel, `inverse` the light one.
|
|
145
|
-
*/
|
|
124
|
+
/** The colour treatment of the panel. `default` is the dark panel, `inverse` the light one. */
|
|
146
125
|
variant?: typeof variants[number];
|
|
147
|
-
/**
|
|
148
|
-
* The presentation of the panel. `icon` is the compact treatment intended for icon triggers:
|
|
149
|
-
* it has no arrow and is always as wide as its content, so `size` and `--tooltip-width` have
|
|
150
|
-
* no effect on it.
|
|
151
|
-
*/
|
|
126
|
+
/** The presentation of the panel. `icon` is the compact, arrow-less treatment. */
|
|
152
127
|
type?: typeof types[number];
|
|
153
|
-
/**
|
|
154
|
-
* When true, a close button is rendered inside the panel.
|
|
155
|
-
*/
|
|
128
|
+
/** When true, a close button is rendered inside the panel and the panel presents as a non-modal dialog, so it needs `heading` or `aria.label` for its accessible name. */
|
|
156
129
|
isDismissible?: boolean;
|
|
157
|
-
/**
|
|
158
|
-
* The text to display in the panel's heading. In dialog mode this also provides the
|
|
159
|
-
* panel's accessible name.
|
|
160
|
-
*/
|
|
130
|
+
/** The text to display in the panel's heading. In dialog mode this also names the panel. */
|
|
161
131
|
heading?: string;
|
|
162
|
-
/**
|
|
163
|
-
* The HTML heading tag to use for the panel's heading. Can be h2-h6.
|
|
164
|
-
*/
|
|
132
|
+
/** The HTML heading tag to use for the panel's heading. Can be h2-h6. */
|
|
165
133
|
headingLevel?: typeof headingLevels[number];
|
|
166
|
-
/**
|
|
167
|
-
* The ARIA labels used for various parts of the tooltip.
|
|
168
|
-
*/
|
|
134
|
+
/** The ARIA labels used for various parts of the tooltip. */
|
|
169
135
|
aria?: AriaProps;
|
|
170
|
-
/**
|
|
171
|
-
* Which interactions request that the panel opens and closes. The component never opens or
|
|
172
|
-
* closes itself: a configured interaction emits `pie-tooltip-open` or `pie-tooltip-close`
|
|
173
|
-
* and the consumer decides whether to honour it by setting `isOpen`. Empty by default, so
|
|
174
|
-
* no interaction is watched at all. Configure `hover` and `focus` together for keyboard
|
|
175
|
-
* reachability, and `click` for pointer users.
|
|
176
|
-
*/
|
|
136
|
+
/** Which interactions request that the panel opens and closes. */
|
|
177
137
|
triggers?: Array<TooltipTrigger>;
|
|
178
138
|
}
|
|
179
139
|
|
package/dist/react.js
CHANGED
|
@@ -11,7 +11,7 @@ const i = e({
|
|
|
11
11
|
onPieTooltipOpen: "pie-tooltip-open",
|
|
12
12
|
// When a configured trigger asks for the panel. Set `isOpen` to `true` in response.
|
|
13
13
|
onPieTooltipClose: "pie-tooltip-close"
|
|
14
|
-
// When the close button is clicked, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
14
|
+
// When the close button is clicked, Escape is pressed, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
15
15
|
}
|
|
16
16
|
}), l = i;
|
|
17
17
|
export {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justeattakeaway/pie-tooltip",
|
|
3
3
|
"description": "PIE Design System Tooltip built using Web Components",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"license": "Apache-2.0",
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@justeattakeaway/pie-components-config": "0.21.4",
|
|
39
|
-
"@justeattakeaway/pie-css": "1.9.
|
|
40
|
-
"@justeattakeaway/pie-monorepo-utils": "0.9.
|
|
39
|
+
"@justeattakeaway/pie-css": "1.9.1",
|
|
40
|
+
"@justeattakeaway/pie-monorepo-utils": "0.9.11"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@justeattakeaway/pie-icon-button": "2.
|
|
43
|
+
"@justeattakeaway/pie-icon-button": "2.8.0",
|
|
44
44
|
"@justeattakeaway/pie-icons-webc": "1.29.4",
|
|
45
45
|
"@justeattakeaway/pie-webc-core": "22.0.0"
|
|
46
46
|
},
|
package/src/defs.ts
CHANGED
|
@@ -21,95 +21,52 @@ export const types = ['default', 'icon'] as const;
|
|
|
21
21
|
export const headingLevels = ['h2', 'h3', 'h4', 'h5', 'h6'] as const;
|
|
22
22
|
export const triggers = ['hover', 'focus', 'click'] as const;
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* The two patterns the panel can present as. Inferred from the `action` slot rather than
|
|
26
|
-
* configured: an empty `action` slot is a tooltip, a filled one is a non-modal dialog.
|
|
27
|
-
*/
|
|
24
|
+
/** The two patterns the panel can present as, inferred from the `action` slot. */
|
|
28
25
|
export const modes = ['tooltip', 'dialog'] as const;
|
|
29
26
|
|
|
30
27
|
export type TooltipMode = typeof modes[number];
|
|
31
28
|
export type TooltipTrigger = typeof triggers[number];
|
|
32
29
|
|
|
33
30
|
type AriaProps = {
|
|
34
|
-
/**
|
|
35
|
-
* The accessible name for the close button. Required whenever `isDismissible` is set.
|
|
36
|
-
*/
|
|
31
|
+
/** The accessible name for the close button. Required whenever `isDismissible` is set. */
|
|
37
32
|
close?: string;
|
|
38
33
|
|
|
39
|
-
/**
|
|
40
|
-
* The accessible name for the panel in dialog mode. Only used when `heading` is not provided.
|
|
41
|
-
*/
|
|
34
|
+
/** The accessible name for the panel in dialog mode, when no `heading` is provided. */
|
|
42
35
|
label?: string;
|
|
43
36
|
};
|
|
44
37
|
|
|
45
38
|
export interface TooltipProps {
|
|
46
|
-
/**
|
|
47
|
-
* The `id` of the element the panel is anchored to. The trigger lives elsewhere in the
|
|
48
|
-
* DOM and is never slotted into the tooltip.
|
|
49
|
-
*/
|
|
39
|
+
/** The `id` of the element the panel is anchored to. */
|
|
50
40
|
trigger?: string;
|
|
51
41
|
|
|
52
|
-
/**
|
|
53
|
-
* When true, the panel is visible. The component never writes to this property: the
|
|
54
|
-
* consumer owns it and updates it in response to the close event.
|
|
55
|
-
*/
|
|
42
|
+
/** When true, the panel is visible. The component never writes to this property. */
|
|
56
43
|
isOpen?: boolean;
|
|
57
44
|
|
|
58
|
-
/**
|
|
59
|
-
* The preferred side of the trigger the panel sits on, and its alignment along the cross axis.
|
|
60
|
-
* The component repositions the panel automatically — flipping to the opposite side and/or
|
|
61
|
-
* shifting its alignment — when the preferred position would collide with the viewport or with
|
|
62
|
-
* a clipping scroll container. This behaviour cannot be disabled.
|
|
63
|
-
*/
|
|
45
|
+
/** The preferred side and alignment; the panel repositions itself to avoid collisions. */
|
|
64
46
|
position?: typeof positions[number];
|
|
65
47
|
|
|
66
|
-
/**
|
|
67
|
-
* How the panel sizes itself. `default` is a fixed 280px and wraps, `fit-to-content` is as
|
|
68
|
-
* wide as its content, and `fill-container` matches the inline size of the trigger's parent
|
|
69
|
-
* element. Not applied when `type` is `icon`.
|
|
70
|
-
*/
|
|
48
|
+
/** How the panel sizes itself. Not applied when `type` is `icon`. */
|
|
71
49
|
size?: typeof sizes[number];
|
|
72
50
|
|
|
73
|
-
/**
|
|
74
|
-
* The colour treatment of the panel. `default` is the dark panel, `inverse` the light one.
|
|
75
|
-
*/
|
|
51
|
+
/** The colour treatment of the panel. `default` is the dark panel, `inverse` the light one. */
|
|
76
52
|
variant?: typeof variants[number];
|
|
77
53
|
|
|
78
|
-
/**
|
|
79
|
-
* The presentation of the panel. `icon` is the compact treatment intended for icon triggers:
|
|
80
|
-
* it has no arrow and is always as wide as its content, so `size` and `--tooltip-width` have
|
|
81
|
-
* no effect on it.
|
|
82
|
-
*/
|
|
54
|
+
/** The presentation of the panel. `icon` is the compact, arrow-less treatment. */
|
|
83
55
|
type?: typeof types[number];
|
|
84
56
|
|
|
85
|
-
/**
|
|
86
|
-
* When true, a close button is rendered inside the panel.
|
|
87
|
-
*/
|
|
57
|
+
/** When true, a close button is rendered inside the panel and the panel presents as a non-modal dialog, so it needs `heading` or `aria.label` for its accessible name. */
|
|
88
58
|
isDismissible?: boolean;
|
|
89
59
|
|
|
90
|
-
/**
|
|
91
|
-
* The text to display in the panel's heading. In dialog mode this also provides the
|
|
92
|
-
* panel's accessible name.
|
|
93
|
-
*/
|
|
60
|
+
/** The text to display in the panel's heading. In dialog mode this also names the panel. */
|
|
94
61
|
heading?: string;
|
|
95
62
|
|
|
96
|
-
/**
|
|
97
|
-
* The HTML heading tag to use for the panel's heading. Can be h2-h6.
|
|
98
|
-
*/
|
|
63
|
+
/** The HTML heading tag to use for the panel's heading. Can be h2-h6. */
|
|
99
64
|
headingLevel?: typeof headingLevels[number];
|
|
100
65
|
|
|
101
|
-
/**
|
|
102
|
-
* The ARIA labels used for various parts of the tooltip.
|
|
103
|
-
*/
|
|
66
|
+
/** The ARIA labels used for various parts of the tooltip. */
|
|
104
67
|
aria?: AriaProps;
|
|
105
68
|
|
|
106
|
-
/**
|
|
107
|
-
* Which interactions request that the panel opens and closes. The component never opens or
|
|
108
|
-
* closes itself: a configured interaction emits `pie-tooltip-open` or `pie-tooltip-close`
|
|
109
|
-
* and the consumer decides whether to honour it by setting `isOpen`. Empty by default, so
|
|
110
|
-
* no interaction is watched at all. Configure `hover` and `focus` together for keyboard
|
|
111
|
-
* reachability, and `click` for pointer users.
|
|
112
|
-
*/
|
|
69
|
+
/** Which interactions request that the panel opens and closes. */
|
|
113
70
|
triggers?: Array<TooltipTrigger>;
|
|
114
71
|
}
|
|
115
72
|
|
package/src/index.ts
CHANGED
|
@@ -37,10 +37,10 @@ import '@justeattakeaway/pie-icons-webc/dist/IconClose.js';
|
|
|
37
37
|
|
|
38
38
|
export * from './defs';
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const FOCUS_PANEL_MAX_ATTEMPTS = 5;
|
|
41
|
+
|
|
42
|
+
const nextFrame = (): Promise<number> => new Promise(requestAnimationFrame);
|
|
41
43
|
|
|
42
|
-
// Returns true if the element establishes a containing block through a property other than
|
|
43
|
-
// `position` — i.e. for both absolute and fixed descendants, not just absolute.
|
|
44
44
|
const createsContainingBlock = (styles: CSSStyleDeclaration): boolean => {
|
|
45
45
|
const isSet = (value: string | undefined) => !!value && value !== 'none';
|
|
46
46
|
|
|
@@ -58,7 +58,6 @@ const createsContainingBlock = (styles: CSSStyleDeclaration): boolean => {
|
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// `size` and `style` containment do not establish a containing block; everything else does.
|
|
62
61
|
if (/\b(paint|layout|content|strict)\b/.test(styles.contain)) {
|
|
63
62
|
return true;
|
|
64
63
|
}
|
|
@@ -67,7 +66,6 @@ const createsContainingBlock = (styles: CSSStyleDeclaration): boolean => {
|
|
|
67
66
|
return true;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
// will-change pre-establishes the containing block before the property is applied.
|
|
71
69
|
return /\b(transform|perspective|filter|backdrop-filter|contain|translate|rotate|scale)\b/.test(styles.willChange);
|
|
72
70
|
};
|
|
73
71
|
|
|
@@ -91,8 +89,6 @@ const flattenedAncestors = (element: Element): Array<Element> => {
|
|
|
91
89
|
return ancestors;
|
|
92
90
|
};
|
|
93
91
|
|
|
94
|
-
// The ancestors that clip the trigger. Overflow on the root element and the body is left out:
|
|
95
|
-
// that is the viewport clip, which every element is subject to and which no panel can escape.
|
|
96
92
|
const collectClippingAncestors = (element: Element): Array<Element> => {
|
|
97
93
|
const { documentElement, body } = element.ownerDocument;
|
|
98
94
|
|
|
@@ -107,9 +103,6 @@ const collectClippingAncestors = (element: Element): Array<Element> => {
|
|
|
107
103
|
});
|
|
108
104
|
};
|
|
109
105
|
|
|
110
|
-
// The region an element clips its descendants to: its padding box, minus any scrollbar.
|
|
111
|
-
// `clientLeft`/`clientTop` are the border widths and `clientWidth`/`clientHeight` the padding box,
|
|
112
|
-
// so together they convert the border-box rect the browser reports into the clip region.
|
|
113
106
|
const getClipRect = (element: Element): DOMRect => {
|
|
114
107
|
const { left, top } = element.getBoundingClientRect();
|
|
115
108
|
const {
|
|
@@ -119,7 +112,6 @@ const getClipRect = (element: Element): DOMRect => {
|
|
|
119
112
|
return new DOMRect(left + clientLeft, top + clientTop, clientWidth, clientHeight);
|
|
120
113
|
};
|
|
121
114
|
|
|
122
|
-
// Intersects two rects. Returns `null` when they do not overlap.
|
|
123
115
|
const intersectRects = (a: DOMRect, b: DOMRect): DOMRect | null => {
|
|
124
116
|
const left = Math.max(a.left, b.left);
|
|
125
117
|
const top = Math.max(a.top, b.top);
|
|
@@ -245,7 +237,7 @@ const getVisibleArea = (boundary: DOMRect, rect: CandidateRect): number => {
|
|
|
245
237
|
/**
|
|
246
238
|
* @tagname pie-tooltip
|
|
247
239
|
* @event {Event} pie-tooltip-open - When a configured trigger asks for the panel. Set `isOpen` to `true` in response.
|
|
248
|
-
* @event {Event} pie-tooltip-close - When the close button is clicked, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
240
|
+
* @event {Event} pie-tooltip-close - When the close button is clicked, Escape is pressed, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
249
241
|
* @slot content - The descriptive content of the panel. Must not contain focusable elements.
|
|
250
242
|
* @slot action - An optional slot for interactive content such as a `pie-button`. Filling this slot switches the panel to a non-modal dialog.
|
|
251
243
|
*/
|
|
@@ -324,6 +316,12 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
324
316
|
|
|
325
317
|
private _openedByClick = false;
|
|
326
318
|
|
|
319
|
+
private readonly _instanceId = crypto.randomUUID();
|
|
320
|
+
|
|
321
|
+
private get _headingId (): string {
|
|
322
|
+
return `pie-tooltip-heading-${this._instanceId}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
327
325
|
static styles = unsafeCSS(styles);
|
|
328
326
|
|
|
329
327
|
private get _mode (): TooltipMode | undefined {
|
|
@@ -331,7 +329,7 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
331
329
|
return undefined;
|
|
332
330
|
}
|
|
333
331
|
|
|
334
|
-
return this._hasActionContent ? 'dialog' : 'tooltip';
|
|
332
|
+
return (this._hasActionContent || this.isDismissible) ? 'dialog' : 'tooltip';
|
|
335
333
|
}
|
|
336
334
|
|
|
337
335
|
protected firstUpdated (): void {
|
|
@@ -369,9 +367,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
369
367
|
this.projectOverTrigger();
|
|
370
368
|
}
|
|
371
369
|
|
|
372
|
-
// Tracking is bound to a specific trigger element and a specific ancestor chain, so a new
|
|
373
|
-
// trigger needs it rebuilt rather than left in place. `startTrackingTrigger` early-returns
|
|
374
|
-
// while a controller exists, so the stop has to come first.
|
|
375
370
|
if (this.isOpen && changedProperties.has('trigger')) {
|
|
376
371
|
this.stopTrackingTrigger();
|
|
377
372
|
}
|
|
@@ -393,8 +388,36 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
393
388
|
super.disconnectedCallback();
|
|
394
389
|
}
|
|
395
390
|
|
|
396
|
-
|
|
397
|
-
|
|
391
|
+
public async focusPanel (): Promise<boolean> {
|
|
392
|
+
if (this._mode !== 'dialog') {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await this.updateComplete;
|
|
397
|
+
await nextFrame();
|
|
398
|
+
|
|
399
|
+
const content = this.renderRoot.querySelector<HTMLElement>(`.${componentClass}-content`);
|
|
400
|
+
|
|
401
|
+
if (!content) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const focusLanded = () => this.shadowRoot?.activeElement === content;
|
|
406
|
+
|
|
407
|
+
for (let attempt = 0; attempt < FOCUS_PANEL_MAX_ATTEMPTS; attempt++) {
|
|
408
|
+
content.focus({ preventScroll: true });
|
|
409
|
+
|
|
410
|
+
if (focusLanded()) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// eslint-disable-next-line no-await-in-loop
|
|
415
|
+
await nextFrame();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
398
421
|
private startTrackingTrigger (): void {
|
|
399
422
|
if (this._triggerTrackingController) {
|
|
400
423
|
return;
|
|
@@ -416,7 +439,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
416
439
|
});
|
|
417
440
|
};
|
|
418
441
|
|
|
419
|
-
// Flagged rather than resolved immediately so window-drag cannot walk ancestors more than once per frame.
|
|
420
442
|
const handleResize = () => {
|
|
421
443
|
this._overlayModeDirty = true;
|
|
422
444
|
handleViewportChange();
|
|
@@ -425,10 +447,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
425
447
|
window.addEventListener('scroll', handleViewportChange, { capture: true, passive: true, signal });
|
|
426
448
|
window.addEventListener('resize', handleResize, { passive: true, signal });
|
|
427
449
|
|
|
428
|
-
// `scroll` is not composed, so its path stops at the shadow root it happened in and a
|
|
429
|
-
// listener on `window` never sees it. A scroll container inside another component's
|
|
430
|
-
// shadow root (`pie-modal`'s, for one) therefore needs its own listener, or the panel
|
|
431
|
-
// detaches from the trigger as soon as that container scrolls.
|
|
432
450
|
const shadowRoots = new Set<ShadowRoot>();
|
|
433
451
|
|
|
434
452
|
flattenedAncestors(this).forEach((ancestor) => {
|
|
@@ -443,12 +461,7 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
443
461
|
root.addEventListener('scroll', handleViewportChange, { capture: true, passive: true, signal });
|
|
444
462
|
});
|
|
445
463
|
|
|
446
|
-
// A trigger inside a container that was `display: none` when the panel opened has no box
|
|
447
|
-
// to measure. `pie-modal` calls `showModal()` from an async `firstUpdated`, so this is the
|
|
448
|
-
// normal case for a tooltip inside a modal that is open on load.
|
|
449
464
|
this._triggerObserver = new ResizeObserver(() => {
|
|
450
|
-
// Geometry changed, so the ancestor chain may have too. Flagged rather than resolved
|
|
451
|
-
// here so the walk runs at most once per frame.
|
|
452
465
|
this._overlayModeDirty = true;
|
|
453
466
|
handleViewportChange();
|
|
454
467
|
});
|
|
@@ -488,13 +501,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
488
501
|
this._hasActionContent = this._assignedActionElements.length > 0;
|
|
489
502
|
}
|
|
490
503
|
|
|
491
|
-
// Picks between `absolute` (browser handles scrolling, but clipped by overflow ancestors) and
|
|
492
|
-
// `fixed` (escapes overflow clips, but must re-offset on every scroll). Switches to `fixed`
|
|
493
|
-
// only when it escapes a clip that `absolute` would not.
|
|
494
|
-
//
|
|
495
|
-
// An overflow ancestor clips a positioned box only if it is that box's containing block or an
|
|
496
|
-
// ancestor of it; a clipper strictly inside the containing block does not clip. So each
|
|
497
|
-
// clipper is counted against a mode only once that mode's containing block has been reached.
|
|
498
504
|
private resolveOverlayMode (): void {
|
|
499
505
|
if (!this._overlayModeDirty) {
|
|
500
506
|
return;
|
|
@@ -509,16 +515,11 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
509
515
|
|
|
510
516
|
const { documentElement, body } = this.ownerDocument;
|
|
511
517
|
|
|
512
|
-
// Resolved on the same triggers as the overlay mode, because both answers change only
|
|
513
|
-
// when the ancestor chain does.
|
|
514
518
|
this._refreshTriggerClippers();
|
|
515
519
|
|
|
516
520
|
flattenedAncestors(this).forEach((element) => {
|
|
517
521
|
const styles = getComputedStyle(element);
|
|
518
522
|
|
|
519
|
-
// `display: contents` generates no box, so it can neither be a containing block nor
|
|
520
|
-
// clip. `display: none` is deliberately not skipped: its computed values still
|
|
521
|
-
// describe the box it will generate, so the answer holds once it is shown.
|
|
522
523
|
if (styles.display === 'contents') {
|
|
523
524
|
return;
|
|
524
525
|
}
|
|
@@ -535,14 +536,8 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
535
536
|
|
|
536
537
|
const clips = styles.overflowX !== 'visible' || styles.overflowY !== 'visible';
|
|
537
538
|
|
|
538
|
-
// Overflow on the root element and on the body propagates to the viewport, which no
|
|
539
|
-
// positioning scheme escapes. Counting it can only make `fixed` look no better than
|
|
540
|
-
// `absolute`, which is what happens while a `pie-modal` is open, because its scroll
|
|
541
|
-
// lock sets `overflow: hidden` on the body.
|
|
542
539
|
const propagatesOverflowToViewport = element === documentElement || element === body;
|
|
543
540
|
|
|
544
|
-
// Counted after the containing-block flags so an ancestor that is both the containing
|
|
545
|
-
// block and the clipper is counted correctly.
|
|
546
541
|
if (clips && !propagatesOverflowToViewport) {
|
|
547
542
|
if (isAtOrAboveAbsoluteContainingBlock) {
|
|
548
543
|
absoluteClippingAncestors.push(element);
|
|
@@ -554,18 +549,12 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
554
549
|
}
|
|
555
550
|
});
|
|
556
551
|
|
|
557
|
-
// The fixed containing block is always at or above the absolute one, so the clippers that
|
|
558
|
-
// apply to a fixed box are a subset of those that apply to an absolute one. A lower count
|
|
559
|
-
// therefore always means a strictly better escape, never a worse one.
|
|
560
552
|
const useFixed = fixedClippingAncestors.length < absoluteClippingAncestors.length;
|
|
561
553
|
|
|
562
554
|
this.style.position = useFixed ? 'fixed' : '';
|
|
563
555
|
this._overlayClippers = useFixed ? fixedClippingAncestors : absoluteClippingAncestors;
|
|
564
556
|
}
|
|
565
557
|
|
|
566
|
-
// Measures the trigger relative to the origin marker (which sits at the containing block's
|
|
567
|
-
// origin) and writes CSS custom properties. Self-referential: correct for any containing
|
|
568
|
-
// block. Physical values; shadow-root CSS uses logical properties for RTL mirroring.
|
|
569
558
|
private projectOverTrigger (): void {
|
|
570
559
|
this._isPositioned = false;
|
|
571
560
|
|
|
@@ -586,16 +575,11 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
586
575
|
this._refreshTriggerClippers();
|
|
587
576
|
}
|
|
588
577
|
|
|
589
|
-
// Anchoring to the trigger's full box would point the panel at a part of the trigger that
|
|
590
|
-
// has been scrolled out of sight, leaving whatever sits over it — a pinned modal footer,
|
|
591
|
-
// for one — between the two. Anchoring to the visible part instead keeps the arrow on the
|
|
592
|
-
// edge the reader can actually see.
|
|
593
578
|
const visibleRect = this._triggerClippers.reduce<DOMRect | null>(
|
|
594
579
|
(rect, clipper) => (rect ? intersectRects(rect, getClipRect(clipper)) : null),
|
|
595
580
|
triggerElement.getBoundingClientRect(),
|
|
596
581
|
);
|
|
597
582
|
|
|
598
|
-
// Nothing of the trigger is left to point at, so there is nothing to describe either.
|
|
599
583
|
this._isAnchorVisible = visibleRect !== null;
|
|
600
584
|
|
|
601
585
|
if (!visibleRect) {
|
|
@@ -734,9 +718,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
734
718
|
});
|
|
735
719
|
}
|
|
736
720
|
|
|
737
|
-
// Cached because `projectOverTrigger` runs on every re-anchoring frame, and collecting these
|
|
738
|
-
// costs a computed style per ancestor. Refreshed when the trigger changes and whenever the
|
|
739
|
-
// overlay mode is resolved, which is the same cadence as an ancestor chain actually changing.
|
|
740
721
|
private _refreshTriggerClippers (): void {
|
|
741
722
|
const triggerElement = this._getTriggerElement();
|
|
742
723
|
|
|
@@ -788,20 +769,20 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
788
769
|
private _rebuildInteractionListeners (): void {
|
|
789
770
|
this._teardownInteractionListeners();
|
|
790
771
|
|
|
791
|
-
if (!this.triggers?.length) return;
|
|
792
|
-
|
|
793
772
|
const controller = new AbortController();
|
|
794
773
|
const { signal } = controller;
|
|
795
774
|
this._interactionController = controller;
|
|
796
775
|
|
|
797
|
-
const triggerEl = this._getTriggerElement();
|
|
798
|
-
|
|
799
776
|
this.ownerDocument.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
800
777
|
if (e.key === 'Escape' && this.isOpen) {
|
|
801
778
|
this._requestClose();
|
|
802
779
|
}
|
|
803
780
|
}, { signal });
|
|
804
781
|
|
|
782
|
+
if (!this.triggers?.length) return;
|
|
783
|
+
|
|
784
|
+
const triggerEl = this._getTriggerElement();
|
|
785
|
+
|
|
805
786
|
if (!triggerEl) return;
|
|
806
787
|
|
|
807
788
|
if (this.triggers.includes('hover')) {
|
|
@@ -814,7 +795,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
814
795
|
this._startHoverCloseTimer();
|
|
815
796
|
}, { signal });
|
|
816
797
|
|
|
817
|
-
// panel mouseenter/leave for the hover bridge (includes the bridge pseudo-element)
|
|
818
798
|
const panel = this.renderRoot.querySelector('.c-tooltip');
|
|
819
799
|
if (panel) {
|
|
820
800
|
panel.addEventListener('mouseenter', () => {
|
|
@@ -828,14 +808,12 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
828
808
|
}
|
|
829
809
|
|
|
830
810
|
if (this.triggers.includes('focus')) {
|
|
831
|
-
// open on focus, close on blur unless focus moved into panel action content
|
|
832
811
|
triggerEl.addEventListener('focusin', () => {
|
|
833
812
|
this._requestOpen();
|
|
834
813
|
}, { signal });
|
|
835
814
|
|
|
836
815
|
triggerEl.addEventListener('focusout', (e: Event) => {
|
|
837
816
|
const related = (e as FocusEvent).relatedTarget as Node | null;
|
|
838
|
-
// relatedTarget is retargeted to the shadow host when focus moves into shadow DOM
|
|
839
817
|
const staysInside = related && (this.contains(related) || related === this);
|
|
840
818
|
if (!staysInside) {
|
|
841
819
|
this._requestClose();
|
|
@@ -848,7 +826,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
848
826
|
e.stopPropagation();
|
|
849
827
|
|
|
850
828
|
if (!this.isOpen) {
|
|
851
|
-
// Only click-opens toggle
|
|
852
829
|
this._openedByClick = true;
|
|
853
830
|
this._requestOpen();
|
|
854
831
|
|
|
@@ -864,7 +841,6 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
864
841
|
this._requestClose();
|
|
865
842
|
}, { signal });
|
|
866
843
|
|
|
867
|
-
// Light-dismiss: click anywhere outside the panel and trigger
|
|
868
844
|
this.ownerDocument.addEventListener('click', (e: Event) => {
|
|
869
845
|
const target = e.composedPath()[0] as Node;
|
|
870
846
|
const isInsidePanel = this.contains(target) || this.shadowRoot?.contains(target);
|
|
@@ -886,9 +862,9 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
886
862
|
const tag = unsafeStatic(this.headingLevel);
|
|
887
863
|
|
|
888
864
|
return html`<${tag}
|
|
889
|
-
id="${
|
|
865
|
+
id="${this._headingId}"
|
|
890
866
|
class="${componentClass}-heading"
|
|
891
|
-
data-test-id="${
|
|
867
|
+
data-test-id="${componentSelector}-heading">${this.heading}</${tag}>`;
|
|
892
868
|
}
|
|
893
869
|
|
|
894
870
|
private renderCloseButton (): TemplateResult {
|
|
@@ -937,7 +913,7 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
937
913
|
[`${componentClass}--type-${type}`]: true,
|
|
938
914
|
[`${componentClass}--size-${size}`]: !isIconType,
|
|
939
915
|
'is-dismissible': !!isDismissible,
|
|
940
|
-
'has-action':
|
|
916
|
+
'has-action': this._hasActionContent === true,
|
|
941
917
|
'has-heading': !!heading,
|
|
942
918
|
};
|
|
943
919
|
|
|
@@ -952,12 +928,15 @@ export class PieTooltip extends PieElement implements TooltipProps {
|
|
|
952
928
|
data-test-id="${componentSelector}"
|
|
953
929
|
role="${ifDefined(mode)}"
|
|
954
930
|
aria-hidden="${!isOpen}"
|
|
955
|
-
aria-labelledby="${isDialog && heading ?
|
|
931
|
+
aria-labelledby="${isDialog && heading ? this._headingId : nothing}"
|
|
956
932
|
aria-label="${isDialog && !heading && aria?.label ? aria.label : nothing}">
|
|
957
933
|
${isIconType ? nothing : html`<div class="${componentClass}-arrow" data-test-id="${componentSelector}-arrow"></div>`}
|
|
958
934
|
<div class="${componentClass}-body">
|
|
959
935
|
${heading ? this.renderHeading() : nothing}
|
|
960
|
-
<div
|
|
936
|
+
<div
|
|
937
|
+
class="${componentClass}-content"
|
|
938
|
+
tabindex="${isDialog ? -1 : nothing}"
|
|
939
|
+
data-test-id="${componentSelector}-content">
|
|
961
940
|
<slot name="content"></slot>
|
|
962
941
|
</div>
|
|
963
942
|
</div>
|
package/src/react.ts
CHANGED
|
@@ -12,7 +12,7 @@ const PieTooltipReact = createComponent({
|
|
|
12
12
|
tagName: 'pie-tooltip',
|
|
13
13
|
events: {
|
|
14
14
|
onPieTooltipOpen: 'pie-tooltip-open' as EventName<Event>, // When a configured trigger asks for the panel. Set `isOpen` to `true` in response.
|
|
15
|
-
onPieTooltipClose: 'pie-tooltip-close' as EventName<Event>, // When the close button is clicked, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
15
|
+
onPieTooltipClose: 'pie-tooltip-close' as EventName<Event>, // When the close button is clicked, Escape is pressed, or a configured trigger asks to dismiss the panel. Set `isOpen` to `false` in response.
|
|
16
16
|
},
|
|
17
17
|
});
|
|
18
18
|
|
package/src/tooltip.scss
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
@use '@justeattakeaway/pie-css/scss' as p;
|
|
2
2
|
|
|
3
|
-
// All twelve position values are generated explicitly
|
|
3
|
+
// All twelve position values are generated explicitly, none inferred.
|
|
4
4
|
$tooltip-alignments: ('', '-start', '-end');
|
|
5
5
|
|
|
6
6
|
// Zero-size, pinned to the containing block's origin. Trigger position is measured relative
|
|
@@ -343,6 +343,13 @@ $tooltip-alignments: ('', '-start', '-end');
|
|
|
343
343
|
overflow-wrap: break-word;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// The content is the programmatic focus target in dialog mode (see `focusPanel`). It is never
|
|
347
|
+
// reached by Tab, which moves between the panel's own controls, so it draws no focus ring of
|
|
348
|
+
// its own: the controls inside the panel carry their own focus indicators.
|
|
349
|
+
.c-tooltip-content:focus {
|
|
350
|
+
outline: none;
|
|
351
|
+
}
|
|
352
|
+
|
|
346
353
|
.c-tooltip.has-action .c-tooltip-action {
|
|
347
354
|
display: flex;
|
|
348
355
|
justify-content: flex-end;
|