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:
+132
-15
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user