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 */
@media (max-width: 991.98px) {
@media (max-width: 991.98px) {
/* Make navbar links larger and easier to tap */
.navbar-nav .nav-link {
padding: .75rem 1rem;
font-size: 1.05rem;
min-height: 44px; /* recommended minimum touch target */
display: block;
margin: .25rem 0;
border-radius: .375rem;
background-color: var(--bs-primary);
color: #fff !important;
text-align: center;
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
}
.navbar-nav .nav-link {
padding: .75rem 1rem;
font-size: 1.05rem;
min-height: 44px; /* recommended minimum touch target */
display: block;
margin: .25rem 0;
border-radius: .375rem;
background-color: var(--bs-primary);
color: #fff !important;
text-align: center;
box-shadow: 0 1px 0 rgba(0,0,0,0.05) inset;
}
/* Make links look like outline buttons for secondary links */
.navbar-nav .nav-link.btn-outline {
background-color: transparent;
color: var(--bs-primary) !important;
border: 1px solid rgba(255,255,255,0.08);
}
.navbar-nav .nav-link.btn-outline {
background-color: transparent;
color: var(--bs-primary) !important;
border: 1px solid rgba(255,255,255,0.08);
}
/* Make dropdown items full-width and larger */
.navbar-nav .dropdown-menu .dropdown-item {
padding: .75rem 1rem;
font-size: 1.05rem;
background: transparent;
color: inherit;
border-radius: .25rem;
margin: .125rem 0;
}
.navbar-nav .dropdown-menu .dropdown-item {
padding: .75rem 1rem;
font-size: 1.05rem;
background: transparent;
color: inherit;
border-radius: .25rem;
margin: .125rem 0;
}
/* When the navbar is collapsed, render dropdown menus as static stacked blocks
so nested items are easy to tap (prevents absolute-position overlap issues). */
.navbar-collapse .dropdown-menu {
position: static !important;
float: none !important;
display: block !important;
width: 100% !important;
margin-top: .25rem;
}
.navbar-collapse .dropdown-menu {
position: static !important;
float: none !important;
display: block !important;
width: 100% !important;
margin-top: .25rem;
}
/* Ensure dropdown toggles are full-width when collapsed */
.navbar-collapse .dropdown > .nav-link {
width: 100%;
text-align: left;
display: block;
}
.navbar-collapse .dropdown > .nav-link {
width: 100%;
text-align: left;
display: block;
}
html, body {
html, body {
/* Slightly increase root font-size so rem-based components scale */
font-size: 182%;
line-height: 1.45;
}
font-size: 182%;
line-height: 1.45;
}
/* Slightly larger headings for clear hierarchy */
h1 { font-size: 1.8rem; }
h2 { font-size: 1.4rem; }
h3 { font-size: 1.15rem; }
h1 { font-size: 1.8rem; }
h2 { font-size: 1.4rem; }
h3 { font-size: 1.15rem; }
/* 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 */
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
.form-control, .btn { font-size: 1rem; padding: .6rem .75rem; }
}
}
</style>
{% block css %}
@@ -224,7 +224,10 @@
<a class="dropdown-item" href="{% url 'sbas:index' %}">SBAs</a>
</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>
</ul>
</li>
@@ -267,12 +270,12 @@
<div class="col-md-12">
<div id="htmx-error"></div>
<span
id="cimar-login-needed"
hx-get='{% url "cimar_status" %}'
hx-trigger="cimar-login-needed from:body"
>
</span>
<span
id="cimar-login-needed"
hx-get='{% url "cimar_status" %}'
hx-trigger="cimar-login-needed from:body"
>
</span>
{% block content %}
{% endblock %}
</div>
@@ -313,10 +316,10 @@
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
<script>
document.addEventListener('DOMContentLoaded', function() {
// 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
// `js-row-selectable` class or contains a checkbox named 'selection'.
//var ancestor = elem.closest('div, section, main, article, form, body');
@@ -336,151 +339,151 @@ document.addEventListener('DOMContentLoaded', function() {
// ancestor = ancestor.parentElement;
//}
// 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"]')); });
return any || null;
}
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;
}
// Helper to build id with optional token
function suffixed(idBase, token) {
return token ? idBase + '-' + token : idBase;
}
function suffixed(idBase, token) {
return token ? idBase + '-' + token : idBase;
}
// Ensure every selectable table has a row-selection control block. If the
// 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.
function ensureControlsForTable(table) {
if (!table) return;
function ensureControlsForTable(table) {
if (!table) return;
// detect an existing nearby toggle button
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
if (existing) return; // already present
var existing = table.previousElementSibling && table.previousElementSibling.querySelector && table.previousElementSibling.querySelector('[id^="toggle-row-selection"]');
if (existing) return; // already present
// 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');
wrapper.className = 'row-selection-controls-wrapper';
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
+ ' <div>'
+ ' <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;">'
+ ' <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>'
+ ' </div>'
+ ' </div>'
+ ' <div>'
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
+ ' </div>'
+ '</div>';
var wrapper = document.createElement('div');
wrapper.className = 'row-selection-controls-wrapper';
wrapper.innerHTML = '<div class="d-flex justify-content-between align-items-center mb-2">'
+ ' <div>'
+ ' <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;">'
+ ' <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>'
+ ' </div>'
+ ' </div>'
+ ' <div>'
+ ' <small class="text-muted">Selected: <span id="selected-count-' + token + '">0</span></small>'
+ ' </div>'
+ '</div>';
// Insert the controls immediately before the table (or its responsive wrapper)
var container = table.closest('.table-responsive') || table;
container.parentNode.insertBefore(wrapper, container);
}
var container = table.closest('.table-responsive') || table;
container.parentNode.insertBefore(wrapper, container);
}
// Create controls for any table that looks selectable but lacks a control block
Array.from(document.querySelectorAll('table')).forEach(function(t){
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
ensureControlsForTable(t);
}
});
Array.from(document.querySelectorAll('table')).forEach(function(t){
if (t.querySelector && (t.querySelector('input[name="selection"]') || t.querySelector('input[type="checkbox"]'))) {
ensureControlsForTable(t);
}
});
// 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)
var token = '';
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
var token = '';
if (toggleBtn.id !== 'toggle-row-selection') token = toggleBtn.id.replace('toggle-row-selection-', '');
var controlsEl = document.getElementById(suffixed('selection-controls', token));
var selectAllBtn = document.getElementById(suffixed('select-all', token));
var clearBtn = document.getElementById(suffixed('clear-selection', token));
var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
var controlsEl = document.getElementById(suffixed('selection-controls', token));
var selectAllBtn = document.getElementById(suffixed('select-all', token));
var clearBtn = document.getElementById(suffixed('clear-selection', token));
var selectedCountSpan = document.getElementById(suffixed('selected-count', token));
var table = findTableForControl(toggleBtn);
if (!table) return; // nothing to do
var table = findTableForControl(toggleBtn);
if (!table) return; // nothing to do
var selectionEnabled = false;
var selectionEnabled = false;
function findRowCheckboxes() {
if (!table) return [];
function findRowCheckboxes() {
if (!table) return [];
// prefer named selection inputs but fall back to any checkbox
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
return checks;
}
var checks = Array.from(table.querySelectorAll('input[type="checkbox"][name="selection"]'));
if (!checks.length) checks = Array.from(table.querySelectorAll('input[type="checkbox"]'));
return checks;
}
function updateSelectedCount() {
if (!selectedCountSpan) return;
var checks = findRowCheckboxes();
var count = checks.filter(function(c){ return c.checked; }).length;
selectedCountSpan.textContent = count;
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
}
function updateSelectedCount() {
if (!selectedCountSpan) return;
var checks = findRowCheckboxes();
var count = checks.filter(function(c){ return c.checked; }).length;
selectedCountSpan.textContent = count;
checks.forEach(function(cb){ var tr = cb.closest('tr'); if (tr) tr.classList.toggle('table-active', cb.checked); });
}
function setCheckboxesDisabled(disabled) {
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
}
function setCheckboxesDisabled(disabled) {
findRowCheckboxes().forEach(function(cb){ cb.disabled = disabled; });
}
function hideSelectionColumn() {
if (!table) return;
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
}
function hideSelectionColumn() {
if (!table) return;
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = 'none'; });
}
function showSelectionColumn() {
if (!table) return;
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
}
function showSelectionColumn() {
if (!table) return;
table.querySelectorAll('th, td').forEach(function(cell){ if (cell.querySelector && cell.querySelector('input[type="checkbox"][name="selection"]')) cell.style.display = ''; });
}
// 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);
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(); });
}
// 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);
if (clearBtn) {
clearBtn.addEventListener('click', function(){ findRowCheckboxes().forEach(function(cb){ if (!cb.disabled) cb.checked = false; }); updateSelectedCount(); });
}
// 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);
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
updateSelectedCount();
});
});
</script>
updateSelectedCount();
});
});
</script>
</body>