You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/08/23 09:16:40 UTC
(causeway) 05/22: fixes application menu dismissal
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch htmx
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit da9fba4475928d3724f9341b1f841d38cbbb1ba8
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Thu Aug 20 20:53:41 2026 +0100
fixes application menu dismissal
---
openspec/changes/fix-menu-dismissal/tasks.md | 14 +++++++-------
.../webcomponents/foundation/src/menubar-element.mjs | 8 +++++---
.../foundation/test/menubar-component.test.mjs | 19 ++++++++++++++++---
.../htmx/petclinic/PetClinicHtmxPlaywrightTest.java | 7 +++++++
4 files changed, 35 insertions(+), 13 deletions(-)
diff --git a/openspec/changes/fix-menu-dismissal/tasks.md b/openspec/changes/fix-menu-dismissal/tasks.md
index 9b105577620..6f8ff141876 100644
--- a/openspec/changes/fix-menu-dismissal/tasks.md
+++ b/openspec/changes/fix-menu-dismissal/tasks.md
@@ -1,15 +1,15 @@
## 1. Regression Coverage
-- [ ] 1.1 Extend the foundation menubar component test to reproduce action-selection and Escape dismissal, including `aria-expanded`, panel `hidden`, action dispatch count, prevented Escape, and disclosure focus assertions.
-- [ ] 1.2 Extend the Petclinic Playwright acceptance test to verify that `Pet Owners > Create` closes the menu before the prompt is used and that Escape closes an open menu without invoking an action.
+- [x] 1.1 Extend the foundation menubar component test to reproduce action-selection and Escape dismissal, including `aria-expanded`, panel `hidden`, action dispatch count, prevented Escape, and disclosure focus assertions.
+- [x] 1.2 Extend the Petclinic Playwright acceptance test to verify that `Pet Owners > Create` closes the menu before the prompt is used and that Escape closes an open menu without invoking an action.
## 2. Menu Dismissal Behavior
-- [ ] 2.1 Update `CausewayMenubarElement` so enabled service-action selection closes the containing menu through the shared disclosure-state path while dispatching the semantic action request exactly once.
-- [ ] 2.2 Ensure Escape gives an expanded menu priority, closes only that menu panel, restores focus to its disclosure, and leaves responsive bar state unchanged.
-- [ ] 2.3 Confirm prompt, scalar result, collection result, and route-navigation focus hand-offs continue to override disclosure fallback focus where appropriate.
+- [x] 2.1 Update `CausewayMenubarElement` so enabled service-action selection closes the containing menu through the shared disclosure-state path while dispatching the semantic action request exactly once.
+- [x] 2.2 Ensure Escape gives an expanded menu priority, closes only that menu panel, restores focus to its disclosure, and leaves responsive bar state unchanged.
+- [x] 2.3 Confirm prompt, scalar result, collection result, and route-navigation focus hand-offs continue to override disclosure fallback focus where appropriate.
## 3. Verification
-- [ ] 3.1 Run the foundation JavaScript test suite and confirm all menubar and interaction tests pass.
-- [ ] 3.2 Run the HTMX Petclinic unit and opt-in Playwright acceptance tests and confirm menu dismissal introduces no browser, GraphQL, focus, or navigation failures.
+- [x] 3.1 Run the foundation JavaScript test suite and confirm all menubar and interaction tests pass.
+- [x] 3.2 Run the HTMX Petclinic unit and opt-in Playwright acceptance tests and confirm menu dismissal introduces no browser, GraphQL, focus, or navigation failures.
diff --git a/viewers/webcomponents/foundation/src/menubar-element.mjs b/viewers/webcomponents/foundation/src/menubar-element.mjs
index bca679c82fe..e6554755844 100644
--- a/viewers/webcomponents/foundation/src/menubar-element.mjs
+++ b/viewers/webcomponents/foundation/src/menubar-element.mjs
@@ -190,6 +190,9 @@ export class CausewayMenubarElement extends HTMLElementBase {
const disclosure = panel?.id
? this.querySelector?.(`[data-causeway-menu-disclosure][aria-controls="${globalThis.CSS?.escape ? CSS.escape(panel.id) : panel.id}"]`)
: null;
+ if (disclosure) {
+ this.#closeMenu(disclosure, {focus: true});
+ }
actionButton.dispatchEvent(createSemanticEvent(CausewaySemanticEvent.ACTION_REQUEST, Object.freeze({
actionId,
serviceLogicalTypeName,
@@ -197,9 +200,6 @@ export class CausewayMenubarElement extends HTMLElementBase {
identity: null,
context
}), {cancelable: true}));
- if (disclosure) {
- this.#closeMenu(disclosure, {focus: true});
- }
}
#handleKeydown(event) {
@@ -209,12 +209,14 @@ export class CausewayMenubarElement extends HTMLElementBase {
const expanded = this.querySelector?.('[data-causeway-menu-disclosure][aria-expanded="true"]');
if (expanded) {
event.preventDefault();
+ event.stopPropagation();
this.#closeMenu(expanded, {focus: true});
return;
}
const barButton = this.querySelector?.('[data-causeway-bar-disclosure][aria-expanded="true"]');
if (barButton) {
event.preventDefault();
+ event.stopPropagation();
this.#toggleDisclosure(barButton, false);
barButton.focus?.();
}
diff --git a/viewers/webcomponents/foundation/test/menubar-component.test.mjs b/viewers/webcomponents/foundation/test/menubar-component.test.mjs
index 74f2474fd29..fee33d28e36 100644
--- a/viewers/webcomponents/foundation/test/menubar-component.test.mjs
+++ b/viewers/webcomponents/foundation/test/menubar-component.test.mjs
@@ -74,6 +74,7 @@ test('menu action selection, outside activation, Escape, and sibling opening clo
const secondPanel = {id: 'second-panel', hidden: true};
const firstDisclosure = fakeDisclosure('first-panel', true);
const secondDisclosure = fakeDisclosure('second-panel', false);
+ const barDisclosure = fakeDisclosure('bar-content', true);
const action = {
disabled: false,
getAttribute(name) {
@@ -89,16 +90,19 @@ test('menu action selection, outside activation, Escape, and sibling opening clo
return bar.dispatchEvent(event);
}
};
- const known = new Set([firstDisclosure, secondDisclosure, firstPanel, secondPanel, action]);
+ const known = new Set([firstDisclosure, secondDisclosure, barDisclosure, firstPanel, secondPanel, action]);
bar.contains = candidate => known.has(candidate);
bar.querySelector = selector => {
if (selector.startsWith('#first-panel')) return firstPanel;
if (selector.startsWith('#second-panel')) return secondPanel;
if (selector.includes('aria-controls="first-panel"')) return firstDisclosure;
if (selector.includes('aria-controls="second-panel"')) return secondDisclosure;
- if (selector.includes('aria-expanded="true"')) {
+ if (selector.includes('[data-causeway-menu-disclosure]') && selector.includes('aria-expanded="true"')) {
return [firstDisclosure, secondDisclosure].find(disclosure => disclosure.getAttribute('aria-expanded') === 'true') ?? null;
}
+ if (selector.includes('[data-causeway-bar-disclosure]') && selector.includes('aria-expanded="true"')) {
+ return barDisclosure.getAttribute('aria-expanded') === 'true' ? barDisclosure : null;
+ }
return null;
};
bar.querySelectorAll = selector => selector.includes('aria-expanded="true"')
@@ -106,11 +110,16 @@ test('menu action selection, outside activation, Escape, and sibling opening clo
: [];
let request;
- bar.addEventListener(CausewaySemanticEvent.ACTION_REQUEST, event => request = event.detail);
+ let requestCount = 0;
+ bar.addEventListener(CausewaySemanticEvent.ACTION_REQUEST, event => {
+ request = event.detail;
+ requestCount += 1;
+ });
const actionClick = new Event('click');
actionClick.target = action;
bar.dispatchEvent(actionClick);
assert.equal(request.actionId, 'welcomeMessage');
+ assert.equal(requestCount, 1);
assert.equal(Object.hasOwn(request, 'focusTarget'), false);
assert.equal(firstDisclosure.getAttribute('aria-expanded'), 'false');
assert.equal(firstPanel.hidden, true);
@@ -137,8 +146,12 @@ test('menu action selection, outside activation, Escape, and sibling opening clo
escape.key = 'Escape';
escape.target = {closest: () => null};
bar.dispatchEvent(escape);
+ assert.equal(escape.defaultPrevented, true);
assert.equal(secondDisclosure.getAttribute('aria-expanded'), 'false');
+ assert.equal(secondPanel.hidden, true);
assert.equal(secondDisclosure.focusCount, 1);
+ assert.equal(barDisclosure.getAttribute('aria-expanded'), 'true');
+ assert.equal(requestCount, 1);
document.body.removeChild(bar);
});
diff --git a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
index 71dfd9bca17..5e13f7f93ae 100644
--- a/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
+++ b/viewers/webcomponents/sample-htmx-petclinic/src/playwright/java/org/apache/causeway/viewer/webcomponents/sample/htmx/petclinic/PetClinicHtmxPlaywrightTest.java
@@ -206,6 +206,11 @@ void routesHomeObjectsHistoryCollectionsAndResponsiveLayout() {
void serviceActionsCoverValidationCancellationScalarAndCollections() {
openHome();
+ openMenu("Pet Owners");
+ menuDisclosure("Pet Owners").press("Escape");
+ assertMenuClosedAndFocused("Pet Owners");
+ assertThat(page.locator(PROMPT).count()).isZero();
+
openMenu("Pet Owners");
page.locator(ROUTE_PAGE).dispatchEvent("click");
assertMenuClosed("Pet Owners");
@@ -311,6 +316,8 @@ void disposableOwnerExercisesCreateAndEveryObjectAction() {
openMenu("Pet Owners");
serviceAction("create").click();
waitForPrompt("create");
+ assertMenuClosed("Pet Owners");
+ assertFocused(parameter("name"));
fillParameter("name", "Playwright Owner");
fillParameter("knownAs", "PW");
fillParameter("telephoneNumber", "020 7000 0000");