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);
}
}