Update Dropdown Focus Behavior to Be More Accessible
This commit is contained in:
1 parent
c21845a579
commit
9092c97b9c
3 files changed
+58
-35
No files matched your search
@@ -1,22 +1,23 @@
|
||||
<ng-template #inputTemplate>
|
||||
<div #searchInput class="dropdown-input" [class.hidden]="!isOpen()" [class.focus-mode]="inputFocusMode()"
|
||||
contenteditable="true" role="textbox" aria-multiline="false" [aria-placeholder]="translation.common.selectOption" [aria-activedescendant]="ariaActiveDescendant()"
|
||||
[attr.placeholder]="translation.common.selectOption" tabindex="-1" spellcheck="false"
|
||||
(input)="onSearch($event)" (click)="handleInputClick($event)" (mousedown)="handleInputMouseDown()" (beforeinput)="preprocessInputContent($event)"></div>
|
||||
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()"></div>
|
||||
</ng-template>
|
||||
|
||||
<div class="dropdown-box">
|
||||
@if (icon()) {
|
||||
<span class="material-symbols-outlined icon-24 prefix-icon">{{ icon() }}</span>
|
||||
<span class="material-symbols-outlined icon-24 prefix-icon" aria-hidden="true">{{ icon() }}</span>
|
||||
}
|
||||
|
||||
@if (recreateKey() % 2 == 0) {<ng-container [ngTemplateOutlet]="inputTemplate" />}
|
||||
@else {<ng-container [ngTemplateOutlet]="inputTemplate" />}
|
||||
|
||||
@if (!isOpen()) {
|
||||
<span class="placeholder">{{ selectedLabel() }}</span>
|
||||
<span class="placeholder" [class.value-empty]="selectedOption() === undefined">{{ selectedLabel() }}</span>
|
||||
}
|
||||
<span class="material-symbols-outlined icon-24 chevron" [class.open]="isOpen()">keyboard_arrow_down</span>
|
||||
<span class="material-symbols-outlined icon-24 chevron" aria-hidden="true" [class.open]="isOpen()">keyboard_arrow_down</span>
|
||||
</div>
|
||||
|
||||
@if (isOpen()) {
|
||||
|
||||
@@ -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,23 +61,24 @@ $default_animation_duration: 0.2s;
|
||||
|
||||
.placeholder {
|
||||
text-overflow: ellipsis;
|
||||
&.value-empty { opacity: 50%; }
|
||||
}
|
||||
|
||||
.dropdown-input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
.dropdown-input {
|
||||
cursor: text;
|
||||
|
||||
.dropdown-input:empty:before {
|
||||
&:focus-visible { outline: none; }
|
||||
&:empty:before {
|
||||
content: attr(placeholder);
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.dropdown-input:not(.focus-mode) {
|
||||
&:not(.focus-mode) {
|
||||
caret-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dropdown-input.hidden {
|
||||
&.hidden {
|
||||
position: absolute;
|
||||
|
||||
width: 0;
|
||||
@@ -85,9 +86,6 @@ $default_animation_duration: 0.2s;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dropdown-input {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.placeholder, .dropdown-input {
|
||||
|
||||
@@ -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<ElementRef<HTMLElement>>('optionItem');
|
||||
|
||||
options = input<Array<DropdownOption | string>>([]);
|
||||
placeholder = input<string>('Select an option');
|
||||
placeholder = input<string>('');
|
||||
ariaLabel = input<string | undefined>(undefined);
|
||||
icon = input<string>();
|
||||
defaultIndex = input<number>();
|
||||
@@ -90,7 +90,7 @@ export class Dropdown implements OnInit {
|
||||
if (isDropdownOpen && index >= 0) {
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollToIndex(index);
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,22 +166,37 @@ 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() {
|
||||
onInputFocusIn() {
|
||||
if (this.isOpen()) this.element.nativeElement.tabIndex = -1;
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user