Skip to content

Commit bc16ff4

Browse files
Merge pull request #2256 from hashicorp/HDS-2718-Tooltip-Modifier-TS-Migration
Hds 2718 tooltip modifier TS migration
2 parents 3139a1a + 2140fbe commit bc16ff4

File tree

4 files changed

+219
-164
lines changed

4 files changed

+219
-164
lines changed

.changeset/nasty-points-wait.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
`Dropdown` - Converted component to TypeScript
66

77
`MenuPrimitive` - Converted component to TypeScript
8+
9+
`TooltipModifier` - Converted modifier to TypeScript

packages/components/src/modifiers/hds-tooltip.js

Lines changed: 0 additions & 164 deletions
This file was deleted.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
// Note: the majority of this code is a porting of the existing tooltip implementation in Cloud UI
7+
// (which was initially implemented in Structure)
8+
9+
import Modifier from 'ember-modifier';
10+
import type { ArgsFor } from 'ember-modifier';
11+
12+
import { assert } from '@ember/debug';
13+
import { registerDestructor } from '@ember/destroyable';
14+
15+
import tippy, { followCursor } from 'tippy.js';
16+
import type {
17+
HideAll as TippyHideAll,
18+
Instance as TippyInstance,
19+
Props as TippyProps,
20+
} from 'tippy.js';
21+
// used by custom SVG arrow:
22+
import 'tippy.js/dist/svg-arrow.css';
23+
24+
export interface HdsTooltipModifierSignature {
25+
Args: {
26+
Positional: [string];
27+
Named: {
28+
options?: TippyProps;
29+
};
30+
};
31+
Element: HTMLElement;
32+
}
33+
34+
function cleanup(instance: HdsTooltipModifier): void {
35+
const { interval, needsTabIndex, tooltip } = instance;
36+
if (needsTabIndex) {
37+
tooltip?.reference?.removeAttribute('tabindex');
38+
}
39+
clearInterval(interval);
40+
tooltip?.destroy();
41+
}
42+
43+
/**
44+
*
45+
* `Tooltip` implements a modifier that uses Tippy.js to display a tooltip.
46+
*
47+
* Sample usage:
48+
* ```
49+
* <div {{hds-tooltip 'Text' options=(hash )}}>Hover me!</div>
50+
* ```
51+
*
52+
* @see https://atomiks.github.io/tippyjs
53+
* @class TooltipModifier
54+
*
55+
*/
56+
export default class HdsTooltipModifier extends Modifier<HdsTooltipModifierSignature> {
57+
didSetup = false;
58+
interval: number | undefined = undefined;
59+
needsTabIndex = false;
60+
tooltip: TippyInstance | undefined = undefined;
61+
62+
constructor(owner: unknown, args: ArgsFor<HdsTooltipModifierSignature>) {
63+
super(owner, args);
64+
registerDestructor(this, cleanup);
65+
}
66+
67+
hideOnEsc = {
68+
name: 'hideOnEsc',
69+
defaultValue: true,
70+
fn({ hide }: { hide: TippyHideAll }) {
71+
function onKeyDown(event: KeyboardEvent): void {
72+
if (event.key === 'Escape') {
73+
hide();
74+
}
75+
}
76+
77+
return {
78+
onShow() {
79+
document.addEventListener('keydown', onKeyDown);
80+
},
81+
onHide() {
82+
document.removeEventListener('keydown', onKeyDown);
83+
},
84+
};
85+
},
86+
};
87+
88+
modify(
89+
element: HdsTooltipModifierSignature['Element'],
90+
positional: HdsTooltipModifierSignature['Args']['Positional'],
91+
named: HdsTooltipModifierSignature['Args']['Named']
92+
): void {
93+
assert('Tooltip must have an element', element);
94+
95+
if (!this.didSetup) {
96+
this.#setup(element, positional, named);
97+
this.didSetup = true;
98+
}
99+
100+
this.#update(element, positional, named);
101+
}
102+
103+
#setup(
104+
element: HdsTooltipModifierSignature['Element'],
105+
positional: HdsTooltipModifierSignature['Args']['Positional'],
106+
named: HdsTooltipModifierSignature['Args']['Named']
107+
): void {
108+
const tooltipProps = this.#getTooltipProps(element, positional, named);
109+
this.tooltip = tippy(element, tooltipProps);
110+
}
111+
112+
#update(
113+
element: HdsTooltipModifierSignature['Element'],
114+
positional: HdsTooltipModifierSignature['Args']['Positional'],
115+
named: HdsTooltipModifierSignature['Args']['Named']
116+
): void {
117+
const tooltipProps = this.#getTooltipProps(element, positional, named);
118+
this.tooltip?.setProps(tooltipProps);
119+
}
120+
121+
#getTooltipProps(
122+
element: HdsTooltipModifierSignature['Element'],
123+
positional: HdsTooltipModifierSignature['Args']['Positional'],
124+
named: HdsTooltipModifierSignature['Args']['Named']
125+
): Partial<TippyProps> {
126+
const { options } = named;
127+
let [content] = positional;
128+
129+
let $anchor: HTMLElement | null = element; // Ensure $anchor can be null
130+
131+
if (typeof options?.triggerTarget === 'string') {
132+
const $el = $anchor;
133+
if (options.triggerTarget === 'parentNode') {
134+
if ($anchor.parentNode instanceof HTMLElement) {
135+
// Type guard
136+
$anchor = $anchor.parentNode;
137+
}
138+
} else {
139+
const queryResult: HTMLElement | null = $anchor.querySelector(
140+
options.triggerTarget
141+
);
142+
if (queryResult) {
143+
$anchor = queryResult;
144+
}
145+
}
146+
if ($anchor instanceof HTMLElement) {
147+
// Ensure $anchor is an HTMLElement before cloning
148+
const clonedElement = $anchor.cloneNode(true) as HTMLElement; // Explicitly cast cloned node to HTMLElement
149+
content = clonedElement.outerHTML; // Now safely access outerHTML
150+
}
151+
$el?.remove(); // Use optional chaining in case $el is null
152+
options.triggerTarget = null;
153+
}
154+
155+
// The {{hds-tooltip}} will just use the HTML content.
156+
if (typeof content === 'undefined' && $anchor instanceof HTMLElement) {
157+
// Ensure $anchor is an HTMLElement before accessing innerHTML
158+
content = $anchor.innerHTML;
159+
$anchor.innerHTML = '';
160+
}
161+
162+
if (options?.trigger === 'manual') {
163+
// If we are manually triggering, a out delay means only show for the
164+
// amount of time specified by the delay.
165+
const delay = options.delay || [];
166+
167+
if (Array.isArray(delay) && delay.length) {
168+
if (typeof delay[1] !== 'undefined') {
169+
options.onShown = (tooltip) => {
170+
clearInterval(this.interval);
171+
this.interval = setTimeout(() => {
172+
tooltip.hide();
173+
}, delay[1] ?? 0);
174+
};
175+
}
176+
}
177+
}
178+
179+
const $trigger = $anchor;
180+
181+
if (!$trigger.hasAttribute('tabindex')) {
182+
this.needsTabIndex = true;
183+
$trigger.setAttribute('tabindex', '0');
184+
}
185+
186+
/* Typescript does not like the previous approach of adding an undefined value
187+
** to the array and then filtering it out.
188+
*/
189+
const plugins =
190+
options?.followCursor !== undefined
191+
? [this.hideOnEsc, followCursor]
192+
: [this.hideOnEsc];
193+
194+
return {
195+
theme: 'hds',
196+
triggerTarget: $trigger,
197+
arrow: `
198+
<svg class="hds-tooltip-pointer" width="16" height="7" viewBox="0 0 16 7" xmlns="http://www.w3.org/2000/svg">
199+
<path d="M0 7H16L9.11989 0.444571C8.49776 -0.148191 7.50224 -0.148191 6.88011 0.444572L0 7Z" />
200+
</svg>`,
201+
// keeps tooltip itself open on hover:
202+
interactive: true,
203+
// fix accessibility features that get messed up with setting interactive: true
204+
aria: {
205+
content: 'describedby',
206+
expanded: false,
207+
},
208+
content: () => content,
209+
plugins,
210+
...options,
211+
};
212+
}
213+
}

0 commit comments

Comments
 (0)