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 (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(); });