From 4d809a52b10f27bcca843ffc5de07ca8f9d9f44b Mon Sep 17 00:00:00 2001 From: Tim de Groot Date: Fri, 25 Sep 2026 00:48:32 +0200 Subject: [PATCH] feat: segmented section tracks in the menu bar and collapsible sidebar groups The section sub-items of a page no longer sit in the menu bar as italic siblings of the pages themselves. Each page that owns sections now carries them in a rounded track placed directly after it, with the section in view marked by a filled pill, so it is clear at a glance which page a section belongs to. Three pages have a track: Overview (its project bars, at most three at a time), Dashboard (Runs, Suites, Tests and Keywords) and Tables, which gets section navigation for the first time - one item per table that is shown, following the order the tables are laid out in. The tracks step aside while the layout is being edited, where the centred undo/redo buttons would otherwise overlap them. In the responsive sidebar the pages become collapsible groups: the page you are on is expanded, a chevron folds its sections away, and opening another page's group navigates there, since section items only exist for the page that is open. Also fixes a pre-existing bug: the overview bar switches (projects by Name, projects by Tag, latest runs, total stats) changed which bars exist but never rebuilt the section buttons, so the sub-menu kept items for bars that had been switched off and missed the ones that appeared. Co-Authored-By: Claude Opus 5 --- docs/customization.md | 27 +- robotframework_dashboard/css/base.css | 102 ++++++- robotframework_dashboard/css/colors.css | 10 + robotframework_dashboard/js/eventlisteners.js | 6 +- robotframework_dashboard/js/layout.js | 3 + robotframework_dashboard/js/menu.js | 279 +++++++++++++++--- .../js/variables/globals.js | 6 + .../templates/dashboard.html | 12 +- .../keywords/dashboard-keywords.resource | 54 ++++ tests/robot/testsuites/02_overview.robot | 22 ++ tests/robot/testsuites/03_dashboard.robot | 30 ++ tests/robot/testsuites/05_tables.robot | 22 ++ tests/robot/testsuites/08_layout.robot | 9 + 13 files changed, 530 insertions(+), 52 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index cc9082e5..c00601f4 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -72,13 +72,38 @@ The **Theme** tab also lets you personalize the navigation bar with your own bra Both settings take effect immediately and persist across page reloads. -## 7. Responsive Menu Bar +## 7. Menu Bar + +### Section Tracks + +Pages that are split into sections carry those sections in a **rounded track** in the menu bar, placed +directly after the page they belong to, so it is always clear which page a section belongs to. The +section you are looking at is marked by a filled pill, which follows you as you scroll. + +Three pages have one: + +- **Overview** — one item per project bar, showing at most three at a time: the section you are + looking at plus its neighbours. Switching the project bars on or off (see the + [Overview settings](/settings#overview-settings-overview-tab)) adds and removes items with them. +- **Dashboard** — *Runs*, *Suites*, *Tests* and *Keywords*. Sections you hide leave the track as well. + In [Unified Mode](/tabs-pages#unified-mode) the whole track disappears, since there are no separate + sections to jump between. +- **Tables** — one item per table that is shown, in the order you arranged the tables in. + +Clicking an item scrolls to that section, expanding it first if it was collapsed. While you are +editing the layout the tracks step aside, so the undo/redo buttons have the middle of the bar to +themselves. + +### Responsive Menu Bar The navigation bar automatically adapts to any screen width — no manual configuration required: - When the viewport becomes too narrow to display all page links, the menu items (*Overview*, *Dashboard*, *Compare*, *Tables*, etc.) are moved into a **slide-in sidebar**. - If the viewport is even smaller and the icon shortcuts also no longer fit, those move into the sidebar too. - A **hamburger button** (☰) appears in the top-right corner whenever items have been moved to the sidebar. Clicking it opens the sidebar; clicking the backdrop or the close button dismisses it. +- In the sidebar each page that has sections becomes a **collapsible group**: the page you are on is + expanded, and the chevron next to a page folds its sections away. Opening the group of another page + takes you to that page. - The sidebar reorganizes itself to reflect the current page order configured by the user. This behavior is fully automatic and requires no action from the user. diff --git a/robotframework_dashboard/css/base.css b/robotframework_dashboard/css/base.css index 3738cff8..c90dbf22 100644 --- a/robotframework_dashboard/css/base.css +++ b/robotframework_dashboard/css/base.css @@ -265,6 +265,10 @@ body.lock-scroll { overflow: hidden; } +#navigation { + min-height: 54px; +} + #mainNavItems { white-space: nowrap; min-width: 0; @@ -282,6 +286,35 @@ body.lock-scroll { display: block; } +.nav-tracks-hidden .nav-track { + display: none; +} +.nav-track { + display: flex; + align-items: center; + /* the navbar row stretches its flex items, which made the track fill the bar top to bottom */ + align-self: center; + gap: 2px; + padding: 3px; + margin: 0 6px; + border-radius: 999px; + background: var(--color-track-bg); + border: 1px solid var(--color-track-border); + flex-shrink: 0; +} +.navbar .nav-track-item { + padding: 3px 13px; + font-size: 0.8125rem; + line-height: 1.3; + border-radius: 999px; + white-space: nowrap; +} +.navbar .nav-track-item.active, +.navbar .nav-track-item.active:hover { + background: var(--color-highlight); + color: var(--color-on-highlight); +} + .sidenav-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; @@ -343,29 +376,80 @@ body.lock-scroll { color: var(--color-text-muted); letter-spacing: 0.05em; } +.sidenav-group-header { + display: flex; + align-items: center; + border-left: 3px solid transparent; +} +.sidenav-group-header:hover { + background: var(--color-menu-hover-bg); +} +.sidenav-group-header.active { + border-left-color: var(--color-highlight); + background: var(--color-highlight-tint); +} .sidenav-body .sidenav-nav-item { + flex: 1; + min-width: 0; + padding: 7px 4px 7px 16px; + font-weight: 500; + color: var(--color-menu-text); + text-decoration: none; + cursor: pointer; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sidenav-group-header:hover .sidenav-nav-item { + color: var(--color-highlight); +} +.sidenav-group-header.active .sidenav-nav-item { + font-weight: 600; + color: var(--color-highlight); +} +.sidenav-group-toggle { + background: none; + border: none; + padding: 8px 14px 8px 8px; + color: var(--color-text-muted); + cursor: pointer; + display: flex; +} +.sidenav-group-header.active .sidenav-group-toggle { + color: var(--color-highlight); +} +.sidenav-group-toggle svg { + width: 15px; + height: 15px; display: block; - padding: 6px 16px; +} +.sidenav-subgroup { + display: flex; + flex-direction: column; + margin: 2px 0 6px 22px; + border-left: 1px solid var(--color-border); +} +.sidenav-body .sidenav-sub-item { + display: block; + padding: 6px 12px 6px 14px; + margin-left: -1px; + font-size: 0.84375rem; color: var(--color-menu-text); text-decoration: none; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - border-left: 3px solid transparent; + border-left: 2px solid transparent; } -.sidenav-body .sidenav-nav-item:hover { - background: rgba(255, 255, 255, 0.06); +.sidenav-body .sidenav-sub-item:hover { + background: var(--color-menu-hover-bg); color: var(--color-highlight); } -.sidenav-body .sidenav-nav-item.active { +.sidenav-body .sidenav-sub-item.active { border-left-color: var(--color-highlight); color: var(--color-highlight); } -.sidenav-body .sidenav-nav-submenu { - padding-left: 32px; - font-style: italic; -} .sidenav-icon-row { display: grid; grid-template-columns: repeat(3, auto); diff --git a/robotframework_dashboard/css/colors.css b/robotframework_dashboard/css/colors.css index c24109bb..472315e0 100644 --- a/robotframework_dashboard/css/colors.css +++ b/robotframework_dashboard/css/colors.css @@ -3,6 +3,11 @@ --color-card: #ffffff; --color-menu-text: var(--color-text); --color-highlight: #3451b2; + --color-on-highlight: #ffffff; + --color-highlight-tint: rgba(52, 81, 178, 0.07); + --color-track-bg: rgba(0, 0, 0, 0.07); + --color-track-border: rgba(0, 0, 0, 0.08); + --color-menu-hover-bg: rgba(0, 0, 0, 0.05); --color-text: #000000; --color-text-muted: darkgrey; --color-border: rgba(0, 0, 0, 0.175); @@ -33,6 +38,11 @@ --color-card: rgba(30, 41, 59, 0.9); --color-menu-text: var(--color-text); --color-highlight: #a8b1ff; + --color-on-highlight: #10192f; + --color-highlight-tint: rgba(168, 177, 255, 0.1); + --color-track-bg: rgba(255, 255, 255, 0.08); + --color-track-border: rgba(255, 255, 255, 0.12); + --color-menu-hover-bg: rgba(255, 255, 255, 0.06); --color-text: #eee; --color-text-muted: #9ca3af; --color-border: rgba(255, 255, 255, 0.15); diff --git a/robotframework_dashboard/js/eventlisteners.js b/robotframework_dashboard/js/eventlisteners.js index fdd25df2..2a13de1f 100644 --- a/robotframework_dashboard/js/eventlisteners.js +++ b/robotframework_dashboard/js/eventlisteners.js @@ -18,7 +18,7 @@ import { arrowDown, arrowRight } from "./variables/svg.js"; import { fullscreenButtons, graphChangeButtons, compareRunIds } from "./variables/graphs.js"; import { toggle_theme, apply_theme_colors, apply_custom_branding } from "./theme.js"; import { add_alert, show_graph_loading, hide_graph_loading, update_graphs_with_loading, show_loading_overlay, hide_loading_overlay } from "./common.js"; -import { setup_data_and_graphs, update_menu } from "./menu.js"; +import { setup_data_and_graphs, update_menu, setup_overview_section_menu_buttons } from "./menu.js"; import { update_dashboard_graphs } from "./graph_creation/all.js"; import { setup_filtered_data_and_filters, @@ -766,6 +766,7 @@ function setup_sections_filters() { update_overview_sections_visibility(); // update all tagged bars update_projectbar_visibility(); + setup_overview_section_menu_buttons(); hide_loading_overlay(); }); }); @@ -784,6 +785,7 @@ function setup_sections_filters() { update_overview_sections_visibility(); // update all named project bars update_projectbar_visibility(); + setup_overview_section_menu_buttons(); hide_loading_overlay(); }); }); @@ -792,11 +794,13 @@ function setup_sections_filters() { settings.switch.latestRuns = !settings.switch.latestRuns update_switch_local_storage("switch.latestRuns", settings.switch.latestRuns); update_overview_sections_visibility(); + setup_overview_section_menu_buttons(); }); document.getElementById("switchTotalStats").addEventListener("click", function () { settings.switch.totalStats = !settings.switch.totalStats update_switch_local_storage("switch.totalStats", settings.switch.totalStats); update_overview_sections_visibility(); + setup_overview_section_menu_buttons(); }); document.getElementById("switchSortFilters").addEventListener("click", function () { settings.switch.sortFilters = !settings.switch.sortFilters diff --git a/robotframework_dashboard/js/layout.js b/robotframework_dashboard/js/layout.js index d407d913..7395c6ee 100644 --- a/robotframework_dashboard/js/layout.js +++ b/robotframework_dashboard/js/layout.js @@ -539,6 +539,8 @@ function customize_layout() { document.querySelectorAll("#iconNavItems > li").forEach(li => { if (!li.querySelector("#saveLayout")) li.classList.add("navbar-disabled"); }); + // the undo/redo buttons sit centred over the navbar, so the tracks step aside while editing + document.getElementById("navigation").classList.add("nav-tracks-hidden"); document.getElementById("layoutHistoryNavItems").hidden = false; layoutHistory = [capture_settings_snapshot()]; layoutHistoryIndex = 0; @@ -551,6 +553,7 @@ function save_layout() { document.getElementById("saveLayout").hidden = true; document.getElementById("mainNavItems").classList.remove("navbar-disabled"); document.querySelectorAll("#iconNavItems > li").forEach(li => li.classList.remove("navbar-disabled")); + document.getElementById("navigation").classList.remove("nav-tracks-hidden"); document.getElementById("layoutHistoryNavItems").hidden = true; layoutHistory = []; layoutHistoryIndex = -1; diff --git a/robotframework_dashboard/js/menu.js b/robotframework_dashboard/js/menu.js index f34c8497..0c3172cf 100644 --- a/robotframework_dashboard/js/menu.js +++ b/robotframework_dashboard/js/menu.js @@ -1,5 +1,5 @@ import { setup_filtered_data_and_filters } from "./filter.js"; -import { areGroupedProjectsPrepared, overviewNavStore } from "./variables/globals.js"; +import { areGroupedProjectsPrepared, overviewNavStore, tablesNavStore } from "./variables/globals.js"; import { space_to_camelcase, fade_in, fade_out } from "./common.js"; import { set_local_storage_item, setup_overview_localstorage } from "./localstorage.js"; import { create_dashboard_graphs } from "./graph_creation/all.js"; @@ -9,6 +9,10 @@ import { setup_graph_view_buttons, setup_overview_order_filters } from "./eventl import { setup_section_order, setup_graph_order, setup_overview_section_layout_buttons } from "./layout.js"; import { setup_information_popups } from "./information.js"; import { prepare_overview, update_overview_prefix_display } from "./graph_creation/overview.js"; +import { arrowDown, arrowRight } from "./variables/svg.js"; + +const PAGE_IDS = ["menuOverview", "menuDashboard", "menuCompare", "menuTables", "openDashboard"]; +const PAGES_WITH_SECTIONS = ["menuOverview", "menuDashboard", "menuTables"]; function get_sticky_height() { const stickyTop = document.getElementById("navigation"); @@ -125,6 +129,9 @@ function setup_menu() { else if (menuSettings.tables) selectedMenu = "menuTables"; } update_menu(selectedMenu); + + // hiding, showing or reordering a table changes what the tables track should hold + document.addEventListener("layout-user-action", () => setup_tables_section_menu_buttons()); } // function to update all graph data, function is called when updating filters and when the page loads @@ -151,6 +158,7 @@ function setup_data_and_graphs(menuUpdate = false, prepareOverviewProjectData = setup_spinner(true); setup_dashboard_section_menu_buttons(); setup_overview_section_menu_buttons(); + setup_tables_section_menu_buttons(); // Always create graphs from scratch because setup_graph_order() // rebuilds all GridStack grids and canvas DOM elements above @@ -205,14 +213,20 @@ function setup_dashboard_section_menu_buttons() { } else { sectionButtons.forEach(btn => btn.hidden = true); } + // an empty track would still draw its pill background, so it goes with its last item + document.getElementById("dashboardNavTrack").hidden = sectionButtons.every(btn => btn.hidden); const sections = Object.keys(sectionMap).map(id => document.getElementById(id)); function update_active_section() { const bestIndex = compute_best_visible_index(sections); const bestMatch = sections[bestIndex]; - sectionButtons.forEach(btn => btn.classList.remove("active")); + sectionButtons.forEach(btn => { + btn.classList.remove("active"); + btn.removeAttribute("aria-current"); + }); if (bestMatch && sectionMap[bestMatch.id]) { sectionMap[bestMatch.id].classList.add("active"); + sectionMap[bestMatch.id].setAttribute("aria-current", "true"); } } @@ -237,11 +251,13 @@ function setup_overview_section_menu_buttons() { const isOverviewActive = !!(settings.menu && settings.menu.overview); const navbar = document.querySelector(".navbar-nav"); const overviewMenuLink = document.getElementById("menuOverview"); - if (!navbar || !overviewMenuLink) return; + const overviewTrack = document.getElementById("overviewNavTrack"); + if (!navbar || !overviewMenuLink || !overviewTrack) return; const existingOverviewButtons = Array.from(navbar.querySelectorAll('a[id^="overview-"][id$="Nav"]')); if (!isOverviewActive) { existingOverviewButtons.forEach(el => el.remove()); + overviewTrack.hidden = true; if (overviewNavStore.scrollHandler) { window.removeEventListener("scroll", overviewNavStore.scrollHandler); overviewNavStore.scrollHandler = null; @@ -255,15 +271,10 @@ function setup_overview_section_menu_buttons() { const sections = Array.from(document.querySelectorAll("#overview .overview-bar")) .filter(el => el.offsetParent !== null); - if (sections.length === 0) return; - - const insertAfter = (newNode, referenceNode) => { - if (referenceNode.nextSibling) { - referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); - } else { - referenceNode.parentNode.appendChild(newNode); - } - }; + if (sections.length === 0) { + overviewTrack.hidden = true; + return; + } const buttonMap = new Map(); const makeButtonForSection = (sectionEl) => { @@ -278,11 +289,9 @@ function setup_overview_section_menu_buttons() { if (!btn) { btn = document.createElement("a"); btn.id = btnId; - btn.className = "nav-item nav-link"; - const label = document.createElement("i"); - label.textContent = baseName; - btn.appendChild(label); - insertAfter(btn, overviewMenuLink); + btn.className = "nav-item nav-link nav-track-item"; + btn.textContent = baseName; + overviewTrack.appendChild(btn); btn.addEventListener("click", (e) => { e.preventDefault(); @@ -294,6 +303,13 @@ function setup_overview_section_menu_buttons() { sections.forEach(makeButtonForSection); + // Sections come and go when the overview bar settings change (projects by name, projects by + // tag), so buttons of sections that no longer exist have to go with them + Array.from(overviewTrack.children).forEach(btn => { + const sectionId = btn.id.replace(/^overview-/, "").replace(/Nav$/, ""); + if (!buttonMap.has(sectionId)) btn.remove(); + }); + // Before attaching new listeners, remove any previous ones to avoid stale closures if (overviewNavStore.scrollHandler) { window.removeEventListener("scroll", overviewNavStore.scrollHandler); @@ -311,6 +327,7 @@ function setup_overview_section_menu_buttons() { if (!stillOverview) { const toRemove = Array.from(navbar.querySelectorAll('a[id^="overview-"][id$="Nav"]')); toRemove.forEach(el => el.remove()); + overviewTrack.hidden = true; if (overviewNavStore.scrollHandler) { window.removeEventListener("scroll", overviewNavStore.scrollHandler); overviewNavStore.scrollHandler = null; @@ -328,28 +345,32 @@ function setup_overview_section_menu_buttons() { if (!settings.show.prefixes && name.startsWith('project_')) { name = name.replace(/^project_/, ''); } - const label = btn.querySelector('i'); - if (label) label.textContent = name; + btn.textContent = name; }); const bestIndex = compute_best_visible_index(sections); const indices = neighbor_indices(bestIndex, sections.length); - // Reorder the visible buttons (up to 3) in the navbar after the Overview link - let last = overviewMenuLink; + // Reorder the visible buttons (up to 3) so they sit in section order inside the track indices.forEach(idx => { const btn = buttonMap.get(sections[idx].id); if (!btn) return; btn.hidden = !showButtons; - if (last.nextSibling !== btn) insertAfter(btn, last); - last = btn; + overviewTrack.appendChild(btn); }); sections.forEach((section, idx) => { const btn = buttonMap.get(section.id); if (!btn) return; btn.hidden = !showButtons || !indices.includes(idx); - btn.classList.toggle("active", showButtons && idx === bestIndex); + const isActive = showButtons && idx === bestIndex; + btn.classList.toggle("active", isActive); + if (isActive) { + btn.setAttribute("aria-current", "true"); + } else { + btn.removeAttribute("aria-current"); + } }); + overviewTrack.hidden = !showButtons || indices.length === 0; }; window.addEventListener("scroll", updateVisibleButtons, { passive: true }); @@ -359,6 +380,101 @@ function setup_overview_section_menu_buttons() { updateVisibleButtons(); } +// function to create and manage the tables page buttons: one per table that is currently +// shown, in the order the user arranged them, highlighting the one that is most visible +function setup_tables_section_menu_buttons() { + const isTablesActive = !!(settings.menu && settings.menu.tables); + const navbar = document.querySelector(".navbar-nav"); + const tablesTrack = document.getElementById("tablesNavTrack"); + if (!navbar || !tablesTrack) return; + + const remove_handlers = () => { + if (tablesNavStore.scrollHandler) { + window.removeEventListener("scroll", tablesNavStore.scrollHandler); + tablesNavStore.scrollHandler = null; + } + if (tablesNavStore.resizeHandler) { + window.removeEventListener("resize", tablesNavStore.resizeHandler); + tablesNavStore.resizeHandler = null; + } + }; + + if (!isTablesActive) { + Array.from(navbar.querySelectorAll('a[id^="tables-"][id$="Nav"]')).forEach(el => el.remove()); + tablesTrack.hidden = true; + remove_handlers(); + return; + } + + const sections = Array.from(document.querySelectorAll("#tables .table-section")) + .filter(el => el.offsetParent !== null); + if (sections.length === 0) { + tablesTrack.hidden = true; + remove_handlers(); + return; + } + + const label_for = (sectionEl) => { + const heading = sectionEl.querySelector("h6"); + const name = heading + ? heading.textContent.replace(/\s*Table$/i, "").trim() + : sectionEl.id.replace(/TableCanvas$/, ""); + return `${name}s`; + }; + + const buttonMap = new Map(); + sections.forEach(sectionEl => { + const btnId = `tables-${sectionEl.id}Nav`; + let btn = document.getElementById(btnId); + if (!btn) { + btn = document.createElement("a"); + btn.id = btnId; + btn.className = "nav-item nav-link nav-track-item"; + btn.addEventListener("click", (e) => { + e.preventDefault(); + expand_and_scroll_to(sectionEl); + }); + } + btn.textContent = label_for(sectionEl); + // re-appending in section order keeps the track following the user's table order + tablesTrack.appendChild(btn); + buttonMap.set(sectionEl.id, btn); + }); + + Array.from(tablesTrack.children).forEach(btn => { + const sectionId = btn.id.replace(/^tables-/, "").replace(/Nav$/, ""); + if (!buttonMap.has(sectionId)) btn.remove(); + }); + + remove_handlers(); + const update_active_table = () => { + if (!(settings.menu && settings.menu.tables)) { + tablesTrack.hidden = true; + remove_handlers(); + return; + } + const bestIndex = compute_best_visible_index(sections); + sections.forEach((section, idx) => { + const btn = buttonMap.get(section.id); + if (!btn) return; + const isActive = idx === bestIndex; + btn.classList.toggle("active", isActive); + if (isActive) { + btn.setAttribute("aria-current", "true"); + } else { + btn.removeAttribute("aria-current"); + } + }); + }; + + window.addEventListener("scroll", update_active_table, { passive: true }); + window.addEventListener("resize", update_active_table); + tablesNavStore.scrollHandler = update_active_table; + tablesNavStore.resizeHandler = update_active_table; + tablesTrack.hidden = false; + update_active_table(); +} + function scroll_to_most_visible_section() { setTimeout(() => { const mostVisibleSectionId = get_most_visible_section(); @@ -424,6 +540,10 @@ function setup_navbar_overflow() { // Capture icon
  • references (they move, but references stay valid) const iconLiEls = Array.from(iconNavUl.children); + // the section tracks are not .nav-item themselves, so they are hidden separately + const trackEls = ['overviewNavTrack', 'dashboardNavTrack', 'tablesNavTrack'] + .map(id => document.getElementById(id)) + .filter(Boolean); let navInSidebar = false; let iconsInSidebar = false; @@ -498,11 +618,104 @@ function setup_navbar_overflow() { // Remaining pages push(byId('menuCompare')); push(byId('menuTables')); + // Tables sub-items in actual DOM table order + if (settings.menu && settings.menu.tables) { + Array.from(document.querySelectorAll('#tables .table-section')) + .filter(el => el.offsetParent !== null || !el.hidden) + .forEach(section => { + const btn = byId(`tables-${section.id}Nav`); + if (btn) items.push(btn); + }); + } push(byId('openDashboard')); return items; } + // Turn the flat, ordered nav item list into one group per page plus its section items + function grouped_sidebar_items() { + const groups = []; + ordered_sidebar_items().forEach(el => { + if (PAGE_IDS.includes(el.id)) { + groups.push({ page: el, subItems: [] }); + } else if (groups.length) { + groups[groups.length - 1].subItems.push(el); + } + }); + return groups; + } + + function build_page_group(group) { + const wrapper = document.createElement('div'); + wrapper.className = 'sidenav-group'; + const isActivePage = group.page.classList.contains('active'); + + const header = document.createElement('div'); + header.className = 'sidenav-group-header' + (isActivePage ? ' active' : ''); + const link = document.createElement('a'); + link.className = 'sidenav-nav-item'; + link.textContent = group.page.textContent; + if (isActivePage) link.setAttribute('aria-current', 'page'); + link.addEventListener('click', () => { + group.page.click(); + close_sidebar(); + }); + header.appendChild(link); + wrapper.appendChild(header); + + if (!PAGES_WITH_SECTIONS.includes(group.page.id)) return wrapper; + + const subGroup = document.createElement('div'); + subGroup.className = 'sidenav-subgroup'; + subGroup.hidden = !isActivePage; + group.subItems.forEach(el => { + const item = document.createElement('a'); + item.className = 'sidenav-sub-item'; + if (el.classList.contains('active')) { + item.classList.add('active'); + item.setAttribute('aria-current', 'true'); + } + item.textContent = el.textContent; + item.addEventListener('click', () => { + el.click(); + close_sidebar(); + subGroup.querySelectorAll('.sidenav-sub-item').forEach(n => { + n.classList.remove('active'); + n.removeAttribute('aria-current'); + }); + item.classList.add('active'); + item.setAttribute('aria-current', 'true'); + }); + subGroup.appendChild(item); + }); + wrapper.appendChild(subGroup); + + const toggle = document.createElement('button'); + toggle.type = 'button'; + toggle.className = 'sidenav-group-toggle'; + const set_toggle_state = (expanded) => { + toggle.setAttribute('aria-expanded', String(expanded)); + toggle.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} ${link.textContent} sections`); + toggle.innerHTML = expanded ? arrowDown : arrowRight; + }; + set_toggle_state(isActivePage); + toggle.addEventListener('click', (event) => { + event.stopPropagation(); + // section items only exist for the page that is open, so expanding another page's + // group means going there first - its group is then expanded on the rebuild + if (!isActivePage) { + group.page.click(); + close_sidebar(); + return; + } + subGroup.hidden = !subGroup.hidden; + set_toggle_state(!subGroup.hidden); + }); + header.appendChild(toggle); + + return wrapper; + } + function build_sidebar_content() { sidenavBody.innerHTML = ''; @@ -512,19 +725,8 @@ function setup_navbar_overflow() { label.textContent = 'Pages'; sidenavBody.appendChild(label); - ordered_sidebar_items().forEach(el => { - const isSubmenu = !!el.querySelector('i'); - const item = document.createElement('a'); - item.className = 'sidenav-nav-item' + (isSubmenu ? ' sidenav-nav-submenu' : ''); - if (el.classList.contains('active')) item.classList.add('active'); - item.textContent = el.textContent; - item.addEventListener('click', () => { - el.click(); - close_sidebar(); - sidenavBody.querySelectorAll('.sidenav-nav-item').forEach(n => n.classList.remove('active')); - item.classList.add('active'); - }); - sidenavBody.appendChild(item); + grouped_sidebar_items().forEach(group => { + sidenavBody.appendChild(build_page_group(group)); }); } @@ -567,6 +769,9 @@ function setup_navbar_overflow() { nav_item_els().forEach(el => { el.style.display = on ? 'none' : ''; }); + trackEls.forEach(el => { + el.style.display = on ? 'none' : ''; + }); } function apply_icons_to_sidebar(on) { diff --git a/robotframework_dashboard/js/variables/globals.js b/robotframework_dashboard/js/variables/globals.js index a3c3e83d..ef908b19 100644 --- a/robotframework_dashboard/js/variables/globals.js +++ b/robotframework_dashboard/js/variables/globals.js @@ -140,6 +140,11 @@ let overviewNavStore = { resizeHandler: null, }; +let tablesNavStore = { + scrollHandler: null, + resizeHandler: null, +}; + var defaultFaviconHref = (() => { const link = document.querySelector("link[rel~='icon']"); return link ? link.getAttribute('href') : null; @@ -178,6 +183,7 @@ export { ignoreSkipsRecent, onlyFailedFolders, overviewNavStore, + tablesNavStore, lastMergeResult, filterRows, defaultFaviconHref diff --git a/robotframework_dashboard/templates/dashboard.html b/robotframework_dashboard/templates/dashboard.html index 5debba8d..171f7e1f 100644 --- a/robotframework_dashboard/templates/dashboard.html +++ b/robotframework_dashboard/templates/dashboard.html @@ -33,13 +33,17 @@ Overview + Dashboard - Runs - Suites - Tests - Keywords + Compare Tables +