Improve Accessibility of Components
This commit is contained in:
1 parent
102360791f
commit
e430b5b600
5 files changed
+31
-16
No files matched your search
@@ -7,11 +7,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (isOpen()) {
|
@if (isOpen()) {
|
||||||
<ul class="dropdown-list" role="listbox">
|
<ul class="dropdown-list" role="listbox" [id]="dropdownId + '-listbox'">
|
||||||
@for (option of normalizedOptions(); track option.value; let i = $index) {
|
@for (option of normalizedOptions(); track option.value; let i = $index) {
|
||||||
<li
|
<li
|
||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
role="option"
|
role="option"
|
||||||
|
[id]="dropdownId + '-option-' + i"
|
||||||
[class.selected]="option.value === value()"
|
[class.selected]="option.value === value()"
|
||||||
[class.focused]="i === focusedIndex()"
|
[class.focused]="i === focusedIndex()"
|
||||||
[attr.aria-selected]="option.value === value()"
|
[attr.aria-selected]="option.value === value()"
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export interface DropdownOption {
|
|||||||
'role': 'combobox',
|
'role': 'combobox',
|
||||||
'aria-haspopup': 'listbox',
|
'aria-haspopup': 'listbox',
|
||||||
'[attr.aria-expanded]': 'isOpen()',
|
'[attr.aria-expanded]': 'isOpen()',
|
||||||
|
'[attr.aria-controls]': "isOpen() ? dropdownId + '-listbox' : null",
|
||||||
|
'[attr.aria-activedescendant]': "isOpen() && focusedIndex() >= 0 ? dropdownId + '-option-' + focusedIndex() : null",
|
||||||
|
'[attr.aria-label]': 'ariaLabel()',
|
||||||
'[class.open]': 'isOpen()',
|
'[class.open]': 'isOpen()',
|
||||||
'tabindex': '0',
|
'tabindex': '0',
|
||||||
'(click)': 'toggle()',
|
'(click)': 'toggle()',
|
||||||
@@ -30,6 +33,7 @@ export interface DropdownOption {
|
|||||||
export class Dropdown {
|
export class Dropdown {
|
||||||
options = input<Array<DropdownOption | string>>([]);
|
options = input<Array<DropdownOption | string>>([]);
|
||||||
placeholder = input<string>('Select an option');
|
placeholder = input<string>('Select an option');
|
||||||
|
ariaLabel = input<string | undefined>(undefined);
|
||||||
icon = input<string>();
|
icon = input<string>();
|
||||||
value = model<any>(null);
|
value = model<any>(null);
|
||||||
selectionChange = output<any>();
|
selectionChange = output<any>();
|
||||||
@@ -37,6 +41,9 @@ export class Dropdown {
|
|||||||
isOpen = signal(false);
|
isOpen = signal(false);
|
||||||
focusedIndex = signal(-1);
|
focusedIndex = signal(-1);
|
||||||
|
|
||||||
|
private static nextId = 0;
|
||||||
|
dropdownId = `dropdown-${Dropdown.nextId++}`;
|
||||||
|
|
||||||
constructor(private el: ElementRef) {}
|
constructor(private el: ElementRef) {}
|
||||||
|
|
||||||
normalizedOptions = computed(() => {
|
normalizedOptions = computed(() => {
|
||||||
@@ -96,7 +103,10 @@ export class Dropdown {
|
|||||||
this.toggle();
|
this.toggle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.focusedIndex.update(i => (i + 1) % this.normalizedOptions().length);
|
this.focusedIndex.update(i => {
|
||||||
|
if (i === -1) return 0;
|
||||||
|
return (i + 1) % this.normalizedOptions().length;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onArrowUp(event: Event) {
|
onArrowUp(event: Event) {
|
||||||
@@ -105,7 +115,10 @@ export class Dropdown {
|
|||||||
this.toggle();
|
this.toggle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.focusedIndex.update(i => (i - 1 + this.normalizedOptions().length) % this.normalizedOptions().length);
|
this.focusedIndex.update(i => {
|
||||||
|
if (i <= 0) return this.normalizedOptions().length - 1;
|
||||||
|
return i - 1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener('document:click', ['$event'])
|
@HostListener('document:click', ['$event'])
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<div class="switch-knob"></div>
|
<div class="switch-knob" aria-hidden="true"></div>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, input, model, output } from '@angular/core'
|
import { Component, input, model, output } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'switch',
|
selector: 'switch',
|
||||||
@@ -6,28 +6,28 @@ import { Component, input, model, output } from '@angular/core'
|
|||||||
templateUrl: './switch.html',
|
templateUrl: './switch.html',
|
||||||
styleUrl: './switch.scss',
|
styleUrl: './switch.scss',
|
||||||
host: {
|
host: {
|
||||||
'role': 'switch',
|
role: 'switch',
|
||||||
'tabindex': '0',
|
tabindex: '0',
|
||||||
'[attr.aria-checked]': 'isActive()',
|
'[attr.aria-checked]': 'isActive()',
|
||||||
'[class.active]': 'isActive()',
|
'[class.active]': 'isActive()',
|
||||||
'[class.reduce-motion]': 'reduceMotion()',
|
'[class.reduce-motion]': 'reduceMotion()',
|
||||||
'(click)': 'onClick()',
|
'(click)': 'onClick()',
|
||||||
'(keydown.enter)': 'onClick()',
|
'(keydown.enter)': 'onClick()',
|
||||||
'(keydown.space)': 'onSpaceDown($event)'
|
'(keydown.space)': 'onSpaceDown($event)',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
export class Switch {
|
export class Switch {
|
||||||
isActive = model<boolean>(false);
|
isActive = model<boolean>(false);
|
||||||
reduceMotion = input<boolean>(
|
reduceMotion = input<boolean>(
|
||||||
typeof window !== 'undefined'
|
typeof window !== 'undefined'
|
||||||
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
: false
|
: false,
|
||||||
);
|
);
|
||||||
toggle = output<boolean>()
|
toggle = output<boolean>();
|
||||||
|
|
||||||
onClick(): void {
|
onClick(): void {
|
||||||
this.isActive.update(value => !value)
|
this.isActive.update((value) => !value);
|
||||||
this.onToggle()
|
this.onToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSpaceDown(event: Event): void {
|
onSpaceDown(event: Event): void {
|
||||||
@@ -36,6 +36,6 @@ export class Switch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onToggle(): void {
|
onToggle(): void {
|
||||||
this.toggle.emit(this.isActive())
|
this.toggle.emit(this.isActive());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
C@import url('https://cdn.jsdelivr.net/npm/nanumsquareneo@0.0.5/nanumsquareneo.css');
|
@import url('https://cdn.jsdelivr.net/npm/nanumsquareneo@0.0.5/nanumsquareneo.css');
|
||||||
@import url('https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.1/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css');
|
@import url('https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.1/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css');
|
||||||
|
@import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard-jp-dynamic-subset.min.css");
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'NanumSquareNeo', 'Wanted Sans Variable', sans-serif;
|
font-family: 'NanumSquareNeo', 'Wanted Sans Variable', 'Pretendard JP', sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user