Skip to content

Commit

Permalink
Merge pull request #12572 from ckeditor/ck/12492-link-actions-view-to…
Browse files Browse the repository at this point in the history
…oltip

Fix (ui): Tooltip should hide when an element the tooltip is attached to also hides. Closes #12492.
  • Loading branch information
mmotyczynska committed Oct 14, 2022
2 parents a157947 + 57fd951 commit ed55863
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 4 deletions.
14 changes: 13 additions & 1 deletion packages/ckeditor5-ui/_src/tooltipmanager.js
Expand Up @@ -11,7 +11,7 @@ import View from './view';
import BalloonPanelView, { generatePositions } from './panel/balloon/balloonpanelview';

import DomEmitterMixin from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
import { global, isVisible, mix, first } from '@ckeditor/ckeditor5-utils';
import { global, isVisible, mix, first, ResizeObserver } from '@ckeditor/ckeditor5-utils';
import { isElement, debounce } from 'lodash-es';

import '../theme/components/tooltip/tooltip.css';
Expand Down Expand Up @@ -299,6 +299,14 @@ export default class TooltipManager {
positions: TooltipManager.getPositioningFunctions( position )
} );

this._resizeObserver = new ResizeObserver( targetDomElement, () => {
// The ResizeObserver will call its callback when the target element hides and the tooltip
// should also disappear (https://github.com/ckeditor/ckeditor5/issues/12492).
if ( !isVisible( targetDomElement ) ) {
this._unpinTooltip();
}
} );

this.balloonPanelView.class = [ BALLOON_CLASS, cssClass ]
.filter( className => className )
.join( ' ' );
Expand Down Expand Up @@ -330,6 +338,10 @@ export default class TooltipManager {

this._currentElementWithTooltip = null;
this._currentTooltipPosition = null;

if ( this._resizeObserver ) {
this._resizeObserver.destroy();
}
}

/**
Expand Down
27 changes: 26 additions & 1 deletion packages/ckeditor5-ui/src/tooltipmanager.ts
Expand Up @@ -11,7 +11,7 @@ import View from './view';
import BalloonPanelView, { generatePositions } from './panel/balloon/balloonpanelview';

import { Emitter as DomEmitter } from '@ckeditor/ckeditor5-utils/src/dom/emittermixin';
import { global, isVisible, first } from '@ckeditor/ckeditor5-utils';
import { global, isVisible, first, ResizeObserver } from '@ckeditor/ckeditor5-utils';
import { isElement, debounce, type DebouncedFunc } from 'lodash-es';

import '../theme/components/tooltip/tooltip.css';
Expand Down Expand Up @@ -88,6 +88,7 @@ export default class TooltipManager extends DomEmitter {

private _currentElementWithTooltip!: HTMLElement | null;
private _currentTooltipPosition!: TooltipPosition | null;
private _resizeObserver!: ResizeObserver | null;
private _pinTooltipDebounced!: DebouncedFunc<( targetDomElement: HTMLElement, data: TooltipData ) => void>;

private readonly _watchdogExcluded!: true;
Expand Down Expand Up @@ -161,6 +162,18 @@ export default class TooltipManager extends DomEmitter {
this.balloonPanelView.class = BALLOON_CLASS;
this.balloonPanelView.content.add( this.tooltipTextView );

/**
* An instance of the resize observer that keeps track on target element visibility,
* when it hides the tooltip should also disappear.
*
* {@link module:core/editor/editorconfig~EditorConfig#balloonToolbar configuration}.
*
* @protected
* @member {module:utils/dom/resizeobserver~ResizeObserver|null}
*
*/
this._resizeObserver = null;

/**
* Stores the reference to the DOM element the tooltip is attached to. `null` when there's no tooltip
* in the UI.
Expand Down Expand Up @@ -382,6 +395,14 @@ export default class TooltipManager extends DomEmitter {
positions: TooltipManager.getPositioningFunctions( position )
} );

this._resizeObserver = new ResizeObserver( targetDomElement, () => {
// The ResizeObserver will call its callback when the target element hides and the tooltip
// should also disappear (https://github.com/ckeditor/ckeditor5/issues/12492).
if ( !isVisible( targetDomElement ) ) {
this._unpinTooltip();
}
} );

this.balloonPanelView.class = [ BALLOON_CLASS, cssClass ]
.filter( className => className )
.join( ' ' );
Expand Down Expand Up @@ -413,6 +434,10 @@ export default class TooltipManager extends DomEmitter {

this._currentElementWithTooltip = null;
this._currentTooltipPosition = null;

if ( this._resizeObserver ) {
this._resizeObserver.destroy();
}
}

/**
Expand Down
Expand Up @@ -88,12 +88,18 @@ describe( 'BalloonToolbar', () => {
} );

afterEach( () => {
sinon.restore();
editorElement.remove();

return editor.destroy();
} );

after( () => {
// Clean up after the ResizeObserver stub in beforeEach(). Even though the global.window.ResizeObserver
// stub is restored, the ResizeObserver class (CKE5 module) keeps the reference to the single native
// observer. Resetting it will allow fresh start for any other test using ResizeObserver.
ResizeObserver._observerInstance = null;
} );

it( 'should create a plugin instance', () => {
expect( balloonToolbar ).to.instanceOf( Plugin );
expect( balloonToolbar ).to.instanceOf( BalloonToolbar );
Expand Down
7 changes: 7 additions & 0 deletions packages/ckeditor5-ui/tests/toolbar/block/blocktoolbar.js
Expand Up @@ -74,6 +74,13 @@ describe( 'BlockToolbar', () => {
return editor.destroy();
} );

after( () => {
// Clean up after the ResizeObserver stub in beforeEach(). Even though the global.window.ResizeObserver
// stub is restored, the ResizeObserver class (CKE5 module) keeps the reference to the single native
// observer. Resetting it will allow fresh start for any other test using ResizeObserver.
ResizeObserver._observerInstance = null;
} );

it( 'should have pluginName property', () => {
expect( BlockToolbar.pluginName ).to.equal( 'BlockToolbar' );
} );
Expand Down
7 changes: 6 additions & 1 deletion packages/ckeditor5-ui/tests/toolbar/toolbarview.js
Expand Up @@ -41,6 +41,11 @@ describe( 'ToolbarView', () => {

after( () => {
clearTranslations();

// Clean up after the ResizeObserver stub in beforeEach(). Even though the global.window.ResizeObserver
// stub is restored, the ResizeObserver class (CKE5 module) keeps the reference to the single native
// observer. Resetting it will allow fresh start for any other test using ResizeObserver.
ResizeObserver._observerInstance = null;
} );

beforeEach( () => {
Expand All @@ -51,7 +56,6 @@ describe( 'ToolbarView', () => {
} );

afterEach( () => {
sinon.restore();
view.destroy();
view.element.remove();
} );
Expand Down Expand Up @@ -1044,6 +1048,7 @@ describe( 'ToolbarView', () => {
} );

afterEach( () => {
testUtils.sinon.restore();
view.element.remove();
view.destroy();
} );
Expand Down
58 changes: 58 additions & 0 deletions packages/ckeditor5-ui/tests/tooltip/tooltipmanager.js
Expand Up @@ -10,6 +10,7 @@ import BalloonPanelView from '../../src/panel/balloon/balloonpanelview';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import global from '@ckeditor/ckeditor5-utils/src/dom/global';

import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
Expand Down Expand Up @@ -719,6 +720,56 @@ describe( 'TooltipManager', () => {
sinon.assert.calledOnce( unpinSpy );
} );
} );

describe( 'when the element disappears', () => {
it( 'should unpin if the element that it was attached was removed from DOM', async () => {
utils.dispatchMouseEnter( elements.a );
utils.waitForTheTooltipToShow( clock );
clock.restore();

// ResizeObserver is asynchronous.
await wait( 100 );

sinon.assert.calledOnce( pinSpy );
sinon.assert.calledWith( pinSpy, {
target: elements.a,
positions: sinon.match.array
} );

unpinSpy = sinon.spy( tooltipManager.balloonPanelView, 'unpin' );

elements.a.remove();

// ResizeObserver is asynchronous.
await wait( 100 );

sinon.assert.calledOnce( unpinSpy );
} );

it( 'should unpin if the element that it was attached was hidden in CSS', async () => {
utils.dispatchMouseEnter( elements.a );
utils.waitForTheTooltipToShow( clock );
clock.restore();

// ResizeObserver is asynchronous.
await wait( 100 );

sinon.assert.calledOnce( pinSpy );
sinon.assert.calledWith( pinSpy, {
target: elements.a,
positions: sinon.match.array
} );

unpinSpy = sinon.spy( tooltipManager.balloonPanelView, 'unpin' );

elements.a.style.display = 'none';

// ResizeObserver is asynchronous.
await wait( 100 );

sinon.assert.calledOnce( unpinSpy );
} );
} );
} );

describe( 'updating tooltip position on EditorUI#update', () => {
Expand Down Expand Up @@ -861,6 +912,7 @@ function getElementsWithTooltips( definitions ) {
}

element.id = name;
element.textContent = 'foo';

document.body.appendChild( element );

Expand Down Expand Up @@ -903,3 +955,9 @@ function getUtils() {
}
};
}

function wait( time ) {
return new Promise( res => {
global.window.setTimeout( res, time );
} );
}

0 comments on commit ed55863

Please sign in to comment.