diff --git a/client/src/controllers/DropdownController.test.js b/client/src/controllers/DropdownController.test.js index 59381ee9b4..919152c316 100644 --- a/client/src/controllers/DropdownController.test.js +++ b/client/src/controllers/DropdownController.test.js @@ -4,11 +4,9 @@ import { DropdownController } from './DropdownController'; describe('DropdownController', () => { let application; - beforeAll(() => { - application?.stop(); - + beforeEach(async () => { document.body.innerHTML = ` -
+
Option @@ -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('Option'); }); - 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); }); }); diff --git a/client/src/controllers/DropdownController.ts b/client/src/controllers/DropdownController.ts index b321c92cd8..379e1281e0 100644 --- a/client/src/controllers/DropdownController.ts +++ b/client/src/controllers/DropdownController.ts @@ -25,13 +25,32 @@ export class DropdownController extends Controller { declare readonly toggleTarget: HTMLButtonElement; declare readonly contentTarget: HTMLDivElement; declare readonly hideOnClickValue: boolean; + declare readonly hasContentTarget: boolean; + tippy?: Instance; connect() { + this.tippy = tippy(this.toggleTarget, this.options); + } + + hide() { + this.tippy?.hide(); + } + + show() { + this.tippy?.show(); + } + + /** + * Default Tippy Options + */ + get options(): Partial { // 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 { plugins.push(hideTooltipOnClickInside); } - /** - * Default Tippy Options - */ - const tippyOptions: Partial = { - 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 { } }, onShown() { - document.dispatchEvent(new CustomEvent('w-dropdown:shown')); + onShown(); }, onHide() { if (hoverTooltipInstance) { @@ -75,7 +100,5 @@ export class DropdownController extends Controller { } }, }; - - tippy(this.toggleTarget, tippyOptions); } }