${label}
` + ); +} + +function separator() { + return createElementFromHtml( + `` + ); +} + +function placeholder(label) { + return createElementFromHtml( + `${label}
` + ); +} + +export default { + dropdown, + menuItem, + heading, + separator, + placeholder, +}; diff --git a/war/src/main/js/components/dropdowns/utils.js b/war/src/main/js/components/dropdowns/utils.js new file mode 100644 index 0000000000..709ad7fdfe --- /dev/null +++ b/war/src/main/js/components/dropdowns/utils.js @@ -0,0 +1,135 @@ +import Templates from "@/components/dropdowns/templates"; +import makeKeyboardNavigable from "@/util/keyboard"; +import tippy from "tippy.js"; +import behaviorShim from "@/util/behavior-shim"; + +const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected"; + +/* + * Generates the dropdowns for the given element + * Preloads the data on hover for speed + * @param element - the element to generate the dropdown for + * @param callback - called to retrieve the list of dropdown items + */ +function generateDropdown(element, callback) { + tippy( + element, + Object.assign({}, Templates.dropdown(), { + onCreate(instance) { + instance.reference.addEventListener("mouseenter", () => { + if (instance.loaded) { + return; + } + + instance.popper.addEventListener("click", () => { + instance.hide(); + }); + + callback(instance); + }); + }, + }) + ); +} + +/* + * Generates the contents for the dropdown + */ +function generateDropdownItems(items) { + const menuItems = document.createElement("div"); + menuItems.classList.add("jenkins-dropdown"); + + items + .map((item) => { + if (item.type === "HEADER") { + return Templates.heading(item.label); + } + + if (item.type === "SEPARATOR") { + return Templates.separator(); + } + + const menuItem = Templates.menuItem(item); + + if (item.subMenu != null) { + tippy( + menuItem, + Object.assign({}, Templates.dropdown(), { + content: generateDropdownItems(item.subMenu()), + trigger: "mouseenter", + placement: "right-start", + offset: [-8, 0], + }) + ); + } + + return menuItem; + }) + .forEach((item) => menuItems.appendChild(item)); + + if (items.length === 0) { + menuItems.appendChild(Templates.placeholder("No items")); + } + + makeKeyboardNavigable( + menuItems, + () => menuItems.querySelectorAll(".jenkins-dropdown__item"), + SELECTED_ITEM_CLASS, + (selectedItem, key) => { + switch (key) { + case "ArrowLeft": { + const root = selectedItem.closest("[data-tippy-root]"); + if (root) { + const tippyReference = root._tippy; + if (tippyReference) { + tippyReference.hide(); + } + } + break; + } + case "ArrowRight": { + const tippyRef = selectedItem._tippy; + if (!tippyRef) { + break; + } + + tippyRef.show(); + tippyRef.props.content + .querySelector(".jenkins-dropdown__item") + .classList.add(SELECTED_ITEM_CLASS); + break; + } + } + }, + (container) => { + const isVisible = + window.getComputedStyle(container).visibility === "visible"; + const isLastDropdown = Array.from( + document.querySelectorAll(".jenkins-dropdown") + ) + .filter((dropdown) => container !== dropdown) + .filter( + (dropdown) => + window.getComputedStyle(dropdown).visibility === "visible" + ) + .every( + (dropdown) => + !( + container.compareDocumentPosition(dropdown) & + Node.DOCUMENT_POSITION_FOLLOWING + ) + ); + + return isVisible && isLastDropdown; + } + ); + + behaviorShim.applySubtree(menuItems); + + return menuItems; +} + +export default { + generateDropdown, + generateDropdownItems, +}; diff --git a/war/src/main/js/util/behavior-shim.js b/war/src/main/js/util/behavior-shim.js index 8154cf163b..273efd6670 100644 --- a/war/src/main/js/util/behavior-shim.js +++ b/war/src/main/js/util/behavior-shim.js @@ -3,4 +3,9 @@ function specify(selector, id, priority, behavior) { Behaviour.specify(selector, id, priority, behavior); } -export default { specify }; +function applySubtree(startNode, includeSelf) { + // eslint-ignore-next-line + Behaviour.applySubtree(startNode, includeSelf); +} + +export default { specify, applySubtree }; diff --git a/war/src/main/js/util/keyboard.js b/war/src/main/js/util/keyboard.js index 4b3e21e6b5..9a2168799d 100644 --- a/war/src/main/js/util/keyboard.js +++ b/war/src/main/js/util/keyboard.js @@ -2,26 +2,29 @@ * @param {Element} container - the container for the items * @param {function(): NodeListOf