Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions src/BootstrapAdminUi/assets/scripts/bootstrap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-env browser */
import * as bootstrap from 'bootstrap';
import { initTooltips, initTooltipsOnLiveRender } from './tooltips';

// Fix dropdowns
(() => {
Expand All @@ -16,10 +17,7 @@ import * as bootstrap from 'bootstrap';
})();

// Initialize tooltips
(() => {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((tooltipTriggerEl) => {
let tooltip = new bootstrap.Tooltip(tooltipTriggerEl);
});
})();
initTooltips(document, bootstrap.Tooltip);
initTooltipsOnLiveRender(document, bootstrap.Tooltip);

window.bootstrap = bootstrap;
22 changes: 22 additions & 0 deletions src/BootstrapAdminUi/assets/scripts/tooltips.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* This file is part of the Sylius package.
*
* (c) Sylius Sp. z o.o.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

export function initTooltips(root, Tooltip) {
root.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((tooltipTriggerEl) => {
Tooltip.getInstance(tooltipTriggerEl)?.dispose();
new Tooltip(tooltipTriggerEl);
});
}

// A live component re-render morphs its markup, so tooltips inside it end up unbound or showing a stale title
export function initTooltipsOnLiveRender(target, Tooltip) {
target.addEventListener('live:connect', (event) => {
event.detail.component.on('render:finished', (component) => initTooltips(component.element, Tooltip));
});
}
78 changes: 78 additions & 0 deletions src/BootstrapAdminUi/tests/JavaScript/tooltips.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { initTooltips, initTooltipsOnLiveRender } from '../../assets/scripts/tooltips.js';

function fakeTooltip() {
const instances = new Map();

class Tooltip {
constructor(element) {
this.element = element;
this.disposed = false;
instances.set(element, this);
}

dispose() {
this.disposed = true;
instances.delete(this.element);
}

static getInstance(element) {
return instances.get(element) ?? null;
}
}

return Tooltip;
}

function root(...triggers) {
return {
querySelectorAll: (selector) => {
assert.equal(selector, '[data-bs-toggle="tooltip"]');

return triggers;
},
};
}

test('binds a tooltip to every trigger', () => {
const Tooltip = fakeTooltip();
const first = {};
const second = {};

initTooltips(root(first, second), Tooltip);

assert.ok(Tooltip.getInstance(first));
assert.ok(Tooltip.getInstance(second));
});

test('replaces a tooltip already bound to a trigger', () => {
const Tooltip = fakeTooltip();
const trigger = {};
initTooltips(root(trigger), Tooltip);
const stale = Tooltip.getInstance(trigger);

initTooltips(root(trigger), Tooltip);

assert.equal(stale.disposed, true);
assert.notEqual(Tooltip.getInstance(trigger), stale);
});

test('rebinds tooltips inside a live component after each render', () => {
const Tooltip = fakeTooltip();
const listeners = {};
const target = { addEventListener: (name, callback) => { listeners[name] = callback; } };
const hooks = {};
const component = {
element: root({}),
on: (name, callback) => { hooks[name] = callback; },
};
const freshTrigger = {};

initTooltipsOnLiveRender(target, Tooltip);
listeners['live:connect']({ detail: { component } });
component.element = root(freshTrigger);
hooks['render:finished'](component);

assert.ok(Tooltip.getInstance(freshTrigger));
});
Loading