وب مدرن، رابط کاربری و تجربهی وب · فصل ۲ — HTML، CSS، Responsive و RTL · 39
Accessible UI Primitives، Focus و Keyboard
میتواند «Accessible UI Primitives, Focus و Keyboard» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خلاصهٔ درس
میتواند «Accessible UI Primitives, Focus و Keyboard» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خروجی این درس
میتواند «Accessible UI Primitives, Focus و Keyboard» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
یک پنل که با ماوس کار میکند، ممکن است با کیبورد عملاً خراب باشد
فرض کنید تیم محصول یک پنل فیلتر ساخته است. کاربر با ماوس روی فیلترها کلیک میکند، چند گزینه را انتخاب میکند، اعمال را میزند و پنل را میبندد. ظاهر تمیز است و feature تمامشده به نظر میرسد. اما کاربر keyboard-only همان مسیر را امتحان میکند: trigger قابل دسترسی است، ولی یکی از کنترلهای سفارشی فقط به click پاسخ میدهد. بعد از باز شدن پنل، تیم مشخص یا verify نکرده است initial focus دقیقاً کجا قرار میگیرد؛ بنابراین active location ممکن است با interaction contract موردنظر سازگار نباشد. طراح برای هماهنگی بصری outline پیشفرض مرورگر را حذف کرده و جایگزین قابلتشخیصی نگذاشته است. بعد از بستن پنل نیز focus به نقطه مفیدی برنمیگردد. هیچکدام از این خطاها در screenshot ثابت معلوم نیست.
در این درس این موارد را چهار باگ جدا نمیبینیم؛ همه بخشی از یک interaction contract هستند. Accessibility چیزی نیست که بعد از پایان طراحی بصری با چند attribute به component اضافه شود. در یک primitive تعاملی محدود، semantics، رفتار keyboard، دیدهشدن focus، جابهجایی focus، تغییر state و evidence باید با هم سازگار باشند.
artifact اصلی درس Accessible Primitive Interaction Contract v1 است. برای هر interaction مهم ثبت میکنید:
- Entry: کاربر از چه کنترلی وارد interaction میشود و با keyboard چگونه به آن میرسد.
- Active focus: قبل و بعد از action، focus دقیقاً روی کدام element است و آیا محل آن قابل تشخیص است.
- Keyboard action: در pattern انتخابشده کدام key عملیات را انجام میدهد.
- State change: در UI یا DOM چه تغییری واقعاً رخ میدهد.
- Exit / restoration: کاربر چگونه از primitive خارج میشود و focus بعد از خروج کجا میرود.
- Evidence: چه چیزی مشاهده شد، چه mutationای اجرا شد، repair چه بود و چه limitationای باقی ماند.
چهار dimension بحرانی داریم: keyboard_operability، focus_visibility، focus_movement و pattern_contract. یعنی feature نباید pointer-only باشد؛ focus باید قابل تشخیص بماند؛ ورود، حرکت، dismissal و restoration باید صریح و قابل پیشبینی باشند؛ و اگر custom composite widget داریم، role/state/key behavior باید یک قرارداد منسجم بسازند، نه مجموعهای از ARIA attributeهای پراکنده.
Non-goalها نیز قفل هستند. اضافهکردن ARIA بهتنهایی keyboard behavior ایجاد نمیکند. Focus با selection یکی نیست. tabindex مثبت راهحل عمومی مرتبسازی نیست. یک keyboard pattern ثابت هم برای همه widgetها وجود ندارد.
Lab این درس یک filter panel محلی با HTML/CSS/JavaScript است. تا جای ممکن از native control استفاده میکنید، یک failure را قبل از اجرا پیشبینی میکنید، آن را مشاهده میکنید، repair محدود انجام میدهید و سپس reasoning را در changed context تکرار میکنید. معیار پایان «ظاهر درست» نیست؛ معیار، evidence قابل ردیابی از interaction است.
سه مسیر، یک evidence bar
میزان توضیح میتواند با سطح learner تغییر کند؛ معیار اثبات تغییر نمیکند.
Foundation route. از native elementها و رفتار focus پیشفرض مرورگر شروع کنید. tab sequence، document.activeElement، focus قابل مشاهده و تفاوت focus با selection را یاد بگیرید. قبل از custom behavior، matrix را ردیفبهردیف پر کنید.
Repair route. از failure واقعی شروع کنید: controlای که فقط click دارد، focus نامرئی، focusی که جای نادرست گیر میکند، یا focusی که بعد از close/remove گم میشود. failure را به یک contract row کاهش دهید، observe کنید، repair کنید و بعد سناریوی کامل را برگردانید. بازنویسی کل component برای فرار از یک symptom، repair محسوب نمیشود.
Experienced route. توضیح syntax پایه را فشرده کنید. از ابتدا interaction pattern، focus ownership، keyboard path و restoration rule را بنویسید. کوچکترین mechanism کافی را پیاده کنید و با mutation و transfer ثابت کنید. تجربه framework جای evidence را نمیگیرد.
دو prerequisite فعالاند. WEB-009 hard prerequisite است: ساختار semantic پایه باید قابل دفاع باشد. این درس document بیمعنا را با ARIA وصله نمیکند. WEB-010 soft prerequisite است: evidence مربوط به responsive layout ممکن است در changed context به کار بیاید، اما اینجا Grid/Flex یا responsive constraints را دوباره آموزش نمیدهیم.
Boundary بعدی هم مهم است: WEB-019 مالک direction/bidi و context مربوط به directional keyهاست. در این درس میتوانیم ثبت کنیم که direction یا locale ممکن است requirement را تغییر دهد، اما یک قانون عمومی و بدون context برای Left/Right Arrow اختراع نمیکنیم. رفتار keyboard وابسته به pattern است.
قاعده عملی این درس:
اول native behavior؛ بعد هر custom behavior را صریح کنید.
Native HTML معمولاً semantics و keyboard behavior پایه را همراه دارد. یک div با click این بسته را ندارد. این به معنی ممنوعبودن custom UI نیست؛ یعنی هزینه مهندسی آن بیشتر است: semantics، input behavior، state sync، focus behavior و regression evidence همگی تحت مسئولیت شما قرار میگیرند.
قبل از implementation سه سؤال را جواب دهید:
- user دقیقاً چه کاری میخواهد انجام دهد؟
- کدام element/pattern این operation را درست مدل میکند؟
- چه observationای میتواند keyboard/focus contract را اثبات یا رد کند؟
اگر پاسخها فقط بعد از نوشتن event handler روشن شوند، طراحی interaction هنوز بالغ نیست.
مدل ذهنی: component boundary → focus path → action → state → restoration → evidence
Focus را border تزئینی نبینید؛ آن را بخشی از state transition در نظر بگیرید.
۱. ابتدا boundary را مشخص کنید
صفحه elementهای focusable زیادی دارد. Primitive محدود ما فقط بخشی از آنها را مالک است. در filter panel، boundary از دکمه فیلترها شروع میشود، controlهای داخل panel را پوشش میدهد و وقتی panel dismiss میشود و focus به target منطقی برمیگردد تمام میشود.
این boundary باید جواب دهد: چه چیزی interaction را آغاز میکند، کاربر داخل آن چگونه عمل میکند، خروج چگونه انجام میشود، و اگر invoker اولیه دیگر وجود نداشت restoration به کجا میرود.
۲. Tab sequence را با navigation داخل composite یکی نگیرید
Tab و Shift+Tab معمولاً focus را بین componentهای focusable صفحه حرکت میدهند. native controlها بدون JavaScript سفارشی در این sequence شرکت میکنند.
Composite widgetها متفاوتاند. tablist، menu، listbox، tree یا grid میتواند چند item داخلی داشته باشد ولی تعداد tab stopهای کمتری expose کند. keyهای pattern-specific، اغلب arrow keyها، ممکن است focus داخلی را جابهجا کنند. این موضوع مجوز استفاده از tabindex مثبت برای مرتبکردن کل صفحه نیست؛ نیاز به focus management در سطح همان pattern است.
نکته کلیدی این است که keyboard convention متعلق به pattern است. اگر فقط role="menu" بگذارید، browser برای شما منطق keyboard menu را نمینویسد. شما semantics را اعلام کردهاید و مسئول behavior متناظر شدهاید.
۳. Focus با selection فرق دارد
Focus محل فعلی interaction keyboard است. در DOM، document.activeElement observation مفیدی است. Selection یک state مستقل است. ممکن است یک tab selected باقی بماند ولی focus جای دیگری باشد؛ یا چند option selected باشند ولی فقط یک نقطه focus وجود داشته باشد.
وقتی این دو را یکی فرض کنیم، bug تولید میشود. مثلاً aria-selected="true" آنقدر پررنگ style میشود که user آن را focus میپندارد، در حالی که element focused هیچ indicator مشخصی ندارد. یا با هر حرکت focus، selection هم ناخواسته تغییر میکند، در حالی که pattern به activation صریح نیاز دارد.
در evidence matrix این دو state جدا ثبت میشوند.
۴. هر programmatic focus باید دلیل داشته باشد
focus() ذاتاً خوب یا بد نیست. وقتی state transition context کاربر را عوض میکند، focus management میتواند continuity را حفظ کند. بازشدن modal، حذف item focused، بستن surface موقت یا پایان عملیات destructive نمونههایی هستند که ممکن است به focus movement صریح نیاز داشته باشند.
سؤال causal است: چه چیزی تغییر کرد و کدام target ادامه تعامل را منطقی نگه میدارد؟
برای dialog-like panel ما، پس از open یک initial target معنیدار انتخاب میشود. پس از close، اگر invoker هنوز موجود و usable باشد، بازگشت focus به آن منطقی است. اگر invoker بهخاطر rerender یا تغییر state حذف شده باشد، contract باید fallback مشخص داشته باشد؛ مثلاً control بعدی یا heading پایدار که عمداً programmatically focusable شده است. «browser خودش یک جایی focus میگذارد» restoration policy نیست.
۵. Focus visible بخشی از interface است
Keyboard operation کافی نیست اگر user نداند focus کجاست. indicator پیشفرض browser معمولاً baseline امنی است. اگر design آن را override میکند، replacement باید در stateها و backgroundهای مورد انتظار قابل تشخیص بماند.
Failure رایج، outline: none سراسری بدون جایگزین است. Failure دیگر focus styleای است که روی یک background محو میشود یا از selection قابل تفکیک نیست.
پس matrix هم focus target و هم focus visibility را ثبت میکند. ممکن است DOM بگوید focus درست جابهجا شده، اما experience کاربر همچنان خراب باشد.
۶. Evidence حلقه را میبندد
برای هر row این chain را ثبت کنید:
entry → active focus → keyboard action → state change → exit/restore → observation
بعد عمداً یک dimension را خراب کنید. قبل از mutation failure را پیشبینی کنید. نتیجه را observe کنید. فقط cause متعلق به این lesson را repair کنید. baseline و changed context را دوباره اجرا کنید. در پایان یک limitation واقعی بنویسید؛ مثلاً اینکه تست در یک browser مشخص انجام شده و conformance کامل روی همه assistive technologyها را ثابت نمیکند.
این روش جلوی دو نتیجه ضعیف را میگیرد: «کد درست به نظر میرسد» و «صفحه درست به نظر میرسد».
Foundation: native controls، focusability و حداقل JavaScript لازم
برای keyboard reliability، معمولاً بهترین شروع این است که behavior موجود platform را دوباره اختراع نکنید.
برای action از button واقعی استفاده کنید
اگر element عملیاتی را activate میکند، از <button> شروع کنید. Button native در tab sequence طبیعی قرار میگیرد و activation keyboard پایه را دارد. یک <div> که click handler دارد، فقط با role="button" به button کامل تبدیل نمیشود. Role semantics را expose میکند ولی همه behaviorهای native را ایجاد نمیکند.
Trigger ساده:
<button id="open-filters" type="button" aria-haspopup="dialog">
فیلترها
</button>
ARIA در اینجا interaction model را تکمیل میکند؛ semantics اصلی action همچنان از button میآید.
ترتیب طبیعی focus را حفظ کنید
قبل از tabindex به source order و native focusability تکیه کنید. tabindex="0" میتواند custom element را وارد sequence کند و tabindex="-1" برای targetی مناسب است که programmatically focus میشود ولی tab stop عادی نیست. مقادیر مثبت مثل tabindex="5" یک ordering system موازی میسازند که با اضافه/حذف componentها شکننده میشود.
برای form control، link و button معمولی معمولاً نیازی به positive tabindex ندارید.
:focus-visible را برای حذف focus استفاده نکنید
میتوانید focus design را customize کنید بدون اینکه آن را پنهان کنید:
:where(button, input, select, [tabindex]):focus-visible {
outline: 3px solid currentColor;
outline-offset: 3px;
}
این snippet visual spec جهانی نیست؛ color/contrast/geometry واقعی باید requirement محصول را پاس کند. نکته مهندسی این است: وقتی indicator پیشفرض browser را override میکنید، مسئولیت کیفیت replacement با شماست.
Focus را مستقیم observe کنید
برای debugging، بعد از transitionهای مهم document.activeElement را ببینید:
function reportFocus(label) {
console.log(label, document.activeElement);
}
این log بهتنهایی proof نیست؛ فقط یکی از observation surfaceهاست و باید با keyboard operation و visible-focus inspection ترکیب شود.
lifecycle را تعریف کنید
برای panel ما چهار responsibility مفهومی مفید است:
openPanel(invoker)، invoker را ثبت کند، panel را باز کند و initial focus target را تعیین کند؛- operationهای داخل panel از native controls یا behavior صریح استفاده کنند؛
closePanel(reason)همه مسیرهای close را همگرا کند؛restoreFocus()focus را به invoker معتبر یا fallback مستند برگرداند.
این architecture تستپذیرتر از چند click handler مستقل است که رفتار focus آنها اتفاقی شکل گرفته است.
Native بودن، proof کامل نیست
Native element baseline قوی است ولی guarantee کامل accessibility نیست. label بد، contrast نامناسب، ساختار اشتباه یا application logic خراب هنوز ممکن است. Scope ما محدود است: native behavior را تا جای ممکن انتخاب کنید و contract keyboard/focus وابسته به feature را واقعاً verify کنید.
توضیح عمیق: custom primitiveها دقیقاً کجا میشکنند
بیشتر failureهای جدی در مرز semantics، input handling و state transition رخ میدهند.
ARIA semantics را اعلام میکند؛ behavior بر عهده implementation است
ARIA role/state/property به assistive technology کمک میکند معنی UI را دریافت کند. اما معمولاً keyboard interaction یک custom widget را خودکار پیاده نمیکند. اگر div را با role="button" button-like کنید، همچنان focusability، activation keys، disabled behavior، focus styling و state synchronization را خودتان مالک هستید.
به همین دلیل جمله «ARIA اضافه کردیم» verdict قبول نیست. سؤال evidence این است که user با keyboard میتواند feature را reach و operate کند و semantics exposed با behavior واقعی هماهنگ است یا نه.
Open شدن فقط نصف lifecycle است
Surface موقت مثل dialog/menu/disclosure context تعامل را تغییر میدهد. در بعضی patternها focus روی trigger میماند و content reveal میشود؛ در بعضیها focus باید وارد surface شود. Rule مستقل از pattern وجود ندارد.
در dialog-like panel این درس، initial target صریح انتخاب میشود. Target باید meaningful باشد، نه صرفاً «اولین focusable در DOM». اگر اولین action destructive است، focus اولیه روی آن ممکن است تصمیم بدی باشد.
Dismissal باید destination داشته باشد
Close میتواند از Cancel، Apply، Escape، route change یا rerender رخ دهد. مگر اینکه product exception مستند داشته باشد، همه close pathها باید به restoration policy مشترک برسند.
Policy قابل دفاع:
- اگر invoker هنوز connected، enabled و منطقی است، آن را focus کن؛
- در غیر این صورت fallback مستند را انتخاب کن؛
- به element نامرتبط فقط بهخاطر راحتی query focus نده.
Changed-context lab عمداً invoker را حذف/جایگزین میکند تا این policy آزمایش شود.
حذف node focused یک boundary مستقل است
اگر operation همان element focused را از DOM حذف کند، browser ممکن است focus را به جایی ببرد که برای user مفید نیست. Listها، notificationها و dynamic filterها مستعد این failure هستند. Contract باید مشخص کند focus به next item، previous item، collection container، invoker یا target پایدار دیگری میرود.
پاسخ universal نیست؛ مهم explicit بودن transition است.
Composite widget یک قرارداد کامل میخواهد
فرض کنید view switcher با گزینههای Grid / List / Compact دارید. اگر واقعاً یک انتخاب از مجموعه است و radio-group model مناسب است، semantics، selected state، tab behavior، arrow behavior و تمایز visual focus/selection باید با هم سازگار باشند. اگر سه command مستقلاند، تحمیل radio semantics اشتباه است.
تصمیم از interaction model شروع میشود، نه از شکل ظاهری component.
در بعضی compositeها roving tabindex باعث میشود فقط یک child در tab sequence باشد و arrow navigation با تغییر tabindex و focus() بین childها حرکت کند. بعضی patternها DOM focus را روی container نگه میدارند و active item را با relationship مناسب expose میکنند. اینها mechanismهای متفاوتاند؛ درس از شما تشخیص bounded decision را میخواهد، نه حفظکردن یک recipe برای همه widgetها.
Shortcut جای navigation پایه را نمیگیرد
Shortcut ممکن است efficiency را بالا ببرد، اما نباید تنها راه دسترسی به feature باشد. همچنین میتواند با browser، OS یا assistive technology conflict کند. Lab ما به navigation عادی و actionهای pattern-specific تکیه میکند و shortcut global نمایشی اضافه نمیکند.
انواع evidence مکمل هماند
Focus ring evidence بصری است. document.activeElement evidence مربوط به DOM است. aria-expanded یا open state evidence state/semantics است. keyboard sequence evidence interaction است. هیچکدام بهتنهایی کل contract را ثابت نمیکند؛ matrix آنها را کنار هم نگه میدارد.
Worked example: filter dialog از requirement تا evidence
یک filter panel کوچک داریم: trigger native، surface dialog-like، form controlهای معمولی، Apply/Cancel و focus restoration صریح.
مرحله ۱: قبل از کد contract را بنویسید
| فاز | قرارداد |
|---|---|
| Entry | Tab به دکمه فیلترها میرسد و keyboard activation کار میکند. |
| Open | panel باز میشود و focus به first meaningful control میرود. |
| Operate | همه گزینههای فیلتر بدون pointer قابل استفادهاند. |
| Visible focus | element فعال indicator قابل تشخیص دارد. |
| Dismiss | Cancel و مسیر Escape پشتیبانیشده توسط pattern، panel را میبندند. |
| Restore | اگر invoker معتبر است focus به آن برمیگردد؛ وگرنه fallback مستند استفاده میشود. |
| Evidence | active element/state، mutation، repair و limitation ثبت میشود. |
این table implementation prescription نیست. اگر product بهجای dialog از disclosure استفاده کند، ruleهای focus/open/dismiss میتوانند متفاوت باشند.
مرحله ۲: building blockهای native را انتخاب کنید
Trigger یک <button> است. Filterها <input>های native هستند. Lab از <dialog> محلی استفاده میکند تا reasoning روی interaction متمرکز بماند و به framework خاص وابسته نشود.
Starter همچنین یک custom action مشکوک دارد تا learner تصمیم بگیرد آن را با button native عوض کند یا contract کامل custom behavior را پیاده کند. وجود role بدون behavior کافی محسوب نمیشود.
مرحله ۳: open/close path را همگرا کنید
let invoker = null;
function openFilters(source) {
invoker = source;
dialog.showModal();
firstMeaningfulControl.focus();
}
function closeFilters(reason) {
dialog.close(reason);
restoreAfterClose();
}
نکته مهم اسم function نیست؛ این است که هر close path به restoration policy مشترک برسد.
مرحله ۴: fallback را صریح کنید
function restoreAfterClose() {
if (invoker?.isConnected && !invoker.disabled) {
invoker.focus();
return;
}
fallbackTarget.focus();
}
Fallback باید از product context و state باقیمانده بعد از transition بهدست آید و justify شود. transfer fixture مستقل عمداً target را از قبل تعیین نمیکند؛ learner باید آن را انتخاب، پیادهسازی و verify کند.
مرحله ۵: baseline evidence بگیرید
فقط با keyboard وارد شوید، panel را open کنید، controlها را operate کنید، visible focus را بررسی کنید، dismiss کنید و document.activeElement را بعد از open و close ثبت کنید. State exposed باید با UI واقعی هماهنگ باشد.
بهجای «works» observation دقیق بنویسید:
- «بعد از keyboard activation،
document.activeElementبرابر keyword input است.» - «روی Cancel، indicator سهپیکسلی در background panel قابل تشخیص است.»
- «بعد از Cancel، focus روی
#open-filtersبرگشته است.»
این جملهها قابل falsify و rerun هستند.
مرحله ۶: failure mutation
Mutation A focus-visible treatment را حذف میکند. Prediction: actionها ممکن است اجرا شوند ولی محل active focus قابل تشخیص نباشد.
Mutation B restoration call را حذف میکند. Prediction: پس از dismissal، continuity interaction از دست میرود.
Mutation C یک custom action را click-only میکند. Prediction: pointer user موفق است ولی keyboard user equivalent operation ندارد.
هر بار فقط یک mutation انجام دهید تا diagnosis دقیق بماند.
مرحله ۷: transfer
در changed context، Apply باعث میشود trigger اولیه با toolbar فشرده جایگزین شود. Invoker قدیمی دیگر connected نیست. restoration code نباید detached node را target کند. fallback جدید تعریف و دلیلش ثبت میشود.
این transfer test است: copy کردن baseline بدون بازطراحی transition کافی نیست.
Failure modeها: shortcutهای ظاهراً منطقی
«Click handler دارد، پس interactive است»
Pointer success فقط pointer را ثابت میکند. اگر custom element action است، focusability و activation pattern را verify کنید یا native element مناسب بگذارید.
«Role گذاشتیم، پس keyboard حل شد»
Role semantics را تغییر میدهد؛ keyboard event model را خودکار کامل نمیکند. ممکن است accessibility tooling role معتبر ببیند ولی behavior همچنان fail باشد.
«Outline زشت بود، حذفش کردیم»
حذف indicator بدون replacement قابل تشخیص، keyboard interface را خراب میکند. indicator را design کنید، state را پاک نکنید.
«Selection همان focus است»
ممکن است selected item و focused item متفاوت باشند. visual stateها باید این تفاوت را قابل فهم نگه دارند.
«Positive tabindex بده تا order درست شود»
این کار order دوم و شکننده میسازد. source/component structure را repair کنید یا focus management pattern-specific داشته باشید.
«هر چیزی باز شد، focus را بفرست روی اولین control»
Initial focus به pattern/task وابسته است. Disclosure ممکن است focus را روی trigger نگه دارد؛ dialog ممکن است target داخلی بخواهد؛ menu قرارداد خودش را دارد.
«Escape باید همهچیز را ببندد»
Escape در بسیاری patternها رایج است، اما universal handler میتواند nested behavior یا component دیگر را خراب کند. ownership key باید bounded باشد.
«Automated checker پاس شد، پس accessibility پاس است»
Checker guardrail است. Evidence این درس actual keyboard path، visible focus، state، mutation/repair و transfer را میخواهد. PASS محلی معادل conformance کامل نیست.
Experienced bridge: focus را مثل API contract ببینید
برای engineer باتجربه، abstraction مفید یک interaction state machine است.
Stateهایی مثل closed, opening, open, closing را تعریف کنید؛ focus owner هر state، input eventهای مجاز، transition تغییر DOM/state، postcondition مربوط به document.activeElement و fallback حذف node را مشخص کنید.
این model در frameworkها هم مفید است. Rendering async ممکن است باعث شود focus() قبل از mount target اجرا شود یا rerender node ذخیرهشده invoker را replace کند. Timing framework-specific است، اما contract ثابت میماند: پس از کاملشدن transition، focus باید postcondition اعلامشده را داشته باشد.
Event ownership را نیز بررسی کنید. اگر nested widget و parent هر دو یک key را handle کنند، broad handler میتواند double action بسازد. Handler باید در componentی باشد که command را مالک است. preventDefault() را فقط وقتی استفاده کنید که behavior پیشفرض عمداً جایگزین شده و alternative کامل ارائه شده است.
برای composite widget، native controls، roving tabindex یا active-descendant model را آگاهانه انتخاب کنید. معیار فقط compact بودن code نیست؛ semantics pattern، visible focus، scrolling، assistive-technology expectations، virtualization و testability مهماند.
سؤال acceptance سطح بالا: «آیا engineer دیگری با خواندن contract میتواند focus بعد از هر transition را پیشبینی کند و sync بودن semantics/state/keyboard/visual را verify کند؟»
Alternative و tradeoff: کوچکترین mechanism کافی را انتخاب کنید
Disclosure native در برابر dialog-like surface
اگر content فقط inline باز میشود و modal context ایجاد نمیکند، disclosure ممکن است کافی باشد و focus روی trigger بماند. اگر task موقت و modal داریم، focus/dismiss obligation متفاوت است. ظاهر floating card بهتنهایی دلیل dialog نیست.
Native controls در برابر custom composite
Button، checkbox، radio و select native مقدار زیادی behavior آماده دارند. Custom composite flexibility بیشتری میدهد ولی responsibility بیشتری هم میآورد. اگر سه command مستقل دارید، سه button شاید از tablist جعلی بهتر باشد. اگر یک انتخاب از مجموعه است، radio-group یا pattern مناسب دیگری ممکن است درست باشد.
Roving tabindex در برابر active descendant
هر دو در context مناسب قابل استفادهاند. Roving tabindex DOM focus را بین itemها حرکت میدهد. Active-descendant میتواند focus DOM را روی container نگه دارد و item فعال را expose کند. Pattern و constraint implementation تصمیم را تعیین میکنند؛ این درس winner جهانی اعلام نمیکند.
Custom focus style در برابر browser default
Custom styling میتواند واضحتر و هماهنگ با design system باشد، اما contrast، forced-colors/high-contrast، backgroundهای مختلف و component stateها را باید خودتان verify کنید. Default browser معمولاً baseline بهتری از «هیچ indicator» است.
Programmatic focus در برابر navigation کاربر
Focus movement میتواند context را بعد از transition حفظ کند؛ حرکت بیش از حد focus هم میتواند user را غافلگیر کند. معیار، predictability و ضرورت در interaction contract است، نه قانون «همیشه» یا «هرگز».
Practice: scaffold را کم کنید، evidence را نه
Guided: فقط transition بازشدن
قبل از code این موارد را بنویسید:
- entry control؛
- activation method؛
- expected state؛
- expected
document.activeElement؛ - expected focus indicator؛
- observationای که failure را ثابت میکند.
فقط همان transition را پیاده/repair کنید و با keyboard نتیجه را ثبت کنید.
Repair: یک dimension خراب
یکی از failureهای starter را انتخاب کنید: click-only custom action، focus indicator suppressed یا نبود restoration policy. symptom را پیشبینی کنید، reproduce کنید، cause محدود را repair کنید و baseline کامل را rerun کنید تا regression پنهان نماند.
Independent transfer: context تغییر میکند
Transfer fixture را طوری تغییر دهید که Apply، trigger اولیه را replace کند یا item focused حذف شود. بدون answer key، مشخص کنید:
- restoration destination جدید چیست؛
- چرا ادامه interaction در آن نقطه coherent است؛
- نتیجه را چگونه observe میکنید؛
- limitation محیط test چیست.
در transfer نهایی، AI نباید decisive implementation یا conclusion را به جای learner انتخاب کند. میتواند بعد از بیان contract، checkهای پیشنهادی بدهد؛ نمیتواند browser evidence اختراع کند.
Scope را کنترل کنید. قرار نیست کل design system را audit کنید. یک primitive bounded را کامل کنید و adjacent issueها را جدا ثبت کنید.
Verification: چه چیزی واقعاً evidence است
Artifact فقط زمانی معتبر است که rowها observation قابل بررسی داشته باشند، نه confidence statement.
Environment را ثبت کنید: browser/version، OS، input method و اینکه assistive technology در test بوده یا نه. اگر AT تست نشده، صریح بنویسید. Keyboard-only evidence را به screen-reader evidence تعمیم ندهید.
برای هر row ثبت کنید:
- scenario و component state؛
- entry control و keyboard action؛
- active element قبل/بعد؛
- state/attribute change مشاهدهشده؛
- visible بودن focus؛
- available بودن operation بدون pointer؛
- exit path و restoration target؛
- observation جدا از inference/decision؛
- mutation و predicted failure؛
- mutation result؛
- repair و repair result؛
- changed-context result؛
- residual limitation.
Observation قوی: «با فشردن Escape در dialog، panel بسته شد و document.activeElement.id برابر open-filters شد.» Observation ضعیف: «keyboard درست بود.»
Limitation قوی: «با keyboard در browser ثبتشده verify شد؛ behavior همه browser/ATها را ثابت نمیکند.» Limitation ضعیف: «شاید bug داشته باشد.»
run_checks.py عمومی lab فقط structure و omissionهای واضح را check میکند و عمداً نمیتواند interaction واقعی browser را certify کند. این separation مهم است: script میتواند shape evidence را validate کند، اما نمیتواند observation learner را جعل کند.
قبل از completion سه pass لازم است:
- Baseline: همه dimensionهای owned پاس شوند.
- Mutation/repair: حداقل یک failure عمدی detect و repair شود.
- Transfer: changed context مستقل حل شود، از جمله وقتی focus target اولیه تغییر یا حذف میشود.
Fail شدن هر critical dimension یعنی feature bounded هنوز accepted نیست.
Assessment و transfer
Assessment پنج عمل متفاوت را پوشش میدهد.
Retrieval: interaction contract و یک non-goal را بیان کنید.
Application: filter primitive را پیاده کنید و برای keyboard operability، visible focus، focus movement/restoration و pattern contract evidence ارائه دهید.
Diagnosis: mutation عمدی مثل حذف focus-visible یا restoration را بررسی کنید و dimension خراب را از observation تشخیص دهید.
Decision: انتخاب mechanism را در برابر alternative قابلقبول دفاع کنید؛ مثلاً native button در برابر custom role-based control، یا disclosure در برابر dialog-like interaction. پاسخ باید requirement تعیینکننده را بیان کند، نه winner جهانی.
Transfer: در changed context که invoker replace شده یا item focused حذف میشود، restoration target جدید را تعریف، justify و مستقل verify کنید.
Application، diagnosis و transfer blocking هستند چون behavior واقعی را میسنجند. توضیح روان نمیتواند implementation غیرقابلاستفاده با keyboard را جبران کند.
Reference، freshness و claim boundary
این درس دقیقاً سه external authority از W3C دارد و scope هر کدام محدود است.
- S-WCAG22 — WCAG 2.2: معیارهای normative مرتبط با keyboard operability و visible focus. از آن implementation recipe کامل نمیسازیم.
- S-APG-KEYBOARD — Developing a Keyboard Interface: guidance رسمی درباره keyboard conventions، focus قابل تشخیص/قابل پیشبینی، tab sequence، composite focus management و تفاوت focus/selection.
- S-APG-PRACTICES — APG Practices: guidance رسمی گستردهتر برای semantics و widget interaction contract.
Source set در 2026-09-10 روی صفحات رسمی جاری W3C recheck شد. نیازی به source expansion نبود و claim boundaryهای Bible حفظ شدند. Review due همچنان 2026-12-31 است.
دو uncertainty صریح باقی میماند. رفتار browser/assistive technology میتواند متفاوت باشد، پس observation محلی را universal نمیکنیم. Product requirement نیز میتواند pattern یا restoration target مناسب را تغییر دهد؛ بنابراین lesson یک contract-and-evidence method آموزش میدهد، نه یک widget implementation ابدی.
Non-claimهای نهایی پابرجا هستند: ARIA خودکار keyboard behavior نمیسازد؛ focus و selection یکی نیستند؛ positive tabindex راهحل عمومی ordering نیست؛ و یک keyboard pattern ثابت برای همه widgetها وجود ندارد.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
WEB-013-S1 retrieval
حداقل قرارداد keyboard/focus برای یک primitive تعاملی محدود را بیان کنید. semantics را از behavior جدا کنید، توضیح دهید چرا focus باید قابل تشخیص بماند و یک non-claim صریح درباره ARIA، تفاوت focus و selection، positive tabindex یا الگوی keyboard همگانی ثبت کنید.
WEB-013-S2 application
WEB013-LAB1 را repair کنید تا primitive فیلتر با keyboard قابل ورود، استفاده و dismissal باشد؛ focus بهصورت واضح دیده شود؛ و پس از بستهشدن primitive، focus به هدف منطقی برگردد. Accessible Primitive Interaction Contract v1 را با observation مستقیم برای entry، operation، state change و exit/restore کامل کنید.
WEB-013-S3 diagnosis
یکی از failure mutationهای عمدی را اجرا کنید: حذف visible focus، حذف focus restoration منطقی، یا تبدیل یک action به custom control فقط-ماوسی. ابتدا failure قابل مشاهده را پیشبینی کنید، mutation را اجرا کنید، dimension شکستهشده قرارداد را diagnose کنید و فقط همان boundary را repair کنید.
WEB-013-S4 decision
برای primitive محدود، یک تصمیم native-first را در برابر alternative سفارشی مبتنی بر role دفاع کنید. مشخص کنید کدام رفتار native را reuse میکنید، چه requirement محصولی ممکن است custom behavior را توجیه کند و چه evidenceای باعث تغییر تصمیم شما میشود.
WEB-013-S5 transfer
changed-context transfer fixture را مستقل حل کنید: یک row action، confirmation primitive را باز میکند و ممکن است پس از تأیید، row فراخواننده حذف شود. keyboard operability و visible focus را حفظ کنید؛ سپس وقتی invoker اصلی دیگر وجود ندارد، یک fallback focus target منطقی انتخاب و توجیه کنید. یک mutation و یک residual limitation ثبت کنید.
ملاکهای ارزیابی (rubric)
Critical dimensions
- Contract correctness — semantics, keyboard behavior, focus visibility and focus movement agree with the bounded primitive.
- Evidence quality — observations are distinguished from inference; mutation and repair are traceable.
- Scope discipline — WEB-009 base semantics, WEB-010 responsive layout and WEB-019 direction/bidi concerns are not silently re-owned.
- Transfer independence — the changed-context case is solved without a decisive answer key.
Blocking rule
S2, S3 and S5 are blocking. A blocking response fails when a required pointer-only path remains, focus is not discernible, focus movement/restoration is unjustified, mutation evidence is missing, or the changed-context decision is copied without accounting for the changed condition.
Evidence rule
A screenshot or a passing static checker alone is not proof. The learner must record direct browser/DOM observations, the tested environment, the mutation outcome, the repair outcome and one residual limitation. AI-generated claims are not accepted as observed evidence.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
هدف
primitive فیلتر محدود را repair کنید تا قرارداد keyboard و focus آن قابل مشاهده و قابل دفاع باشد. starter عمداً خراب است؛ PASS شدن public structural checks را accessibility verdict تلقی نکنید.
مسیر الزامی
- قبل از تغییر code، expected contract را ثبت کنید.
- فقط با keyboard وارد primitive شوید، actionها را اجرا کنید و آن را dismiss کنید.
- focus را واضح و قابل تشخیص کنید.
- نیاز pointer-only در custom action را حذف کنید؛ هرجا product contract اجازه میدهد native control را ترجیح دهید.
- focus restoration منطقی بعد از dismissal را پیادهسازی یا توجیه کنید.
- با
mutation_probe.pyیک mutation عمدی اجرا کنید؛ قبل از اجرا failure را پیشبینی کنید، سپس observe، diagnose و repair کنید. - transfer تغییرکرده را حل کنید؛ در آن invoker ممکن است حذف شود.
evidence/learner_evidence.jsonرا با observation مستقیم و یک residual limitation کامل کنید.
Boundary
responsive layout یا directional-key behavior را بازطراحی نکنید؛ این موارد متعلق به درسهای مجاورند مگر اینکه مستقیماً مانع قرارداد محدود keyboard/focus شوند.
حالتهای public checker
پیش از repair، برای اعتبارسنجی starter ارائهشده python run_checks.py --structure-only را اجرا کنید. پس از تکمیل repair و evidence rowها، python run_checks.py را بدون آن flag اجرا کنید. PASS فقط completeness ساختاری/evidence را نشان میدهد؛ رفتار واقعی browser همچنان باید توسط خودتان observe و ثبت شود.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)