Angular Aria
Angular Aria چیست؟
ساخت کامپوننتهای accessible در نگاه اول ساده به نظر میرسد، اما پیادهسازی آنها مطابق راهنماهای Accessibility در W3C به تلاش قابل توجه و تخصص accessibility نیاز دارد.
Angular Aria مجموعهای از directiveهای headless و accessible است که patternهای رایج WAI-ARIA را پیادهسازی میکنند. این directiveها keyboard interactionها، attributeهای ARIA، مدیریت focus و پشتیبانی از screen reader را مدیریت میکنند. تنها کاری که شما باید انجام دهید فراهم کردن ساختار HTML، styling با CSS و business logic است!
نصب
npm install @angular/ariayarn add @angular/ariapnpm add @angular/ariabun add @angular/ariaShowcase
برای مثال، یک toolbar menu را در نظر بگیریم. با اینکه ممکن است در ظاهر فقط یک ردیف «ساده» از buttonها با logic مشخص باشد، keyboard navigation و screen readerها برای کسانی که با accessibility آشنا نیستند پیچیدگیهای غیرمنتظره زیادی اضافه میکنند.
import {Component} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {}<div ngToolbar aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
#bold="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="bold.selected()"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
#italic="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="italic.selected()"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
#underlined="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="underlined.selected()"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
#leftAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="leftAlign.selected()"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
#centerAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="centerAlign.selected()"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
#rightAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="rightAlign.selected()"
translate="no"
>
format_align_right
</button>
</div>
</div>@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
}
.group {
gap: 0.5rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
font-size: 1.25rem;
background-color: transparent;
color: var(--primary-contrast);
}
[ngToolbarWidget]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 10%, transparent);
}
[ngToolbarWidget]:active {
background-color: color-mix(in srgb, var(--primary-contrast) 15%, transparent);
}
[ngToolbarWidget]:focus {
outline-offset: -1px;
outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}
[ngToolbarWidget][aria-pressed="true"],
[ngToolbarWidget][aria-checked="true"] {
color: color-mix(in srgb, var(--hot-pink) 80%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 10%, transparent);
}import {Component} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {}<div ngToolbar class="material-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
#bold="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="bold.selected()"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
#italic="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="italic.selected()"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
#underlined="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="underlined.selected()"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
#leftAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="leftAlign.selected()"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
#centerAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="centerAlign.selected()"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
#rightAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="rightAlign.selected()"
translate="no"
>
format_align_right
</button>
</div>
</div>@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
}
[ngToolbar] {
display: flex;
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
width: 3rem;
height: 3rem;
font-size: 1.25rem;
border-radius: 2rem;
color: color-mix(in srgb, var(--vivid-pink) 70%, var(--full-contrast));
background-color: color-mix(in srgb, var(--vivid-pink) 20%, transparent);
transition:
width 0.2s ease-in-out,
background-color 0.15s ease-in-out,
color 0.15s ease-in-out;
}
[ngToolbarWidget]:focus {
outline-offset: 2px;
outline: 2px solid var(--vivid-pink);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
background-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--full-contrast));
color: var(--page-background);
}
[ngToolbarWidget][aria-checked='true'] {
width: 4.5rem;
}
.group {
display: flex;
gap: 0.5rem;
}
.separator {
width: 1px;
margin: 0 1rem;
height: calc(100% - 2rem);
background-color: var(--quinary-contrast);
}import {Component} from '@angular/core';
import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';
@Component({
selector: 'app-root',
templateUrl: 'app.html',
styleUrl: 'app.css',
imports: [Toolbar, ToolbarWidget, ToolbarWidgetGroup],
})
export class App {}<div ngToolbar class="retro-toolbar" aria-label="Text Formatting Tools">
<div class="group">
<button
ngToolbarWidget
value="undo"
type="button"
aria-label="undo"
class="material-symbols-outlined"
translate="no"
>
undo
</button>
<button
ngToolbarWidget
value="redo"
type="button"
aria-label="redo"
class="material-symbols-outlined"
translate="no"
>
redo
</button>
</div>
<div class="separator" role="separator"></div>
<div class="group">
<button
ngToolbarWidget
value="bold"
type="button"
aria-label="bold"
#bold="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="bold.selected()"
translate="no"
>
format_bold
</button>
<button
ngToolbarWidget
value="italic"
type="button"
aria-label="italic"
#italic="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="italic.selected()"
translate="no"
>
format_italic
</button>
<button
ngToolbarWidget
value="underlined"
type="button"
aria-label="underlined"
#underlined="ngToolbarWidget"
class="material-symbols-outlined"
[aria-pressed]="underlined.selected()"
translate="no"
>
format_underlined
</button>
</div>
<div class="separator" role="separator"></div>
<div ngToolbarWidgetGroup role="radiogroup" class="group" aria-label="Text alignment options">
<button
ngToolbarWidget
role="radio"
type="button"
value="align left"
aria-label="align left"
#leftAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="leftAlign.selected()"
translate="no"
>
format_align_left
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align center"
aria-label="align center"
#centerAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="centerAlign.selected()"
translate="no"
>
format_align_center
</button>
<button
ngToolbarWidget
role="radio"
type="button"
value="align right"
aria-label="align right"
#rightAlign="ngToolbarWidget"
class="material-symbols-outlined"
[aria-checked]="rightAlign.selected()"
translate="no"
>
format_align_right
</button>
</div>
</div>@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--vivid-pink) 80%, var(--page-background));
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
[ngToolbar] {
gap: 1.5rem;
display: flex;
padding: 1rem;
}
.group {
gap: 1rem;
display: flex;
}
.separator {
width: 1px;
align-self: center;
height: calc(100% - 1rem);
background-color: var(--quinary-contrast);
}
[ngToolbarWidget] {
border: none;
outline: none;
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
color: var(--page-background);
background-color: var(--retro-button-color);
box-shadow: var(--retro-clickable-shadow);
transition:
transform 0.1s,
box-shadow 0.1s;
}
[ngToolbarWidget]:focus,
[ngToolbarWidget]:hover {
transform: translate(1px, 1px);
}
[ngToolbarWidget]:active,
[ngToolbarWidget][aria-pressed='true'],
[ngToolbarWidget][aria-checked='true'] {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));
}
[ngToolbarWidget]:focus {
outline-offset: 4px;
outline: 4px dashed var(--retro-button-color);
}در همین یک سناریو، developerها باید این موارد را در نظر بگیرند:
- Keyboard navigation. کاربران باید بتوانند menu را با Enter یا Space باز کنند، با arrow keyها بین optionها حرکت کنند، با Enter انتخاب کنند و با Escape ببندند.
- Screen readerها باید state مربوط به menu، تعداد optionها و اینکه کدام option focus دارد را announce کنند.
- Focus management باید focus را منطقی بین trigger و menu itemها جابهجا کند.
- زبانهای راستبهچپ به قابلیت navigate کردن در جهت معکوس نیاز دارند.
چه چیزهایی شامل میشود؟
Angular Aria شامل directiveهایی با مستندات جامع، مثالهای کارکرده و API reference برای patternهای تعاملی رایج است:
جستجو و انتخاب
| کامپوننت | توضیح |
|---|---|
| Autocomplete | Text input همراه با suggestionهای فیلترشده که هنگام typing کاربر ظاهر میشوند |
| Listbox | لیستهای option با single-select یا multi-select و keyboard navigation |
| Select | pattern مربوط به dropdown تکانتخابی با keyboard navigation |
| Multiselect | pattern مربوط به dropdown چندانتخابی با نمایش compact |
| Combobox | directive primitive که یک text input را با popup هماهنگ میکند |
Navigation و call to action
| کامپوننت | توضیح |
|---|---|
| Menu | dropdown menuها با submenuهای nested و keyboard shortcutها |
| Menubar | navigation bar افقی برای menuهای persistent برنامه |
| Toolbar | مجموعههای گروهبندیشده از controlها با keyboard navigation منطقی |
سازماندهی محتوا
| کامپوننت | توضیح |
|---|---|
| Accordion | panelهای محتوایی collapsible که میتوانند جداگانه یا انحصاری expand شوند |
| Tabs | interfaceهای tabدار با modeهای activation خودکار یا دستی |
| Tree | لیستهای سلسلهمراتبی با قابلیت expand/collapse |
| Grid | نمایش دوبعدی data با keyboard navigation سلولبهسلول |
چه زمانی از Angular Aria استفاده کنیم
Angular Aria زمانی مناسب است که به کامپوننتهای تعاملی accessible نیاز دارید که با WCAG سازگار باشند و styling سفارشی داشته باشند. مثالها:
- ساخت design system - تیم شما یک component library با استانداردهای visual مشخص نگه میدارد که به implementationهای accessible نیاز دارد.
- Enterprise component libraryها - در حال ساخت کامپوننتهای reusable برای چند برنامه درون یک سازمان هستید.
- نیازهای brand سفارشی - interface باید با design specificationهای دقیق match شود که component libraryهای pre-styled به سادگی نمیتوانند فراهم کنند.
چه زمانی از Angular Aria استفاده نکنیم
Angular Aria ممکن است برای هر سناریویی مناسب نباشد:
- کامپوننتهای pre-styled - اگر به کامپوننتهایی نیاز دارید که بدون styling سفارشی کامل به نظر برسند، به جای آن از Angular Material استفاده کنید.
- Formهای ساده - controlهای native HTML مثل
<button>و<input type="radio">برای use caseهای ساده accessibility داخلی فراهم میکنند. - Rapid prototyping - وقتی conceptها را سریع validate میکنید، component libraryهای pre-styled زمان development اولیه را کاهش میدهند.
قدمهای بعدی
از side nav یا فهرست بالا یک کامپوننت را بررسی کنید، یا با Toolbar شروع کنید تا یک مثال کامل از نحوه کار directiveهای Angular Aria ببینید!