Skip to content

Commit e89175c

Browse files
committed
refactor: Tooltip event handlers
* Don't create separate event handlers per tooltip instance; move the invoke logic inside the controller. * Move the triggers parsing inside the controller as well. * "Cache" the animation objects since they do not change between calls. * Run the opening animation on initial rendering when `open` is set. * JSDoc improvements.
1 parent 838cfbc commit e89175c

File tree

6 files changed

+243
-148
lines changed

6 files changed

+243
-148
lines changed

src/components/popover/popover.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export default class IgcPopoverComponent extends LitElement {
6464
private dispose?: ReturnType<typeof autoUpdate>;
6565
private target?: Element;
6666

67-
@query('#container', true)
67+
@query('#container')
6868
private _container!: HTMLElement;
6969

7070
@queryAssignedElements({ slot: 'anchor', flatten: true })
Lines changed: 131 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,173 @@
11
import type { ReactiveController } from 'lit';
2+
import service from './tooltip-service.js';
23
import type IgcTooltipComponent from './tooltip.js';
34

45
type TooltipAnchor = Element | null | undefined;
5-
type TooltipTriggers = {
6-
show: string[];
7-
hide: string[];
6+
7+
type TooltipCallbacks = {
8+
onShow: (event?: Event) => unknown;
9+
onHide: (event?: Event) => unknown;
810
};
911

1012
class TooltipController implements ReactiveController {
1113
private readonly _tooltip: IgcTooltipComponent;
12-
private _showTriggers: string[] = [];
13-
private _hideTriggers: string[] = [];
14+
private _anchor: TooltipAnchor;
1415

15-
constructor(tooltip: IgcTooltipComponent) {
16-
this._tooltip = tooltip;
17-
this._tooltip.addController(this);
16+
private _options!: TooltipCallbacks;
17+
private _showTriggers = new Set(['pointerenter']);
18+
private _hideTriggers = new Set(['pointerleave']);
19+
20+
/**
21+
* Returns the current tooltip anchor target if any.
22+
*/
23+
public get anchor(): TooltipAnchor {
24+
return this._anchor;
1825
}
1926

2027
/**
21-
* Sets the current collections of show/hide triggers on the given anchor for the tooltip.
22-
* Removes any previously set triggers.
28+
* Removes all triggers from the previous `anchor` target and rebinds the current
29+
* sets back to the new value if it exists.
2330
*/
24-
public set(anchor: TooltipAnchor, triggers: TooltipTriggers): void {
25-
if (!anchor) {
26-
return;
31+
public set anchor(value: TooltipAnchor) {
32+
this._dispose();
33+
this._anchor = value;
34+
35+
for (const each of this._showTriggers) {
36+
this._anchor?.addEventListener(each, this);
2737
}
2838

29-
const { show, hide } = triggers;
30-
this._showTriggers = show;
31-
this._hideTriggers = hide;
39+
for (const each of this._hideTriggers) {
40+
this._anchor?.addEventListener(each, this);
41+
}
42+
}
3243

33-
this.remove(anchor);
44+
/**
45+
* Returns the current set of hide triggers as a comma-separated string.
46+
*/
47+
public get hideTriggers(): string {
48+
return Array.from(this._hideTriggers).join();
49+
}
3450

35-
for (const trigger of show) {
36-
anchor.addEventListener(trigger, this._tooltip[showOnTrigger]);
51+
/**
52+
* Sets a new set of hide triggers from a comma-separated string.
53+
*
54+
* @remarks
55+
* If the tooltip already has an `anchor` bound it will remove the old
56+
* set of triggers from it and rebind it with the new one.
57+
*/
58+
public set hideTriggers(value: string) {
59+
const triggers = parseTriggers(value);
60+
61+
if (this._anchor) {
62+
this._toggleTriggers(this._hideTriggers, triggers);
3763
}
3864

39-
for (const trigger of hide) {
40-
anchor.addEventListener(trigger, this._tooltip[hideOnTrigger]);
65+
this._hideTriggers = triggers;
66+
}
67+
68+
/**
69+
* Returns the current set of show triggers as a comma-separated string.
70+
*/
71+
public get showTriggers(): string {
72+
return Array.from(this._showTriggers).join();
73+
}
74+
75+
/**
76+
* Sets a new set of show triggers from a comma-separated string.
77+
*
78+
* @remarks
79+
* If the tooltip already has an `anchor` bound it will remove the old
80+
* set of triggers from it and rebind it with the new one.
81+
*/
82+
public set showTriggers(value: string) {
83+
const triggers = parseTriggers(value);
84+
85+
if (this._anchor) {
86+
this._toggleTriggers(this._showTriggers, triggers);
4187
}
88+
89+
this._showTriggers = triggers;
90+
}
91+
92+
constructor(tooltip: IgcTooltipComponent, options: TooltipCallbacks) {
93+
this._tooltip = tooltip;
94+
this._options = options;
95+
this._tooltip.addController(this);
4296
}
4397

44-
/** Removes all tooltip trigger events from the given anchor */
45-
public remove(anchor?: TooltipAnchor): void {
46-
if (!anchor) {
47-
return;
98+
private _toggleTriggers(previous: Set<string>, current: Set<string>): void {
99+
for (const each of previous) {
100+
this._anchor?.removeEventListener(each, this);
101+
}
102+
103+
for (const each of current) {
104+
this._anchor?.addEventListener(each, this);
48105
}
106+
}
49107

50-
for (const trigger of this._showTriggers) {
51-
anchor.removeEventListener(trigger, this._tooltip[showOnTrigger]);
108+
private _dispose(): void {
109+
for (const each of this._showTriggers) {
110+
this._anchor?.removeEventListener(each, this);
52111
}
53112

54-
for (const trigger of this._hideTriggers) {
55-
anchor.removeEventListener(trigger, this._tooltip[hideOnTrigger]);
113+
for (const each of this._hideTriggers) {
114+
this._anchor?.removeEventListener(each, this);
56115
}
116+
117+
this._anchor = null;
57118
}
58119

59120
/** @internal */
60121
public hostConnected(): void {
61-
this._tooltip.addEventListener(
62-
'pointerenter',
63-
this._tooltip[showOnTrigger]
64-
);
65-
this._tooltip.addEventListener(
66-
'pointerleave',
67-
this._tooltip[hideOnTrigger]
68-
);
122+
this._tooltip.addEventListener('pointerenter', this);
123+
this._tooltip.addEventListener('pointerleave', this);
69124
}
70125

71126
/** @internal */
72127
public hostDisconnected(): void {
73-
this._tooltip.removeEventListener(
74-
'pointerenter',
75-
this._tooltip[showOnTrigger]
76-
);
77-
this._tooltip.removeEventListener(
78-
'pointerleave',
79-
this._tooltip[hideOnTrigger]
80-
);
128+
// console.log('disconnected callback');
129+
this._tooltip.hide();
130+
service.remove(this._tooltip);
131+
this._tooltip.removeEventListener('pointerenter', this);
132+
this._tooltip.removeEventListener('pointerleave', this);
133+
}
134+
135+
/** @internal */
136+
public handleEvent(event: Event): void {
137+
// Tooltip handlers
138+
if (event.target === this._tooltip) {
139+
switch (event.type) {
140+
case 'pointerenter':
141+
this._options.onShow.call(this._tooltip, event);
142+
break;
143+
case 'pointerleave':
144+
this._options.onHide.call(this._tooltip, event);
145+
break;
146+
default:
147+
return;
148+
}
149+
}
150+
151+
// Anchor handlers
152+
if (event.target === this._anchor) {
153+
if (this._showTriggers.has(event.type)) {
154+
this._options.onShow.call(this._tooltip, event);
155+
}
156+
157+
if (this._hideTriggers.has(event.type)) {
158+
this._options.onHide.call(this._tooltip, event);
159+
}
160+
}
81161
}
82162
}
83163

84-
export const showOnTrigger = Symbol();
85-
export const hideOnTrigger = Symbol();
164+
function parseTriggers(string: string): Set<string> {
165+
return new Set((string ?? '').split(',').map((part) => part.trim()));
166+
}
86167

87168
export function addTooltipController(
88-
host: IgcTooltipComponent
169+
host: IgcTooltipComponent,
170+
options: TooltipCallbacks
89171
): TooltipController {
90-
return new TooltipController(host);
172+
return new TooltipController(host, options);
91173
}

src/components/tooltip/tooltip-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { escapeKey } from '../common/controllers/key-bindings.js';
33
import { isEmpty, last } from '../common/util.js';
44
import type IgcTooltipComponent from './tooltip.js';
55

6-
type TooltipHideCallback = () => unknown | Promise<unknown>;
6+
type TooltipHideCallback = () => unknown;
77

88
class TooltipEscapeCallbacks {
99
private _collection = new Map<IgcTooltipComponent, TooltipHideCallback>();

src/components/tooltip/tooltip.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ describe('Tooltip', () => {
284284
await elementUpdated(tooltip);
285285

286286
expect(tooltip).dom.to.equal(
287-
'<igc-tooltip>It works!</igc-tooltip>',
287+
'<igc-tooltip sticky>It works!</igc-tooltip>',
288288
DIFF_OPTIONS
289289
);
290290
expect(tooltip).shadowDom.to.equal(

0 commit comments

Comments
 (0)