0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-25 05:02:57 +01:00

Remove initTooltips util & move plugins to controller files

- Fully remove initTooltips util and replace with Controller usage
- Move plugins to their used Controller files
- Export hideTooltipOnEsc as it's used by other code
- Add unit tests for plugin behaviour in the controller tests
- Fixes #10668
- Built on #10869
This commit is contained in:
LB Johnston 2023-09-14 08:25:26 +10:00 committed by Thibaud Colas
parent f0a67975db
commit edc5991f50
10 changed files with 186 additions and 166 deletions

View File

@ -17,7 +17,7 @@ import ComboBox, {
comboBoxNoResults,
comboBoxTriggerLabel,
} from '../../ComboBox/ComboBox';
import { hideTooltipOnEsc } from '../../../includes/initTooltips';
import { hideTooltipOnEsc } from '../../../controllers/TooltipController';
import {
addErrorMessages,
removeErrorMessages,

View File

@ -1,17 +1,21 @@
import { Application } from '@hotwired/stimulus';
import { DropdownController } from './DropdownController';
jest.useFakeTimers();
describe('DropdownController', () => {
let application;
beforeEach(async () => {
document.body.innerHTML = `
<div data-controller="w-dropdown" data-action="custom:show->w-dropdown#show custom:hide->w-dropdown#hide">
<button type="button" data-w-dropdown-target="toggle" aria-label="Actions"></button>
<div data-w-dropdown-target="content">
<a href="/">Option</a>
</div>
</div>`;
<section>
<div data-controller="w-dropdown" data-action="custom:show->w-dropdown#show custom:hide->w-dropdown#hide">
<button id="toggle" type="button" data-w-dropdown-target="toggle" aria-label="Actions"></button>
<div data-w-dropdown-target="content">
<a href="/">Option</a>
</div>
</div>
</section>`;
application = Application.start();
application.register('w-dropdown', DropdownController);
@ -33,6 +37,7 @@ describe('DropdownController', () => {
afterEach(() => {
jest.restoreAllMocks();
document.body.innerHTML = '';
application?.stop();
});
@ -76,6 +81,28 @@ describe('DropdownController', () => {
);
});
it("should ensure the tooltip closes on 'esc' keydown", async () => {
const toggle = document.getElementById('toggle');
expect(toggle.getAttribute('aria-expanded')).toBe('false');
toggle.dispatchEvent(new Event('click'));
await jest.runAllTimersAsync();
// check the tooltip is open
expect(toggle.getAttribute('aria-expanded')).toBe('true');
// now press the escape key
document
.querySelector('section')
.dispatchEvent(
new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }),
);
await Promise.resolve();
expect(toggle.getAttribute('aria-expanded')).toBe('false');
});
it('should support methods to show and hide the dropdown', async () => {
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);

View File

@ -1,11 +1,82 @@
import { Controller } from '@hotwired/stimulus';
import tippy, { Content, Props, Instance } from 'tippy.js';
import {
hideTooltipOnBreadcrumbExpandAndCollapse,
hideTooltipOnClickInside,
hideTooltipOnEsc,
rotateToggleIcon,
} from '../includes/initTooltips';
import { hideTooltipOnEsc } from './TooltipController';
/**
* Prevents the tooltip from staying open when the breadcrumbs
* expand and push the toggle button in the layout.
*/
const hideTooltipOnBreadcrumbsChange = {
name: 'hideTooltipOnBreadcrumbAndCollapse',
fn({ hide }: Instance) {
function onBreadcrumbExpandAndCollapse() {
hide();
}
return {
onShow() {
document.addEventListener(
'w-breadcrumbs:opened',
onBreadcrumbExpandAndCollapse,
);
document.addEventListener(
'w-breadcrumbs:closed',
onBreadcrumbExpandAndCollapse,
);
},
onHide() {
document.removeEventListener(
'w-breadcrumbs:closed',
onBreadcrumbExpandAndCollapse,
);
document.removeEventListener(
'w-breadcrumbs:opened',
onBreadcrumbExpandAndCollapse,
);
},
};
},
};
/**
* Hides tooltip when clicking inside.
*/
const hideTooltipOnClickInside = {
name: 'hideTooltipOnClickInside',
defaultValue: true,
fn(instance: Instance) {
const onClick = () => instance.hide();
return {
onShow() {
instance.popper.addEventListener('click', onClick);
},
onHide() {
instance.popper.removeEventListener('click', onClick);
},
};
},
};
/**
* If the toggle button has a toggle arrow,
* rotate it when open and closed.
*/
const rotateToggleIcon = {
name: 'rotateToggleIcon',
fn(instance: Instance) {
const dropdownIcon = instance.reference.querySelector('.icon-arrow-down');
if (!dropdownIcon) {
return {};
}
return {
onShow: () => dropdownIcon.classList.add('w-rotate-180'),
onHide: () => dropdownIcon.classList.remove('w-rotate-180'),
};
},
};
/**
* A Tippy.js tooltip with interactive "dropdown" options.
@ -65,16 +136,6 @@ export class DropdownController extends Controller<HTMLElement> {
});
}
const plugins = [
hideTooltipOnEsc,
hideTooltipOnBreadcrumbExpandAndCollapse,
rotateToggleIcon,
];
if (this.hideOnClickValue) {
plugins.push(hideTooltipOnClickInside);
}
const onShown = () => {
this.dispatch('shown');
};
@ -88,7 +149,7 @@ export class DropdownController extends Controller<HTMLElement> {
theme: 'dropdown',
...(this.hasOffsetValue && { offset: this.offsetValue }),
placement: 'bottom',
plugins,
plugins: this.plugins,
onShow() {
if (hoverTooltipInstance) {
hoverTooltipInstance.disable();
@ -104,4 +165,12 @@ export class DropdownController extends Controller<HTMLElement> {
},
};
}
get plugins() {
return [
hideTooltipOnBreadcrumbsChange,
hideTooltipOnEsc,
rotateToggleIcon,
].concat(this.hideOnClickValue ? [hideTooltipOnClickInside] : []);
}
}

View File

@ -67,6 +67,29 @@ describe('TooltipController', () => {
expect(tooltip.dataset.placement).toEqual('bottom'); // the default placement
});
it("should ensure the tooltip closes on 'esc' keydown", async () => {
const tooltipTrigger = document.getElementById('tooltip-default');
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
tooltipTrigger.dispatchEvent(new Event('mouseenter'));
await Promise.resolve();
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(1);
// now press the escape key
document
.querySelector('section')
.dispatchEvent(
new KeyboardEvent('keydown', { bubbles: true, key: 'Escape' }),
);
await Promise.resolve();
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
});
it('should create a tooltip that accepts a different placement value', async () => {
const tooltipTrigger = document.getElementById('tooltip-custom');

View File

@ -1,6 +1,30 @@
import { Controller } from '@hotwired/stimulus';
import tippy, { Placement, Props, Instance } from 'tippy.js';
import { hideTooltipOnEsc } from '../includes/initTooltips';
import { domReady } from '../utils/domReady';
/**
* Hides tooltip when escape key is pressed.
*/
export const hideTooltipOnEsc = {
name: 'hideOnEsc',
defaultValue: true,
fn({ hide }: Instance) {
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
hide();
}
}
return {
onShow() {
document.addEventListener('keydown', onKeyDown);
},
onHide() {
document.removeEventListener('keydown', onKeyDown);
},
};
},
};
/**
* A Tippy.js tooltip with simple popover content.
@ -51,12 +75,29 @@ export class TooltipController extends Controller<HTMLElement> {
return {
content: this.contentValue,
placement: this.placementValue,
plugins: [hideTooltipOnEsc],
plugins: this.plugins,
...(this.hasOffsetValue && { offset: this.offsetValue }),
};
}
get plugins() {
return [hideTooltipOnEsc];
}
disconnect() {
this.tippy?.destroy();
}
/**
* Ensure we have backwards compatibility for any data-tippy usage on initial load.
*
* @deprecated RemovedInWagtail70
*/
static afterLoad() {
domReady().then(() => {
tippy('[data-tippy-content]', {
plugins: [hideTooltipOnEsc],
});
});
}
}

View File

@ -1,6 +1,5 @@
import $ from 'jquery';
import { ChooserModal } from '../../includes/chooserModal';
import { initTooltips } from '../../includes/initTooltips';
const PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
browse(modal, jsonData) {
@ -140,7 +139,6 @@ const PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
});
}
ajaxifyBrowseResults();
initTooltips();
/*
Focus on the search box when opening the modal.

View File

@ -1,4 +1,3 @@
import { initTooltips } from '../../includes/initTooltips';
import { initTabs } from '../../includes/tabs';
import initSidePanel from '../../includes/sidePanel';
import {
@ -11,7 +10,6 @@ import { initMinimap } from '../../components/Minimap';
* Add in here code to run once the page is loaded.
*/
document.addEventListener('DOMContentLoaded', () => {
initTooltips();
initTabs();
initSidePanel();
initCollapsiblePanels();
@ -25,11 +23,3 @@ window.addEventListener('load', () => {
initAnchoredPanels();
initMinimap();
});
/**
* When search results are successful, reinitialise widgets
* that could be inside the newly injected DOM.
*/
window.addEventListener('w-swap:success', () => {
initTooltips(); // reinitialise any tooltips
});

View File

@ -1,6 +1,5 @@
import $ from 'jquery';
import { initTabs } from './tabs';
import { initTooltips } from './initTooltips';
import { gettext } from '../utils/gettext';
const validateCreationForm = (form) => {
@ -231,9 +230,6 @@ class ChooserModalOnloadHandlerFactory {
// Reinitialize tabs to hook up tab event listeners in the modal
if (this.modalHasTabs(modal)) initTabs();
// Reinitialise any tooltips
initTooltips();
this.updateMultipleChoiceSubmitEnabledState(modal);
$('[data-multiple-choice-select]', containerElement).on('change', () => {
this.updateMultipleChoiceSubmitEnabledState(modal);

View File

@ -1,16 +0,0 @@
import * as tippy from 'tippy.js';
import { initTooltips } from './initTooltips';
jest.spyOn(tippy, 'default');
beforeEach(() => jest.clearAllMocks());
describe('initTooltips', () => {
it('should call the Tippy util with the [data-tippy-content] attribute', () => {
expect(tippy.default).not.toHaveBeenCalled();
initTooltips();
expect(tippy.default).toHaveBeenCalledWith('[data-tippy-content]', {
plugins: [expect.objectContaining({ name: 'hideOnEsc' })],
});
});
});

View File

@ -1,108 +0,0 @@
import tippy, { Instance } from 'tippy.js';
/**
* Hides tooltip when escape key is pressed
*/
export const hideTooltipOnEsc = {
name: 'hideOnEsc',
defaultValue: true,
fn({ hide }: Instance) {
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
hide();
}
}
return {
onShow() {
document.addEventListener('keydown', onKeyDown);
},
onHide() {
document.removeEventListener('keydown', onKeyDown);
},
};
},
};
/**
* Hides tooltip when clicking inside.
*/
export const hideTooltipOnClickInside = {
name: 'hideTooltipOnClickInside',
defaultValue: true,
fn(instance: Instance) {
const onClick = () => instance.hide();
return {
onShow() {
instance.popper.addEventListener('click', onClick);
},
onHide() {
instance.popper.removeEventListener('click', onClick);
},
};
},
};
/**
* Prevents the tooltip from staying open when the breadcrumbs expand and push the toggle button in the layout
*/
export const hideTooltipOnBreadcrumbExpandAndCollapse = {
name: 'hideTooltipOnBreadcrumbAndCollapse',
fn({ hide }: Instance) {
function onBreadcrumbExpandAndCollapse() {
hide();
}
return {
onShow() {
document.addEventListener(
'w-breadcrumbs:opened',
onBreadcrumbExpandAndCollapse,
);
document.addEventListener(
'w-breadcrumbs:closed',
onBreadcrumbExpandAndCollapse,
);
},
onHide() {
document.removeEventListener(
'w-breadcrumbs:closed',
onBreadcrumbExpandAndCollapse,
);
document.removeEventListener(
'w-breadcrumbs:opened',
onBreadcrumbExpandAndCollapse,
);
},
};
},
};
/**
* If the toggle button has a toggle arrow, rotate it when open and closed
*/
export const rotateToggleIcon = {
name: 'rotateToggleIcon',
fn(instance: Instance) {
const dropdownIcon = instance.reference.querySelector('.icon-arrow-down');
if (!dropdownIcon) {
return {};
}
return {
onShow: () => dropdownIcon.classList.add('w-rotate-180'),
onHide: () => dropdownIcon.classList.remove('w-rotate-180'),
};
},
};
/**
* Default Tippy Tooltips
*/
export function initTooltips() {
tippy('[data-tippy-content]', {
plugins: [hideTooltipOnEsc],
});
}