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:
parent
120367acbb
commit
e9f3639c7c
5 changed files with 154 additions and 80 deletions
|
|
@ -14,44 +14,45 @@
|
||||||
<swp-edit-row>
|
<swp-edit-row>
|
||||||
<swp-edit-label>@Model.LabelCategory</swp-edit-label>
|
<swp-edit-label>@Model.LabelCategory</swp-edit-label>
|
||||||
<swp-select data-value="@Model.CategoryValue">
|
<swp-select data-value="@Model.CategoryValue">
|
||||||
<button type="button" popovertarget="category-select" aria-expanded="false">
|
<button type="button" aria-expanded="false">
|
||||||
<swp-select-value>@Model.Category</swp-select-value>
|
<swp-select-value>@Model.Category</swp-select-value>
|
||||||
<i class="ph ph-caret-down"></i>
|
<i class="ph ph-caret-down"></i>
|
||||||
</button>
|
</button>
|
||||||
<div popover id="category-select">
|
<swp-select-dropdown>
|
||||||
<swp-select-option data-value="kombi" class="@(Model.CategoryValue == "kombi" ? "selected" : "")">Kombi-behandlinger</swp-select-option>
|
<swp-select-option data-value="kombi" class="@(Model.CategoryValue == "kombi" ? "selected" : "")">Kombi-behandlinger</swp-select-option>
|
||||||
<swp-select-option data-value="klip" class="@(Model.CategoryValue == "klip" ? "selected" : "")">Klip</swp-select-option>
|
<swp-select-option data-value="klip" class="@(Model.CategoryValue == "klip" ? "selected" : "")">Klip</swp-select-option>
|
||||||
<swp-select-option data-value="farve" class="@(Model.CategoryValue == "farve" ? "selected" : "")">Farve</swp-select-option>
|
<swp-select-option data-value="farve" class="@(Model.CategoryValue == "farve" ? "selected" : "")">Farve</swp-select-option>
|
||||||
<swp-select-option data-value="behandlinger" class="@(Model.CategoryValue == "behandlinger" ? "selected" : "")">Behandlinger</swp-select-option>
|
<swp-select-option data-value="behandlinger" class="@(Model.CategoryValue == "behandlinger" ? "selected" : "")">Behandlinger</swp-select-option>
|
||||||
<swp-select-option data-value="styling" class="@(Model.CategoryValue == "styling" ? "selected" : "")">Styling</swp-select-option>
|
<swp-select-option data-value="styling" class="@(Model.CategoryValue == "styling" ? "selected" : "")">Styling</swp-select-option>
|
||||||
</div>
|
</swp-select-dropdown>
|
||||||
</swp-select>
|
</swp-select>
|
||||||
</swp-edit-row>
|
</swp-edit-row>
|
||||||
<swp-edit-row>
|
<swp-edit-row>
|
||||||
<swp-edit-label>@Model.LabelCalendarColor</swp-edit-label>
|
<swp-edit-label>@Model.LabelCalendarColor</swp-edit-label>
|
||||||
<swp-select data-value="@Model.CalendarColor">
|
<swp-select data-value="@Model.CalendarColor">
|
||||||
<button type="button" popovertarget="color-select" aria-expanded="false">
|
<button type="button" aria-expanded="false">
|
||||||
|
<swp-color-dot class="is-@Model.CalendarColor"></swp-color-dot>
|
||||||
<swp-select-value>@Model.CalendarColorLabel</swp-select-value>
|
<swp-select-value>@Model.CalendarColorLabel</swp-select-value>
|
||||||
<i class="ph ph-caret-down"></i>
|
<i class="ph ph-caret-down"></i>
|
||||||
</button>
|
</button>
|
||||||
<div popover id="color-select">
|
<swp-select-dropdown>
|
||||||
<swp-select-option data-value="red" class="@(Model.CalendarColor == "red" ? "selected" : "")">Rød</swp-select-option>
|
<swp-select-option data-value="red" class="@(Model.CalendarColor == "red" ? "selected" : "")"><swp-color-dot class="is-red"></swp-color-dot><span>Rød</span></swp-select-option>
|
||||||
<swp-select-option data-value="pink" class="@(Model.CalendarColor == "pink" ? "selected" : "")">Pink</swp-select-option>
|
<swp-select-option data-value="pink" class="@(Model.CalendarColor == "pink" ? "selected" : "")"><swp-color-dot class="is-pink"></swp-color-dot><span>Pink</span></swp-select-option>
|
||||||
<swp-select-option data-value="purple" class="@(Model.CalendarColor == "purple" ? "selected" : "")">Lilla</swp-select-option>
|
<swp-select-option data-value="purple" class="@(Model.CalendarColor == "purple" ? "selected" : "")"><swp-color-dot class="is-purple"></swp-color-dot><span>Lilla</span></swp-select-option>
|
||||||
<swp-select-option data-value="deep-purple" class="@(Model.CalendarColor == "deep-purple" ? "selected" : "")">Mørk lilla</swp-select-option>
|
<swp-select-option data-value="deep-purple" class="@(Model.CalendarColor == "deep-purple" ? "selected" : "")"><swp-color-dot class="is-deep-purple"></swp-color-dot><span>Mørk lilla</span></swp-select-option>
|
||||||
<swp-select-option data-value="indigo" class="@(Model.CalendarColor == "indigo" ? "selected" : "")">Indigo</swp-select-option>
|
<swp-select-option data-value="indigo" class="@(Model.CalendarColor == "indigo" ? "selected" : "")"><swp-color-dot class="is-indigo"></swp-color-dot><span>Indigo</span></swp-select-option>
|
||||||
<swp-select-option data-value="blue" class="@(Model.CalendarColor == "blue" ? "selected" : "")">Blå</swp-select-option>
|
<swp-select-option data-value="blue" class="@(Model.CalendarColor == "blue" ? "selected" : "")"><swp-color-dot class="is-blue"></swp-color-dot><span>Blå</span></swp-select-option>
|
||||||
<swp-select-option data-value="light-blue" class="@(Model.CalendarColor == "light-blue" ? "selected" : "")">Lyseblå</swp-select-option>
|
<swp-select-option data-value="light-blue" class="@(Model.CalendarColor == "light-blue" ? "selected" : "")"><swp-color-dot class="is-light-blue"></swp-color-dot><span>Lyseblå</span></swp-select-option>
|
||||||
<swp-select-option data-value="cyan" class="@(Model.CalendarColor == "cyan" ? "selected" : "")">Cyan</swp-select-option>
|
<swp-select-option data-value="cyan" class="@(Model.CalendarColor == "cyan" ? "selected" : "")"><swp-color-dot class="is-cyan"></swp-color-dot><span>Cyan</span></swp-select-option>
|
||||||
<swp-select-option data-value="teal" class="@(Model.CalendarColor == "teal" ? "selected" : "")">Teal</swp-select-option>
|
<swp-select-option data-value="teal" class="@(Model.CalendarColor == "teal" ? "selected" : "")"><swp-color-dot class="is-teal"></swp-color-dot><span>Teal</span></swp-select-option>
|
||||||
<swp-select-option data-value="green" class="@(Model.CalendarColor == "green" ? "selected" : "")">Grøn</swp-select-option>
|
<swp-select-option data-value="green" class="@(Model.CalendarColor == "green" ? "selected" : "")"><swp-color-dot class="is-green"></swp-color-dot><span>Grøn</span></swp-select-option>
|
||||||
<swp-select-option data-value="light-green" class="@(Model.CalendarColor == "light-green" ? "selected" : "")">Lysegrøn</swp-select-option>
|
<swp-select-option data-value="light-green" class="@(Model.CalendarColor == "light-green" ? "selected" : "")"><swp-color-dot class="is-light-green"></swp-color-dot><span>Lysegrøn</span></swp-select-option>
|
||||||
<swp-select-option data-value="lime" class="@(Model.CalendarColor == "lime" ? "selected" : "")">Lime</swp-select-option>
|
<swp-select-option data-value="lime" class="@(Model.CalendarColor == "lime" ? "selected" : "")"><swp-color-dot class="is-lime"></swp-color-dot><span>Lime</span></swp-select-option>
|
||||||
<swp-select-option data-value="yellow" class="@(Model.CalendarColor == "yellow" ? "selected" : "")">Gul</swp-select-option>
|
<swp-select-option data-value="yellow" class="@(Model.CalendarColor == "yellow" ? "selected" : "")"><swp-color-dot class="is-yellow"></swp-color-dot><span>Gul</span></swp-select-option>
|
||||||
<swp-select-option data-value="amber" class="@(Model.CalendarColor == "amber" ? "selected" : "")">Amber</swp-select-option>
|
<swp-select-option data-value="amber" class="@(Model.CalendarColor == "amber" ? "selected" : "")"><swp-color-dot class="is-amber"></swp-color-dot><span>Amber</span></swp-select-option>
|
||||||
<swp-select-option data-value="orange" class="@(Model.CalendarColor == "orange" ? "selected" : "")">Orange</swp-select-option>
|
<swp-select-option data-value="orange" class="@(Model.CalendarColor == "orange" ? "selected" : "")"><swp-color-dot class="is-orange"></swp-color-dot><span>Orange</span></swp-select-option>
|
||||||
<swp-select-option data-value="deep-orange" class="@(Model.CalendarColor == "deep-orange" ? "selected" : "")">Mørk orange</swp-select-option>
|
<swp-select-option data-value="deep-orange" class="@(Model.CalendarColor == "deep-orange" ? "selected" : "")"><swp-color-dot class="is-deep-orange"></swp-color-dot><span>Mørk orange</span></swp-select-option>
|
||||||
</div>
|
</swp-select-dropdown>
|
||||||
</swp-select>
|
</swp-select>
|
||||||
</swp-edit-row>
|
</swp-edit-row>
|
||||||
<swp-edit-row>
|
<swp-edit-row>
|
||||||
|
|
@ -123,7 +124,10 @@
|
||||||
<textarea id="description" rows="4">@Model.Description</textarea>
|
<textarea id="description" rows="4">@Model.Description</textarea>
|
||||||
|
|
||||||
<swp-section-label class="spaced">@Model.LabelImage</swp-section-label>
|
<swp-section-label class="spaced">@Model.LabelImage</swp-section-label>
|
||||||
<swp-btn class="secondary">@Model.LabelUploadImage</swp-btn>
|
<swp-add-button>
|
||||||
|
<i class="ph ph-upload-simple"></i>
|
||||||
|
@Model.LabelUploadImage
|
||||||
|
</swp-add-button>
|
||||||
</swp-card>
|
</swp-card>
|
||||||
</div>
|
</div>
|
||||||
</swp-detail-grid>
|
</swp-detail-grid>
|
||||||
|
|
|
||||||
|
|
@ -655,19 +655,9 @@ swp-auto-id {
|
||||||
=========================================== */
|
=========================================== */
|
||||||
swp-note-field {
|
swp-note-field {
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
padding: var(--spacing-6);
|
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);
|
border-radius: var(--radius-md);
|
||||||
resize: vertical;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--color-teal);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -678,6 +678,34 @@ swp-user-email {
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
FORM INPUTS (shared base styling)
|
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 {
|
swp-form-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 */
|
/* Date range inputs */
|
||||||
swp-date-range {
|
swp-date-range {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -183,11 +183,11 @@ swp-notification-intro {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
SELECT DROPDOWN (Popover API)
|
SELECT DROPDOWN
|
||||||
=========================================== */
|
=========================================== */
|
||||||
swp-select {
|
swp-select {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-select button {
|
swp-select button {
|
||||||
|
|
@ -202,8 +202,7 @@ swp-select button {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 150ms ease;
|
transition: all 150ms ease;
|
||||||
min-width: 160px;
|
width: 100%;
|
||||||
anchor-name: --select-trigger;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
|
|
@ -227,35 +226,37 @@ swp-select button i {
|
||||||
transition: transform 150ms ease;
|
transition: transform 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-select button[aria-expanded="true"] i {
|
swp-select.open button i {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-select [popover] {
|
swp-select-dropdown {
|
||||||
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
position-anchor: --select-trigger;
|
top: 100%;
|
||||||
top: anchor(bottom);
|
left: 0;
|
||||||
left: anchor(left);
|
z-index: 100;
|
||||||
margin: var(--spacing-1) 0 0 0;
|
margin-top: var(--spacing-1);
|
||||||
padding: var(--spacing-2);
|
padding: var(--spacing-2);
|
||||||
|
min-width: 100%;
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
min-width: anchor-size(width);
|
|
||||||
max-height: 280px;
|
max-height: 280px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
|
||||||
|
|
||||||
swp-select [popover]:popover-open {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swp-select.open swp-select-dropdown {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
swp-select-option {
|
swp-select-option {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-3);
|
||||||
padding: var(--spacing-2) var(--spacing-3);
|
padding: var(--spacing-2) var(--spacing-3);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -272,4 +273,21 @@ swp-select-option {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
color: var(--color-teal);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
*
|
*
|
||||||
* Handles generic UI controls functionality:
|
* Handles generic UI controls functionality:
|
||||||
* - Toggle sliders (Ja/Nej switches)
|
* - 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
|
* Initialize all select dropdowns on the page
|
||||||
* Uses Popover API for dropdown behavior
|
|
||||||
*/
|
*/
|
||||||
private initSelectDropdowns(): void {
|
private initSelectDropdowns(): void {
|
||||||
document.querySelectorAll('swp-select').forEach(select => {
|
document.querySelectorAll('swp-select').forEach(select => {
|
||||||
const trigger = select.querySelector('button');
|
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');
|
const options = select.querySelectorAll('swp-select-option');
|
||||||
|
|
||||||
if (!trigger || !popover) return;
|
if (!trigger || !dropdown) return;
|
||||||
|
|
||||||
// Update aria-expanded on toggle
|
// Toggle dropdown on button click
|
||||||
popover.addEventListener('toggle', (e: Event) => {
|
trigger.addEventListener('click', (e) => {
|
||||||
const event = e as ToggleEvent;
|
e.stopPropagation();
|
||||||
trigger.setAttribute('aria-expanded', event.newState === 'open' ? 'true' : 'false');
|
const isOpen = select.classList.toggle('open');
|
||||||
|
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle option selection
|
// Handle option selection
|
||||||
|
|
@ -70,11 +70,18 @@ export class ControlsController {
|
||||||
valueEl.textContent = label;
|
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
|
// Update data-value on select element
|
||||||
(select as HTMLElement).dataset.value = value;
|
(select as HTMLElement).dataset.value = value;
|
||||||
|
|
||||||
// Close popover
|
// Close dropdown
|
||||||
popover.hidePopover();
|
select.classList.remove('open');
|
||||||
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
// Dispatch custom event
|
// Dispatch custom event
|
||||||
select.dispatchEvent(new CustomEvent('change', {
|
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');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue