Skip to content

Commit

Permalink
Merge pull request #15784 from ckeditor/ck/dynamic-tooltips
Browse files Browse the repository at this point in the history
Other (ui): Tooltip will now hide if `data-cke-tooltip-text` is removed while the tooltip is open.

Other (ui): Tooltip position will be updated if `data-cke-tooltip-position` changes while the tooltip is open.
  • Loading branch information
scofalik committed Feb 7, 2024
2 parents 6ba8e5c + 8d3e734 commit 039b302
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 4 deletions.
49 changes: 45 additions & 4 deletions packages/ckeditor5-ui/src/tooltipmanager.ts
Expand Up @@ -122,6 +122,11 @@ export default class TooltipManager extends DomEmitterMixin() {
*/
private _resizeObserver: ResizeObserver | null = null;

/**
* An instance of the mutation observer that keeps track on target element attributes changes.
*/
private _mutationObserver: MutationObserverWrapper | null = null;

/**
* A debounced version of {@link #_pinTooltip}. Tooltips show with a delay to avoid flashing and
* to improve the UX.
Expand Down Expand Up @@ -179,6 +184,10 @@ export default class TooltipManager extends DomEmitterMixin() {
this.balloonPanelView.class = BALLOON_CLASS;
this.balloonPanelView.content.add( this.tooltipTextView );

this._mutationObserver = createMutationObserver( () => {
this._updateTooltipPosition();
} );

this._pinTooltipDebounced = debounce( this._pinTooltip, 600 );

this.listenTo( global.document, 'mouseenter', this._onEnterOrFocus.bind( this ), { useCapture: true } );
Expand Down Expand Up @@ -374,6 +383,8 @@ export default class TooltipManager extends DomEmitterMixin() {
}
} );

this._mutationObserver!.attach( targetDomElement );

this.balloonPanelView.class = [ BALLOON_CLASS, cssClass ]
.filter( className => className )
.join( ' ' );
Expand Down Expand Up @@ -407,25 +418,29 @@ export default class TooltipManager extends DomEmitterMixin() {
if ( this._resizeObserver ) {
this._resizeObserver.destroy();
}

this._mutationObserver!.detach();
}

/**
* Updates the position of the tooltip so it stays in sync with the element it is pinned to.
*
* Hides the tooltip when the element is no longer visible in DOM.
* Hides the tooltip when the element is no longer visible in DOM or the tooltip text was removed.
*/
private _updateTooltipPosition() {
const tooltipData = getTooltipData( this._currentElementWithTooltip! );

// This could happen if the tooltip was attached somewhere in a contextual content toolbar and the toolbar
// disappeared (e.g. removed an image).
if ( !isVisible( this._currentElementWithTooltip ) ) {
// disappeared (e.g. removed an image), or the tooltip text was removed.
if ( !isVisible( this._currentElementWithTooltip ) || !tooltipData.text ) {
this._unpinTooltip();

return;
}

this.balloonPanelView.pin( {
target: this._currentElementWithTooltip!,
positions: TooltipManager.getPositioningFunctions( this._currentTooltipPosition! )
positions: TooltipManager.getPositioningFunctions( tooltipData.position )
} );
}
}
Expand Down Expand Up @@ -453,3 +468,29 @@ function getTooltipData( element: HTMLElement ): TooltipData {
cssClass: element.dataset.ckeTooltipClass || ''
};
}

// Creates a simple `MutationObserver` instance wrapper that observes changes in the tooltip-related attributes of the given element.
// Used instead of the `MutationObserver` from the engine for simplicity.
function createMutationObserver( callback: ( ...args: Array<any> ) => unknown ): MutationObserverWrapper {
const mutationObserver = new MutationObserver( () => {
callback();
} );

return {
attach( element ) {
mutationObserver.observe( element, {
attributes: true,
attributeFilter: [ 'data-cke-tooltip-text', 'data-cke-tooltip-position' ]
} );
},

detach() {
mutationObserver.disconnect();
}
};
}

interface MutationObserverWrapper {
attach: ( element: Node ) => void;
detach: () => void;
}
35 changes: 35 additions & 0 deletions packages/ckeditor5-ui/tests/tooltip/tooltipmanager.js
Expand Up @@ -551,6 +551,24 @@ describe( 'TooltipManager', () => {
} );
} );
} );

it( 'should update the position if the attribute was changed', async () => {
utils.dispatchMouseEnter( elements.a );
utils.waitForTheTooltipToShow( clock );
clock.restore();

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

expect( elements.a.dataset.ckeTooltipPosition ).to.equal( undefined );
sinon.assert.calledOnce( pinSpy );

elements.a.dataset.ckeTooltipPosition = 'e';

await wait( 100 );

sinon.assert.calledTwice( pinSpy );
} );
} );

describe( 'hiding tooltips', () => {
Expand Down Expand Up @@ -771,6 +789,23 @@ describe( 'TooltipManager', () => {
sinon.assert.calledOnce( unpinSpy );
} );
} );

it( 'when the tooltip text gets removed', async () => {
utils.dispatchMouseEnter( elements.a );
utils.waitForTheTooltipToShow( clock );
clock.restore();

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

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

elements.a.dataset.ckeTooltipText = '';

await wait( 100 );

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

describe( 'updating tooltip position on EditorUI#update', () => {
Expand Down

0 comments on commit 039b302

Please sign in to comment.