Enhance service detail UI with improved select and color controls

Refactors select dropdown functionality to use custom implementation
Adds color dot support for color selection
Improves keyboard navigation and interaction for select dropdowns

Modernizes UI components with more flexible and interactive controls
This commit is contained in:
Janus C. H. Knudsen 2026-01-16 23:25:05 +01:00
parent 120367acbb
commit e9f3639c7c
5 changed files with 154 additions and 80 deletions

View file

@ -655,19 +655,9 @@ swp-auto-id {
=========================================== */
swp-note-field {
textarea {
width: 100%;
min-height: 80px;
padding: var(--spacing-6);
font-size: var(--font-size-base);
font-family: var(--font-family);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
resize: vertical;
&:focus {
outline: none;
border-color: var(--color-teal);
}
}
}

View file

@ -678,6 +678,34 @@ swp-user-email {
/* ===========================================
FORM INPUTS (shared base styling)
=========================================== */
/* Global textarea styling */
textarea {
width: 100%;
padding: var(--spacing-3) var(--spacing-4);
font-size: var(--font-size-base);
font-family: var(--font-family);
color: var(--color-text);
background: var(--color-background-alt);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
resize: vertical;
&::placeholder {
color: var(--color-text-muted);
}
&:hover {
background: var(--color-background);
}
&:focus {
outline: none;
background: var(--color-surface);
border-color: var(--color-teal);
}
}
swp-form-group {
display: flex;
flex-direction: column;
@ -861,28 +889,6 @@ swp-form-input {
}
}
/* Textarea */
textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 14px;
font-family: var(--font-family);
color: var(--color-text);
background: var(--color-surface);
resize: vertical;
&::placeholder {
color: var(--color-text-muted);
}
&:focus {
outline: none;
border-color: var(--color-teal);
}
}
/* Date range inputs */
swp-date-range {
display: flex;

View file

@ -183,11 +183,11 @@ swp-notification-intro {
}
/* ===========================================
SELECT DROPDOWN (Popover API)
SELECT DROPDOWN
=========================================== */
swp-select {
position: relative;
display: inline-block;
display: block;
}
swp-select button {
@ -202,8 +202,7 @@ swp-select button {
border: 1px solid transparent;
cursor: pointer;
transition: all 150ms ease;
min-width: 160px;
anchor-name: --select-trigger;
width: 100%;
&:hover {
background: var(--color-background);
@ -227,35 +226,37 @@ swp-select button i {
transition: transform 150ms ease;
}
swp-select button[aria-expanded="true"] i {
swp-select.open button i {
transform: rotate(180deg);
}
swp-select [popover] {
swp-select-dropdown {
display: none;
position: absolute;
position-anchor: --select-trigger;
top: anchor(bottom);
left: anchor(left);
margin: var(--spacing-1) 0 0 0;
top: 100%;
left: 0;
z-index: 100;
margin-top: var(--spacing-1);
padding: var(--spacing-2);
min-width: 100%;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
min-width: anchor-size(width);
max-height: 280px;
overflow-y: auto;
}
swp-select [popover]:popover-open {
display: flex;
flex-direction: column;
gap: 2px;
}
swp-select.open swp-select-dropdown {
display: flex;
}
swp-select-option {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-2) var(--spacing-3);
border-radius: var(--radius-sm);
cursor: pointer;
@ -272,4 +273,21 @@ swp-select-option {
font-weight: var(--font-weight-medium);
color: var(--color-teal);
}
&.highlighted {
background: var(--color-background-alt);
outline: 2px solid var(--color-teal);
outline-offset: -2px;
}
}
/* ===========================================
COLOR DOT (for color pickers)
=========================================== */
swp-color-dot {
width: 18px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
background: var(--b-primary);
}

View file

@ -3,7 +3,7 @@
*
* Handles generic UI controls functionality:
* - Toggle sliders (Ja/Nej switches)
* - Select dropdowns (Popover API)
* - Select dropdowns
*/
/**
@ -38,20 +38,20 @@ export class ControlsController {
/**
* Initialize all select dropdowns on the page
* Uses Popover API for dropdown behavior
*/
private initSelectDropdowns(): void {
document.querySelectorAll('swp-select').forEach(select => {
const trigger = select.querySelector('button');
const popover = select.querySelector('[popover]') as HTMLElement | null;
const dropdown = select.querySelector('swp-select-dropdown');
const options = select.querySelectorAll('swp-select-option');
if (!trigger || !popover) return;
if (!trigger || !dropdown) return;
// Update aria-expanded on toggle
popover.addEventListener('toggle', (e: Event) => {
const event = e as ToggleEvent;
trigger.setAttribute('aria-expanded', event.newState === 'open' ? 'true' : 'false');
// Toggle dropdown on button click
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = select.classList.toggle('open');
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// Handle option selection
@ -70,11 +70,18 @@ export class ControlsController {
valueEl.textContent = label;
}
// Update color dot if present (for color pickers)
const triggerDot = trigger.querySelector('swp-color-dot');
if (triggerDot && value) {
triggerDot.className = `is-${value}`;
}
// Update data-value on select element
(select as HTMLElement).dataset.value = value;
// Close popover
popover.hidePopover();
// Close dropdown
select.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
// Dispatch custom event
select.dispatchEvent(new CustomEvent('change', {
@ -84,5 +91,54 @@ export class ControlsController {
});
});
});
// Click outside to close any open dropdown
document.addEventListener('click', () => {
this.closeAllSelects();
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
const openSelect = document.querySelector('swp-select.open');
if (!openSelect) return;
// Escape to close
if (e.key === 'Escape') {
this.closeAllSelects();
return;
}
// Letter key to jump to option
if (e.key.length === 1 && /[a-zA-ZæøåÆØÅ]/.test(e.key)) {
const options = openSelect.querySelectorAll('swp-select-option');
const letter = e.key.toLowerCase();
for (const option of options) {
const text = option.textContent?.trim().toLowerCase() || '';
if (text.startsWith(letter)) {
// Scroll into view and highlight
option.scrollIntoView({ block: 'nearest' });
options.forEach(o => o.classList.remove('highlighted'));
option.classList.add('highlighted');
break;
}
}
}
// Enter to select highlighted option
if (e.key === 'Enter') {
const highlighted = openSelect.querySelector('swp-select-option.highlighted') as HTMLElement;
if (highlighted) {
highlighted.click();
}
}
});
}
private closeAllSelects(): void {
document.querySelectorAll('swp-select.open').forEach(select => {
select.classList.remove('open');
select.querySelector('button')?.setAttribute('aria-expanded', 'false');
});
}
}