keyborg 1.0.0 → 1.1.0-alpha.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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Keyborg ⌨️🤖
2
+
3
+ Keyborg is a library that tracks the state of current keyboard input on a web page through focus events.
4
+
5
+ **It does not do anything invasive to the DOM** but provides an event subscription system that allows users to choose how they want to react to changes in focus.
6
+
7
+ ## Getting started
8
+
9
+ ### Installation
10
+
11
+ ```bash
12
+ # NPM
13
+ npm install --save keyborg
14
+ # Yarn
15
+ yarn add keyborg
16
+ ```
17
+
18
+ ### Usage
19
+
20
+ ```js
21
+ import { createKeyborg } from "keyborg";
22
+
23
+ // initializes keyborg on the current window
24
+ const keyborg = createKeyborg(window);
25
+
26
+ // This is called every time the keyboard input state changes
27
+ const handler = (isUsingKeyboard) => {
28
+ if (isUsingKeyboard) {
29
+ document.body.setAttribute("data-is-keyboard", "true");
30
+ } else {
31
+ document.body.removeAttribute("data-is-keyboard");
32
+ }
33
+ };
34
+
35
+ keyborg.subscribe(handler);
36
+ keyborg.unsubscribe(handler);
37
+ ```
38
+
39
+ ## Contributing
40
+
41
+ Pretty simple currently, you only need to know about theese commands
42
+
43
+ - `npm install` - install dependencies
44
+ - `npm run build` - builds the library
45
+ - `npm run format:fix` - runs prettier to format code
46
+ - `npm run lint:fix` - runs eslint and fixes issues
47
+
48
+ This project welcomes contributions and suggestions. Most contributions require you to agree to a
49
+ Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
50
+ the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
51
+
52
+ When you submit a pull request, a CLA bot will automatically determine whether you need to provide
53
+ a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
54
+ provided by the bot. You will only need to do this once across all repos using our CLA.
55
+
56
+ This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
57
+ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
58
+ contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
59
+
60
+ ## Trademarks
61
+
62
+ This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
63
+ trademarks or logos is subject to and must follow
64
+ [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
65
+ Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
66
+ Any use of third-party trademarks or logos are subject to those third-party's policies.
package/dist/Keyborg.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { Disposable } from './WeakRefInstance';
5
+ import { Disposable } from "./WeakRefInstance";
6
6
  interface WindowWithKeyborg extends Window {
7
7
  __keyborg?: {
8
8
  core: KeyborgCore;
@@ -14,7 +14,7 @@ export interface Disposable {
14
14
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef}
15
15
  * @internal
16
16
  */
17
- export declare class WeakRefInstance<T extends Disposable | {}> {
17
+ export declare class WeakRefInstance<T extends Disposable | object> {
18
18
  private _weakRef?;
19
19
  private _instance?;
20
20
  constructor(instance: T);
package/dist/index.d.ts CHANGED
@@ -2,5 +2,6 @@
2
2
  * Copyright (c) Microsoft Corporation. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- export { Keyborg, createKeyborg, disposeKeyborg } from './Keyborg';
6
- export { getLastFocusedProgrammatically, nativeFocus, KEYBORG_FOCUSIN, KeyborgFocusInEvent, KeyborgFocusInEventDetails } from './FocusEvent';
5
+ export { Keyborg, createKeyborg, disposeKeyborg } from "./Keyborg";
6
+ export { getLastFocusedProgrammatically, nativeFocus, KEYBORG_FOCUSIN, KeyborgFocusInEvent, KeyborgFocusInEventDetails, } from "./FocusEvent";
7
+ export declare const version: string;
package/dist/index.js CHANGED
@@ -1,8 +1,501 @@
1
+ 'use strict';
1
2
 
2
- 'use strict'
3
+ Object.defineProperty(exports, '__esModule', { value: true });
3
4
 
4
- if (process.env.NODE_ENV === 'production') {
5
- module.exports = require('./keyborg.cjs.production.min.js')
6
- } else {
7
- module.exports = require('./keyborg.cjs.development.js')
5
+ /*!
6
+ * Copyright (c) Microsoft Corporation. All rights reserved.
7
+ * Licensed under the MIT License.
8
+ */
9
+ // IE11 compat, checks if WeakRef is supported
10
+ const _canUseWeakRef = typeof WeakRef !== "undefined";
11
+ /**
12
+ * WeakRef wrapper around a HTMLElement that also supports IE11
13
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef}
14
+ * @internal
15
+ */
16
+
17
+ class WeakRefInstance {
18
+ constructor(instance) {
19
+ if (_canUseWeakRef && typeof instance === "object") {
20
+ this._weakRef = new WeakRef(instance);
21
+ } else {
22
+ this._instance = instance;
23
+ }
24
+ }
25
+ /**
26
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef/deref}
27
+ */
28
+
29
+
30
+ deref() {
31
+ var _a, _b, _c;
32
+
33
+ let instance;
34
+
35
+ if (this._weakRef) {
36
+ instance = (_a = this._weakRef) === null || _a === void 0 ? void 0 : _a.deref();
37
+
38
+ if (!instance) {
39
+ delete this._weakRef;
40
+ }
41
+ } else {
42
+ instance = this._instance;
43
+
44
+ if ((_c = (_b = instance) === null || _b === void 0 ? void 0 : _b.isDisposed) === null || _c === void 0 ? void 0 : _c.call(_b)) {
45
+ delete this._instance;
46
+ }
47
+ }
48
+
49
+ return instance;
50
+ }
51
+
52
+ }
53
+
54
+ /*!
55
+ * Copyright (c) Microsoft Corporation. All rights reserved.
56
+ * Licensed under the MIT License.
57
+ */
58
+ const KEYBORG_FOCUSIN = "keyborg:focusin";
59
+
60
+ function canOverrideNativeFocus(win) {
61
+ const HTMLElement = win.HTMLElement;
62
+ const origFocus = HTMLElement.prototype.focus;
63
+ let isCustomFocusCalled = false;
64
+
65
+ HTMLElement.prototype.focus = function focus() {
66
+ isCustomFocusCalled = true;
67
+ };
68
+
69
+ const btn = win.document.createElement("button");
70
+ btn.focus();
71
+ HTMLElement.prototype.focus = origFocus;
72
+ return isCustomFocusCalled;
73
+ }
74
+
75
+ let _canOverrideNativeFocus = false;
76
+ /**
77
+ * Guarantees that the native `focus` will be used
78
+ */
79
+
80
+ function nativeFocus(element) {
81
+ const focus = element.focus;
82
+
83
+ if (focus.__keyborgNativeFocus) {
84
+ focus.__keyborgNativeFocus.call(element);
85
+ } else {
86
+ element.focus();
87
+ }
88
+ }
89
+ /**
90
+ * Overrides the native `focus` and setups the keyborg focus event
91
+ */
92
+
93
+ function setupFocusEvent(win) {
94
+ const kwin = win;
95
+
96
+ if (!_canOverrideNativeFocus) {
97
+ _canOverrideNativeFocus = canOverrideNativeFocus(kwin);
98
+ }
99
+
100
+ const origFocus = kwin.HTMLElement.prototype.focus;
101
+
102
+ if (origFocus.__keyborgNativeFocus) {
103
+ // Already set up.
104
+ return;
105
+ }
106
+
107
+ kwin.HTMLElement.prototype.focus = focus;
108
+ const data = kwin.__keyborgData = {
109
+ focusInHandler: e => {
110
+ var _a;
111
+
112
+ const target = e.target;
113
+
114
+ if (!target) {
115
+ return;
116
+ }
117
+
118
+ const event = document.createEvent("HTMLEvents");
119
+ event.initEvent(KEYBORG_FOCUSIN, true, true);
120
+ const details = {
121
+ relatedTarget: e.relatedTarget || undefined
122
+ };
123
+
124
+ if (_canOverrideNativeFocus || data.lastFocusedProgrammatically) {
125
+ details.isFocusedProgrammatically = target === ((_a = data.lastFocusedProgrammatically) === null || _a === void 0 ? void 0 : _a.deref());
126
+ data.lastFocusedProgrammatically = undefined;
127
+ }
128
+
129
+ event.details = details;
130
+ target.dispatchEvent(event);
131
+ }
132
+ };
133
+ kwin.document.addEventListener("focusin", kwin.__keyborgData.focusInHandler, true);
134
+
135
+ function focus() {
136
+ const keyborgNativeFocusEvent = kwin.__keyborgData;
137
+
138
+ if (keyborgNativeFocusEvent) {
139
+ keyborgNativeFocusEvent.lastFocusedProgrammatically = new WeakRefInstance(this);
140
+ } // eslint-disable-next-line prefer-rest-params
141
+
142
+
143
+ return origFocus.apply(this, arguments);
144
+ }
145
+
146
+ focus.__keyborgNativeFocus = origFocus;
147
+ }
148
+ /**
149
+ * Removes keyborg event listeners and custom focus override
150
+ * @param win The window that stores keyborg focus events
151
+ */
152
+
153
+ function disposeFocusEvent(win) {
154
+ const kwin = win;
155
+ const proto = kwin.HTMLElement.prototype;
156
+ const origFocus = proto.focus.__keyborgNativeFocus;
157
+ const keyborgNativeFocusEvent = kwin.__keyborgData;
158
+
159
+ if (keyborgNativeFocusEvent) {
160
+ kwin.document.removeEventListener("focusin", keyborgNativeFocusEvent.focusInHandler, true);
161
+ delete kwin.__keyborgData;
162
+ }
163
+
164
+ if (origFocus) {
165
+ proto.focus = origFocus;
166
+ }
167
+ }
168
+ /**
169
+ * @param win The window that stores keyborg focus events
170
+ * @returns The last element focused with element.focus()
171
+ */
172
+
173
+ function getLastFocusedProgrammatically(win) {
174
+ var _a;
175
+
176
+ const keyborgNativeFocusEvent = win.__keyborgData;
177
+ return keyborgNativeFocusEvent ? ((_a = keyborgNativeFocusEvent.lastFocusedProgrammatically) === null || _a === void 0 ? void 0 : _a.deref()) || null : undefined;
178
+ }
179
+
180
+ /*!
181
+ * Copyright (c) Microsoft Corporation. All rights reserved.
182
+ * Licensed under the MIT License.
183
+ */
184
+ const KeyTab = 9;
185
+ const KeyEsc = 27;
186
+ const _dismissTimeout = 500; // When Esc is pressed and the focused is not moved
187
+ // during _dismissTimeout time, dismiss the keyboard
188
+ // navigation mode.
189
+
190
+ let _lastId = 0;
191
+ /**
192
+ * Source of truth for all the keyborg core instances and the current keyboard navigation state
193
+ */
194
+
195
+ class KeyborgState {
196
+ constructor() {
197
+ this.__keyborgCoreRefs = {};
198
+ this._isNavigatingWithKeyboard = false;
199
+ }
200
+
201
+ add(keyborg) {
202
+ const id = keyborg.id;
203
+
204
+ if (!(id in this.__keyborgCoreRefs)) {
205
+ this.__keyborgCoreRefs[id] = new WeakRefInstance(keyborg);
206
+ }
207
+ }
208
+
209
+ remove(id) {
210
+ delete this.__keyborgCoreRefs[id];
211
+
212
+ if (Object.keys(this.__keyborgCoreRefs).length === 0) {
213
+ this._isNavigatingWithKeyboard = false;
214
+ }
215
+ }
216
+
217
+ setVal(isNavigatingWithKeyboard) {
218
+ if (this._isNavigatingWithKeyboard === isNavigatingWithKeyboard) {
219
+ return;
220
+ }
221
+
222
+ this._isNavigatingWithKeyboard = isNavigatingWithKeyboard;
223
+
224
+ for (const id of Object.keys(this.__keyborgCoreRefs)) {
225
+ const ref = this.__keyborgCoreRefs[id];
226
+ const keyborg = ref.deref();
227
+
228
+ if (keyborg) {
229
+ keyborg.update(isNavigatingWithKeyboard);
230
+ } else {
231
+ this.remove(id);
232
+ }
233
+ }
234
+ }
235
+
236
+ getVal() {
237
+ return this._isNavigatingWithKeyboard;
238
+ }
239
+
240
+ }
241
+
242
+ const _state = /*#__PURE__*/new KeyborgState();
243
+ /**
244
+ * Manages a collection of Keyborg instances in a window/document and updates keyborg state
245
+ */
246
+
247
+
248
+ class KeyborgCore {
249
+ constructor(win) {
250
+ this._isMouseUsed = false;
251
+
252
+ this._onFocusIn = e => {
253
+ if (this._isMouseUsed) {
254
+ this._isMouseUsed = false;
255
+ return;
256
+ }
257
+
258
+ if (_state.getVal()) {
259
+ return;
260
+ }
261
+
262
+ const details = e.details;
263
+
264
+ if (!details.relatedTarget) {
265
+ return;
266
+ }
267
+
268
+ if (details.isFocusedProgrammatically || details.isFocusedProgrammatically === undefined) {
269
+ // The element is focused programmatically, or the programmatic focus detection
270
+ // is not working.
271
+ return;
272
+ }
273
+
274
+ _state.setVal(true);
275
+ };
276
+
277
+ this._onMouseDown = e => {
278
+ if (e.buttons === 0 || e.clientX === 0 && e.clientY === 0 && e.screenX === 0 && e.screenY === 0) {
279
+ // This is most likely an event triggered by the screen reader to perform
280
+ // an action on an element, do not dismiss the keyboard navigation mode.
281
+ return;
282
+ }
283
+
284
+ this._isMouseUsed = true;
285
+
286
+ _state.setVal(false);
287
+ };
288
+
289
+ this._onKeyDown = e => {
290
+ const isNavigatingWithKeyboard = _state.getVal();
291
+
292
+ if (!isNavigatingWithKeyboard && e.keyCode === KeyTab) {
293
+ _state.setVal(true);
294
+ } else if (isNavigatingWithKeyboard && e.keyCode === KeyEsc) {
295
+ this._scheduleDismiss();
296
+ }
297
+ };
298
+
299
+ this.id = "c" + ++_lastId;
300
+ this._win = win;
301
+ const doc = win.document;
302
+ doc.addEventListener(KEYBORG_FOCUSIN, this._onFocusIn, true); // Capture!
303
+
304
+ doc.addEventListener("mousedown", this._onMouseDown, true); // Capture!
305
+
306
+ win.addEventListener("keydown", this._onKeyDown, true); // Capture!
307
+
308
+ setupFocusEvent(win);
309
+
310
+ _state.add(this);
311
+ }
312
+
313
+ dispose() {
314
+ const win = this._win;
315
+
316
+ if (win) {
317
+ if (this._dismissTimer) {
318
+ win.clearTimeout(this._dismissTimer);
319
+ this._dismissTimer = undefined;
320
+ }
321
+
322
+ disposeFocusEvent(win);
323
+ const doc = win.document;
324
+ doc.removeEventListener(KEYBORG_FOCUSIN, this._onFocusIn, true); // Capture!
325
+
326
+ doc.removeEventListener("mousedown", this._onMouseDown, true); // Capture!
327
+
328
+ win.removeEventListener("keydown", this._onKeyDown, true); // Capture!
329
+
330
+ delete this._win;
331
+
332
+ _state.remove(this.id);
333
+ }
334
+ }
335
+
336
+ isDisposed() {
337
+ return !!this._win;
338
+ }
339
+ /**
340
+ * Updates all keyborg instances with the keyboard navigation state
341
+ */
342
+
343
+
344
+ update(isNavigatingWithKeyboard) {
345
+ var _a, _b;
346
+
347
+ const keyborgs = (_b = (_a = this._win) === null || _a === void 0 ? void 0 : _a.__keyborg) === null || _b === void 0 ? void 0 : _b.refs;
348
+
349
+ if (keyborgs) {
350
+ for (const id of Object.keys(keyborgs)) {
351
+ Keyborg.update(keyborgs[id], isNavigatingWithKeyboard);
352
+ }
353
+ }
354
+ }
355
+
356
+ _scheduleDismiss() {
357
+ const win = this._win;
358
+
359
+ if (win) {
360
+ if (this._dismissTimer) {
361
+ win.clearTimeout(this._dismissTimer);
362
+ this._dismissTimer = undefined;
363
+ }
364
+
365
+ const was = win.document.activeElement;
366
+ this._dismissTimer = win.setTimeout(() => {
367
+ this._dismissTimer = undefined;
368
+ const cur = win.document.activeElement;
369
+
370
+ if (was && cur && was === cur) {
371
+ // Esc was pressed, currently focused element hasn't changed.
372
+ // Just dismiss the keyboard navigation mode.
373
+ _state.setVal(false);
374
+ }
375
+ }, _dismissTimeout);
376
+ }
377
+ }
378
+
379
+ }
380
+ /**
381
+ * Used to determine the keyboard navigation state
382
+ */
383
+
384
+
385
+ class Keyborg {
386
+ constructor(win) {
387
+ this._cb = [];
388
+ this._id = "k" + ++_lastId;
389
+ this._win = win;
390
+ const current = win.__keyborg;
391
+
392
+ if (current) {
393
+ this._core = current.core;
394
+ current.refs[this._id] = this;
395
+ } else {
396
+ this._core = new KeyborgCore(win);
397
+ win.__keyborg = {
398
+ core: this._core,
399
+ refs: {
400
+ [this._id]: this
401
+ }
402
+ };
403
+ }
404
+ }
405
+
406
+ static create(win) {
407
+ return new Keyborg(win);
408
+ }
409
+
410
+ static dispose(instance) {
411
+ instance.dispose();
412
+ }
413
+ /**
414
+ * Updates all subscribed callbacks with the keyboard navigation state
415
+ */
416
+
417
+
418
+ static update(instance, isNavigatingWithKeyboard) {
419
+ instance._cb.forEach(callback => callback(isNavigatingWithKeyboard));
420
+ }
421
+
422
+ dispose() {
423
+ var _a;
424
+
425
+ const current = (_a = this._win) === null || _a === void 0 ? void 0 : _a.__keyborg;
426
+
427
+ if (current === null || current === void 0 ? void 0 : current.refs[this._id]) {
428
+ delete current.refs[this._id];
429
+
430
+ if (Object.keys(current.refs).length === 0) {
431
+ current.core.dispose(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
432
+
433
+ delete this._win.__keyborg;
434
+ }
435
+ } else if (process.env.NODE_ENV === 'development') {
436
+ console.error("Keyborg instance " + this._id + " is being disposed incorrectly.");
437
+ }
438
+
439
+ this._cb = [];
440
+ delete this._core;
441
+ delete this._win;
442
+ }
443
+ /**
444
+ * @returns Whether the user is navigating with keyboard
445
+ */
446
+
447
+
448
+ isNavigatingWithKeyboard() {
449
+ return _state.getVal();
450
+ }
451
+ /**
452
+ * @param callback - Called when the keyboard navigation state changes
453
+ */
454
+
455
+
456
+ subscribe(callback) {
457
+ this._cb.push(callback);
458
+ }
459
+ /**
460
+ * @param callback - Registered with subscribe
461
+ */
462
+
463
+
464
+ unsubscribe(callback) {
465
+ const index = this._cb.indexOf(callback);
466
+
467
+ if (index >= 0) {
468
+ this._cb.splice(index, 1);
469
+ }
470
+ }
471
+ /**
472
+ * Manually set the keyboard navigtion state
473
+ */
474
+
475
+
476
+ setVal(isNavigatingWithKeyboard) {
477
+ _state.setVal(isNavigatingWithKeyboard);
478
+ }
479
+
480
+ }
481
+ function createKeyborg(win) {
482
+ return Keyborg.create(win);
8
483
  }
484
+ function disposeKeyborg(instance) {
485
+ Keyborg.dispose(instance);
486
+ }
487
+
488
+ /*!
489
+ * Copyright (c) Microsoft Corporation. All rights reserved.
490
+ * Licensed under the MIT License.
491
+ */
492
+ const version = "1.1.0-alpha.2";
493
+
494
+ exports.KEYBORG_FOCUSIN = KEYBORG_FOCUSIN;
495
+ exports.Keyborg = Keyborg;
496
+ exports.createKeyborg = createKeyborg;
497
+ exports.disposeKeyborg = disposeKeyborg;
498
+ exports.getLastFocusedProgrammatically = getLastFocusedProgrammatically;
499
+ exports.nativeFocus = nativeFocus;
500
+ exports.version = version;
501
+ //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"keyborg.cjs.development.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}