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

DropdownController - fix test & refine events & methods

- Test that uses setTimeout never ran as Tippy relies on `transitionend` events, which JSDom will not dispatch. Rework tests to correctly test this custom event gets dispatched
- Use Stimulus dispatch method for custom event dispatching as the controller name (e.g. 'w-dropdown') will automatically be added, work around TypeScript bug in Stimulus which should be fixed in a future release https://github.com/hotwired/stimulus/issues/642
- Add show/hide methods, pull out options to own get method
- Add unit tests for show/hide, content being in dropdown
- Relates to #10557
This commit is contained in:
LB Johnston 2023-06-28 20:28:03 +10:00 committed by LB (Ben Johnston)
parent fef8c4ac3c
commit aa3863f17c
2 changed files with 91 additions and 21 deletions

View File

@ -4,11 +4,9 @@ import { DropdownController } from './DropdownController';
describe('DropdownController', () => {
let application;
beforeAll(() => {
application?.stop();
beforeEach(async () => {
document.body.innerHTML = `
<div data-controller="w-dropdown">
<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>
@ -17,25 +15,74 @@ describe('DropdownController', () => {
application = Application.start();
application.register('w-dropdown', DropdownController);
await Promise.resolve(requestAnimationFrame);
// set all animation durations to 0 so that tests can ignore animation delays
// Tippy relies on transitionend which is not yet supported in JSDom
// https://github.com/jsdom/jsdom/issues/1781
document
.querySelectorAll('[data-controller="w-dropdown"]')
.forEach((element) => {
application
.getControllerForElementAndIdentifier(element, 'w-dropdown')
.tippy.setProps({ duration: 0 }); // tippy will merge props with whatever has already been set
});
});
it('initialises Tippy.js on connect', () => {
afterEach(() => {
jest.restoreAllMocks();
application?.stop();
});
it('initialises Tippy.js on connect and shows content in a dropdown', () => {
const toggle = document.querySelector('[data-w-dropdown-target="toggle"]');
const content = document.querySelector(
'[data-w-dropdown-target="content"]',
);
expect(toggle.getAttribute('aria-expanded')).toBe('false');
expect(content).toBe(null);
toggle.dispatchEvent(new Event('click'));
const expandedContent = document.querySelectorAll('[role="tooltip"]');
expect(expandedContent).toHaveLength(1);
expect(expandedContent[0].innerHTML).toContain('<a href="/">Option</a>');
});
it('triggers custom event on activation', () => {
it('triggers custom event on activation', async () => {
const toggle = document.querySelector('[data-w-dropdown-target="toggle"]');
const mock = jest.fn();
document.addEventListener('w-dropdown:shown', mock);
const mock = new Promise((resolve) => {
document.addEventListener('w-dropdown:shown', (event) => {
resolve(event);
});
});
toggle.dispatchEvent(new Event('click'));
// Leave time for animation to complete.
setTimeout(() => {
expect(mock).toHaveBeenCalled();
}, 500);
const event = await mock;
expect(event).toEqual(
expect.objectContaining({ type: 'w-dropdown:shown', target: document }),
);
});
it('should support methods to show and hide the dropdown', async () => {
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
const dropdownElement = document.querySelector(
'[data-controller="w-dropdown"]',
);
dropdownElement.dispatchEvent(new CustomEvent('custom:show'));
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(1);
dropdownElement.dispatchEvent(new CustomEvent('custom:hide'));
expect(document.querySelectorAll('[role="tooltip"]')).toHaveLength(0);
});
});

View File

@ -25,13 +25,32 @@ export class DropdownController extends Controller<HTMLElement> {
declare readonly toggleTarget: HTMLButtonElement;
declare readonly contentTarget: HTMLDivElement;
declare readonly hideOnClickValue: boolean;
declare readonly hasContentTarget: boolean;
tippy?: Instance<Props>;
connect() {
this.tippy = tippy(this.toggleTarget, this.options);
}
hide() {
this.tippy?.hide();
}
show() {
this.tippy?.show();
}
/**
* Default Tippy Options
*/
get options(): Partial<Props> {
// If the dropdown toggle uses an ARIA label, use this as a hover tooltip.
const hoverTooltip = this.toggleTarget.getAttribute('aria-label');
let hoverTooltipInstance: Instance;
this.contentTarget.hidden = false;
if (this.hasContentTarget) {
this.contentTarget.hidden = false;
}
if (hoverTooltip) {
hoverTooltipInstance = tippy(this.toggleTarget, {
@ -51,11 +70,17 @@ export class DropdownController extends Controller<HTMLElement> {
plugins.push(hideTooltipOnClickInside);
}
/**
* Default Tippy Options
*/
const tippyOptions: Partial<Props> = {
content: this.contentTarget as Content,
const onShown = () => {
this.dispatch('shown', {
// work around for target type bug https://github.com/hotwired/stimulus/issues/642
target: ((key = 'document') => window[key])(),
});
};
return {
...(this.hasContentTarget
? { content: this.contentTarget as Content }
: {}),
trigger: 'click',
interactive: true,
theme: 'dropdown',
@ -67,7 +92,7 @@ export class DropdownController extends Controller<HTMLElement> {
}
},
onShown() {
document.dispatchEvent(new CustomEvent('w-dropdown:shown'));
onShown();
},
onHide() {
if (hoverTooltipInstance) {
@ -75,7 +100,5 @@ export class DropdownController extends Controller<HTMLElement> {
}
},
};
tippy(this.toggleTarget, tippyOptions);
}
}