From 424839a0a81ed24be29b5b5a3dd50c52d6010d52 Mon Sep 17 00:00:00 2001 From: Andrew Gliga Date: Tue, 14 Jan 2025 08:44:55 -0800 Subject: [PATCH] feat(floating-ui): added support for floating-ui in dropdowns (#2523) --- dist/chips-combobox/chips-combobox.css | 2 +- dist/combobox/combobox.css | 9 +- .../filter-menu-button/filter-menu-button.css | 9 +- dist/listbox-button/listbox-button.css | 9 +- dist/menu-button/menu-button.css | 10 +- package-lock.json | 16 +-- package.json | 2 +- src/routes/main.js | 129 +++++++++++++++++- src/sass/chips-combobox/chips-combobox.scss | 2 +- .../stories/chips-combobox.stories.js | 16 +-- src/sass/combobox/stories/combobox.stories.js | 22 +-- .../filter-menu-button.scss | 12 +- .../stories/filter-menu-button.stories.js | 16 +-- .../listbox-button/stories/base.stories.js | 20 +-- .../listbox-button/stories/cascade.stories.js | 6 +- .../stories/dimensions.stories.js | 2 +- .../stories/error/error.base.stories.js | 16 +-- .../stories/error/error.cascade.stories.js | 6 +- .../stories/error/error.dimensions.stories.js | 2 +- .../stories/error/error.form.stories.js | 4 +- .../listbox-button/stories/form.stories.js | 6 +- .../stories/subtitle.stories.js | 16 +-- src/sass/menu-button/stories/base.stories.js | 30 ++-- .../menu-button/stories/cascade.stories.js | 6 +- .../menu-button/stories/dimensions.stories.js | 2 +- .../stories/fake-menu-button.stories.js | 8 +- src/sass/mixins/private/_dropdown-mixins.scss | 11 +- .../stories/phone-input.stories.js | 18 +-- 28 files changed, 273 insertions(+), 134 deletions(-) diff --git a/dist/chips-combobox/chips-combobox.css b/dist/chips-combobox/chips-combobox.css index 98ce4ea2f..2a448d0d3 100644 --- a/dist/chips-combobox/chips-combobox.css +++ b/dist/chips-combobox/chips-combobox.css @@ -60,7 +60,7 @@ transition: transform 0.2s linear; } -.chips-combobox .combobox__listbox { +.chips-combobox .combobox__listbox--set-position { top: calc(100% + var(--spacing-150)); } diff --git a/dist/combobox/combobox.css b/dist/combobox/combobox.css index 409dc1799..95129242d 100644 --- a/dist/combobox/combobox.css +++ b/dist/combobox/combobox.css @@ -38,12 +38,17 @@ span.combobox { display: none; left: 0; max-height: 400px; - min-width: 100%; overflow-y: auto; position: absolute; + top: 0; + width: -moz-fit-content; + width: fit-content; + z-index: 2; +} +.combobox__listbox--set-position { + min-width: 100%; top: calc(100% + 4px); width: auto; - z-index: 2; } .combobox__listbox--reverse, diff --git a/dist/filter-menu-button/filter-menu-button.css b/dist/filter-menu-button/filter-menu-button.css index c7576a688..2ac9fcb9e 100644 --- a/dist/filter-menu-button/filter-menu-button.css +++ b/dist/filter-menu-button/filter-menu-button.css @@ -152,15 +152,18 @@ button.filter-menu-button__button[aria-pressed="true"][disabled]:hover { border-radius: 16px; box-shadow: var(--bubble-shadow); display: none; - max-width: 288px; min-width: 144px; overflow: hidden; position: absolute; - top: calc(100% + 8px); + top: 0; width: max-content; z-index: 1; } +.filter-menu-button__menu--set-position { + top: calc(100% + 8px); +} + button.filter-menu-button__button[aria-expanded="true"] + .filter-menu-button__menu { display: block; @@ -168,7 +171,7 @@ button.filter-menu-button__button[aria-expanded="true"] .filter-menu-button__items { margin-top: 8px; - max-height: calc(50vh - 40px); + max-height: 400px; min-width: 100%; overflow-y: auto; position: relative; diff --git a/dist/listbox-button/listbox-button.css b/dist/listbox-button/listbox-button.css index f91f6665c..fa8a73018 100644 --- a/dist/listbox-button/listbox-button.css +++ b/dist/listbox-button/listbox-button.css @@ -37,12 +37,17 @@ div.listbox-button__listbox { display: none; left: 0; max-height: 400px; - min-width: 100%; overflow-y: auto; position: absolute; + top: 0; + width: -moz-fit-content; + width: fit-content; + z-index: 2; +} +div.listbox-button__listbox--set-position { + min-width: 100%; top: calc(100% + 4px); width: auto; - z-index: 2; } [dir="rtl"] div.listbox-button__listbox { left: unset; diff --git a/dist/menu-button/menu-button.css b/dist/menu-button/menu-button.css index f11bd65a9..16fb104b7 100644 --- a/dist/menu-button/menu-button.css +++ b/dist/menu-button/menu-button.css @@ -23,13 +23,19 @@ display: none; left: 0; max-height: 400px; - min-width: 100%; outline: 0; overflow-y: auto; position: absolute; + top: 0; + width: -moz-fit-content; + width: fit-content; + z-index: 2; +} +.fake-menu-button__menu--set-position, +.menu-button__menu--set-position { + min-width: 100%; top: calc(100% + 4px); width: auto; - z-index: 2; } [dir="rtl"] .fake-menu-button__menu, [dir="rtl"] .menu-button__menu { diff --git a/package-lock.json b/package-lock.json index 0dfbbbe50..3e9eb16d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", "@ebay/browserslist-config": "^2.10.0", - "@floating-ui/dom": "^1.6.12", + "@floating-ui/dom": "^1.6.13", "@marko/run": "^0.5.13", "@marko/run-adapter-static": "^0.2.1", "@percy/cli": "^1.30.5", @@ -2871,19 +2871,19 @@ } }, "node_modules/@floating-ui/dom": { - "version": "1.6.12", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", - "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", "dev": true, "dependencies": { "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.8" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", - "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "dev": true }, "node_modules/@gulpjs/messages": { diff --git a/package.json b/package.json index 969d1ae82..42f3f7525 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@commitlint/cli": "^19.6.1", "@commitlint/config-conventional": "^19.6.0", "@ebay/browserslist-config": "^2.10.0", - "@floating-ui/dom": "^1.6.12", + "@floating-ui/dom": "^1.6.13", "@marko/run": "^0.5.13", "@marko/run-adapter-static": "^0.2.1", "@percy/cli": "^1.30.5", diff --git a/src/routes/main.js b/src/routes/main.js index f35266924..7ae180479 100644 --- a/src/routes/main.js +++ b/src/routes/main.js @@ -41,6 +41,7 @@ import { flip, computePosition, shift, + size, offset, arrow, inline, @@ -82,6 +83,60 @@ const debounce = (func, wait) => { }; }; +class PopperDropdown { + constructor(widgetEl, hostSelector, overlaySelector) { + this.el = widgetEl; + if (!hostSelector) { + this.host = widgetEl; + } else { + this.host = widgetEl.querySelector(hostSelector); + } + this.overlay = widgetEl.querySelector(overlaySelector); + + if (this.host && this.overlay) { + this.isInitialized = true; + } + } + + attachEvents() { + this.el.addEventListener("expander-expand", () => { + this.show(); + }); + this.el.addEventListener("expander-collapse", () => { + this.hide(); + }); + } + + init() { + this.cleanup = autoUpdate( + this.host, + this.overlay, + this.update.bind(this), + ); + } + + update() { + if (this.isInitialized) { + computePosition(this.host, this.overlay, { + placement: "bottom-start", + middleware: [offset(4), flip(), shift()], + }).then(({ x, y }) => { + Object.assign(this.overlay.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + } + } + + show() { + this.init(); + } + hide() { + if (this.cleanup) this.cleanup(); + } +} + // BUSY BUTTON document.getElementById("busy-button")?.addEventListener("click", function () { const button = this; @@ -132,17 +187,31 @@ document.querySelectorAll(".expand-btn").forEach(function (el) { }); }); -document - .querySelectorAll(".filter-menu-button--form button") - .forEach(function (el) { - el.addEventListener("click", function () { - const isExpanded = this.getAttribute("aria-expanded") === "true"; - this.setAttribute("aria-expanded", !isExpanded); - }); +document.querySelectorAll(".filter-menu-button--form").forEach(function (el) { + const popperDropdown = new PopperDropdown( + el, + "button", + ".filter-menu-button__menu", + ); + el.querySelector("button").addEventListener("click", function () { + const isExpanded = this.getAttribute("aria-expanded") === "true"; + this.setAttribute("aria-expanded", !isExpanded); + if (isExpanded) { + popperDropdown.hide(); + } else { + popperDropdown.show(); + } }); +}); // FAKE MENU BUTTON document.querySelectorAll(".fake-menu-button").forEach(function (widgetEl) { + let popperDropdown = new PopperDropdown( + widgetEl, + "button", + ".fake-menu-button__menu", + ); + let hostSelector = ".icon-btn"; if (widgetEl.querySelector(".expand-btn")) { hostSelector = ".expand-btn"; @@ -160,12 +229,21 @@ document.querySelectorAll(".fake-menu-button").forEach(function (widgetEl) { hostSelector, }), ); + popperDropdown.attachEvents(); }); // COMBOBOX document.querySelectorAll(".combobox").forEach(function (widgetEl) { pageWidgets.push(new Combobox(widgetEl)); + if (!widgetEl.parentElement.classList.contains("chips-combobox")) { + let popperDropdown = new PopperDropdown( + widgetEl, + "input", + ".combobox__listbox", + ); + popperDropdown.attachEvents(); + } widgetEl.addEventListener("makeup-combobox-change", logEvent); }); @@ -557,6 +635,12 @@ document.querySelectorAll(".listbox-button").forEach(function (widgetEl) { return; } + let popperDropdown = new PopperDropdown( + widgetEl, + "button", + ".listbox-button__listbox", + ); + const options = { autoSelect: widgetEl.dataset.makeupAutoSelect === "true", buttonLabelSelector: ".btn__text", @@ -567,6 +651,7 @@ document.querySelectorAll(".listbox-button").forEach(function (widgetEl) { pageWidgets.push(new ListboxButton(widgetEl, options)); + popperDropdown.attachEvents(); widgetEl.addEventListener("makeup-listbox-button-change", (e) => { console.log(e.type, e.detail); }); @@ -585,6 +670,12 @@ document pageWidgets.push(new ListboxButton(widgetEl, options)); + let popperDropdown = new PopperDropdown( + widgetEl, + "button", + ".listbox-button__listbox", + ); + widgetEl.addEventListener("makeup-listbox-button-change", (e) => { console.log(e.type, e.detail); const selectedOption = widgetEl.querySelector( @@ -595,14 +686,24 @@ document ).textContent = `+${selectedOption.querySelector("span.fflag")?.dataset.countryCode}`; }); + + popperDropdown.attachEvents(); }); document.querySelectorAll(".menu-button").forEach(function (widgetEl) { + const popperDropdown = new PopperDropdown( + widgetEl, + "button", + ".menu-button__menu", + ); + const widget = new MenuButton(widgetEl, { menuSelector: ".menu-button__menu", buttonTextSelector: `.btn__text`, }); + popperDropdown.attachEvents(); + widget.menu.el.addEventListener("makeup-menu-select", logEvent); widget.menu.el.addEventListener("makeup-menu-change", logEvent); }); @@ -614,9 +715,16 @@ document expandedClass: "filter-menu-button--expanded", menuSelector: ".filter-menu-button__menu", }); + const popperDropdown = new PopperDropdown( + widgetEl, + "button", + ".filter-menu-button__menu", + ); widget.menu.el.addEventListener("makeup-menu-select", logEvent); widget.menu.el.addEventListener("makeup-menu-change", logEvent); + + popperDropdown.attachEvents(); }); document.querySelectorAll(".menu").forEach(function (widgetEl) { @@ -859,6 +967,13 @@ document.querySelectorAll(".field").forEach(function (elCharContainer) { document .querySelectorAll(".chips-combobox") .forEach(function (elChipsCombobox) { + let popperDropdown = new PopperDropdown( + elChipsCombobox, + null, + ".combobox__listbox", + ); + popperDropdown.attachEvents(); + const elChipsItems = elChipsCombobox.querySelector( ".chips-combobox__items", ), diff --git a/src/sass/chips-combobox/chips-combobox.scss b/src/sass/chips-combobox/chips-combobox.scss index f7313b23a..7bdb1fec2 100644 --- a/src/sass/chips-combobox/chips-combobox.scss +++ b/src/sass/chips-combobox/chips-combobox.scss @@ -62,7 +62,7 @@ transition: transform 200ms linear; } -.chips-combobox .combobox__listbox { +.chips-combobox .combobox__listbox--set-position { top: calc(100% + var(--spacing-150)); } diff --git a/src/sass/chips-combobox/stories/chips-combobox.stories.js b/src/sass/chips-combobox/stories/chips-combobox.stories.js index 61b773289..5ba45ad13 100644 --- a/src/sass/chips-combobox/stories/chips-combobox.stories.js +++ b/src/sass/chips-combobox/stories/chips-combobox.stories.js @@ -10,7 +10,7 @@ export const empty = () => ` -
+
Soccer @@ -37,7 +37,7 @@ export const fluid = () => ` -
+
Soccer @@ -95,7 +95,7 @@ export const prefilled = () => ` -
+
Soccer @@ -273,7 +273,7 @@ export const manyChips = () => ` -
+
Soccer @@ -331,7 +331,7 @@ export const expanded = () => ` -
+
Soccer @@ -389,7 +389,7 @@ export const textSpacing = () => ` -
+
Soccer @@ -447,7 +447,7 @@ export const disabledState = () => ` -
+
Soccer @@ -505,7 +505,7 @@ export const errorState = () => ` -
+
Soccer diff --git a/src/sass/combobox/stories/combobox.stories.js b/src/sass/combobox/stories/combobox.stories.js index e7c08ad72..42b60f5fb 100644 --- a/src/sass/combobox/stories/combobox.stories.js +++ b/src/sass/combobox/stories/combobox.stories.js @@ -8,7 +8,7 @@ export const collapsed = () => ` -
+
Option 1 @@ -29,7 +29,7 @@ export const expanded = () => ` -
+
Option 1 @@ -50,7 +50,7 @@ export const textSpacing = () => ` -
+
Option 1 @@ -71,7 +71,7 @@ export const disabled = () => ` -
+
Option 1 @@ -92,7 +92,7 @@ export const large = () => ` -
+
Option 1 @@ -113,7 +113,7 @@ export const longOptions = () => ` -
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. @@ -136,7 +136,7 @@ export const actionable = () => ` -
+
Option 1 @@ -161,7 +161,7 @@ export const RTL = () => ` -
+
Option 1 @@ -186,7 +186,7 @@ export const actionableRTL = () => ` -
+
Option 1 @@ -212,7 +212,7 @@ export const inheritColour = () => ` -
+
Option 1 @@ -235,7 +235,7 @@ export const inheritFontSize = () => ` -
+
Option 1 diff --git a/src/sass/filter-menu-button/filter-menu-button.scss b/src/sass/filter-menu-button/filter-menu-button.scss index 9a9a4c227..5f4ef6a0f 100644 --- a/src/sass/filter-menu-button/filter-menu-button.scss +++ b/src/sass/filter-menu-button/filter-menu-button.scss @@ -126,15 +126,18 @@ button.filter-menu-button__button[aria-pressed="true"][aria-disabled="true"]:hov border-radius: 16px; box-shadow: var(--bubble-shadow); display: none; - max-width: 288px; /* TODO remove max-width restriction to filter-menu styles */ min-width: 144px; overflow: hidden; position: absolute; - top: calc(100% + 8px); + top: 0; width: max-content; z-index: 1; } +.filter-menu-button__menu--set-position { + top: calc(100% + 8px); +} + button.filter-menu-button__button[aria-expanded="true"] + .filter-menu-button__menu { display: block; @@ -142,10 +145,7 @@ button.filter-menu-button__button[aria-expanded="true"] .filter-menu-button__items { margin-top: 8px; - max-height: calc( - 50vh - 40px - ); /* half of the viewport height || TODO remove max-height restriction to filter-menu styles */ - + max-height: 400px; min-width: 100%; overflow-y: auto; position: relative; diff --git a/src/sass/filter-menu-button/stories/filter-menu-button.stories.js b/src/sass/filter-menu-button/stories/filter-menu-button.stories.js index 9c493cb1a..61ecbc066 100644 --- a/src/sass/filter-menu-button/stories/filter-menu-button.stories.js +++ b/src/sass/filter-menu-button/stories/filter-menu-button.stories.js @@ -10,7 +10,7 @@ export const collapsed = () => ` -
+