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");
|
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
|
// API helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -307,11 +428,17 @@
|
|||||||
const vaults = await api("/api/vaults");
|
const vaults = await api("/api/vaults");
|
||||||
allVaults = vaults;
|
allVaults = vaults;
|
||||||
const container = document.getElementById("vault-tree");
|
const container = document.getElementById("vault-tree");
|
||||||
const filter = document.getElementById("vault-filter");
|
|
||||||
const quickSelect = document.getElementById("vault-quick-select");
|
|
||||||
container.innerHTML = "";
|
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) => {
|
vaults.forEach((v) => {
|
||||||
// Sidebar tree entry
|
// Sidebar tree entry
|
||||||
@ -326,17 +453,6 @@
|
|||||||
|
|
||||||
const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
|
const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
|
||||||
container.appendChild(childContainer);
|
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();
|
syncVaultSelectors();
|
||||||
@ -1329,6 +1445,7 @@
|
|||||||
async function init() {
|
async function init() {
|
||||||
initTheme();
|
initTheme();
|
||||||
initHeaderMenu();
|
initHeaderMenu();
|
||||||
|
initCustomDropdowns();
|
||||||
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
|
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
|
||||||
document.getElementById("header-logo").addEventListener("click", goHome);
|
document.getElementById("header-logo").addEventListener("click", goHome);
|
||||||
initSearch();
|
initSearch();
|
||||||
|
|||||||
@ -95,9 +95,16 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="menu-list-content">
|
<span class="menu-list-content">
|
||||||
<span class="menu-list-title">Vault</span>
|
<span class="menu-list-title">Vault</span>
|
||||||
<select id="vault-filter" class="menu-select menu-list-select">
|
<div class="custom-dropdown" id="vault-filter-dropdown">
|
||||||
<option value="all">Tous les vaults</option>
|
<button class="custom-dropdown-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||||
</select>
|
<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>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<button class="menu-list-row menu-list-button" id="theme-toggle" type="button" role="menuitem">
|
<button class="menu-list-row menu-list-button" id="theme-toggle" type="button" role="menuitem">
|
||||||
@ -139,10 +146,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-tree" id="sidebar-tree">
|
<div class="sidebar-tree" id="sidebar-tree">
|
||||||
<div class="sidebar-quick-select">
|
<div class="custom-dropdown sidebar-dropdown" id="vault-quick-select-dropdown">
|
||||||
<select id="vault-quick-select" class="menu-select sidebar-quick-select-control">
|
<button class="custom-dropdown-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
|
||||||
<option value="all">Tous les vaults</option>
|
<span class="custom-dropdown-selected">Tous les vaults</span>
|
||||||
</select>
|
<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>
|
</div>
|
||||||
<button class="sidebar-panel-toggle" id="vault-panel-toggle" type="button" aria-expanded="true" aria-controls="vault-panel-content">
|
<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>
|
<span class="sidebar-section-title">Vaults</span>
|
||||||
|
|||||||
@ -288,7 +288,7 @@ a:hover {
|
|||||||
padding: 4px 22px 4px 0;
|
padding: 4px 22px 4px 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: transparent;
|
background: inherit;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
@ -296,6 +296,22 @@ a:hover {
|
|||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 160ms ease;
|
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 {
|
.menu-select:hover, .menu-select:focus {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@ -305,7 +321,126 @@ a:hover {
|
|||||||
margin-top: 1px;
|
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 {
|
.main-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -1566,7 +1701,18 @@ body.resizing-v {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-quick-select {
|
.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 {
|
.sidebar-panel-toggle {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user