Skip to content

Commit 46fad44

Browse files
vursenclaude
andauthored
refactor: expose tooltip open/close via TooltipController (#11568)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 64de616 commit 46fad44

8 files changed

Lines changed: 81 additions & 85 deletions

File tree

packages/component-base/src/tooltip-controller.d.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,6 @@ export class TooltipController extends SlotController {
4242
*/
4343
manual: boolean;
4444

45-
/**
46-
* When true, the tooltip is opened programmatically.
47-
* Only works if `manual` is set to `true`.
48-
*/
49-
opened: boolean;
50-
5145
/**
5246
* Position of the tooltip with respect to its target.
5347
*/
@@ -74,11 +68,6 @@ export class TooltipController extends SlotController {
7468
*/
7569
setManual(manual: boolean): void;
7670

77-
/**
78-
* Toggle opened state on the slotted tooltip.
79-
*/
80-
setOpened(opened: boolean): void;
81-
8271
/**
8372
* Set default position for the slotted tooltip.
8473
* This can be overridden by setting the position
@@ -96,4 +85,18 @@ export class TooltipController extends SlotController {
9685
* Set an HTML element to attach the tooltip to.
9786
*/
9887
setTarget(target: HTMLElement): void;
88+
89+
/**
90+
* Schedule opening the slotted tooltip. Respects the tooltip's
91+
* configured `hoverDelay` / `focusDelay` and the shared warm-up state.
92+
* No-op when no tooltip is slotted.
93+
*/
94+
open(options?: { hover?: boolean; focus?: boolean; immediate?: boolean }): void;
95+
96+
/**
97+
* Schedule closing the slotted tooltip. Respects the tooltip's
98+
* configured `hideDelay` unless `immediate` is true.
99+
* No-op when no tooltip is slotted.
100+
*/
101+
close(immediate?: boolean): void;
99102
}

packages/component-base/src/tooltip-controller.js

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ export class TooltipController extends SlotController {
3939
tooltipNode.manual = this.manual;
4040
}
4141

42-
if (this.opened !== undefined) {
43-
tooltipNode.opened = this.opened;
44-
}
45-
4642
if (this.position !== undefined) {
4743
tooltipNode._position = this.position;
4844
}
@@ -113,19 +109,6 @@ export class TooltipController extends SlotController {
113109
}
114110
}
115111

116-
/**
117-
* Toggle opened state on the slotted tooltip.
118-
* @param {boolean} opened
119-
*/
120-
setOpened(opened) {
121-
this.opened = opened;
122-
123-
const tooltipNode = this.node;
124-
if (tooltipNode) {
125-
tooltipNode.opened = opened;
126-
}
127-
}
128-
129112
/**
130113
* Set default position for the slotted tooltip.
131114
* This can be overridden by setting the position
@@ -168,6 +151,34 @@ export class TooltipController extends SlotController {
168151
}
169152
}
170153

154+
/**
155+
* Schedule opening the slotted tooltip. Respects the tooltip's
156+
* configured `hoverDelay` / `focusDelay` and the shared warm-up state.
157+
* No-op when no tooltip is slotted.
158+
*
159+
* @param {{ hover?: boolean, focus?: boolean, immediate?: boolean }} [options]
160+
*/
161+
open(options) {
162+
const tooltipNode = this.node;
163+
if (tooltipNode && tooltipNode.isConnected) {
164+
tooltipNode._stateController.open(options);
165+
}
166+
}
167+
168+
/**
169+
* Schedule closing the slotted tooltip. Respects the tooltip's
170+
* configured `hideDelay` unless `immediate` is true.
171+
* No-op when no tooltip is slotted.
172+
*
173+
* @param {boolean} [immediate]
174+
*/
175+
close(immediate) {
176+
const tooltipNode = this.node;
177+
if (tooltipNode) {
178+
tooltipNode._stateController.close(immediate);
179+
}
180+
}
181+
171182
/** @private */
172183
__onContentChange(event) {
173184
this.__notifyChange(event.target);

packages/component-base/test/tooltip-controller.test.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,6 @@ describe('TooltipController', () => {
5555
expect(tooltip.manual).to.be.false;
5656
});
5757

58-
it('should update tooltip opened using controller setOpened method', () => {
59-
controller.setOpened(true);
60-
expect(tooltip.opened).to.be.true;
61-
62-
controller.setOpened(false);
63-
expect(tooltip.opened).to.be.false;
64-
});
65-
6658
it('should update tooltip shouldShow using controller shouldShow method', () => {
6759
const shouldShow = () => true;
6860
controller.setShouldShow(shouldShow);
@@ -85,6 +77,25 @@ describe('TooltipController', () => {
8577
controller.setAriaTarget(input);
8678
expect(tooltip.ariaTarget).to.equal(input);
8779
});
80+
81+
it('should delegate open to the tooltip state controller', () => {
82+
tooltip._stateController = { open: sinon.spy(), close: sinon.spy() };
83+
controller.open({ hover: true });
84+
expect(tooltip._stateController.open).to.be.calledOnceWith({ hover: true });
85+
});
86+
87+
it('should not open the tooltip state controller when the tooltip is disconnected', () => {
88+
tooltip._stateController = { open: sinon.spy(), close: sinon.spy() };
89+
tooltip.remove();
90+
controller.open({ hover: true });
91+
expect(tooltip._stateController.open).to.be.not.called;
92+
});
93+
94+
it('should delegate close to the tooltip state controller', () => {
95+
tooltip._stateController = { open: sinon.spy(), close: sinon.spy() };
96+
controller.close(true);
97+
expect(tooltip._stateController.close).to.be.calledOnceWith(true);
98+
});
8899
});
89100

90101
describe('lazy tooltip', () => {
@@ -139,18 +150,6 @@ describe('TooltipController', () => {
139150
expect(tooltip.manual).to.be.false;
140151
});
141152

142-
it('should update lazy tooltip opened using controller setOpened method', async () => {
143-
controller.setOpened(true);
144-
145-
host.appendChild(tooltip);
146-
await nextFrame();
147-
148-
expect(tooltip.opened).to.be.true;
149-
150-
controller.setOpened(false);
151-
expect(tooltip.opened).to.be.false;
152-
});
153-
154153
it('should update lazy tooltip shouldShow using controller shouldShow method', async () => {
155154
const shouldShow = () => true;
156155
controller.setShouldShow(shouldShow);
@@ -238,5 +237,13 @@ describe('TooltipController', () => {
238237
await nextFrame();
239238
expect(host.hasAttribute('has-tooltip')).to.be.false;
240239
});
240+
241+
it('should not throw when calling open before a tooltip is slotted', () => {
242+
expect(() => controller.open({ hover: true })).to.not.throw();
243+
});
244+
245+
it('should not throw when calling close before a tooltip is slotted', () => {
246+
expect(() => controller.close(true)).to.not.throw();
247+
});
241248
});
242249
});

packages/grid/src/vaadin-grid-mixin.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,7 @@ export const GridMixin = (superClass) =>
882882
this._tooltipController.setContext(this.getEventContext(event));
883883

884884
// Trigger opening using the corresponding delay.
885-
tooltip._stateController.open({
885+
this._tooltipController.open({
886886
focus: event.type === 'focusin',
887887
hover: event.type === 'mouseenter',
888888
});
@@ -918,10 +918,7 @@ export const GridMixin = (superClass) =>
918918

919919
/** @protected */
920920
_hideTooltip(immediate) {
921-
const tooltip = this._tooltipController && this._tooltipController.node;
922-
if (tooltip) {
923-
tooltip._stateController.close(immediate);
924-
}
921+
this._tooltipController.close(immediate);
925922
}
926923

927924
/**

packages/menu-bar/src/vaadin-menu-bar-mixin.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Debouncer } from '@vaadin/component-base/src/debounce.js';
1515
import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js';
1616
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
1717
import { SlotController } from '@vaadin/component-base/src/slot-controller.js';
18+
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
1819

1920
/**
2021
* Custom Lit directive for rendering item components
@@ -310,12 +311,16 @@ export const MenuBarMixin = (superClass) =>
310311
},
311312
});
312313

313-
this.addController(this._subMenuController);
314-
this.addController(this._overflowController);
314+
this._tooltipController = new TooltipController(this);
315+
this._tooltipController.setManual(true);
315316

316317
this.addEventListener('mousedown', () => this._hideTooltip(true));
317318
this.addEventListener('mouseleave', () => this._hideTooltip());
318319

320+
this.addController(this._tooltipController);
321+
this.addController(this._subMenuController);
322+
this.addController(this._overflowController);
323+
319324
this._container = this.shadowRoot.querySelector('[part="container"]');
320325
}
321326

@@ -662,7 +667,7 @@ export const MenuBarMixin = (superClass) =>
662667
this._tooltipController.setContext({ item: button.item });
663668

664669
// Trigger opening using the corresponding delay.
665-
tooltip._stateController.open({
670+
this._tooltipController.open({
666671
hover: isHover,
667672
focus: !isHover,
668673
});
@@ -672,11 +677,8 @@ export const MenuBarMixin = (superClass) =>
672677

673678
/** @protected */
674679
_hideTooltip(immediate) {
675-
const tooltip = this._tooltipController && this._tooltipController.node;
676-
if (tooltip) {
677-
this._tooltipController.setContext({ item: null });
678-
tooltip._stateController.close(immediate);
679-
}
680+
this._tooltipController.setContext({ item: null });
681+
this._tooltipController.close(immediate);
680682
}
681683

682684
/** @private */

packages/menu-bar/src/vaadin-menu-bar.js

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { html, LitElement } from 'lit';
99
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
1010
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
1111
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
12-
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
1312
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
1413
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
1514
import { menuBarStyles } from './styles/vaadin-menu-bar-base-styles.js';
@@ -97,15 +96,6 @@ class MenuBar extends MenuBarMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoI
9796
`;
9897
}
9998

100-
/** @protected */
101-
ready() {
102-
super.ready();
103-
104-
this._tooltipController = new TooltipController(this);
105-
this._tooltipController.setManual(true);
106-
this.addController(this._tooltipController);
107-
}
108-
10999
/**
110100
* Fired when either a submenu item or menu bar button without nested children is clicked.
111101
*

test/integration/grid-tooltip.test.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -249,18 +249,6 @@ describe('tooltip', () => {
249249

250250
expect(spy.calledOnce).to.be.false;
251251
});
252-
253-
it('should not set tooltip opened if there is no tooltip', async () => {
254-
const spy = sinon.spy(grid._tooltipController, 'setOpened');
255-
256-
tooltip.remove();
257-
await nextFrame();
258-
259-
const cell = getCell(grid, 0);
260-
mouseenter(cell);
261-
262-
expect(spy.calledOnce).to.be.false;
263-
});
264252
});
265253

266254
describe('cell not fully visible', () => {

test/integration/menu-bar-tooltip.test.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ describe('menu-bar with tooltip', () => {
194194
it('should not set tooltip properties if there is no tooltip', async () => {
195195
const spyTarget = sinon.spy(menuBar._tooltipController, 'setTarget');
196196
const spyContent = sinon.spy(menuBar._tooltipController, 'setContext');
197-
const spyOpened = sinon.spy(menuBar._tooltipController, 'setOpened');
198197

199198
tooltip.remove();
200199
await nextRender();
@@ -203,7 +202,6 @@ describe('menu-bar with tooltip', () => {
203202

204203
expect(spyTarget.called).to.be.false;
205204
expect(spyContent.called).to.be.false;
206-
expect(spyOpened.called).to.be.false;
207205
});
208206

209207
it('should not override a custom generator', () => {

0 commit comments

Comments
 (0)