Refactor navbar styles and improve dropdown accessibility for better mobile usability

This commit is contained in:
Ross
2025-12-08 12:12:00 +00:00
parent 4ab2e3a15e
commit f156f4c85e
+173 -170
View File
@@ -110,76 +110,76 @@
} }
} }
/* Improve navbar tap targets and mobile dropdown behaviour */ /* Improve navbar tap targets and mobile dropdown behaviour */
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
/* Make navbar links larger and easier to tap */ /* Make navbar links larger and easier to tap */
.navbar-nav .nav-link { .navbar-nav .nav-link {
padding: .75rem 1rem; padding: .75rem 1rem;
font-size: 1.05rem; font-size: 1.05rem;
min-height: 44px; /* recommended minimum touch target */ min-height: 44px; /* recommended minimum touch target */
display: block; display: block;
margin: .25rem 0; margin: .25rem 0;
border-radius: .375rem; border-radius: .375rem;
background-color: var(--bs-primary); background-color: var(--bs-primary);
color: #fff !important; color: #fff !important;
text-align: center; text-align: center;
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset; box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
} }
/* Make links look like outline buttons for secondary links */ /* Make links look like outline buttons for secondary links */
.navbar-nav .nav-link.btn-outline { .navbar-nav .nav-link.btn-outline {
background-color: transparent; background-color: transparent;
color: var(--bs-primary) !important; color: var(--bs-primary) !important;
border: 1px solid rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.08);
} }
/* Make dropdown items full-width and larger */ /* Make dropdown items full-width and larger */
.navbar-nav .dropdown-menu .dropdown-item { .navbar-nav .dropdown-menu .dropdown-item {
padding: .75rem 1rem; padding: .75rem 1rem;
font-size: 1.05rem; font-size: 1.05rem;
background: transparent; background: transparent;
color: inherit; color: inherit;
border-radius: .25rem; border-radius: .25rem;
margin: .125rem 0; margin: .125rem 0;
} }
/* When the navbar is collapsed, render dropdown menus as static stacked blocks /* When the navbar is collapsed, render dropdown menus as static stacked blocks
so nested items are easy to tap (prevents absolute-position overlap issues). */ so nested items are easy to tap (prevents absolute-position overlap issues). */
.navbar-collapse .dropdown-menu { .navbar-collapse .dropdown-menu {
position: static !important; position: static !important;
float: none !important; float: none !important;
display: block !important; display: block !important;
width: 100% !important; width: 100% !important;
margin-top: .25rem; margin-top: .25rem;
} }
/* Ensure dropdown toggles are full-width when collapsed */ /* Ensure dropdown toggles are full-width when collapsed */
.navbar-collapse .dropdown > .nav-link { .navbar-collapse .dropdown > .nav-link {
width: 100%; width: 100%;
text-align: left; text-align: left;
display: block; display: block;
} }
html, body { html, body {
/* Slightly increase root font-size so rem-based components scale */ /* Slightly increase root font-size so rem-based components scale */
font-size: 182%; font-size: 182%;
line-height: 1.45; line-height: 1.45;
} }
/* Slightly larger headings for clear hierarchy */ /* Slightly larger headings for clear hierarchy */
h1 { font-size: 1.8rem; } h1 { font-size: 1.8rem; }
h2 { font-size: 1.4rem; } h2 { font-size: 1.4rem; }
h3 { font-size: 1.15rem; } h3 { font-size: 1.15rem; }
/* Improve spacing for paragraphs and lists */ /* Improve spacing for paragraphs and lists */
p, li { font-size: 1rem; } p, li { font-size: 1rem; }
/* Make form controls and buttons a bit larger for touch */ /* Make form controls and buttons a bit larger for touch */
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; } .form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
} }
</style> </style>
{% block css %} {% block css %}
@@ -224,7 +224,10 @@
<a class="dropdown-item" href="{% url 'sbas:index' %}">SBAs</a> <a class="dropdown-item" href="{% url 'sbas:index' %}">SBAs</a>
</li> </li>
<li> <li>
<a class="dropdown-item" href="{% url 'generic:examcollection_list' %}">Exam Collection</a> <a class="dropdown-item d-flex justify-content-between align-items-center" href="{% url 'generic:examcollection_list' %}">
<span><i class="bi bi-collection me-1" aria-hidden="true"></i> Exam Collections</span>
<span class="badge bg-secondary ms-2">Manage</span>
</a>
</li> </li>
</ul> </ul>
</li> </li>
@@ -267,12 +270,12 @@
<div class="col-md-12"> <div class="col-md-12">
<div id="htmx-error"></div> <div id="htmx-error"></div>
<span <span
id="cimar-login-needed" id="cimar-login-needed"
hx-get='{% url "cimar_status" %}' hx-get='{% url "cimar_status" %}'
hx-trigger="cimar-login-needed from:body" hx-trigger="cimar-login-needed from:body"
> >
</span> </span>
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</div> </div>
@@ -313,10 +316,10 @@
}); });
</script> </script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Wire up all row-selection control blocks to the nearest .js-row-selectable table. // Wire up all row-selection control blocks to the nearest .js-row-selectable table.
function findTableForControl(elem) { function findTableForControl(elem) {
// Walk up ancestors and look for a table that either has the // Walk up ancestors and look for a table that either has the
// `js-row-selectable` class or contains a checkbox named 'selection'. // `js-row-selectable` class or contains a checkbox named 'selection'.
//var ancestor = elem.closest('div, section, main, article, form, body'); //var ancestor = elem.closest('div, section, main, article, form, body');
@@ -336,151 +339,151 @@ document.addEventListener('DOMContentLoaded', function() {
// ancestor = ancestor.parentElement; // ancestor = ancestor.parentElement;
//} //}
// fallback to any table on the page that looks like it has selection // fallback to any table on the page that looks like it has selection
var any = document.querySelector('table.js-row-selectable') //|| Array.from(document.querySelectorAll('table')).find(function(t){ return t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]')); }); var any = document.querySelector('table.js-row-selectable') //|| Array.from(document.querySelectorAll('table')).find(function(t){ return t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]')); });
return any || null; return any || null;
} }
// Helper to build id with optional token // Helper to build id with optional token
function suffixed(idBase, token) { function suffixed(idBase, token) {
return token ? idBase + '-' + token : idBase; return token ? idBase + '-' + token : idBase;
} }
// Ensure every selectable table has a row-selection control block. If the // Ensure every selectable table has a row-selection control block. If the
// page author didn't render the controls, create them automatically and // page author didn't render the controls, create them automatically and
// insert them before the table so the behavior is available out-of-the-box. // insert them before the table so the behavior is available out-of-the-box.
function ensureControlsForTable(table) { function ensureControlsForTable(table) {
if (!table) return; if (!table) return;
// detect an existing nearby toggle button // detect an existing nearby toggle button
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]'); var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
if (existing) return; // already present if (existing) return; // already present
// generate a short token for unique IDs // generate a short token for unique IDs
var token = (Date.now().toString(16) + Math.floor(Math.random()*0xffff).toString(16)); var token = (Date.now().toString(16) + Math.floor(Math.random()*0xffff).toString(16));
var wrapper = document.createElement('div'); var wrapper = document.createElement('div');
wrapper.className = 'row-selection-controls-wrapper'; wrapper.className = 'row-selection-controls-wrapper';
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">' wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
+ ' <div>' + ' <div>'
+ ' <button class="btn btn-outline-secondary btn-sm" id="toggle-row-selection-' + token + '" type="button">Enable row selection</button>' + ' <button class="btn btn-outline-secondary btn-sm" id="toggle-row-selection-' + token + '" type="button">Enable row selection</button>'
+ ' <div id="selection-controls-' + token + '" class="btn-group ms-2" role="group" style="display:none;">' + ' <div id="selection-controls-' + token + '" class="btn-group ms-2" role="group" style="display:none;">'
+ ' <button class="btn btn-sm btn-outline-primary" id="select-all-' + token + '">Select all</button>' + ' <button class="btn btn-sm btn-outline-primary" id="select-all-' + token + '">Select all</button>'
+ ' <button class="btn btn-sm btn-outline-secondary" id="clear-selection-' + token + '">Clear</button>' + ' <button class="btn btn-sm btn-outline-secondary" id="clear-selection-' + token + '">Clear</button>'
+ ' </div>' + ' </div>'
+ ' </div>' + ' </div>'
+ ' <div>' + ' <div>'
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>' + ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
+ ' </div>' + ' </div>'
+ '</div>'; + '</div>';
// Insert the controls immediately before the table (or its responsive wrapper) // Insert the controls immediately before the table (or its responsive wrapper)
var container = table.closest('.table-responsive') || table; var container = table.closest('.table-responsive') || table;
container.parentNode.insertBefore(wrapper, container); container.parentNode.insertBefore(wrapper, container);
} }
// Create controls for any table that looks selectable but lacks a control block // Create controls for any table that looks selectable but lacks a control block
Array.from(document.querySelectorAll('table')).forEach(function(t){ Array.from(document.querySelectorAll('table')).forEach(function(t){
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) { if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
ensureControlsForTable(t); ensureControlsForTable(t);
} }
}); });
// For every toggle button (supports both unsuffixed and suffixed IDs) // For every toggle button (supports both unsuffixed and suffixed IDs)
document.querySelectorAll('button[id^="toggle-row-selection"]').forEach(function(toggleBtn) { document.querySelectorAll('button[id^="toggle-row-selection"]').forEach(function(toggleBtn) {
// derive token (empty for unsuffixed) // derive token (empty for unsuffixed)
var token = ''; var token = '';
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', ''); if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
var controlsEl = document.getElementById(suffixed('selection-controls', token)); var controlsEl = document.getElementById(suffixed('selection-controls', token));
var selectAllBtn = document.getElementById(suffixed('select-all', token)); var selectAllBtn = document.getElementById(suffixed('select-all', token));
var clearBtn = document.getElementById(suffixed('clear-selection', token)); var clearBtn = document.getElementById(suffixed('clear-selection', token));
var selectedCountSpan = document.getElementById(suffixed('selected-count', token)); var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
var table = findTableForControl(toggleBtn); var table = findTableForControl(toggleBtn);
if (!table) return; // nothing to do if (!table) return; // nothing to do
var selectionEnabled = false; var selectionEnabled = false;
function findRowCheckboxes() { function findRowCheckboxes() {
if (!table) return []; if (!table) return [];
// prefer named selection inputs but fall back to any checkbox // prefer named selection inputs but fall back to any checkbox
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]')); var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]')); if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
return checks; return checks;
} }
function updateSelectedCount() { function updateSelectedCount() {
if (!selectedCountSpan) return; if (!selectedCountSpan) return;
var checks = findRowCheckboxes(); var checks = findRowCheckboxes();
var count = checks.filter(function(c){ return c.checked; }).length; var count = checks.filter(function(c){ return c.checked; }).length;
selectedCountSpan.textContent = count; selectedCountSpan.textContent = count;
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); }); checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
} }
function setCheckboxesDisabled(disabled) { function setCheckboxesDisabled(disabled) {
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; }); findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
} }
function hideSelectionColumn() { function hideSelectionColumn() {
if (!table) return; if (!table) return;
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; }); table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
} }
function showSelectionColumn() { function showSelectionColumn() {
if (!table) return; if (!table) return;
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; }); table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
} }
// initialise state // initialise state
setCheckboxesDisabled(true);
hideSelectionColumn();
// row click toggling (attach to table to avoid global handlers)
table.addEventListener('click', function(e){
if (!selectionEnabled) return;
var tag = (e.target.tagName || '').toLowerCase();
if (['input','a','button','select','textarea','label'].indexOf(tag) !== -1) return;
var tr = e.target.closest('tr');
if (!tr || !table.contains(tr)) return;
var cb = tr.querySelector('input[type="checkbox"][name="selection"]') || tr.querySelector('input[type="checkbox"]');
if (cb && !cb.disabled) { cb.checked = !cb.checked; tr.classList.toggle('table-active', cb.checked); updateSelectedCount(); }
}, true);
// listen for changes within the table to keep counts in sync
table.addEventListener('change', function(e){ if (e.target && e.target.matches && e.target.matches('input[type="checkbox"]')) updateSelectedCount(); }, true);
// wire controls
if (toggleBtn) {
toggleBtn.addEventListener('click', function() {
selectionEnabled = !selectionEnabled;
if (selectionEnabled) {
toggleBtn.textContent = 'Disable row selection';
if (controlsEl) controlsEl.style.display = '';
setCheckboxesDisabled(false);
showSelectionColumn();
} else {
toggleBtn.textContent = 'Enable row selection';
if (controlsEl) controlsEl.style.display = 'none';
setCheckboxesDisabled(true); setCheckboxesDisabled(true);
hideSelectionColumn(); hideSelectionColumn();
findRowCheckboxes().forEach(function(cb){ cb.checked = false; });
updateSelectedCount();
}
});
}
if (selectAllBtn) { // row click toggling (attach to table to avoid global handlers)
selectAllBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = true; }); updateSelectedCount(); }); table.addEventListener('click', function(e){
} if (!selectionEnabled) return;
var tag = (e.target.tagName || '').toLowerCase();
if (['input','a','button','select','textarea','label'].indexOf(tag) !== -1) return;
var tr = e.target.closest('tr');
if (!tr || !table.contains(tr)) return;
var cb = tr.querySelector('input[type="checkbox"][name="selection"]') || tr.querySelector('input[type="checkbox"]');
if (cb && !cb.disabled) { cb.checked = !cb.checked; tr.classList.toggle('table-active', cb.checked); updateSelectedCount(); }
}, true);
if (clearBtn) { // listen for changes within the table to keep counts in sync
clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); }); table.addEventListener('change', function(e){ if (e.target && e.target.matches && e.target.matches('input[type="checkbox"]')) updateSelectedCount(); }, true);
}
// wire controls
if (toggleBtn) {
toggleBtn.addEventListener('click', function() {
selectionEnabled = !selectionEnabled;
if (selectionEnabled) {
toggleBtn.textContent = 'Disable row selection';
if (controlsEl) controlsEl.style.display = '';
setCheckboxesDisabled(false);
showSelectionColumn();
} else {
toggleBtn.textContent = 'Enable row selection';
if (controlsEl) controlsEl.style.display = 'none';
setCheckboxesDisabled(true);
hideSelectionColumn();
findRowCheckboxes().forEach(function(cb){ cb.checked = false; });
updateSelectedCount();
}
});
}
if (selectAllBtn) {
selectAllBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = true; }); updateSelectedCount(); });
}
if (clearBtn) {
clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); });
}
// initial count // initial count
updateSelectedCount(); updateSelectedCount();
}); });
}); });
</script> </script>
</body> </body>