Replace native select dropdowns with custom styled dropdowns featuring improved visual consistency, dark mode support, and enhanced accessibility with ARIA attributes

This commit is contained in:
2026-03-22 00:56:55 -04:00
parent 29c1618472
commit 565c3c8a27
3 changed files with 300 additions and 25 deletions
+132 -15
View File
@@ -160,6 +160,127 @@
menuDropdown.classList.remove("active");
}
// ---------------------------------------------------------------------------
// Custom Dropdowns
// ---------------------------------------------------------------------------
function initCustomDropdowns() {
document.querySelectorAll('.custom-dropdown').forEach(dropdown => {
const trigger = dropdown.querySelector('.custom-dropdown-trigger');
const options = dropdown.querySelectorAll('.custom-dropdown-option');
const hiddenInput = dropdown.querySelector('input[type="hidden"]');
const selectedText = dropdown.querySelector('.custom-dropdown-selected');
if (!trigger) return;
// Toggle dropdown
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = dropdown.classList.contains('open');
// Close all other dropdowns
document.querySelectorAll('.custom-dropdown.open').forEach(d => {
if (d !== dropdown) d.classList.remove('open');
});
dropdown.classList.toggle('open', !isOpen);
trigger.setAttribute('aria-expanded', !isOpen);
});
// Handle option selection
options.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const value = option.getAttribute('data-value');
const text = option.textContent;
// Update hidden input
if (hiddenInput) {
hiddenInput.value = value;
// Trigger change event
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Update selected text
if (selectedText) {
selectedText.textContent = text;
}
// Update visual selection
options.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
// Close dropdown
dropdown.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
});
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', () => {
document.querySelectorAll('.custom-dropdown.open').forEach(dropdown => {
dropdown.classList.remove('open');
const trigger = dropdown.querySelector('.custom-dropdown-trigger');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
});
});
}
// Helper to populate custom dropdown options
function populateCustomDropdown(dropdownId, optionsList, defaultValue) {
const dropdown = document.getElementById(dropdownId);
if (!dropdown) return;
const optionsContainer = dropdown.querySelector('.custom-dropdown-menu');
const hiddenInput = dropdown.querySelector('input[type="hidden"]');
const selectedText = dropdown.querySelector('.custom-dropdown-selected');
if (!optionsContainer) return;
// Clear existing options (keep the first one if it's the default)
optionsContainer.innerHTML = '';
// Add new options
optionsList.forEach(opt => {
const li = document.createElement('li');
li.className = 'custom-dropdown-option';
li.setAttribute('role', 'option');
li.setAttribute('data-value', opt.value);
li.textContent = opt.text;
if (opt.value === defaultValue) {
li.classList.add('selected');
if (selectedText) selectedText.textContent = opt.text;
if (hiddenInput) hiddenInput.value = opt.value;
}
optionsContainer.appendChild(li);
});
// Re-initialize click handlers
optionsContainer.querySelectorAll('.custom-dropdown-option').forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const value = option.getAttribute('data-value');
const text = option.textContent;
if (hiddenInput) {
hiddenInput.value = value;
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
if (selectedText) {
selectedText.textContent = text;
}
optionsContainer.querySelectorAll('.custom-dropdown-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
dropdown.classList.remove('open');
const trigger = dropdown.querySelector('.custom-dropdown-trigger');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
});
});
}
// ---------------------------------------------------------------------------
// API helpers
// ---------------------------------------------------------------------------
@@ -307,11 +428,17 @@
const vaults = await api("/api/vaults");
allVaults = vaults;
const container = document.getElementById("vault-tree");
const filter = document.getElementById("vault-filter");
const quickSelect = document.getElementById("vault-quick-select");
container.innerHTML = "";
filter.innerHTML = '<option value="all">Tous les vaults</option>';
quickSelect.innerHTML = '<option value="all">Tous les vaults</option>';
// Prepare dropdown options
const dropdownOptions = [
{ value: "all", text: "Tous les vaults" },
...vaults.map(v => ({ value: v.name, text: v.name }))
];
// Populate custom dropdowns
populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all");
populateCustomDropdown("vault-quick-select-dropdown", dropdownOptions, "all");
vaults.forEach((v) => {
// Sidebar tree entry
@@ -326,17 +453,6 @@
const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
container.appendChild(childContainer);
// Vault filter dropdown
const opt = document.createElement("option");
opt.value = v.name;
opt.textContent = v.name;
filter.appendChild(opt);
const quickOpt = document.createElement("option");
quickOpt.value = v.name;
quickOpt.textContent = v.name;
quickSelect.appendChild(quickOpt);
});
syncVaultSelectors();
@@ -1329,6 +1445,7 @@
async function init() {
initTheme();
initHeaderMenu();
initCustomDropdowns();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
document.getElementById("header-logo").addEventListener("click", goHome);
initSearch();