diff --git a/packages/component-base/src/tooltip-controller.d.ts b/packages/component-base/src/tooltip-controller.d.ts index 2e64c69a642..f5fc81b40b2 100644 --- a/packages/component-base/src/tooltip-controller.d.ts +++ b/packages/component-base/src/tooltip-controller.d.ts @@ -42,12 +42,6 @@ export class TooltipController extends SlotController { */ manual: boolean; - /** - * When true, the tooltip is opened programmatically. - * Only works if `manual` is set to `true`. - */ - opened: boolean; - /** * Position of the tooltip with respect to its target. */ @@ -74,11 +68,6 @@ export class TooltipController extends SlotController { */ setManual(manual: boolean): void; - /** - * Toggle opened state on the slotted tooltip. - */ - setOpened(opened: boolean): void; - /** * Set default position for the slotted tooltip. * This can be overridden by setting the position @@ -96,4 +85,18 @@ export class TooltipController extends SlotController { * Set an HTML element to attach the tooltip to. */ setTarget(target: HTMLElement): void; + + /** + * Schedule opening the slotted tooltip. Respects the tooltip's + * configured `hoverDelay` / `focusDelay` and the shared warm-up state. + * No-op when no tooltip is slotted. + */ + open(options?: { hover?: boolean; focus?: boolean; immediate?: boolean }): void; + + /** + * Schedule closing the slotted tooltip. Respects the tooltip's + * configured `hideDelay` unless `immediate` is true. + * No-op when no tooltip is slotted. + */ + close(immediate?: boolean): void; } diff --git a/packages/component-base/src/tooltip-controller.js b/packages/component-base/src/tooltip-controller.js index 78369425927..a45822b2e20 100644 --- a/packages/component-base/src/tooltip-controller.js +++ b/packages/component-base/src/tooltip-controller.js @@ -39,10 +39,6 @@ export class TooltipController extends SlotController { tooltipNode.manual = this.manual; } - if (this.opened !== undefined) { - tooltipNode.opened = this.opened; - } - if (this.position !== undefined) { tooltipNode._position = this.position; } @@ -113,19 +109,6 @@ export class TooltipController extends SlotController { } } - /** - * Toggle opened state on the slotted tooltip. - * @param {boolean} opened - */ - setOpened(opened) { - this.opened = opened; - - const tooltipNode = this.node; - if (tooltipNode) { - tooltipNode.opened = opened; - } - } - /** * Set default position for the slotted tooltip. * This can be overridden by setting the position @@ -168,6 +151,34 @@ export class TooltipController extends SlotController { } } + /** + * Schedule opening the slotted tooltip. Respects the tooltip's + * configured `hoverDelay` / `focusDelay` and the shared warm-up state. + * No-op when no tooltip is slotted. + * + * @param {{ hover?: boolean, focus?: boolean, immediate?: boolean }} [options] + */ + open(options) { + const tooltipNode = this.node; + if (tooltipNode && tooltipNode.isConnected) { + tooltipNode._stateController.open(options); + } + } + + /** + * Schedule closing the slotted tooltip. Respects the tooltip's + * configured `hideDelay` unless `immediate` is true. + * No-op when no tooltip is slotted. + * + * @param {boolean} [immediate] + */ + close(immediate) { + const tooltipNode = this.node; + if (tooltipNode) { + tooltipNode._stateController.close(immediate); + } + } + /** @private */ __onContentChange(event) { this.__notifyChange(event.target); diff --git a/packages/component-base/test/tooltip-controller.test.js b/packages/component-base/test/tooltip-controller.test.js index 762f73cd42b..3452d2c8419 100644 --- a/packages/component-base/test/tooltip-controller.test.js +++ b/packages/component-base/test/tooltip-controller.test.js @@ -55,14 +55,6 @@ describe('TooltipController', () => { expect(tooltip.manual).to.be.false; }); - it('should update tooltip opened using controller setOpened method', () => { - controller.setOpened(true); - expect(tooltip.opened).to.be.true; - - controller.setOpened(false); - expect(tooltip.opened).to.be.false; - }); - it('should update tooltip shouldShow using controller shouldShow method', () => { const shouldShow = () => true; controller.setShouldShow(shouldShow); @@ -85,6 +77,25 @@ describe('TooltipController', () => { controller.setAriaTarget(input); expect(tooltip.ariaTarget).to.equal(input); }); + + it('should delegate open to the tooltip state controller', () => { + tooltip._stateController = { open: sinon.spy(), close: sinon.spy() }; + controller.open({ hover: true }); + expect(tooltip._stateController.open).to.be.calledOnceWith({ hover: true }); + }); + + it('should not open the tooltip state controller when the tooltip is disconnected', () => { + tooltip._stateController = { open: sinon.spy(), close: sinon.spy() }; + tooltip.remove(); + controller.open({ hover: true }); + expect(tooltip._stateController.open).to.be.not.called; + }); + + it('should delegate close to the tooltip state controller', () => { + tooltip._stateController = { open: sinon.spy(), close: sinon.spy() }; + controller.close(true); + expect(tooltip._stateController.close).to.be.calledOnceWith(true); + }); }); describe('lazy tooltip', () => { @@ -139,18 +150,6 @@ describe('TooltipController', () => { expect(tooltip.manual).to.be.false; }); - it('should update lazy tooltip opened using controller setOpened method', async () => { - controller.setOpened(true); - - host.appendChild(tooltip); - await nextFrame(); - - expect(tooltip.opened).to.be.true; - - controller.setOpened(false); - expect(tooltip.opened).to.be.false; - }); - it('should update lazy tooltip shouldShow using controller shouldShow method', async () => { const shouldShow = () => true; controller.setShouldShow(shouldShow); @@ -238,5 +237,13 @@ describe('TooltipController', () => { await nextFrame(); expect(host.hasAttribute('has-tooltip')).to.be.false; }); + + it('should not throw when calling open before a tooltip is slotted', () => { + expect(() => controller.open({ hover: true })).to.not.throw(); + }); + + it('should not throw when calling close before a tooltip is slotted', () => { + expect(() => controller.close(true)).to.not.throw(); + }); }); }); diff --git a/packages/grid/src/vaadin-grid-mixin.js b/packages/grid/src/vaadin-grid-mixin.js index 47a5298fab6..7b820ce7309 100644 --- a/packages/grid/src/vaadin-grid-mixin.js +++ b/packages/grid/src/vaadin-grid-mixin.js @@ -882,7 +882,7 @@ export const GridMixin = (superClass) => this._tooltipController.setContext(this.getEventContext(event)); // Trigger opening using the corresponding delay. - tooltip._stateController.open({ + this._tooltipController.open({ focus: event.type === 'focusin', hover: event.type === 'mouseenter', }); @@ -918,10 +918,7 @@ export const GridMixin = (superClass) => /** @protected */ _hideTooltip(immediate) { - const tooltip = this._tooltipController && this._tooltipController.node; - if (tooltip) { - tooltip._stateController.close(immediate); - } + this._tooltipController.close(immediate); } /** diff --git a/packages/menu-bar/src/vaadin-menu-bar-mixin.js b/packages/menu-bar/src/vaadin-menu-bar-mixin.js index c2c09e21abc..07eedf318df 100644 --- a/packages/menu-bar/src/vaadin-menu-bar-mixin.js +++ b/packages/menu-bar/src/vaadin-menu-bar-mixin.js @@ -15,6 +15,7 @@ import { Debouncer } from '@vaadin/component-base/src/debounce.js'; import { I18nMixin } from '@vaadin/component-base/src/i18n-mixin.js'; import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js'; import { SlotController } from '@vaadin/component-base/src/slot-controller.js'; +import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js'; /** * Custom Lit directive for rendering item components @@ -310,12 +311,16 @@ export const MenuBarMixin = (superClass) => }, }); - this.addController(this._subMenuController); - this.addController(this._overflowController); + this._tooltipController = new TooltipController(this); + this._tooltipController.setManual(true); this.addEventListener('mousedown', () => this._hideTooltip(true)); this.addEventListener('mouseleave', () => this._hideTooltip()); + this.addController(this._tooltipController); + this.addController(this._subMenuController); + this.addController(this._overflowController); + this._container = this.shadowRoot.querySelector('[part="container"]'); } @@ -662,7 +667,7 @@ export const MenuBarMixin = (superClass) => this._tooltipController.setContext({ item: button.item }); // Trigger opening using the corresponding delay. - tooltip._stateController.open({ + this._tooltipController.open({ hover: isHover, focus: !isHover, }); @@ -672,11 +677,8 @@ export const MenuBarMixin = (superClass) => /** @protected */ _hideTooltip(immediate) { - const tooltip = this._tooltipController && this._tooltipController.node; - if (tooltip) { - this._tooltipController.setContext({ item: null }); - tooltip._stateController.close(immediate); - } + this._tooltipController.setContext({ item: null }); + this._tooltipController.close(immediate); } /** @private */ diff --git a/packages/menu-bar/src/vaadin-menu-bar.js b/packages/menu-bar/src/vaadin-menu-bar.js index b0ce7fc53cd..d6f486eba9f 100644 --- a/packages/menu-bar/src/vaadin-menu-bar.js +++ b/packages/menu-bar/src/vaadin-menu-bar.js @@ -9,7 +9,6 @@ import { html, LitElement } from 'lit'; import { defineCustomElement } from '@vaadin/component-base/src/define.js'; import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js'; import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js'; -import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js'; import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js'; import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; import { menuBarStyles } from './styles/vaadin-menu-bar-base-styles.js'; @@ -97,15 +96,6 @@ class MenuBar extends MenuBarMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoI `; } - /** @protected */ - ready() { - super.ready(); - - this._tooltipController = new TooltipController(this); - this._tooltipController.setManual(true); - this.addController(this._tooltipController); - } - /** * Fired when either a submenu item or menu bar button without nested children is clicked. * diff --git a/test/integration/grid-tooltip.test.js b/test/integration/grid-tooltip.test.js index 5c589b2a864..07389735a83 100644 --- a/test/integration/grid-tooltip.test.js +++ b/test/integration/grid-tooltip.test.js @@ -249,18 +249,6 @@ describe('tooltip', () => { expect(spy.calledOnce).to.be.false; }); - - it('should not set tooltip opened if there is no tooltip', async () => { - const spy = sinon.spy(grid._tooltipController, 'setOpened'); - - tooltip.remove(); - await nextFrame(); - - const cell = getCell(grid, 0); - mouseenter(cell); - - expect(spy.calledOnce).to.be.false; - }); }); describe('cell not fully visible', () => { diff --git a/test/integration/menu-bar-tooltip.test.js b/test/integration/menu-bar-tooltip.test.js index 226d05f4ebc..f56c7caf4a8 100644 --- a/test/integration/menu-bar-tooltip.test.js +++ b/test/integration/menu-bar-tooltip.test.js @@ -194,7 +194,6 @@ describe('menu-bar with tooltip', () => { it('should not set tooltip properties if there is no tooltip', async () => { const spyTarget = sinon.spy(menuBar._tooltipController, 'setTarget'); const spyContent = sinon.spy(menuBar._tooltipController, 'setContext'); - const spyOpened = sinon.spy(menuBar._tooltipController, 'setOpened'); tooltip.remove(); await nextRender(); @@ -203,7 +202,6 @@ describe('menu-bar with tooltip', () => { expect(spyTarget.called).to.be.false; expect(spyContent.called).to.be.false; - expect(spyOpened.called).to.be.false; }); it('should not override a custom generator', () => {