diff --git a/CHANGELOG.MD b/CHANGELOG.MD index da30eb07..28629065 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -6,6 +6,7 @@ Please make sure to read changes before updating, as we're undergoing some break ## Unreleased +- Keep all top navigation menus reachable on smaller screens and embedded reports, with an automatic drawer and scrollable dropdowns. - Keep section Show/Hide controls working in embedded reports with nonce-based script policies, for Online and offline reports. - Apply table word breaking through valid CSS and allow the first column to wrap when WordBreak is selected. diff --git a/Resources/CSS/jquery.hsmenu-dropdown.css b/Resources/CSS/jquery.hsmenu-dropdown.css index 20f72f58..c65eae4a 100644 --- a/Resources/CSS/jquery.hsmenu-dropdown.css +++ b/Resources/CSS/jquery.hsmenu-dropdown.css @@ -7,312 +7,186 @@ License: MIT License Copyright (c) 2018 - Asif Mughal */ - -.dropdown-items, .dropdown-heading, .menu-items, .menu-subitems { - margin: 0; - padding: 0 -} - -.codehim-dropdown { - font-size: 16px; - font-style: normal; - font-weight: 300; - font-smooth: antialiased; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - .codehim-dropdown { - width: 100%; - height: 50px; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; - transition: 0s; + width: 100%; + height: 50px; + box-sizing: border-box; + display: flex; + align-items: center; + font-size: 16px; + font-style: normal; + font-weight: 300; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-tap-highlight-color: transparent; + position: relative; } - .codehim-dropdown.sticky { - position: fixed; - top: 0; - left: 0; - z-index: 999; - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5); - -webkit-box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5); -} - -/* Global Style for Codehim Dropdowns */ - -.codehim-dropdown a { - outline: 0; -} - -.has-child span.parent+ul, .menu-items { - background: #fff; - color: black; - list-style: none; - display: none; -} - -.dropdown-heading:after { - font-family: "Font Awesome 5 Free"; - font-weight: 900; - display: inline-block; - content: "\f107"; -} - -.dropdown-heading.active:after { - transform: rotateZ(180deg); - -webkit-transform: rotateZ(180deg); - -moz-transform: rotateZ(180deg); -} - -.dropdown-heading, .dropdown-heading.active, .dropdown-heading:hover, .dropdown-heading:after, .dropdown-heading.active:after { - transition: all .3s ease-in-out; - -webkit-transition: all .3s ease-in-out; - -moz-transition: all .3s ease-in-out; -} - -/* Home Link Style */ - -li.home-link a { - text-decoration: none; - /* color: rgba(255, 255, 255, 0.7); */ - text-align: center; - font-size: 20px; - display: inline-block; - width: 45px; - height: 50px; - background: rgba(0, 0, 0, 0.3); - line-height: 50px; -} - -/* Dim background effect */ - -.dim-overlay { - display: none; -} - -.dim-overlay:before { - content: ""; - background-color: rgba(0, 0, 0, .5); - height: 100%; - /* for zombies browsers */ - height: 100vh; - left: 0; - position: fixed; - top: 0; - width: 100%; - overflow: hidden; - z-index: 2; -} - -.menu-items li a { - display: block; - padding: 10px; - color: #444; - cursor: pointer; - text-decoration: none; - font-size: 13px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - -.has-child span.parent:hover, .menu-items li a:hover { - background: #e0e0e0; -} - -.has-child span.parent:after { - font-family: "Font Awesome 5 Free"; - float: right; - font-weight: 900; - content: "\f107"; - padding-top: 2px; -} - -.main-links { - text-decoration: none; -} - -/* Codehim Dropdowns Mobile View */ - -@media only screen and (max-width: 480px) { - .codehim-dropdown { - position: relative; - font-size: 14px; - } - .codehim-dropdown .icon { - margin: 0 10px 0 5px; - font-size: 16px; - } - /* The Off Canvas Menu */ - .dropdown-items { - height: 100%; - /* for zombies browsers */ - height: 100vh; - display: none; - overflow: auto; - box-sizing: border-box; - position: fixed; - top: 0; - z-index: 999; - } - .menu-items { - background: #fff; - } - .menu-items li a { - padding-left: 15px; - background: transparent; - } - .dropdown-items li { - border-bottom: 1px dotted rgba(255, 255, 255, 0.16); - overflow: hidden; - } - /* hamburger menu icon to reveal off canvas menu on mobile devices */ - .hamburger { - position: absolute; - width: 32px; - height: 32px; - top: 8px; - border-radius: 50%; - cursor: pointer; - -webkit-tap-highlight-color: transparent; - text-align: center; - color: rgba(255, 255, 255, 0.8); - font-size: 20px; - line-height: 32px; - } - .hamburger:hover { - background: rgba(0, 0, 0, 0.15); - } - .hamburger:after { - font-family: "Font Awesome 5 Free"; - content: "\f0c9"; - font-weight: 900; - } - .hamburger.left { - left: 10px; - } - .hamburger.right { - right: 10px; - } - .dropdown-items .parent+ul { - background: #fff; - margin: 0; - box-sizing: border-box; - display: none; - } - /* Sub Menu list item */ - .dropdown-items .parent+ul li a { - padding-left: 30px; - } - .main-links, .dropdown-heading { - display: block; - padding: 15px; - /* color: rgba(255, 255, 255, 0.8); */ - cursor: pointer; - } - .main-links:hover, .dropdown-heading:hover { - background: rgba(0, 0, 0, 0.15); - color: #fff; - } - .dropdown-heading:after { - margin-left: 8px; - font-family: "Font Awesome 5 Free"; - font-weight: 900; - display: inline-block; - content: "\f107"; - } - /* Sub Dropdowns heading */ - .has-child span.parent { - padding: 15px; - border-bottom: 1px solid #e0e0e0; - display: block; - background: transparent; - } - .has-child span.parent:after { - content: "\f107"; - margin-right: 15px; - font-weight: 700; - } -} - -/* Codehim Dropdowns PC View */ - -@media only screen and (min-width: 480px) { - .menu-items { - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); - /* Setting thru PowerShell - width: 150px; - */ - position: absolute; - left: 2px; - } - .menu-items li {} - .dropdown-items { - list-style: none; - } - /* A list of main items */ - .dropdown-items li { - display: inline; - position: relative; - } - a.main-links, .dropdown-heading { - cursor: pointer; - /* color: rgba(255, 255, 255, 0.7); */ - display: inline-block; - padding: 6px; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - font-weight: 400; - text-decoration: none; - } - .main-links:hover, .dropdown-heading.active, .dropdown-heading:hover { - color: #fff; - } - .dropdown-heading:after { - margin-left: 8px; - } - .has-child span.parent:after { - content: "\f105"; - } - /* Multilevel Dropdown */ - .has-child { - position: relative; - } - /* The visible heading (parent) for multilevel dropdown items */ - .has-child span.parent { - display: inline-block; - width: 100%; - box-sizing: border-box; - padding: 10px; - font-size: 13px; - font-weight: 400; - } - .has-child ul { - position: absolute; - display: block; - /* Setting thru PowerShell - width: 140px; - */ - left: 100%; - z-index: 3; - top: 0; - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); - display: none; - } -} - -/* Logo functionality, may need different placement */ - -.dropdown-logo { - list-style: none; + position: fixed; + top: 0; + left: 0; + z-index: 999; + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5); +} +.codehim-dropdown ul { + list-style: none; + margin: 0; + padding: 0; +} +.codehim-dropdown .dropdown-items { + display: flex; + align-items: center; + flex: 0 0 auto; + white-space: nowrap; +} +.codehim-dropdown .dropdown-items > li { flex: 0 0 auto; } +.codehim-dropdown .menu-items, +.codehim-dropdown .menu-subitems { + display: none; + background: #fff; + color: black; + box-sizing: border-box; +} +.codehim-dropdown .dropdown-heading:after, +.codehim-dropdown .parent:after { + font-family: "Font Awesome 5 Free"; + font-weight: 900; + content: "\f107"; + display: inline-block; + margin-left: 8px; +} +.codehim-dropdown .dropdown-heading.active:after { transform: rotate(180deg); } +.codehim-dropdown .main-links, +.codehim-dropdown .dropdown-heading { + display: inline-block; + padding: 6px; + cursor: pointer; + font-weight: 400; + text-decoration: none; +} +.codehim-dropdown .dropdown-heading:hover, +.codehim-dropdown .dropdown-heading.active, +.codehim-dropdown .main-links:hover { color: #fff; } +.codehim-dropdown a.dropdown-heading { color: inherit; } +.codehim-dropdown .home-link a { + text-decoration: none; + text-align: center; + font-size: 20px; + display: inline-block; + width: 45px; + height: 50px; + line-height: 50px; +} +.codehim-dropdown .menu-items a, +.codehim-dropdown .has-child > .parent { + display: block; + box-sizing: border-box; + padding: 10px; + color: #444; + cursor: pointer; + text-decoration: none; + font-size: 13px; + font-weight: 400; + white-space: normal; + overflow-wrap: anywhere; +} +.codehim-dropdown .has-child > .parent:hover, +.codehim-dropdown .menu-items a:hover { background: #e0e0e0; } +.codehim-dropdown a:focus-visible, +.codehim-dropdown [role=button]:focus-visible, +.codehim-dropdown button:focus-visible { + outline: 2px solid currentColor; + outline-offset: -3px; +} +.codehim-dropdown .home-logo { + margin-left: auto; + padding-right: 15px; + flex: 0 0 auto; + max-width: calc(100% - 60px); +} +.codehim-dropdown .home-logo img { + display: block; + width: auto; + max-width: 100%; + height: 50px; + object-fit: contain; +} +.codehim-dropdown .hamburger[hidden] { display: none; } +.codehim-dropdown .hamburger { + display: none; + border: 0; + padding: 0; + background: transparent; + color: inherit; + font-size: 20px; + cursor: pointer; + width: 40px; + height: 40px; +} +.codehim-dropdown .hamburger:after { content: "\2630"; } +.codehim-dropdown .hamburger.active:after { content: "\00d7"; } +.codehim-dim-overlay { + display: none; + position: fixed; + inset: 50px 0 0; + background: rgba(0, 0, 0, .5); + z-index: 998; +} +/* The script selects compact mode from the measured menu and logo width. */ +.codehim-dropdown.is-compact .hamburger { + display: block; + position: absolute; + top: 5px; +} +.codehim-dropdown.is-compact .hamburger.left { left: 5px; } +.codehim-dropdown.is-compact .hamburger.right { right: 5px; } +.codehim-dropdown.is-compact .hamburger.right ~ .home-logo { + margin-left: 15px; + margin-right: auto; +} +.codehim-dropdown.is-compact .dropdown-items { + display: block; + position: fixed; + top: 50px; + bottom: 0; + max-width: 100%; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + white-space: normal; +} +.codehim-dropdown.is-compact .dropdown-items > li { + display: block; + border-bottom: 1px dotted rgba(255, 255, 255, 0.16); +} +.codehim-dropdown.is-compact .dropdown-heading, +.codehim-dropdown.is-compact .main-links { + display: block; + padding: 15px; +} +.codehim-dropdown.is-compact .menu-items, +.codehim-dropdown.is-compact .menu-subitems { + position: static; + width: 100%; +} +.codehim-dropdown.is-compact .menu-items a { padding-left: 15px; } +.codehim-dropdown.is-compact .menu-subitems { padding-left: 15px; } +.codehim-dropdown.is-compact .has-child > .parent { padding: 15px; } +/* Desktop dropdowns stay inside the frame, including tall and nested menus. */ +.codehim-dropdown:not(.is-compact) .menu-items, +.codehim-dropdown:not(.is-compact) .menu-subitems { + position: fixed; + max-width: calc(100vw - 16px); + overflow-y: auto; + overscroll-behavior: contain; + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); +} +.codehim-dropdown:not(.is-compact) .parent:after { + float: right; + content: "\f105"; +} +/* Animated list transforms must not become containing blocks for fixed flyouts. */ +.codehim-dropdown .menu-items > li.animated, +.codehim-dropdown .menu-subitems > li.animated { + animation-fill-mode: none; } - -.home-logo img { - width: auto; - height: 50px; - position: fixed; - top: 0px; - right: 0px; - padding-right: 15px; -} \ No newline at end of file diff --git a/Resources/JS/jquery.hsmenu-dropdown.js b/Resources/JS/jquery.hsmenu-dropdown.js index d2ff6795..70251016 100644 --- a/Resources/JS/jquery.hsmenu-dropdown.js +++ b/Resources/JS/jquery.hsmenu-dropdown.js @@ -7,337 +7,173 @@ * Copyright (c) 2018 - Asif Mughal */ (function ($) { - $.fn.CodehimDropdown = function (options) { - var setting = $.extend({ - skin: { - "background-color": "#161B22", - "color": 'green' - }, //skin color for menubar and drawer - skinHome: { - "background-color": "#161B22", - "color": 'red' - }, //skin color for home icon and background - slideUpOther: true, //close other opened dropdowns if user trigger new one - sticky: true, //fixed menubar on top, false to static - /* Some settings for mobile devices (small screen) when horizontal menubar converted into offcanvas menu */ - listAnimation: "fadeInUp", //Animate list items when revealed - subListAnimation: "fadeInUp", //Animate sub items when revealed - //@info: any animation can be used, put the name of animation class name from animate.css in the above options - dimOverlay: true, //possible options true or false - offCanvasSpeed: "default", //possible options are default, fast, faster, slow and slower - offCanvasDirection: "left", //possible options are left and right - offCanvasWidth: 290, //the width of drawer on mobile devices in pixels - onOffCanvasOpen: function () { }, //your custom function to execute when menu open on mobile devices - onOffCanvasClose: function () { }, //your custom function to execute when menu closed - - }, options); - - return this.each(function () { - - var target = $(this); - var devWidth = $(window).width(); //available width of the device - var devHeight = $(window).height(); //available height of the device - var dimOverlay = $div(); //creates div element for dim overlay - var hamburger = $div(); //creates div element for hamburger menu icon - var offCanvas = $(target).find(".dropdown-items"); //looks on (small screen) mobile devices - var homeLogo = $(target).find("li.home-link a"); // logo - - - //Applying Customization - - if (setting.offCanvasDirection == "right") { - $(offCanvas).css({ - 'right': -(setting.offCanvasWidth), - 'display': 'block', - }); - - $(hamburger).addClass("hamburger right"); - } else { - $(offCanvas).css({ - 'left': -(setting.offCanvasWidth), - 'display': 'block', - }); - $(hamburger).addClass("hamburger left"); - } - - if (setting.dimOverlay == true) { - $(dimOverlay).addClass("dim-overlay").insertAfter(target); - - } - - //$(target).addClass(setting.skin).prepend(hamburger); - $(target).css(setting.skin).prepend(hamburger); - $(offCanvas).css(setting.skin); - $(homeLogo).css(setting.skinHome); - //$(offCanvas).addClass(setting.skin); - - - //OffCanvas Speed - var speed = setting.offCanvasSpeed; - //assign values (in milliseconds) to speed - switch (speed) { - case "slow": - speed = 1000; - - break; - case "slower": - - speed = 2000; - break; - - case "fast": - - speed = 300; - break; - - case "faster": - - speed = 150; - break; - - default: - speed = 400; - }; - - if (setting.sticky == true) { - $(target).addClass("sticky"); - - $("main").css({ - 'marginTop': 50, - }); - } - - - $(".dropdown-heading").click(function () { - - var thisMenu = $(this).find(" + .menu-items"); - - var thisList = $(thisMenu).children("li"); - - for (var i = 0; i < thisList.length; i++) { - - $(thisList).eq(i).addClass("animated" + " " + setting.listAnimation).css({ - 'animationDelay': "0." + i + "s", - - }); - - } - - - $(this).toggleClass("active"); - - - $(thisMenu).slideToggle(); - - - //close other dropdowns if opened - - if (setting.slideUpOther == true) { - - $(".menu-items").not(thisMenu).slideUp(); - $(".dropdown-heading").not(this).removeClass("active"); - } - - }); - - - $(".parent").on("click", function () { - - var subMenu = $(this).siblings("ul"); - - $(subMenu).slideToggle(); - - var thisList = $(subMenu).children("li"); - - for (var i = 0; i < thisList.length; i++) { - - $(thisList).eq(i).addClass("animated" + " " + setting.subListAnimation).css({ - 'animationDelay': "0." + i + "s", - - }); - - } - //close other sub items if opened - $(".has-child ul").not(subMenu).not($(this).parents("ul")).slideUp(); - - }); - - - // Mobile Design - - $(hamburger).click(function () { - $(this).toggleClass("active"); - - openMenu(); - - $(dimOverlay).fadeIn(); - - - }); - - var offCanvasDirection = setting.offCanvasDirection; - - //Open Menu - function openMenu() { - - switch (offCanvasDirection) { - case "left": - $(offCanvas).animate({ - 'left': 0, - - }, speed, function () { - - setting.onOffCanvasOpen(); - - }); - - break; - - case "right": - - $(offCanvas).animate({ - 'right': 0, - - }, speed, function () { - - setting.onOffCanvasOpen(); - - }); - - break; - - default: - alert("you have missing the off canvas direction"); - - - }; - - } //end open menu - - - //Close Menu - function closeMenu() { - - switch (offCanvasDirection) { - case "left": - $(offCanvas).animate({ - 'left': -(setting.offCanvasWidth), - - }, speed, function () { - - $(hamburger).removeClass("active"); - setting.onOffCanvasClose(); - - }); - - break; - - case "right": - - $(offCanvas).animate({ - 'right': -(setting.offCanvasWidth), - - }, speed, function () { - - $(hamburger).removeClass("active"); - setting.onOffCanvasClose(); - - }); - - break; - - default: - alert("you have missing the off canvas direction"); - - - }; - - } //end close menu - - - //close menu if user click outside of it - $(window).click(function (e) { - if ($(e.target).closest('.codehim-dropdown').length) { - return; - } - if ($(e.target).closest(offCanvas).length) { - return; - } - - //check if menu really opened - if ($(hamburger).hasClass("active")) { - - closeMenu(); - - } - - $(dimOverlay).fadeOut(); - - $(".menu-items").slideUp(); - $(".dropdown-heading").removeClass("active"); - - }); - - - // To get everything responsive - - responsive(); //responsive on document ready - - $(window).on("resize", function () { - - responsive(); - - - }); - - - function responsive(menuItemsWidth) { - - //to get the fresh device dimensions - devWidth = $(window).width(); - devHeight = $(window).height(); - - if (devWidth > 480) { - $(offCanvas).width('100%'); - - } else { - $(offCanvas).width(setting.offCanvasWidth); - } - - var mainLink = $(".dropdown-items").children("li"); - - //getting the width of main items shown in the menubar - menuItemsWidth = 0; - for (var w = -1; w < mainLink.length; ++w) { - - menuItemsWidth += $(mainLink).eq(w).width(); - - if (menuItemsWidth >= devWidth && devWidth > 480) { - $(mainLink).eq(w).css({ - 'display': 'none', - - }); - - } else { - $(mainLink).eq(w).css({ - 'display': '', //default - - }); - - } - - - }; - - } //reponsive - - - function $div() { - return document.createElement("div"); - } - - - }); - }; - -})(jQuery); \ No newline at end of file + $.fn.CodehimDropdown = function (options) { + var setting = $.extend({ + skin: { "background-color": "#161B22", "color": "green" }, + skinHome: { "background-color": "#161B22", "color": "red" }, + slideUpOther: true, + sticky: true, + listAnimation: "fadeInUp", + subListAnimation: "fadeInUp", + dimOverlay: true, + offCanvasSpeed: "default", + offCanvasDirection: "left", + offCanvasWidth: 290, + onOffCanvasOpen: function () { }, + onOffCanvasClose: function () { } + }, options); + + return this.each(function () { + var target = $(this); + if (target.data("codehim-ready")) { return; } + target.data("codehim-ready", true); + var offCanvas = target.children(".dropdown-items"); + var hamburger = $(''); + var dimOverlay = $('
'); + var headings = target.find(".dropdown-heading, .has-child > .parent"); + var compact = false; + var opened = false; + var direction = setting.offCanvasDirection === "right" ? "right" : "left"; + var speeds = { slow: 1000, slower: 2000, fast: 300, faster: 150, "default": 400 }; + var speed = speeds[setting.offCanvasSpeed] || speeds.default; + + target.css(setting.skin).prepend(hamburger); + offCanvas.css(setting.skin); + target.find("li.home-link a").css(setting.skinHome).attr("aria-label", "Home"); + hamburger.addClass(direction); + headings.attr("aria-expanded", "false"); + headings.not("a[href]").attr({ role: "button", tabindex: "0" }); + if (setting.dimOverlay) { dimOverlay.insertAfter(target); } + if (setting.sticky) { target.addClass("sticky"); } + + function closeDropdowns(menus) { + menus.stop(true, true).hide().prev(".dropdown-heading, .parent") + .removeClass("active").attr("aria-expanded", "false"); + } + + function setDrawer(open, restoreFocus) { + var changed = opened !== open; + opened = open; + hamburger.toggleClass("active", open).attr("aria-expanded", String(open)); + offCanvas.attr("aria-hidden", String(!open)).prop("inert", !open); + var position = {}; + position[direction] = open ? 0 : -offCanvas.outerWidth(); + offCanvas.stop(true, true).animate(position, speed); + dimOverlay.stop(true, true).toggle(open && setting.dimOverlay); + if (!open) { closeDropdowns(target.find(".menu-items, .menu-subitems")); } + if (restoreFocus) { hamburger.trigger("focus"); } + if (changed) { + if (open) { setting.onOffCanvasOpen(); } + else { setting.onOffCanvasClose(); } + } + } + + // Fixed dropdowns scroll independently without clipping their nested flyouts. + function positionMenu(menu) { + if (compact || !menu.is(":visible")) { return; } + var parent = menu.prev()[0].getBoundingClientRect(); + var nested = menu.hasClass("menu-subitems"); + var width = menu.outerWidth(); + var viewportWidth = document.documentElement.clientWidth; + var viewportHeight = document.documentElement.clientHeight; + var left = nested ? parent.right : parent.left; + if (nested && left + width > viewportWidth - 8) { left = parent.left - width; } + left = Math.max(8, Math.min(left, viewportWidth - width - 8)); + var top = nested ? Math.min(parent.top, viewportHeight - Math.min(menu[0].scrollHeight, viewportHeight - 16) - 8) : target[0].getBoundingClientRect().bottom; + top = Math.max(0, Math.min(top, viewportHeight - 48)); + menu.css({ left: left, top: top, "max-height": Math.max(0, viewportHeight - top - 8) }); + } + + headings.on("click", function () { + var heading = $(this); + var menu = heading.next("ul"); + var open = heading.attr("aria-expanded") !== "true"; + if (setting.slideUpOther || heading.hasClass("parent")) { + var siblings = heading.hasClass("parent") ? target.find(".menu-subitems") : target.find(".menu-items"); + closeDropdowns(siblings.not(menu).not(heading.parents("ul"))); + } + closeDropdowns(menu.find(".menu-subitems")); + heading.toggleClass("active", open).attr("aria-expanded", String(open)); + menu.stop(true, true).css("max-height", "").toggle(open); + if (open) { + heading.parents("li").removeClass("animated " + setting.listAnimation + " " + setting.subListAnimation); + positionMenu(menu); + var animation = heading.hasClass("parent") ? setting.subListAnimation : setting.listAnimation; + menu.children("li").addClass("animated " + animation); + } + }).on("keydown", function (event) { + if (event.key === " " || (event.key === "Enter" && !$(this).is("a[href]"))) { + event.preventDefault(); + $(this).trigger("click"); + } + }); + + hamburger.on("click", function () { setDrawer(!opened, false); }); + target.on("keydown", function (event) { + if (event.key !== "Escape") { return; } + if (compact && opened) { setDrawer(false, true); } + else { + var menu = $(event.target).closest(".menu-items, .menu-subitems"); + var heading = menu.length ? menu.prev() : $(event.target); + closeDropdowns(menu.length ? menu : target.find(".menu-items, .menu-subitems")); + heading.trigger("focus"); + } + }); + target.on("click", ".menu-items a, .home-link a", function () { + if (compact && opened) { setDrawer(false, true); } + else { closeDropdowns(target.find(".menu-items, .menu-subitems")); } + }); + $(document).on("click", function (event) { + if (target[0].contains(event.target)) { return; } + if (compact && opened) { setDrawer(false, false); } + closeDropdowns(target.find(".menu-items, .menu-subitems")); + }); + target.find(".menu-items, .menu-subitems").on("scroll", function () { + if (!compact) { + $(this).find(".menu-subitems").each(function () { positionMenu($(this)); }); + } + }); + + function responsive() { + var wasCompact = compact; + var hadFocus = target[0].contains(document.activeElement); + // Measure the unwrapped desktop row, including gaps, icons and the logo. + target.removeClass("is-compact"); + offCanvas.css({ width: "", left: "", right: "" }); + var available = target[0].clientWidth; + var logoWidth = target.children(".home-logo").outerWidth() || 0; + compact = available <= 480 || offCanvas[0].scrollWidth + logoWidth > available; + target.toggleClass("is-compact", compact); + hamburger.prop("hidden", !compact); + if (compact) { + offCanvas.css({ width: Math.min(setting.offCanvasWidth, available), left: "", right: "" }); + if (!wasCompact) { setDrawer(false, hadFocus); } + else { + var position = {}; + position[direction] = opened ? 0 : -offCanvas.outerWidth(); + offCanvas.stop(true, true).css(position); + } + } else { + if (wasCompact) { + setDrawer(false, false); + } + offCanvas.stop(true, true).css({ left: "", right: "" }).removeAttr("aria-hidden").prop("inert", false); + if (wasCompact && hadFocus) { offCanvas.find("a, [role=button]").first().trigger("focus"); } + } + target.find(".menu-items, .menu-subitems").css({ left: "", top: "", "max-height": "" }).each(function () { + positionMenu($(this)); + }); + } + + responsive(); + $(window).on("resize", responsive).on("scroll", function () { + if (!compact) { closeDropdowns(target.find(".menu-items, .menu-subitems")); } + }); + target.find(".home-logo img").on("load", responsive); + if (window.ResizeObserver) { + new ResizeObserver(responsive).observe(target[0]); + } + if (document.fonts && document.fonts.ready) { document.fonts.ready.then(responsive); } + }); + }; +})(jQuery); diff --git a/Tests/Browser/Invoke-TopNavigationTest.ps1 b/Tests/Browser/Invoke-TopNavigationTest.ps1 new file mode 100644 index 00000000..6932d0ce --- /dev/null +++ b/Tests/Browser/Invoke-TopNavigationTest.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS +Checks that generated top navigation remains reachable after resizing. +.DESCRIPTION +Serve ReportDirectory at BaseUri through a loopback HTTP server before running. +PSParseHTML owns the headless Edge session; no browser installation is performed. +Exercises both embedded and Online reports, keyboard controls and nested menus. +.PARAMETER ReportDirectory +Directory served by the loopback server for the generated reports. +.PARAMETER BaseUri +Loopback HTTP address serving ReportDirectory. +.PARAMETER CaptureDirectory +Optional directory for screenshots of the desktop, drawer and short-frame states. +.EXAMPLE +./Tests/Browser/Invoke-TopNavigationTest.ps1 -ReportDirectory D:/Scratch/navigation -BaseUri http://127.0.0.1:58990/ +.OUTPUTS +Strings describing each successful navigation check. Throws on failure. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)][string] $ReportDirectory, + [Parameter(Mandatory)][uri] $BaseUri, + [string] $CaptureDirectory +) + +$ErrorActionPreference = 'Stop' +if ($BaseUri.Scheme -ne 'http' -or $BaseUri.Host -ne '127.0.0.1') { + throw 'BaseUri must point to the loopback HTTP server serving ReportDirectory.' +} +Import-Module (Join-Path $PSScriptRoot '../../PSWriteHTML.psd1') -Force +Import-Module PSParseHTML -MinimumVersion 3.0.3 -ErrorAction Stop +$null = New-Item -ItemType Directory -Path $ReportDirectory -Force +if ($CaptureDirectory) { $null = New-Item -ItemType Directory -Path $CaptureDirectory -Force } +$Logo = 'data:image/svg+xml;base64,' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('')) +$IconPath = Join-Path $ReportDirectory 'navigation-icon.svg' +[IO.File]::WriteAllText($IconPath, '') +$Results = [System.Collections.Generic.List[string]]::new() + +foreach ($Mode in @('offline', 'online')) { + $ReportName = "top-navigation-$Mode.html" + $null = New-HTML -TitleText 'Responsive report navigation' -FavIcon $IconPath -FilePath (Join-Path $ReportDirectory $ReportName) -Online:($Mode -eq 'online') { + New-HTMLNavTop -Logo $Logo -MenuItemsWidth 300px { + foreach ($Name in @('Overall Information', 'Forest Information', 'Domains Information', 'Group Policies', 'Security and Compliance', 'Operations')) { + $HeadingOptions = @{} + if ($Name -eq 'Overall Information') { $HeadingOptions.Href = '#heading-href' } + if ($Name -eq 'Forest Information') { $HeadingOptions.InternalPageID = 'heading-internal' } + New-NavTopMenu -Name $Name -IconSolid folder @HeadingOptions { + foreach ($Number in 1..20) { + New-NavLink -Name "$Name item $Number" -Href "#item-$Number" + } + New-NavLink -Name 'Nested reports' { + New-NavLink -Name 'Nested destination' -Href '#nested-destination' + New-NavLink -Name 'Deeper reports' { + New-NavLink -Name 'Deep destination' -Href '#deep-destination' + } + } + } + } + } + New-HTMLSection -HeaderText 'Report overview' { + New-HTMLText -Text 'All navigation groups remain available when this report is resized.' + } + } + $ReportUri = [uri]::new($BaseUri, $ReportName).AbsoluteUri + $Session = Start-HtmlBrowserSession -Url $ReportUri -Browser Chromium -BrowserChannel msedge -Visible:$false -ViewportWidth 1600 -ViewportHeight 800 -ErrorAction Stop + try { + $Session.Page.SetDefaultTimeout(10000) + function Assert-Navigation { + param([string] $Expression, [string] $Case) + try { + $null = $Session.Page.WaitForFunctionAsync($Expression).GetAwaiter().GetResult() + } catch { + throw "Navigation case '$Mode / $Case' failed: $($_.Exception.Message)" + } + $Results.Add("$Mode / $Case") + } + Assert-Navigation '() => document.fonts.status === "loaded" && !document.querySelector("nav").classList.contains("is-compact") && document.querySelectorAll(".dropdown-heading").length === 6' 'Wide layout shows all groups' + $Session.Page.Locator('.dropdown-heading').Nth(0).PressAsync('Enter').GetAwaiter().GetResult() + Assert-Navigation '() => location.hash === "#heading-href"' 'Enter follows a heading Href' + $Session.Page.Locator('.dropdown-heading').Nth(1).PressAsync('Enter').GetAwaiter().GetResult() + Assert-Navigation '() => location.hash === "#heading-internal"' 'Enter follows a heading InternalPageID' + foreach ($Width in @(1024, 480, 390, 240)) { + $Session.Page.SetViewportSizeAsync($Width, 650).GetAwaiter().GetResult() + Assert-Navigation '() => document.querySelector("nav").classList.contains("is-compact") && document.querySelector(".dropdown-items").inert' "Closed drawer at ${Width}px excludes hidden links from keyboard navigation" + $Session.Page.Locator('.hamburger').PressAsync('Enter').GetAwaiter().GetResult() + Assert-Navigation '() => { const row = document.querySelector(".dropdown-items"); return !row.inert && Math.abs(row.getBoundingClientRect().left) < 1 && [...row.children].every(li => getComputedStyle(li).display !== "none") && document.documentElement.scrollWidth <= innerWidth; }' "All groups reachable at ${Width}px" + if ($CaptureDirectory -and $Width -eq 390) { + Save-HtmlBrowserScreenshot -Session $Session -OutFile (Join-Path $CaptureDirectory "$Mode-drawer.png") -ErrorAction Stop | Out-Null + } + $Session.Page.Locator('.hamburger').PressAsync('Escape').GetAwaiter().GetResult() + Assert-Navigation '() => document.querySelector(".hamburger").getAttribute("aria-expanded") === "false" && document.activeElement.matches(".hamburger")' "Escape closes drawer and restores focus at ${Width}px" + } + $Session.Page.Locator('.hamburger').PressAsync('Enter').GetAwaiter().GetResult() + $Session.Page.SetViewportSizeAsync(1600, 360).GetAwaiter().GetResult() + Assert-Navigation '() => !document.querySelector("nav").classList.contains("is-compact") && !document.querySelector(".dropdown-items").inert && !document.querySelector(".codehim-dim-overlay").getClientRects().length && document.activeElement.matches(".home-link a")' 'Widening an open drawer restores the desktop row and visible keyboard focus' + $Session.Page.Locator('.dropdown-heading').Last.PressAsync('Space').GetAwaiter().GetResult() + Assert-Navigation '() => { const menu = document.querySelector(".dropdown-items > li:last-child > ul"); const r = menu.getBoundingClientRect(); return r.height > 0 && r.bottom <= innerHeight && r.right <= innerWidth && menu.scrollHeight > menu.clientHeight; }' 'Long right-edge dropdown fits the short viewport and scrolls' + $Session.Page.Locator('.dropdown-items > li:last-child .parent').First.ClickAsync().GetAwaiter().GetResult() + Assert-Navigation '() => { const menu = document.querySelector(".dropdown-items > li:last-child .menu-subitems"); const r = menu.getBoundingClientRect(); return r.height > 0 && r.left >= 0 && r.right <= innerWidth && r.bottom <= innerHeight; }' 'Nested flyout remains inside the viewport' + $Session.Page.Locator('.dropdown-items > li:last-child .parent').Last.ClickAsync().GetAwaiter().GetResult() + $Session.Page.Locator('a[href="#deep-destination"]').Last.ClickAsync().GetAwaiter().GetResult() + Assert-Navigation '() => location.hash === "#deep-destination" && [...document.querySelectorAll(".menu-items, .menu-subitems")].every(menu => !menu.getClientRects().length)' 'Deep destination navigates and closes dropdowns' + if ($CaptureDirectory) { + $Session.Page.Locator('.dropdown-heading').First.ClickAsync().GetAwaiter().GetResult() + Save-HtmlBrowserScreenshot -Session $Session -OutFile (Join-Path $CaptureDirectory "$Mode-short.png") -ErrorAction Stop | Out-Null + } + $Session.Page.SetViewportSizeAsync(390, 360).GetAwaiter().GetResult() + Assert-Navigation '() => document.querySelector("nav").classList.contains("is-compact")' 'Short mobile viewport selects drawer' + $Session.Page.Locator('.hamburger').ClickAsync().GetAwaiter().GetResult() + $Session.Page.Locator('.dropdown-heading').Last.ClickAsync().GetAwaiter().GetResult() + $Session.Page.Locator('.dropdown-items > li:last-child a[href="#item-20"]').ClickAsync().GetAwaiter().GetResult() + Assert-Navigation '() => location.hash === "#item-20" && document.querySelector(".dropdown-items").inert' 'Last link remains reachable by scrolling the short drawer' + $Session.Page.Locator('.hamburger').ClickAsync().GetAwaiter().GetResult() + $Session.Page.Locator('.hamburger').ClickAsync().GetAwaiter().GetResult() + Assert-Navigation '() => document.querySelector(".hamburger").getAttribute("aria-expanded") === "false"' 'Toggle closes an open drawer' + + $HostName = "top-navigation-frame-$Mode.html" + [IO.File]::WriteAllText((Join-Path $ReportDirectory $HostName), "