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:
Bruno Charest 2026-03-22 00:56:55 -04:00
parent 29c1618472
commit 565c3c8a27
3 changed files with 300 additions and 25 deletions

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();

View File

@ -95,9 +95,16 @@
</span>
<span class="menu-list-content">
<span class="menu-list-title">Vault</span>
<select id="vault-filter" class="menu-select menu-list-select">
<option value="all">Tous les vaults</option>
</select>
<div class="custom-dropdown" id="vault-filter-dropdown">
<button class="custom-dropdown-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
<span class="custom-dropdown-selected">Tous les vaults</span>
<i data-lucide="chevron-down" style="width:14px;height:14px"></i>
</button>
<ul class="custom-dropdown-menu" role="listbox" id="vault-filter-options">
<li role="option" data-value="all" class="custom-dropdown-option selected">Tous les vaults</li>
</ul>
<input type="hidden" id="vault-filter" value="all">
</div>
</span>
</label>
<button class="menu-list-row menu-list-button" id="theme-toggle" type="button" role="menuitem">
@ -139,10 +146,15 @@
</div>
<div class="sidebar-tree" id="sidebar-tree">
<div class="sidebar-quick-select">
<select id="vault-quick-select" class="menu-select sidebar-quick-select-control">
<option value="all">Tous les vaults</option>
</select>
<div class="custom-dropdown sidebar-dropdown" id="vault-quick-select-dropdown">
<button class="custom-dropdown-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
<span class="custom-dropdown-selected">Tous les vaults</span>
<i data-lucide="chevron-down" style="width:14px;height:14px"></i>
</button>
<ul class="custom-dropdown-menu" role="listbox" id="vault-quick-select-options">
<li role="option" data-value="all" class="custom-dropdown-option selected">Tous les vaults</li>
</ul>
<input type="hidden" id="vault-quick-select" value="all">
</div>
<button class="sidebar-panel-toggle" id="vault-panel-toggle" type="button" aria-expanded="true" aria-controls="vault-panel-content">
<span class="sidebar-section-title">Vaults</span>

View File

@ -288,7 +288,7 @@ a:hover {
padding: 4px 22px 4px 0;
border: none;
border-radius: 0;
background: transparent;
background: inherit;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
@ -296,6 +296,22 @@ a:hover {
outline: none;
cursor: pointer;
transition: color 160ms ease;
color-scheme: dark;
background-color: transparent;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.menu-select option {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
[data-theme="dark"] .menu-select,
[data-theme="dark"] .menu-select option {
background-color: #1e1e1e;
color: #e0e0e0;
}
.menu-select:hover, .menu-select:focus {
color: var(--accent);
@ -305,7 +321,126 @@ a:hover {
margin-top: 1px;
}
/* --- Main body --- */
/* Force dark mode on all select elements */
select {
color-scheme: dark;
}
[data-theme="dark"] select,
[data-theme="dark"] select option {
background-color: #161b22 !important;
color: #e6edf3 !important;
}
[data-theme="light"] select,
[data-theme="light"] select option {
background-color: #f6f8fa !important;
color: #1f2328 !important;
}
/* Custom Dropdown Styles */
.custom-dropdown {
position: relative;
width: 100%;
}
.custom-dropdown-trigger {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
cursor: pointer;
transition: border-color 200ms ease, background 200ms ease;
}
.custom-dropdown-trigger:hover {
border-color: var(--accent);
background: var(--bg-hover);
}
.custom-dropdown-trigger[aria-expanded="true"] {
border-color: var(--accent);
}
.custom-dropdown-selected {
flex: 1;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.custom-dropdown-menu {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
max-height: 240px;
overflow-y: auto;
z-index: 1000;
list-style: none;
padding: 4px;
margin: 0;
}
.custom-dropdown.open .custom-dropdown-menu {
display: block;
}
.custom-dropdown-option {
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--text-primary);
transition: background 150ms ease, color 150ms ease;
}
.custom-dropdown-option:hover,
.custom-dropdown-option:focus {
background: var(--bg-hover);
color: var(--accent);
}
.custom-dropdown-option.selected {
background: color-mix(in srgb, var(--accent) 15%, transparent);
color: var(--accent);
}
/* Sidebar dropdown specific */
.sidebar-dropdown {
padding: 0 16px 12px;
}
.sidebar-dropdown .custom-dropdown-trigger {
background: var(--bg-sidebar);
}
/* Menu list dropdown specific */
.menu-list-select-row .custom-dropdown {
margin-top: 4px;
}
.menu-list-select-row .custom-dropdown-trigger {
padding: 6px 10px;
font-size: 0.78rem;
background: var(--bg-primary);
}
.main-body {
display: flex;
flex: 1;
@ -1566,7 +1701,18 @@ body.resizing-v {
}
.sidebar-quick-select {
padding: 0 12px 12px;
padding: 0 16px 12px;
}
.sidebar-quick-select select {
color-scheme: dark;
background-color: var(--bg-secondary);
color: var(--text-primary);
}
[data-theme="dark"] .sidebar-quick-select select {
background-color: #1e1e1e;
color: #e0e0e0;
}
.sidebar-panel-toggle {