// accessible-input.component.ts import { Component, Input, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms'; @Component({ selector: 'app-accessible-input', standalone: true, template: ` <div class="field-container"> <label [id]="labelId" [for]="inputId" class="label"> {{ label }} <span *ngIf="required" aria-hidden="true">*</span> </label> <input [id]="inputId" [type]="type" [formControl]="control" [attr.aria-labelledby]="labelId" [attr.aria-describedby]="errorId" [attr.aria-invalid]="control.invalid && control.touched" [attr.aria-required]="required" (blur)="onTouched()" (input)="($any($event.target).value)" class="input" [class.error]="control.invalid && control.touched" /> <!-- محفظه خطا که به aria-describedby متصل شده --> <div [id]="errorId" role="alert" *ngIf="control.invalid && control.touched" class="error-message" > {{ getErrorMessage() }} </div> </div> `, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AccessibleInputComponent), multi: true } ] }) export class AccessibleInputComponent implements ControlValueAccessor { @Input() label = ''; @Input() type = 'text'; @Input() required = false; @Input() control = new FormControl(); inputId = `input-${Math.random().toString(36)}`; labelId = `label-${Math.random().toString(36)}`; errorId = `error-${Math.random().toString(36)}`; // پیادهسازی ControlValueAccessor ... }
👆 نکته مهم: با
aria-describedby، صفحهخوان هنگام فوکوس روی فیلد، متن خطا را هم میخواند.role="alert"هم باعث میشود خطا به محض ظاهر شدن، اعلام شود.
مدیریت فوکوس پس از ارسال ناموفق (Focus Management):
typescript
// seminar-form.component.ts import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; @Component({...}) export class SeminarFormComponent implements AfterViewInit { @ViewChild('firstInput') firstInput!: ElementRef; seminarForm = this.fb.group({ name: ['', Validators.required], email: ['', [Validators.required, Validators.email]], // ... }); constructor(private fb: FormBuilder, private focusService: FocusService) {} () { if (this.seminarForm.invalid) { // ۱. مارک کردن همه فیلدها به عنوان touched this.seminarForm.markAllAsTouched(); // ۲. پیدا کردن اولین فیلد خطادار (با استفاده از کلیدهای Object) const firstErrorField = Object.keys(this.seminarForm.controls) .find(key => this.seminarForm.get(key)?.invalid); // ۳. حرکت فوکوس به آن فیلد با یک سرویس متمرکز if (firstErrorField) { this.focusService.focusById(`input-${firstErrorField}`); } } } }
سرویس FocusService برای کیبورد:
typescript
// services/focus.service.ts import { Injectable, Renderer2, RendererFactory2 } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class FocusService { private renderer: Renderer2; constructor(rendererFactory: RendererFactory2) { this.renderer = rendererFactory.createRenderer(null, null); } focusById(id: string) { const element = document.getElementById(id); if (element) { // تاخیر کوچک برای اطمینان از رندر شدن DOM setTimeout(() => { element.focus({ preventScroll: false }); // preventScroll:false یعنی صفحه اسکرول میخورد تا المان بیاید بالا // برای تاکید بصری، هایلایت میکنیم (برای کاربران بینا هم مفید است) this.renderer.addClass(element, 'focused-highlight'); }, 100); } } }
استفاده از inputmode و سایز مناسب لمسی:
html
<!-- در کامپوننت AccessibleInput --> <input [id]="inputId" [type]="type" [inputmode]="inputMode" <!-- مثلاً برای موبایل: 'numeric' یا 'email' --> [autocapitalize]="autocapitalize" <!-- 'off' برای ایمیل/نام --> [style.min-height.px]="48" <!-- استاندارد WCAG برای لمسی: حداقل ۴۴px --> [style.padding.px]="12" class="touch-friendly" />
مدیریت رویدادهای لمسی برای المانهای سفارشی (مثل DatePicker):
typescript
// accessible-datepicker.component.ts // برای اینکه هم کلیک و هم لمس و هم کیبورد کار کند، از (click) و (keydown) استفاده میکنیم: template: ` <button (click)="toggleCalendar()" (keydown.enter)="toggleCalendar()" (keydown.space)="toggleCalendar(); $event.preventDefault()" [style.min-height.px]="48" [style.min-width.px]="48" class="datepicker-trigger" > انتخاب تاریخ </button> <div *ngIf="isOpen" class="calendar-grid" role="grid"> <!-- هر روز با (click) و (keydown.enter) --> </div> `
👆 نکته موبایل: دکمهها را حداقل ۴۴ در ۴۴ پیکسل میکنیم تا انگشت به راحتی بزند. همچنین
inputmode="numeric"روی فیلد موبایل باعث میشود کیبورد شمارهای باز شود نه کیبورد کامل.
وقتی خطایی ظاهر میشود یا عملیات خاصی انجام میشود:
typescript
import { LiveAnnouncer } from '@angular/cdk/a11y'; @Component({...}) export class SeminarFormComponent { constructor(private liveAnnouncer: LiveAnnouncer) {} onFormSubmit() { if (this.seminarForm.valid) { this.liveAnnouncer.announce('فرم شما با موفقیت ثبت شد', 'polite'); // polite = تا پایان جمله فعلی کاربر صبر میکند، interrupt = قطع میکند } else { this.liveAnnouncer.announce('لطفاً خطاهای قرمز رنگ را تصحیح کنید', 'assertive'); } } }