@longsightgroup/qti3-a11y 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -3
- package/src/index.ts +366 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longsightgroup/qti3-a11y",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Accessibility contracts and proof metadata for qti3 QTI 3 item interactions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a11y",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"dist/*.d.ts.map",
|
|
30
30
|
"dist/*.js",
|
|
31
31
|
"dist/*.js.map",
|
|
32
|
-
"!dist/*.test.*"
|
|
32
|
+
"!dist/*.test.*",
|
|
33
|
+
"src/**/*.ts",
|
|
34
|
+
"!src/**/*.test.ts"
|
|
33
35
|
],
|
|
34
36
|
"type": "module",
|
|
35
37
|
"exports": {
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
"registry": "https://registry.npmjs.org"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
|
-
"@longsightgroup/qti3-core": "0.1.
|
|
48
|
+
"@longsightgroup/qti3-core": "0.1.2"
|
|
47
49
|
},
|
|
48
50
|
"scripts": {}
|
|
49
51
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { interactionSupport, type QtiInteractionType } from "@longsightgroup/qti3-core";
|
|
2
|
+
|
|
3
|
+
export interface InteractionA11yContract {
|
|
4
|
+
interactionType: QtiInteractionType;
|
|
5
|
+
keyboardRequired: boolean;
|
|
6
|
+
requiresAccessibleName: boolean;
|
|
7
|
+
requiresValidationMessageAssociation: boolean;
|
|
8
|
+
primaryRole: string;
|
|
9
|
+
focusStrategy: string;
|
|
10
|
+
keyboardModel: string[];
|
|
11
|
+
requiredStates: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ManualAssistiveTechnologyScript {
|
|
15
|
+
assistiveTechnology: "VoiceOver" | "NVDA" | "JAWS";
|
|
16
|
+
platform: string;
|
|
17
|
+
browser: string;
|
|
18
|
+
appliesTo: QtiInteractionType[];
|
|
19
|
+
setup: string[];
|
|
20
|
+
procedure: string[];
|
|
21
|
+
expectedResults: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AccessibilityProofEntry {
|
|
25
|
+
interactionType: QtiInteractionType;
|
|
26
|
+
primaryRole: string;
|
|
27
|
+
keyboardRequired: boolean;
|
|
28
|
+
keyboardModel: string[];
|
|
29
|
+
proof: {
|
|
30
|
+
automated: string[];
|
|
31
|
+
manual: string[];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const a11yContracts: InteractionA11yContract[] = interactionSupport.map((support) =>
|
|
36
|
+
contractForInteraction(support.interactionType as QtiInteractionType),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const accessibilityProofMatrix: AccessibilityProofEntry[] = a11yContracts.map(
|
|
40
|
+
(contract) => ({
|
|
41
|
+
interactionType: contract.interactionType,
|
|
42
|
+
primaryRole: contract.primaryRole,
|
|
43
|
+
keyboardRequired: contract.keyboardRequired,
|
|
44
|
+
keyboardModel: contract.keyboardModel,
|
|
45
|
+
proof: {
|
|
46
|
+
automated: automatedProofFor(contract),
|
|
47
|
+
manual: manualProofFor(contract),
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const manualAssistiveTechnologyScripts: ManualAssistiveTechnologyScript[] = [
|
|
53
|
+
{
|
|
54
|
+
assistiveTechnology: "VoiceOver",
|
|
55
|
+
platform: "macOS",
|
|
56
|
+
browser: "Safari or Chromium",
|
|
57
|
+
appliesTo: targetInteractions(),
|
|
58
|
+
setup: [
|
|
59
|
+
"Start the manual harness with pnpm dev.",
|
|
60
|
+
"Open the harness in the browser and enable VoiceOver.",
|
|
61
|
+
"Load each reference fixture from the fixture selector.",
|
|
62
|
+
],
|
|
63
|
+
procedure: [
|
|
64
|
+
"Navigate from the item body into the interaction with standard VoiceOver navigation.",
|
|
65
|
+
"Confirm the prompt, role, current value or selection state, and validation message are announced.",
|
|
66
|
+
"Complete the response using keyboard-only commands.",
|
|
67
|
+
"Score the item and navigate to any feedback or updated state.",
|
|
68
|
+
],
|
|
69
|
+
expectedResults: [
|
|
70
|
+
"Every interaction has a meaningful accessible name and role.",
|
|
71
|
+
"Keyboard operation reaches and completes the interaction without pointer input.",
|
|
72
|
+
"Validation messages are announced through the control description when present.",
|
|
73
|
+
"Focus order follows the visual and DOM order of the item.",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
assistiveTechnology: "NVDA",
|
|
78
|
+
platform: "Windows",
|
|
79
|
+
browser: "Firefox or Chromium",
|
|
80
|
+
appliesTo: targetInteractions(),
|
|
81
|
+
setup: [
|
|
82
|
+
"Start the manual harness with pnpm dev on the test machine or open it from a reachable host.",
|
|
83
|
+
"Open the harness in the browser and enable NVDA browse mode.",
|
|
84
|
+
"Load each reference fixture from the fixture selector.",
|
|
85
|
+
],
|
|
86
|
+
procedure: [
|
|
87
|
+
"Use heading, form-field, and Tab navigation to enter the interaction.",
|
|
88
|
+
"Confirm NVDA announces the role, name, value, selected state, and invalid state where applicable.",
|
|
89
|
+
"Switch modes only when NVDA or the browser requires it for native controls.",
|
|
90
|
+
"Complete the response, score the item, and verify feedback or validation announcements.",
|
|
91
|
+
],
|
|
92
|
+
expectedResults: [
|
|
93
|
+
"Native controls expose expected roles through the accessibility tree.",
|
|
94
|
+
"Composite interactions expose each operable part in deterministic order.",
|
|
95
|
+
"Selected, pressed, invalid, and described states are announced when applicable.",
|
|
96
|
+
"No fixture requires pointer-only operation.",
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
assistiveTechnology: "JAWS",
|
|
101
|
+
platform: "Windows",
|
|
102
|
+
browser: "Chromium",
|
|
103
|
+
appliesTo: targetInteractions(),
|
|
104
|
+
setup: [
|
|
105
|
+
"Start the manual harness with pnpm dev on the test machine or open it from a reachable host.",
|
|
106
|
+
"Open the harness in Chromium and enable JAWS.",
|
|
107
|
+
"Load each reference fixture from the fixture selector.",
|
|
108
|
+
],
|
|
109
|
+
procedure: [
|
|
110
|
+
"Use virtual cursor and Tab navigation to reach the interaction.",
|
|
111
|
+
"Read the prompt, control role, current value, validation message, and feedback region.",
|
|
112
|
+
"Complete the response with keyboard-only commands.",
|
|
113
|
+
"Score the item and verify the attempt state can be reviewed without losing focus context.",
|
|
114
|
+
],
|
|
115
|
+
expectedResults: [
|
|
116
|
+
"JAWS announces a stable role and name for every operable control.",
|
|
117
|
+
"Validation and feedback are reachable after scoring.",
|
|
118
|
+
"Graphic, point, drawing, and custom-host fixtures expose a keyboard-operable fallback or control.",
|
|
119
|
+
"The item can be completed without hidden instructions or product-specific UI.",
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
function contractForInteraction(interactionType: QtiInteractionType): InteractionA11yContract {
|
|
125
|
+
const base = {
|
|
126
|
+
interactionType,
|
|
127
|
+
keyboardRequired: interactionType !== "media",
|
|
128
|
+
requiresAccessibleName: true,
|
|
129
|
+
requiresValidationMessageAssociation: true,
|
|
130
|
+
primaryRole: "group",
|
|
131
|
+
focusStrategy: "Focus enters the interaction group and then each native control in DOM order.",
|
|
132
|
+
keyboardModel: [
|
|
133
|
+
"Tab moves through controls.",
|
|
134
|
+
"Enter or Space commits native control changes.",
|
|
135
|
+
],
|
|
136
|
+
requiredStates: ["aria-invalid", "aria-describedby"],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (interactionType === "choice" || interactionType === "hottext") {
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
primaryRole: "radiogroup or group",
|
|
143
|
+
keyboardModel: [
|
|
144
|
+
"Tab moves into each radio or checkbox.",
|
|
145
|
+
"Space toggles the focused option.",
|
|
146
|
+
],
|
|
147
|
+
requiredStates: ["checked", "aria-invalid", "aria-describedby"],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (interactionType === "order" || interactionType === "graphicOrder") {
|
|
152
|
+
return {
|
|
153
|
+
...base,
|
|
154
|
+
primaryRole: "group",
|
|
155
|
+
keyboardModel: [
|
|
156
|
+
"Tab moves through each item handle and its move buttons.",
|
|
157
|
+
"Arrow Up, Arrow Down, Arrow Left, or Arrow Right reorders the focused item handle.",
|
|
158
|
+
"Arrow icon buttons provide an explicit move-button fallback.",
|
|
159
|
+
],
|
|
160
|
+
requiredStates: [
|
|
161
|
+
"position in accessible name",
|
|
162
|
+
"disabled",
|
|
163
|
+
"aria-invalid",
|
|
164
|
+
"aria-describedby",
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (
|
|
170
|
+
interactionType === "associate" ||
|
|
171
|
+
interactionType === "graphicAssociate" ||
|
|
172
|
+
interactionType === "match"
|
|
173
|
+
) {
|
|
174
|
+
return {
|
|
175
|
+
...base,
|
|
176
|
+
primaryRole: "group",
|
|
177
|
+
keyboardModel: [
|
|
178
|
+
"Tab moves through source tokens, target tokens, selected pair chips, and remove controls.",
|
|
179
|
+
"Enter or Space selects one source token and one target token to create a pair.",
|
|
180
|
+
"Remove buttons delete selected pairs.",
|
|
181
|
+
"Pointer drag from a source token to a target token is a progressive enhancement.",
|
|
182
|
+
],
|
|
183
|
+
requiredStates: ["aria-pressed", "selected pair text", "aria-invalid", "aria-describedby"],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (interactionType === "gapMatch" || interactionType === "graphicGapMatch") {
|
|
188
|
+
return {
|
|
189
|
+
...base,
|
|
190
|
+
primaryRole: "group",
|
|
191
|
+
focusStrategy:
|
|
192
|
+
"Focus moves through source tokens, target-gap buttons, and remove controls in DOM order.",
|
|
193
|
+
keyboardModel: [
|
|
194
|
+
"Enter or Space selects a source token.",
|
|
195
|
+
"Enter or Space on a target gap assigns the selected source.",
|
|
196
|
+
"Remove buttons clear assigned gaps.",
|
|
197
|
+
"Pointer drag from a source token to a target gap is a progressive enhancement.",
|
|
198
|
+
],
|
|
199
|
+
requiredStates: [
|
|
200
|
+
"aria-pressed",
|
|
201
|
+
"assigned source in accessible name",
|
|
202
|
+
"aria-invalid",
|
|
203
|
+
"aria-describedby",
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (interactionType === "inlineChoice") {
|
|
209
|
+
return {
|
|
210
|
+
...base,
|
|
211
|
+
primaryRole: "combobox",
|
|
212
|
+
focusStrategy: "Focus lands directly on the inline choice control.",
|
|
213
|
+
keyboardModel: ["Native select commands choose an inline option."],
|
|
214
|
+
requiredStates: ["value", "aria-invalid", "aria-describedby"],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (interactionType === "textEntry" || interactionType === "extendedText") {
|
|
219
|
+
return {
|
|
220
|
+
...base,
|
|
221
|
+
primaryRole: "textbox",
|
|
222
|
+
focusStrategy: "Focus lands directly on the text entry field.",
|
|
223
|
+
keyboardModel: ["Typing edits the response.", "Tab leaves the field."],
|
|
224
|
+
requiredStates: ["value", "aria-invalid", "aria-describedby"],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (interactionType === "hotspot") {
|
|
229
|
+
return {
|
|
230
|
+
...base,
|
|
231
|
+
primaryRole: "group",
|
|
232
|
+
focusStrategy: "Focus moves through positioned hotspot buttons over the object image.",
|
|
233
|
+
keyboardModel: [
|
|
234
|
+
"Tab moves through hotspot buttons.",
|
|
235
|
+
"Enter or Space selects the focused hotspot.",
|
|
236
|
+
],
|
|
237
|
+
requiredStates: ["aria-pressed", "aria-invalid", "aria-describedby"],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (interactionType === "selectPoint" || interactionType === "positionObject") {
|
|
242
|
+
return {
|
|
243
|
+
...base,
|
|
244
|
+
primaryRole: "button",
|
|
245
|
+
focusStrategy: "Focus lands on the coordinate surface.",
|
|
246
|
+
keyboardModel: [
|
|
247
|
+
"Arrow keys move the selected coordinate by one unit.",
|
|
248
|
+
"Shift plus arrow keys move by ten units.",
|
|
249
|
+
"Enter or Space commits the selected coordinate.",
|
|
250
|
+
],
|
|
251
|
+
requiredStates: [
|
|
252
|
+
"selected coordinate in accessible name",
|
|
253
|
+
"aria-invalid",
|
|
254
|
+
"aria-describedby",
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (interactionType === "slider") {
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
primaryRole: "slider",
|
|
263
|
+
focusStrategy: "Focus lands directly on the range input.",
|
|
264
|
+
keyboardModel: ["Native range input keys change the value."],
|
|
265
|
+
requiredStates: ["value", "aria-invalid", "aria-describedby"],
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (interactionType === "drawing") {
|
|
270
|
+
return {
|
|
271
|
+
...base,
|
|
272
|
+
primaryRole: "img",
|
|
273
|
+
focusStrategy: "Focus lands on the drawing surface and then on auxiliary commands.",
|
|
274
|
+
keyboardModel: [
|
|
275
|
+
"Pointer input draws freehand strokes.",
|
|
276
|
+
"Enter or Space creates a deterministic keyboard stroke.",
|
|
277
|
+
"Clear drawing removes all strokes.",
|
|
278
|
+
],
|
|
279
|
+
requiredStates: ["accessible name", "aria-invalid", "aria-describedby"],
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (interactionType === "upload") {
|
|
284
|
+
return {
|
|
285
|
+
...base,
|
|
286
|
+
primaryRole: "button",
|
|
287
|
+
focusStrategy: "Focus lands on the native file input.",
|
|
288
|
+
keyboardModel: ["Native file input commands choose a file."],
|
|
289
|
+
requiredStates: ["selected file name", "aria-invalid", "aria-describedby"],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (interactionType === "media") {
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
keyboardRequired: false,
|
|
297
|
+
primaryRole: "audio, video, image, or link",
|
|
298
|
+
focusStrategy: "Focus follows native media controls when the media type exposes them.",
|
|
299
|
+
keyboardModel: ["Native media controls provide their platform keyboard behavior."],
|
|
300
|
+
requiredStates: ["accessible name"],
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (interactionType === "endAttempt") {
|
|
305
|
+
return {
|
|
306
|
+
...base,
|
|
307
|
+
primaryRole: "button",
|
|
308
|
+
focusStrategy: "Focus lands on the end-attempt button.",
|
|
309
|
+
keyboardModel: ["Enter or Space activates the button."],
|
|
310
|
+
requiredStates: ["accessible name"],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (interactionType === "portableCustom") {
|
|
315
|
+
return {
|
|
316
|
+
...base,
|
|
317
|
+
primaryRole: "group",
|
|
318
|
+
focusStrategy: "Focus enters the portable custom host and its fallback control.",
|
|
319
|
+
keyboardModel: [
|
|
320
|
+
"The host integration must expose a keyboard-operable response control.",
|
|
321
|
+
"The fallback text input accepts a response when no integration has rendered.",
|
|
322
|
+
],
|
|
323
|
+
requiredStates: ["host metadata", "aria-invalid", "aria-describedby"],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
...base,
|
|
329
|
+
primaryRole: "unsupported",
|
|
330
|
+
focusStrategy: "Deprecated custom interaction is parsed for diagnostics but not rendered.",
|
|
331
|
+
keyboardRequired: false,
|
|
332
|
+
keyboardModel: ["No runtime keyboard contract is provided for deprecated custom interaction."],
|
|
333
|
+
requiredStates: ["deprecated diagnostic"],
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function targetInteractions(): QtiInteractionType[] {
|
|
338
|
+
return interactionSupport.map((support) => support.interactionType as QtiInteractionType);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function automatedProofFor(contract: InteractionA11yContract): string[] {
|
|
342
|
+
const proof = [
|
|
343
|
+
"accessibility contract unit coverage in @longsightgroup/qti3-a11y",
|
|
344
|
+
"manual harness reference fixture renders without axe-core violations",
|
|
345
|
+
"operable fixture controls expose accessible names in Playwright",
|
|
346
|
+
"operable fixture controls use standard tab order in Playwright",
|
|
347
|
+
"response serialization and fixture scoring coverage",
|
|
348
|
+
];
|
|
349
|
+
if (contract.requiresValidationMessageAssociation) {
|
|
350
|
+
proof.push("validation message association contract");
|
|
351
|
+
}
|
|
352
|
+
proof.push("forced-colors, reduced-motion, and narrow viewport browser checks");
|
|
353
|
+
return proof;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function manualProofFor(contract: InteractionA11yContract): string[] {
|
|
357
|
+
const proof = [
|
|
358
|
+
"VoiceOver manual script",
|
|
359
|
+
"NVDA manual script",
|
|
360
|
+
"JAWS manual script",
|
|
361
|
+
"focus order inspection",
|
|
362
|
+
"accessible name, role, state, and value announcement inspection",
|
|
363
|
+
];
|
|
364
|
+
if (contract.keyboardRequired) proof.push("keyboard-only completion without pointer input");
|
|
365
|
+
return proof;
|
|
366
|
+
}
|