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:
parent
29c1618472
commit
565c3c8a27
147
frontend/app.js
147
frontend/app.js
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user