@@ -2,7 +2,12 @@ import {
22 AccessibilityNode ,
33 createAccessibilityTree ,
44} from "./createAccessibilityTree" ;
5- import type { CommandOptions , ScreenReader } from "@guidepup/guidepup" ;
5+ import {
6+ CommandOptions ,
7+ MacOSModifiers ,
8+ ScreenReader ,
9+ WindowsModifiers ,
10+ } from "@guidepup/guidepup" ;
611import {
712 ERR_VIRTUAL_MISSING_CONTAINER ,
813 ERR_VIRTUAL_NOT_STARTED ,
@@ -16,6 +21,11 @@ export interface StartOptions extends CommandOptions {
1621 container : HTMLElement ;
1722}
1823
24+ const defaultUserEventOptions = {
25+ delay : null ,
26+ skipHover : true ,
27+ } ;
28+
1929const observedAttributes = [
2030 ...aria . keys ( ) ,
2131 "type" ,
@@ -34,7 +44,6 @@ const observedAttributes = [
3444
3545// TODO: handle aria-live, role="polite", role="alert", and other interruptions.
3646// TODO: announce sensible attribute values, e.g. clicked, disabled, etc.
37- // TODO: consider making the role, accessibleName, accessibleDescription, etc. available via their own APIs.
3847
3948const observeDOM = ( function ( ) {
4049 const MutationObserver = window . MutationObserver ;
@@ -147,14 +156,29 @@ export class Virtual implements ScreenReader {
147156 ) ;
148157 }
149158
159+ /**
160+ * Detect whether the screen reader is supported for the current OS.
161+ *
162+ * @returns {Promise<boolean> }
163+ */
150164 async detect ( ) {
151165 return true ;
152166 }
153167
168+ /**
169+ * Detect whether the screen reader is the default screen reader for the current OS.
170+ *
171+ * @returns {Promise<boolean> }
172+ */
154173 async default ( ) {
155174 return false ;
156175 }
157176
177+ /**
178+ * Turn the screen reader on.
179+ *
180+ * @param {object } [options] Additional options.
181+ */
158182 async start ( { container } : StartOptions = { container : null } ) {
159183 if ( ! container ) {
160184 throw new Error ( ERR_VIRTUAL_MISSING_CONTAINER ) ;
@@ -176,6 +200,9 @@ export class Virtual implements ScreenReader {
176200 return ;
177201 }
178202
203+ /**
204+ * Turn the screen reader off.
205+ */
179206 async stop ( ) {
180207 this . #disconnectDOMObserver?.( ) ;
181208 this . #invalidateTreeCache( ) ;
@@ -188,6 +215,9 @@ export class Virtual implements ScreenReader {
188215 return ;
189216 }
190217
218+ /**
219+ * Move the screen reader cursor to the previous location.
220+ */
191221 async previous ( ) {
192222 this . #checkContainer( ) ;
193223
@@ -206,6 +236,9 @@ export class Virtual implements ScreenReader {
206236 return ;
207237 }
208238
239+ /**
240+ * Move the screen reader cursor to the next location.
241+ */
209242 async next ( ) {
210243 this . #checkContainer( ) ;
211244
@@ -227,45 +260,138 @@ export class Virtual implements ScreenReader {
227260 return ;
228261 }
229262
263+ /**
264+ * Perform the default action for the item in the screen reader cursor.
265+ */
230266 async act ( ) {
231267 this . #checkContainer( ) ;
232268
233- notImplemented ( ) ;
269+ if ( ! this . #activeNode) {
270+ return ;
271+ }
272+
273+ const target = this . #activeNode. node as HTMLElement ;
274+
275+ // TODO: verify that is appropriate for all default actions
276+ await userEvent . click ( target , defaultUserEventOptions ) ;
234277
235278 return ;
236279 }
237280
281+ /**
282+ * Interact with the item under the screen reader cursor.
283+ */
238284 async interact ( ) {
239285 this . #checkContainer( ) ;
240286
241287 return ;
242288 }
243289
290+ /**
291+ * Stop interacting with the current item.
292+ */
244293 async stopInteracting ( ) {
245294 this . #checkContainer( ) ;
246295
247296 return ;
248297 }
249298
250- async press ( ) {
299+ /**
300+ * Press a key on the active item.
301+ *
302+ * `key` can specify the intended [keyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key)
303+ * value or a single character to generate the text for. A superset of the `key` values can be found
304+ * [on the MDN key values page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values). Examples of the keys are:
305+ *
306+ * `F1` - `F20`, `Digit0` - `Digit9`, `KeyA` - `KeyZ`, `Backquote`, `Minus`, `Equal`, `Backslash`, `Backspace`, `Tab`,
307+ * `Delete`, `Escape`, `ArrowDown`, `End`, `Enter`, `Home`, `Insert`, `PageDown`, `PageUp`, `ArrowRight`, `ArrowUp`, etc.
308+ *
309+ * Following modification shortcuts are also supported: `Shift`, `Control`, `Alt`, `Meta` (OS permitting).
310+ *
311+ * Holding down `Shift` will type the text that corresponds to the `key` in the upper case.
312+ *
313+ * If `key` is a single character, it is case-sensitive, so the values `a` and `A` will generate different respective
314+ * texts.
315+ *
316+ * Shortcuts such as `key: "Control+f"` or `key: "Control+Shift+f"` are supported as well. When specified with the
317+ * modifier, modifier is pressed and being held while the subsequent key is being pressed.
318+ *
319+ * ```ts
320+ * await keyboard.press("Control+f");
321+ * ```
322+ *
323+ * @param {string } key Name of the key to press or a character to generate, such as `ArrowLeft` or `a`.
324+ */
325+ async press ( key : string ) {
251326 this . #checkContainer( ) ;
252327
253- notImplemented ( ) ;
328+ if ( ! this . #activeNode) {
329+ return ;
330+ }
331+
332+ const rawKeys = key . replaceAll ( "{" , "{{" ) . replaceAll ( "[" , "[[" ) . split ( "+" ) ;
333+ const modifiers = [ ] ;
334+ const keys = [ ] ;
335+
336+ rawKeys . forEach ( ( rawKey ) => {
337+ if (
338+ typeof MacOSModifiers [ rawKey ] !== "undefined" ||
339+ typeof WindowsModifiers [ rawKey ] !== "undefined"
340+ ) {
341+ modifiers . push ( rawKey ) ;
342+ } else {
343+ keys . push ( rawKey ) ;
344+ }
345+ } ) ;
346+
347+ const keyboardCommand = [
348+ ...modifiers . map ( ( modifier ) => `{${ modifier } >}` ) ,
349+ ...keys ,
350+ ...modifiers . reverse ( ) . map ( ( modifier ) => `{/${ modifier } }` ) ,
351+ ] . join ( "" ) ;
352+
353+ await this . click ( ) ;
354+ await userEvent . keyboard ( keyboardCommand , defaultUserEventOptions ) ;
254355
255356 return ;
256357 }
257358
258- async type ( ) {
359+ /**
360+ * Type text into the active item.
361+ *
362+ * To press a special key, like `Control` or `ArrowDown`, use `virtual.press(key)`.
363+ *
364+ * ```ts
365+ * await virtual.type("my-username");
366+ * await virtual.press("Enter");
367+ * ```
368+ *
369+ * @param {string } text Text to type into the active item.
370+ */
371+ async type ( text : string ) {
259372 this . #checkContainer( ) ;
260373
261- notImplemented ( ) ;
374+ if ( ! this . #activeNode) {
375+ return ;
376+ }
377+
378+ const target = this . #activeNode. node as HTMLElement ;
379+ await userEvent . type ( target , text , defaultUserEventOptions ) ;
262380
263381 return ;
264382 }
265383
384+ /**
385+ * Perform a screen reader command.
386+ *
387+ * @param {any } command Screen reader command to execute.
388+ */
266389 async perform ( ) {
267390 this . #checkContainer( ) ;
268391
392+ // TODO: decide what this means as there is no established "common" command
393+ // set for different screen readers.
394+
269395 notImplemented ( ) ;
270396
271397 return ;
@@ -287,29 +413,52 @@ export class Virtual implements ScreenReader {
287413 const keys = key . repeat ( clickCount ) ;
288414 const target = this . #activeNode. node as HTMLElement ;
289415
290- await userEvent . pointer ( [ { target } , { keys, target } ] ) ;
416+ await userEvent . pointer (
417+ [ { target } , { keys, target } ] ,
418+ defaultUserEventOptions
419+ ) ;
291420
292421 return ;
293422 }
294423
424+ /**
425+ * Get the last spoken phrase.
426+ *
427+ * @returns {Promise<string> } The last spoken phrase.
428+ */
295429 async lastSpokenPhrase ( ) {
296430 this . #checkContainer( ) ;
297431
298432 return this . #spokenPhraseLog. at ( - 1 ) ?? "" ;
299433 }
300434
435+ /**
436+ * Get the text of the item in the screen reader cursor.
437+ *
438+ * @returns {Promise<string> } The item's text.
439+ */
301440 async itemText ( ) {
302441 this . #checkContainer( ) ;
303442
304443 return this . #itemTextLog. at ( - 1 ) ?? "" ;
305444 }
306445
446+ /**
447+ * Get the log of all spoken phrases for this screen reader instance.
448+ *
449+ * @returns {Promise<string[]> } The spoken phrase log.
450+ */
307451 async spokenPhraseLog ( ) {
308452 this . #checkContainer( ) ;
309453
310454 return this . #spokenPhraseLog;
311455 }
312456
457+ /**
458+ * Get the log of all visited item text for this screen reader instance.
459+ *
460+ * @returns {Promise<string[]> } The item text log.
461+ */
313462 async itemTextLog ( ) {
314463 this . #checkContainer( ) ;
315464
0 commit comments