diff --git a/src/app/components/dropdown/dropdown.html b/src/app/components/dropdown/dropdown.html
index 24dc812..53eaada 100644
--- a/src/app/components/dropdown/dropdown.html
+++ b/src/app/components/dropdown/dropdown.html
@@ -1,22 +1,23 @@
+ contenteditable="true" role="textbox" aria-multiline="false" [aria-placeholder]="placeholder().trim().length > 0 ? placeholder() : translation.common.selectOption" [aria-activedescendant]="ariaActiveDescendant()"
+ [attr.placeholder]="placeholder().trim().length > 0 ? placeholder() : translation.common.selectOption" tabindex="-1" spellcheck="false"
+ (input)="onSearch($event)" (click)="handleInputClick($event)" (mousedown)="handleInputMouseDown()" (beforeinput)="preprocessInputContent($event)"
+ (focus)="onInputFocusIn()" (focusout)="onInputFocusOut()">
@if (icon()) {
- {{ icon() }}
+ {{ icon() }}
}
@if (recreateKey() % 2 == 0) {}
@else {}
@if (!isOpen()) {
- {{ selectedLabel() }}
+ {{ selectedLabel() }}
}
- keyboard_arrow_down
+ keyboard_arrow_down
@if (isOpen()) {
diff --git a/src/app/components/dropdown/dropdown.scss b/src/app/components/dropdown/dropdown.scss
index f408140..2a50a2a 100644
--- a/src/app/components/dropdown/dropdown.scss
+++ b/src/app/components/dropdown/dropdown.scss
@@ -39,7 +39,7 @@ $default_animation_duration: 0.2s;
max-width: 100%;
- &:focus-within {
+ &:has(.dropdown-input.focus-mode:focus-visible) {
outline: 2px solid list.nth(library.$brand_palette, 5);
outline-offset: 2px;
}
@@ -61,33 +61,31 @@ $default_animation_duration: 0.2s;
.placeholder {
text-overflow: ellipsis;
- }
-
- .dropdown-input:focus-visible {
- outline: none;
- }
-
- .dropdown-input:empty:before {
- content: attr(placeholder);
- opacity: 50%;
- }
-
- .dropdown-input:not(.focus-mode) {
- caret-color: transparent;
- user-select: none;
- }
-
- .dropdown-input.hidden {
- position: absolute;
-
- width: 0;
- height: 0;
- opacity: 0;
- visibility: hidden;
+ &.value-empty { opacity: 50%; }
}
.dropdown-input {
cursor: text;
+
+ &:focus-visible { outline: none; }
+ &:empty:before {
+ content: attr(placeholder);
+ opacity: 50%;
+ }
+
+ &:not(.focus-mode) {
+ caret-color: transparent;
+ user-select: none;
+ }
+
+ &.hidden {
+ position: absolute;
+
+ width: 0;
+ height: 0;
+ opacity: 0;
+ visibility: hidden;
+ }
}
.placeholder, .dropdown-input {
diff --git a/src/app/components/dropdown/dropdown.ts b/src/app/components/dropdown/dropdown.ts
index 9319c6d..a1ca66a 100644
--- a/src/app/components/dropdown/dropdown.ts
+++ b/src/app/components/dropdown/dropdown.ts
@@ -1,19 +1,19 @@
import {
Component,
+ computed,
+ effect,
+ ElementRef,
+ HostListener,
+ inject,
input,
model,
- output,
- signal,
- HostListener,
- ElementRef,
- computed,
OnInit,
- inject,
- effect,
- viewChild,
- viewChildren,
- untracked,
+ output,
PLATFORM_ID,
+ signal,
+ untracked,
+ viewChild,
+ viewChildren
} from '@angular/core';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { TranslationService } from '../../services/translation.service';
@@ -39,7 +39,7 @@ export interface DropdownOption {
'[class.open]': 'isOpen()',
tabindex: '0',
'(click)': 'toggle()',
- '(focus)': 'onFocus()',
+ '(focusin)': 'onFocusIn($event)',
'(mousedown)': 'onMouseDown($event)',
'(keydown)': 'onKeyDown($event)',
'(keydown.enter)': 'onEnter($event)',
@@ -56,7 +56,7 @@ export class Dropdown implements OnInit {
optionItems = viewChildren>('optionItem');
options = input>([]);
- placeholder = input('Select an option');
+ placeholder = input('');
ariaLabel = input(undefined);
icon = input();
defaultIndex = input();
@@ -90,7 +90,7 @@ export class Dropdown implements OnInit {
if (isDropdownOpen && index >= 0) {
requestAnimationFrame(() => {
this.scrollToIndex(index);
- })
+ });
}
});
@@ -166,23 +166,38 @@ export class Dropdown implements OnInit {
this.focusedIndex.set(this.normalizedOptions().findIndex((o) => o.value === this.value()));
setTimeout(() => {
- const searchInputElement = this.searchInput()!.nativeElement;
+ let wasFocusVisible = this.element.nativeElement.matches(':focus-visible');
+ this.focusSearchInput();
- searchInputElement.focus();
- this.setCursorToEnd(searchInputElement);
+ if (wasFocusVisible) this.inputFocusMode.set(true);
}, 0);
}
}
- onFocus() {
- if (this.isOpen()) {
- const searchInputElement = this.searchInput()!.nativeElement;
+ onInputFocusIn() {
+ if (this.isOpen()) this.element.nativeElement.tabIndex = -1;
+ }
- searchInputElement.focus();
- this.setCursorToEnd(searchInputElement);
+ onInputFocusOut() {
+ if (this.isOpen()) this.element.nativeElement.tabIndex = undefined;
+ }
+
+ onFocusIn(event: FocusEvent) {
+ if (this.element.nativeElement.contains(event.relatedTarget)) return;
+
+ if (this.isOpen()) {
+ this.focusSearchInput();
+ this.inputFocusMode.set(true);
}
}
+ private focusSearchInput() {
+ const searchInputElement = this.searchInput()!.nativeElement;
+
+ searchInputElement.focus();
+ this.setCursorToEnd(searchInputElement);
+ }
+
private checkIfInputFocus(event: Event) {
let triggerInputFocus = this.searchInput()!.nativeElement.contains(
event.target! as HTMLElement,
@@ -198,6 +213,7 @@ export class Dropdown implements OnInit {
if (isFocusInside) this.element.nativeElement.focus({ preventScroll: true });
if (isDropdownOpen) this.recreateKey.update((v) => (v + 1) % 65536);
+ this.element.nativeElement.tabIndex = undefined;
this.focusedIndex.set(-1);
}
@@ -234,6 +250,8 @@ export class Dropdown implements OnInit {
onArrowDown(event: Event) {
event.preventDefault();
+ if (!this.inputFocusMode()) this.inputFocusMode.set(true);
+
if (!this.isOpen()) {
this.toggle();
return;
@@ -250,6 +268,8 @@ export class Dropdown implements OnInit {
onArrowUp(event: Event) {
event.preventDefault();
+ if (!this.inputFocusMode()) this.inputFocusMode.set(true);
+
if (!this.isOpen()) {
this.toggle();
return;
@@ -332,8 +352,12 @@ export class Dropdown implements OnInit {
}
}
+ selectedOption = computed(() => {
+ return this.normalizedOptions().find((o) => o.value === this.value());
+ })
+
selectedLabel = computed(() => {
- const selected = this.normalizedOptions().find((o) => o.value === this.value());
+ const selected = this.selectedOption();
return selected ? selected.label : this.placeholder();
});