Common graph drilldown hamburger toggle across all themes (#8308)

* Graph drilldown: common hamburger toggle across all themes

The graph action icons were revealed by hovering to the right of the graph, implemented per-theme (dark/midwinter/sunrise main.js) and always-on elsewhere.

Hide the icons by default behind a small fa-bars hamburger overlaid on the graph's top-right. Clicking it opens the icon strip, which stays until a second click, leaving the strip, or an outside click. The behaviour is now centralized in include/layout.js (initGraphDrilldown) and include/css/graph_drilldown.css, and the per-theme hover JS is removed.

* changelog: 8308 feature fragment

* Graph drilldown: address review (a11y, positioning, spikekill, POT)

- Anchor the toggle to the shrink-wrapped graph holder (width:fit-content) so the hamburger sits on the graph's top-right, not the full-width column edge.

- Keep the hamburger focusable while visually hidden (opacity instead of display:none); reveal on hover, keyboard focus, open, and hover-less devices.

- Expose aria-expanded on the toggle and update it in every open/close path; add aria-haspopup/aria-label.

- Close any open SpikeKill submenu (spikeKillClose) when the strip closes or switches, matching the removed per-theme handlers.

- Remove the now-duplicate Midwinter top-right ellipsis pseudo-element.

- Add the new 'Graph Actions' msgid to locales/po/cacti.pot.
This commit is contained in:
TheWitness authored and GitHub committed 2026-10-11 17:20:06 -04:00
1 parent 06c32ab13f
commit e25a67c31f
9 files changed
+154 -135

No files matched your search

+1
View File
@@ -0,0 +1 @@
Make the graph drilldown action icons a common hamburger toggle across all themes instead of per-theme hover reveal
+87
View File
@@ -0,0 +1,87 @@
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
+-------------------------------------------------------------------------+
*/
/*
* Common, theme-agnostic drilldown behaviour: the action icons are hidden
* behind a hamburger glyph overlaid on the top-right of the graph. Clicking
* the glyph opens the icon strip; it closes on a second click, on leaving the
* strip, or on an outside click (see initGraphDrilldown() in layout.js).
* Loaded after each theme's main.css so these rules win.
*/
.graphWrapperOuter > div:first-child {
position: relative;
width: -moz-fit-content;
width: fit-content;
max-width: 100%;
}
.graphDrillDownToggle {
position: absolute;
top: 0;
right: 0;
width: auto;
height: auto;
padding: 0;
margin: 0;
text-align: right;
z-index: 6;
}
.graphDrillDownToggle .graphDrillDownMenu {
display: inline-block;
opacity: 0;
cursor: pointer;
margin: 3px;
padding: 2px 6px;
border-radius: 3px;
line-height: 1;
font-size: 13px;
color: #ffffff;
background: rgba(0, 0, 0, 0.55);
transition: opacity 0.12s ease-in-out;
}
/* Keep the toggle focusable (never display:none) and reveal it on hover,
keyboard focus, the open state, and on devices that cannot hover. */
.graphWrapperOuter:hover .graphDrillDownToggle .graphDrillDownMenu,
.graphDrillDownToggle .graphDrillDownMenu:focus,
.graphDrillDownToggle.open .graphDrillDownMenu {
opacity: 1;
}
@media (hover: none) {
.graphDrillDownToggle .graphDrillDownMenu {
opacity: 1;
}
}
.graphDrillDownToggle.open .graphDrillDownMenu {
background: rgba(0, 0, 0, 0.82);
}
.graphDrillDownToggle .iconWrapper {
display: none;
}
.graphDrillDownToggle.open .iconWrapper {
display: inline-block;
margin: 0 3px 3px 3px;
padding: 5px 7px;
border-radius: 3px;
background: rgba(0, 0, 0, 0.82);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4);
text-align: center;
}
.graphDrillDownToggle.open .iconWrapper .iconLink {
text-decoration: none;
}
+58
View File
@@ -3847,6 +3847,8 @@ function cactiReady() {
applySkin();
initGraphDrilldown();
setupEllipsis();
handleUserMenu();
@@ -4847,6 +4849,62 @@ function refreshGraphs() {
});
}
function initGraphDrilldown() {
// Common graph drilldown behaviour for every theme (styled in graph_drilldown.css).
// A hamburger glyph over the graph's top-right toggles the action icons; they
// close on a second click, on leaving the strip, or on an outside click.
$(document).off('.graphdd');
function closeGraphDrilldown(container) {
container.removeClass('open');
container.find('.graphDrillDownMenu').attr('aria-expanded', 'false');
// Close an open SpikeKill submenu so it is not orphaned with the strip.
if (typeof spikeKillClose == 'function') {
spikeKillClose();
}
}
$(document).on('click.graphdd', '.graphDrillDownToggle .graphDrillDownMenu', function (event) {
event.preventDefault();
event.stopPropagation();
var menu = $(this);
var container = menu.closest('.graphDrillDownToggle');
var willOpen = !container.hasClass('open');
$('.graphDrillDownToggle.open').not(container).each(function () {
closeGraphDrilldown($(this));
});
if (willOpen) {
container.addClass('open');
menu.attr('aria-expanded', 'true');
} else {
closeGraphDrilldown(container);
}
});
$(document).on('keydown.graphdd', '.graphDrillDownToggle .graphDrillDownMenu', function (event) {
if (event.keyCode == 13 || event.keyCode == 32) {
event.preventDefault();
$(this).trigger('click');
}
});
$(document).on('mouseleave.graphdd', '.graphDrillDownToggle.open', function () {
closeGraphDrilldown($(this));
});
$(document).on('click.graphdd', function (event) {
if ($(event.target).closest('.graphDrillDownToggle').length == 0) {
$('.graphDrillDownToggle.open').each(function () {
closeGraphDrilldown($(this));
});
}
});
}
function initializeGraphs(disable_cache) {
disable_cache = (typeof disable_cache == 'undefined') ? false : true;
-37
View File
@@ -92,43 +92,6 @@ function themeReady() {
}
});
// Hide the graph icons until you hover
$('.graphDrillDown').on('mouseenter', function() {
element = $(this);
// hide the previously shown element
if (element.attr('id').replace('dd', '') != graphMenuElement && graphMenuElement > 0) {
$('#dd'+graphMenuElement).find('.iconWrapper:first').hide(300);
}
clearTimeout(graphMenuTimer);
graphMenuTimer = setTimeout(function() { showGraphMenu(element); }, 400);
}).on('mouseleave', function() {
element = $(this);
clearTimeout(graphMenuTimer);
graphMenuTimer = setTimeout(function() { hideGraphMenu(element); }, 400);
if (typeof spikeKillClose == 'function') {
spikeKillClose();
}
});
function showGraphMenu(element) {
element.find('.spikekillMenu').menu('disable');
element.find('.iconWrapper').show(300, function() {
graphMenuElement = element.attr('id').replace('dd', '');;
$(this).find('.spikekillMenu').menu('enable');
$(this).css('display', 'block');
});
}
function hideGraphMenu(element) {
element.find('.spikekillMenu').menu('disable');
element.find('.iconWrapper').hide(300, function() {
$(this).find('.spikekillMenu').menu('enable');
});
}
setNavigationScroll();
}
@@ -1428,24 +1428,6 @@ legend::before {
.graphWrapper {
width: fit-content;
position: relative;
&:hover {
&::after {
color: var(--text-color-normal);
background: var(--background-content);
content: "\22EE";
display: block;
position: absolute;
right: 0;
top: 0;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-bottom-left-radius: 4px;
border-top-right-radius: 4px;
opacity: 0.98;
cursor: pointer;
}
}
}
.graphimage {
-41
View File
@@ -840,47 +840,6 @@ function setupDefaultElements() {
$('.import_text').text(fileText);
}
// Hide the graph icons until you hover
$('.graphDrillDown').on('mouseenter', function () {
element = $(this);
// hide the previously shown element
if (element.attr('id').replace('dd', '') != graphMenuElement && graphMenuElement > 0) {
$('#dd' + graphMenuElement).find('.iconWrapper:first').hide(300);
}
clearTimeout(graphMenuTimer);
graphMenuTimer = setTimeout(function () {
showGraphMenu(element);
}, 400);
}).on('mouseleave', function () {
element = $(this);
clearTimeout(graphMenuTimer);
graphMenuTimer = setTimeout(function () {
hideGraphMenu(element);
}, 400);
if (typeof spikeKillClose == 'function') {
spikeKillClose();
}
});
function showGraphMenu(element) {
element.find('.spikekillMenu').menu('disable');
element.find('.iconWrapper').show(300, function () {
graphMenuElement = element.attr('id').replace('dd', '');
$(this).find('.spikekillMenu').menu('enable');
$(this).css('display', 'block');
});
}
function hideGraphMenu(element) {
element.find('.spikekillMenu').menu('disable');
element.find('.iconWrapper').hide(300, function () {
$(this).find('.spikekillMenu').menu('enable');
});
}
setNavigationScroll();
}
}
-37
View File
@@ -154,43 +154,6 @@ function themeReady() {
$('.import_text').text(fileText);
}
// Hide the graph icons until you hover
$('.graphDrillDown').on('mouseenter', function() {
element = $(this);
// hide the previously shown element
if (element.attr('id').replace('dd', '') != graphMenuElement && graphMenuElement > 0) {
$('#dd'+graphMenuElement).find('.iconWrapper:first').hide(300);
}
clearTimeout(graphMenuTimer);
graphMenuTimer = setTimeout(function() { showGraphMenu(element); }, 400);
}).on('mouseleave', function() {
element = $(this);
clearTimeout(graphMenuTimer);
graphMenuTimer = setTimeout(function() { hideGraphMenu(element); }, 400);
if (typeof spikeKillClose == 'function') {
spikeKillClose();
}
});
function showGraphMenu(element) {
element.find('.spikekillMenu').menu('disable');
element.find('.iconWrapper').show(300, function() {
graphMenuElement = element.attr('id').replace('dd', '');;
$(this).find('.spikekillMenu').menu('enable');
$(this).css('display', 'block');
});
}
function hideGraphMenu(element) {
element.find('.spikekillMenu').menu('disable');
element.find('.iconWrapper').hide(300, function() {
$(this).find('.spikekillMenu').menu('enable');
});
}
setNavigationScroll();
}
+4 -2
View File
@@ -380,7 +380,7 @@ function html_graph_area(array &$graph_array, string $no_graphs_message = '', st
<div>
<div class='graphWrapper' id='wrapper_<?php print $graph['local_graph_id']?>' graph_width='<?php print $graph['width']; ?>' graph_height='<?php print $graph['height']; ?>' title_font_size='<?php print((read_user_setting('custom_fonts') == 'on') ? read_user_setting('title_size') : read_config_option('title_size')); ?>'></div>
<?php if (is_realm_allowed(27)) { ?>
<div id='dd<?php print $graph['local_graph_id']; ?>' class='noprint graphDrillDown'>
<div id='dd<?php print $graph['local_graph_id']; ?>' class='noprint graphDrillDown graphDrillDownToggle'>
<?php graph_drilldown_icons($graph['local_graph_id'], 'graph_buttons', $tree_id, $branch_id); ?>
</div>
<?php } ?>
@@ -499,7 +499,7 @@ function html_graph_thumbnail_area(array &$graph_array, string $no_graphs_messag
<div>
<div class='graphWrapper' id='wrapper_<?php print $graph['local_graph_id']?>' graph_width='<?php print read_user_setting('default_width'); ?>' graph_height='<?php print read_user_setting('default_height'); ?>'></div>
<?php if (is_realm_allowed(27)) { ?>
<div id='dd<?php print $graph['local_graph_id']; ?>' class='noprint graphDrillDown'>
<div id='dd<?php print $graph['local_graph_id']; ?>' class='noprint graphDrillDown graphDrillDownToggle'>
<?php graph_drilldown_icons($graph['local_graph_id'], 'graph_buttons_thumbnails', $tree_id, $branch_id); ?>
</div>
<?php } ?>
@@ -542,6 +542,7 @@ function graph_drilldown_icons(int $local_graph_id, string $type = 'graph_button
WHERE id = ?',
[$local_graph_id]);
print "<span class='graphDrillDownMenu noprint' role='button' tabindex='0' aria-haspopup='true' aria-expanded='false' aria-label='" . __esc('Graph Actions') . "' title='" . __esc('Graph Actions') . "'><i class='fa fa-bars'></i></span>";
print "<div class='iconWrapper'>";
print "<a class='iconLink utils' href='#' role='link' id='graph_" . $local_graph_id . "_util'><i class='drillDown ti ti-settings-filled actionCog' title='" . __esc('Graph Details, Zooming and Debugging Utilities') . "'></i></a><br>";
print "<a class='iconLink csvexport' href='#' role='link' id='graph_" . $local_graph_id . "_csv'><i class='drillDown ti ti-file-type-csv fileCSV' title='" . __esc('CSV Export of Graph Data') . "'></i></a><br>";
@@ -3438,6 +3439,7 @@ function html_common_header(string $title, string $selectedTheme = '') : void {
// Global styles
print get_md5_include_css('include/themes/' . $selectedTheme . '/main.css');
print get_md5_include_css('include/themes/print.css');
print get_md5_include_css('include/css/graph_drilldown.css');
// Global scripts
print get_md5_include_js('include/js/screenfull.js', true);
+4
View File
@@ -15553,6 +15553,10 @@ msgstr ""
msgid "Data Query:"
msgstr ""
#: lib/html.php
msgid "Graph Actions"
msgstr ""
#: lib/html.php lib/html_graph.php
msgid "Graph Details, Zooming and Debugging Utilities"
msgstr ""