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:
parent
f0a67975db
commit
edc5991f50
@ -17,7 +17,7 @@ import ComboBox, {
|
||||
comboBoxNoResults,
|
||||
comboBoxTriggerLabel,
|
||||
} from '../../ComboBox/ComboBox';
|
||||
import { hideTooltipOnEsc } from '../../../includes/initTooltips';
|
||||
import { hideTooltipOnEsc } from '../../../controllers/TooltipController';
|
||||
import {
|
||||
addErrorMessages,
|
||||
removeErrorMessages,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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] : []);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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' })],
|
||||
});
|
||||
});
|
||||
});
|
@ -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],
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user