Skip to content

Bootstrap 5 dropdown auto-init does not respond to synthetic or programmatic clicks on dropdown-toggle #2981

@bjagg

Description

@bjagg

Problem

data-bs-toggle="dropdown" toggles inside the portal chrome (the per-portlet Options menu, possibly others) do not open via JavaScript-dispatched clicks. Specifically:

  • Playwright's Locator.click() on a .portlet-options-menu .dropdown-toggle element completes successfully but leaves aria-expanded at "false" — the menu does not render.
  • Native HTMLElement.click() from a page.evaluate() block has the same outcome.
  • Dispatching synthetic MouseEvent("click", { bubbles: true }), PointerEvent("pointerdown"), MouseEvent("mousedown"), etc., from JS produces the same outcome.
  • Bootstrap's auto-init did register an instancebootstrap.Dropdown.getInstance(toggleEl) returns a non-null Dropdown object — so the wiring exists.
  • Calling bootstrap.Dropdown.getOrCreateInstance(toggleEl).show() directly opens the menu reliably and aria-expanded flips to "true". So the Bootstrap component itself is healthy; only the click→delegate path is broken.

This pattern points to a listener somewhere in uPortal's own chrome JS that is swallowing the click event before it bubbles to document, where Bootstrap 5's delegated [data-bs-toggle="dropdown"] handler lives. Candidates: jQuery UI draggable's mousedown/click plumbing on .up-portlet-wrapper, jQuery click delegations registered against .up-portlet-options-item ancestors, or something in modern-layout-preferences.js. The exact culprit is not yet identified.

Why this matters beyond the test suite

The same JS-delivered click path that Playwright uses is what assistive tech (screen readers' click activation), keyboard-driven flows that synthesize clicks, and any custom uPortal JS that programmatically opens the menu would use. Real human pointer/touch clicks may still work — that's the symptom variance — but anything that delivers a click programmatically will silently fail to open the menu.

Reproduction

  1. Deploy uPortal 5.17.5 (with the Bootstrap dedupe fix merged) to a Tomcat with the respondr skin and a quickstart layout.

  2. Log in as a regular user. Navigate to any tab with a movable portlet (e.g. Calendar on the Welcome tab).

  3. Open DevTools and run:

    const toggle = document.querySelector(
      ".up-portlet-wrapper:has(.portlet-title a[title='Calendar']) .portlet-options-menu .dropdown-toggle"
    );
    
    // Confirm Bootstrap auto-init wired the instance:
    bootstrap.Dropdown.getInstance(toggle); // → returns a Dropdown object (auto-init ran)
    
    // But neither of these opens the menu — aria-expanded stays "false":
    toggle.click();
    toggle.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    
    // This *does* open the menu:
    bootstrap.Dropdown.getOrCreateInstance(toggle).show();

The same behavior was observed against the Bookmarks portlet's Options menu and against any maximized portlet's Options menu, so it isn't Calendar-specific — it's the chrome.

Workaround in uPortal-start tests

uPortal-start/tests/ux/utils/ux-general-utils.ts now exposes an openDropdown(toggle) helper that goes through Bootstrap's public API:

export async function openDropdown(toggle: Locator): Promise<void> {
  await expect(toggle).toBeVisible();
  await toggle.evaluate((el) => {
    const bs = (window as any).bootstrap;
    if (!bs?.Dropdown) throw new Error("Bootstrap Dropdown not loaded");
    bs.Dropdown.getOrCreateInstance(el as HTMLElement).show();
  });
  await expect(toggle).toHaveAttribute("aria-expanded", "true");
}

smoke/favorites.spec.ts and smoke/portlet-options.spec.ts use it everywhere they previously did toggle.click(). The suite is now stable at 117/117 across multiple full-suite runs.

The tradeoff: those tests no longer exercise the click-pathway specifically, so a regression in the click→Bootstrap chain won't surface in CI. That's the gap this issue tracks.

Suggested investigation steps

  1. In a running portal page, attach a capture-phase click listener at document level: document.addEventListener('click', e => console.log('click target:', e.target, 'phase:', e.eventPhase, 'cancelBubble:', e.cancelBubble), true);. Then call toggle.click() from DevTools. If the listener doesn't fire, the click is being canceled at the element or via stopImmediatePropagation upstream of document.
  2. If the document-level listener does fire but the Bootstrap delegate doesn't, check whether jQuery's $('document').off('click.bs.dropdown.data-api') was called somewhere during page init (Bootstrap 5's data-api uses native listeners, but if a jQuery plugin did this against jQuery's own delegate set, that's a smell worth chasing).
  3. Search media/skins/common/javascript/ and modern-layout-preferences.js for stopImmediatePropagation / preventDefault against click/mousedown.

Related

  • #2980 (Bootstrap dedupe fix) — fixed a different symptom in the same chain (Bootstrap loaded twice → click opened then immediately closed). After that fix, dropdown is loaded once, the auto-init runs, but synthetic clicks still don't fire the open. Different problem, same surface.
  • The favorites/portlet-options Playwright suite history in uPortal-start PR UP-4702: Add support for encrypted values to the property files with… #692 captures the diagnostic walkthrough.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions