You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2026/09/10 05:29:19 UTC

(myfaces-tobago) branch main updated: fix(dropdown): position

This is an automated email from the ASF dual-hosted git repository.

henningn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git


The following commit(s) were added to refs/heads/main by this push:
     new 1b53c337e2 fix(dropdown): position
1b53c337e2 is described below

commit 1b53c337e2ff9adac3d56ca729614a6dca714b06
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Tue Sep 8 09:28:16 2026 +0200

    fix(dropdown): position
    
    fix: Sometimes the dropdown menu opens to the left or tries to use the whole page width, even if there is enough space on the right side.
    
    It was actually two bugs.
    First: To determine if the content of a dropdown fits in the given size, the max-width inline style was compared with offsetWidth. The issue: max-width can be a float value, for example 120.5px. But offsetWidth is a rounded integer value. Instead of 120.5px, offsetWidth has the value 121px.
    
    Second bug affects only Firefox (current version is 155.0.1). To determine if the content of a dropdown fits in the given size, there must be no horizonal scrollbar. But if the .tobago-dropdown-menu class has a max-height set via CSS class and the content has a min-width set, there might be a scrollbar in firefox while calculating the dropdown position. Sadly I could not implement an automated test for this. So there is only a manual test.
    
    * add tests
    * fix both previously described issues
    
    Issue: TOBAGO-2541
---
 .../z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.css  |  27 +++++
 .../dropdown/TOBAGO-2541/TOBAGO-2541.xhtml         |  44 ++++++++
 .../src/main/ts/tobago-dropdown-menu.test.ts       | 113 +++++++++++++++++++++
 .../src/main/ts/tobago-dropdown-menu.ts            |   7 +-
 4 files changed, 190 insertions(+), 1 deletion(-)

diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.css b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.css
new file mode 100644
index 0000000000..c5b2a8d118
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.css
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+tobago-page .tobago-page-menuStore > .tobago-dropdown-menu.show[data-tobago-for="page:mainForm:dropdownForm"] {
+  background-color: orange;
+  max-height: 80px;
+}
+
+tobago-page .tobago-page-menuStore > .tobago-dropdown-menu.show[data-tobago-for="page:mainForm:dropdownForm"] .minWidth300 {
+  min-width: 300px;
+}
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.xhtml
new file mode 100644
index 0000000000..1e09ffb009
--- /dev/null
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/900-test/z-manual/dropdown/TOBAGO-2541/TOBAGO-2541.xhtml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<ui:composition template="/main.xhtml"
+                xmlns="http://www.w3.org/1999/xhtml"
+                xmlns:f="jakarta.faces.core"
+                xmlns:tc="http://myfaces.apache.org/tobago/component"
+                xmlns:ui="jakarta.faces.facelets">
+
+  <tc:style file="TOBAGO-2541.css"/>
+
+  <p>This is a test fails in Firefox 155.0.1, but is not reproducible in Playwright.</p>
+  <p><code>.tobago-dropdown-menu</code> has <code>max-height</code> set. The facet panel has a <code>min-width</code>.
+    This leads (only in Firefox) to a horizonal scrollbar while calculating the dropdown position.
+    The final dropdown has the style <code>max-height</code> set which overwrite the <code>max-height</code> value from
+    the CSS class.</p>
+
+  <tc:button id="dropdownForm" label="Dropdown Form" omit="true">
+    <f:facet name="panel">
+      <tc:panel styleClass="minWidth300">
+        <tc:in/>
+        <tc:flexLayout columns="auto">
+          <tc:button label="Button 1" autoSpacing="false"/>
+          <tc:button label="Button 2" autoSpacing="false"/>
+        </tc:flexLayout>
+      </tc:panel>
+    </f:facet>
+  </tc:button>
+</ui:composition>
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.test.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.test.ts
new file mode 100644
index 0000000000..14edbe548f
--- /dev/null
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.test.ts
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import "./tobago-dropdown-menu";
+import {DropdownMenu, DropdownMenuAlignment} from "./tobago-dropdown-menu";
+
+beforeAll(() => {
+  document.dispatchEvent(new Event("tobago.init"));
+  document.body.innerHTML = `<tobago-page><div class="tobago-page-menuStore"></div></tobago-page>`;
+});
+
+afterEach(() => {
+  jest.restoreAllMocks();
+  document.body.replaceChildren();
+});
+
+test("Panel-facet position and opening direction issue TOBAGO-2541", () => {
+  const tobagoDropdownElement = `
+<tobago-dropdown id="dropdownForm" class="dropdown" data-tobago-auto-close="outside">
+  <button type="button" id="dropdownForm::command" name="dropdownForm" aria-expanded="false"
+          class="tobago-button btn btn-secondary tobago-auto-spacing dropdown-toggle">
+    <tobago-behavior event="click" client-id="dropdownForm" field-id="dropdownForm::command" omit="omit"></tobago-behavior>
+    <span>Dropdown Button</span>
+  </button>
+  <div class="tobago-dropdown-menu" aria-labelledby="dropdownForm::command" name="dropdownForm"
+       data-tobago-for="dropdownForm">
+    <div class="tobago-panel-facet">
+      <tobago-panel id="panelFacet">
+        <p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p>
+      </tobago-panel>
+    </div>
+  </div>
+</tobago-dropdown>`;
+  document.querySelector("tobago-page").insertAdjacentHTML("beforeend", tobagoDropdownElement);
+
+  const toggle = document.querySelector<HTMLButtonElement>(".dropdown-toggle");
+  toggle.click();
+
+  const referenceElement = document.querySelector<HTMLButtonElement>(".dropdown-toggle");
+  jest.spyOn(referenceElement, "getBoundingClientRect").mockReturnValue({
+    bottom: 549,
+    height: 38,
+    left: 298.5,
+    right: 444.859375,
+    top: 511,
+    width: 146.359375,
+    x: 298.5,
+    y: 511,
+    toJSON: () => ({})
+  });
+
+  let dropdownMenuElement = document.querySelector<HTMLDivElement>(".tobago-dropdown-menu");
+  jest.spyOn(dropdownMenuElement, "getBoundingClientRect").mockImplementation(() => ({
+    bottom: dropdownMenuElement.style.top === "" && dropdownMenuElement.style.bottom === "0px" ? 637 : 82,
+    height: 66,
+    left: 16,
+    right: 1375,
+    top: 16,
+    width: dropdownMenuElement.style.maxWidth !== "" ? parseFloat(dropdownMenuElement.style.maxWidth) : 1359,
+    x: 16,
+    y: 16,
+    toJSON: () => ({})
+  }));
+  /* scrollWidth, clientWidth and offsetWidth are used in dropdownContentFit()
+     dropdownContentFit() is called after the first execution of calcHorizontalPositioningAndMaxWidth()
+     The values for scrollWidth, clientWidth and offsetWidth are from the Chrome browser. */
+  Object.defineProperty(dropdownMenuElement, "scrollWidth", {configurable: true, value: 1091});
+  Object.defineProperty(dropdownMenuElement, "clientWidth", {configurable: true, value: 1091});
+  /* The issue for TOBAGO-2541 is that offsetWidth is a rounded integer value. Actually, the offsetWidth should be
+     "1076.5px" but is rounded up to 1077. With this, the dropdownContentFit() function returns false. */
+  Object.defineProperty(dropdownMenuElement, "offsetWidth", {
+    configurable: true,
+    get: () => Math.round(parseFloat(dropdownMenuElement.style.maxWidth))
+  });
+  Object.defineProperty(window, "innerHeight", {configurable: true, value: 637});
+  /*  getComputedStyle() is called once for the dropdownMenuElement. */
+  const computedStyle = document.createElement("div").style;
+  computedStyle.marginTop = "16px";
+  computedStyle.marginBottom = "2px";
+  jest.spyOn(window, "getComputedStyle").mockReturnValue(computedStyle);
+
+  const dropdownMenu = new DropdownMenu(dropdownMenuElement, referenceElement, [dropdownMenuElement, referenceElement], false, DropdownMenuAlignment.start);
+
+  dropdownMenu.show();
+
+  dropdownMenuElement = document.querySelector<HTMLDivElement>(".tobago-dropdown-menu");
+  expect(toggle.ariaExpanded).toBe("true");
+  expect(dropdownMenuElement.classList.contains("show")).toBe(true);
+
+  expect(dropdownMenuElement.style.maxWidth).toBe("1076.5px");
+  expect(dropdownMenuElement.style.maxHeight).toBe("495px");
+  expect(dropdownMenuElement.style.left).toBe("282.5px");
+  expect(dropdownMenuElement.style.right).toBe("");
+  expect(dropdownMenuElement.style.top).toBe("");
+  expect(dropdownMenuElement.style.bottom).toBe("126px");
+  expect(dropdownMenuElement.style.marginBottom).toBe("var(--tobago-dropdown-menu-component-offset)");
+});
diff --git a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.ts b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.ts
index 642eee1e91..cd4f788acb 100644
--- a/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/ts/tobago-dropdown-menu.ts
@@ -154,6 +154,10 @@ export class DropdownMenu {
   private updatePosition(): void {
     const refElementRect = this.referenceElement.getBoundingClientRect();
 
+    /* After updating the position, max-height is set as an inline-style. Make sure no CSS class set max-height for this
+       calculation. see: TOBAGO-2541 */
+    this.dropdownMenuElement.style.maxHeight = "initial";
+
     //calc horizontal positioning and max-width
     switch (this.alignment) {
       case DropdownMenuAlignment.start:
@@ -245,7 +249,8 @@ export class DropdownMenu {
 
   private get dropdownContentFit(): boolean {
     const element = this.dropdownMenuElement;
-    return element.scrollWidth <= element.clientWidth && element.offsetWidth <= parseFloat(element.style.maxWidth);
+    return element.scrollWidth <= element.clientWidth
+        && element.getBoundingClientRect().width <= parseFloat(element.style.maxWidth);
   }
 
   private get fixedFooter(): HTMLElement {