arrowtab 0.0.4 → 0.1.0
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 +129 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/caret.d.ts +5 -0
- package/dist/lib/caret.js +136 -0
- package/dist/lib/caret.js.map +1 -0
- package/dist/lib/changesValueOnArrowKeys.d.ts +3 -0
- package/dist/lib/changesValueOnArrowKeys.js +15 -0
- package/dist/lib/changesValueOnArrowKeys.js.map +1 -0
- package/dist/lib/debugMode.d.ts +5 -5
- package/dist/lib/debugMode.js +28 -32
- package/dist/lib/debugMode.js.map +1 -1
- package/dist/lib/directions.d.ts +13 -0
- package/dist/lib/directions.js +17 -0
- package/dist/lib/directions.js.map +1 -0
- package/dist/lib/dom.d.ts +16 -0
- package/dist/lib/dom.js +62 -0
- package/dist/lib/dom.js.map +1 -0
- package/dist/lib/focusHistory.d.ts +11 -0
- package/dist/lib/focusHistory.js +72 -0
- package/dist/lib/focusHistory.js.map +1 -0
- package/dist/lib/focusTracker.d.ts +12 -0
- package/dist/lib/focusTracker.js +43 -0
- package/dist/lib/focusTracker.js.map +1 -0
- package/dist/lib/focusable.d.ts +8 -0
- package/dist/lib/focusable.js +135 -0
- package/dist/lib/focusable.js.map +1 -0
- package/dist/lib/geometry.d.ts +35 -0
- package/dist/lib/geometry.js +70 -0
- package/dist/lib/geometry.js.map +1 -0
- package/dist/lib/hasDisabledKeys.d.ts +4 -3
- package/dist/lib/hasDisabledKeys.js +5 -25
- package/dist/lib/hasDisabledKeys.js.map +1 -1
- package/dist/lib/initArrowTab.d.ts +12 -2
- package/dist/lib/initArrowTab.js +133 -70
- package/dist/lib/initArrowTab.js.map +1 -1
- package/dist/lib/initArrowTab.test.d.ts +1 -0
- package/dist/lib/initArrowTab.test.js +689 -0
- package/dist/lib/initArrowTab.test.js.map +1 -0
- package/dist/lib/navigateEvent.d.ts +16 -0
- package/dist/lib/navigateEvent.js +9 -0
- package/dist/lib/navigateEvent.js.map +1 -0
- package/dist/lib/rankCandidates.d.ts +11 -0
- package/dist/lib/rankCandidates.js +31 -0
- package/dist/lib/rankCandidates.js.map +1 -0
- package/dist/lib/scope.d.ts +11 -0
- package/dist/lib/scope.js +60 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/lib/strategies.d.ts +21 -10
- package/dist/lib/strategies.js +63 -106
- package/dist/lib/strategies.js.map +1 -1
- package/dist/lib/strategies.test.d.ts +1 -0
- package/dist/lib/strategies.test.js +88 -0
- package/dist/lib/strategies.test.js.map +1 -0
- package/dist/lib/widgets.d.ts +5 -0
- package/dist/lib/widgets.js +50 -0
- package/dist/lib/widgets.js.map +1 -0
- package/dist/lib/wrap.d.ts +12 -0
- package/dist/lib/wrap.js +21 -0
- package/dist/lib/wrap.js.map +1 -0
- package/package.json +2 -1
- package/dist/lib/distances.d.ts +0 -13
- package/dist/lib/distances.js +0 -15
- package/dist/lib/distances.js.map +0 -1
- package/dist/lib/getAngle.d.ts +0 -4
- package/dist/lib/getAngle.js +0 -15
- package/dist/lib/getAngle.js.map +0 -1
- package/dist/lib/getFocusable.d.ts +0 -1
- package/dist/lib/getFocusable.js +0 -29
- package/dist/lib/getFocusable.js.map +0 -1
- package/dist/lib/getXyDistance.d.ts +0 -7
- package/dist/lib/getXyDistance.js +0 -12
- package/dist/lib/getXyDistance.js.map +0 -1
- package/dist/lib/hasTextSelection.d.ts +0 -4
- package/dist/lib/hasTextSelection.js +0 -53
- package/dist/lib/hasTextSelection.js.map +0 -1
- package/dist/lib/positions.d.ts +0 -8
- package/dist/lib/positions.js +0 -16
- package/dist/lib/positions.js.map +0 -1
- package/dist/lib/preventNativeArrowKeyPresses.d.ts +0 -4
- package/dist/lib/preventNativeArrowKeyPresses.js +0 -24
- package/dist/lib/preventNativeArrowKeyPresses.js.map +0 -1
- package/dist/lib/sumBy.d.ts +0 -1
- package/dist/lib/sumBy.js +0 -2
- package/dist/lib/sumBy.js.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,12 @@ ArrowTab slightly modifies the behavior of some input elements to enhance keyboa
|
|
|
14
14
|
|
|
15
15
|
To revert to the native behavior, press `Shift` + `Arrow Key`.
|
|
16
16
|
|
|
17
|
+
Arrow keys combined with `Alt`, `Ctrl` or `Meta` / `Cmd` are left to the browser and OS (e.g. word jumps or history navigation).
|
|
18
|
+
|
|
19
|
+
If there is nothing to navigate to, the arrow key keeps its native behavior, so the page can still scroll. Only fields that would change their value (e.g. number inputs, selects, radio buttons) never react to arrow keys.
|
|
20
|
+
|
|
21
|
+
Text fields keep the arrow keys as long as the caret can move: left / right leave an input once the caret is at the start / end, up / down leave a textarea or `contenteditable` once the caret is in the first / last line (soft wrapped lines included).
|
|
22
|
+
|
|
17
23
|
## Installation
|
|
18
24
|
|
|
19
25
|
pnpm:
|
|
@@ -80,7 +86,10 @@ cleanup()
|
|
|
80
86
|
You can pass options to the `initArrowTab` function. For example:
|
|
81
87
|
|
|
82
88
|
```ts
|
|
83
|
-
initArrowTab({
|
|
89
|
+
initArrowTab({
|
|
90
|
+
debug: true,
|
|
91
|
+
autoDetectHistory: true,
|
|
92
|
+
})
|
|
84
93
|
```
|
|
85
94
|
|
|
86
95
|
### debug
|
|
@@ -103,6 +112,109 @@ If you click on a focusable element, debug information will be logged to the con
|
|
|
103
112
|
|
|
104
113
|
To deactivate the visual debug mode, press `Esc`.
|
|
105
114
|
|
|
115
|
+
### autoDetectHistory
|
|
116
|
+
|
|
117
|
+
default: `false`
|
|
118
|
+
|
|
119
|
+
If `true`, ArrowTab detects dialogs (`role="dialog"`, `role="alertdialog"`, `<dialog>`) and refocuses the element that was focused before the dialog opened, once the dialog closes. Dialogs rendered into portal wrappers and nested dialogs are supported.
|
|
120
|
+
|
|
121
|
+
ArrowTab only steps in if focus got lost. Dialog libraries that restore focus themselves are left alone.
|
|
122
|
+
|
|
123
|
+
### strategy
|
|
124
|
+
|
|
125
|
+
default: `'spatial'`
|
|
126
|
+
|
|
127
|
+
How the next element is chosen:
|
|
128
|
+
|
|
129
|
+
- `'spatial'`: Compares the edges of the elements, like Android's focus navigation. Elements in the same row / column win, elements of different sizes and slightly misaligned elements are handled well.
|
|
130
|
+
- `'grid'`: Compares the centers of the elements. Up / down jump to the nearest row and then to the nearest column, left / right stay in the current row.
|
|
131
|
+
- A custom function. It gets the rect of the focused element, all candidates and the direction and returns the candidates sorted by preference:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { initArrowTab, spatial, type Strategy } from 'arrowtab'
|
|
135
|
+
|
|
136
|
+
const strategy: Strategy = ({ origin, candidates, direction }) =>
|
|
137
|
+
spatial({
|
|
138
|
+
origin,
|
|
139
|
+
direction,
|
|
140
|
+
candidates: candidates.filter(
|
|
141
|
+
({ element }) => !element.matches('.skip-me'),
|
|
142
|
+
),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
initArrowTab({ strategy })
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### root
|
|
149
|
+
|
|
150
|
+
default: the whole document
|
|
151
|
+
|
|
152
|
+
Only navigate inside this element. Arrow keys outside of it are ignored.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
initArrowTab({ root: document.querySelector('#app') })
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Calling `initArrowTab` twice for the same root logs a warning. Call `cleanup()` of the first instance before initializing it again.
|
|
159
|
+
|
|
160
|
+
## Events
|
|
161
|
+
|
|
162
|
+
Before focus moves, ArrowTab dispatches a cancelable `arrowtab:navigate` event on the focused element (or the document if nothing is focused). It bubbles and crosses shadow roots. Call `preventDefault()` to stop the navigation:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { navigateEventName } from 'arrowtab'
|
|
166
|
+
|
|
167
|
+
document.addEventListener(navigateEventName, (event) => {
|
|
168
|
+
const { from, to, direction } = event.detail
|
|
169
|
+
if (to.matches('.locked')) event.preventDefault()
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Special Cases
|
|
174
|
+
|
|
175
|
+
### Going back
|
|
176
|
+
|
|
177
|
+
Pressing the opposite arrow key returns to the element focus came from, e.g. down from a narrow button to a wide one and up again lands on the same narrow button.
|
|
178
|
+
|
|
179
|
+
### Hidden elements and scrolling
|
|
180
|
+
|
|
181
|
+
Elements that are completely hidden by an ancestor with `overflow: hidden` or `overflow: clip` (e.g. inactive carousel slides) are skipped. Elements in scroll containers stay reachable.
|
|
182
|
+
|
|
183
|
+
Focusing an element scrolls as little as possible. If you have a sticky header, set [`scroll-padding-top`](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-padding-top) on the scroll container so focused elements don't end up behind it.
|
|
184
|
+
|
|
185
|
+
### Shadow DOM
|
|
186
|
+
|
|
187
|
+
Focusable elements inside open shadow roots are found, so ArrowTab works with web components. `data-arrowtab` attributes on the host apply to its shadow content. `autoDetectHistory` doesn't detect dialogs inside shadow roots.
|
|
188
|
+
|
|
189
|
+
### Modals and scopes
|
|
190
|
+
|
|
191
|
+
If focus is inside a dialog (`<dialog>`, `role="dialog"`, `role="alertdialog"` or `aria-modal="true"`), arrow keys keep it inside. Non-modal dialogs can still be left with `Tab`.
|
|
192
|
+
|
|
193
|
+
If focus is behind an open modal (`aria-modal="true"` or a `<dialog>` opened with `showModal()`), the next arrow key moves it into the modal.
|
|
194
|
+
|
|
195
|
+
Use `data-arrowtab="scope"` to keep navigation inside any other container, or `data-arrowtab="wrap"` to also jump to the opposite edge at the end.
|
|
196
|
+
|
|
197
|
+
### ARIA widgets
|
|
198
|
+
|
|
199
|
+
Composite widgets implement their own arrow key navigation, so ArrowTab leaves the keys to them:
|
|
200
|
+
|
|
201
|
+
| Role | Keys left to the widget |
|
|
202
|
+
| ------------------------------------------------ | ----------------------- |
|
|
203
|
+
| `grid`, `treegrid`, `radiogroup` | all |
|
|
204
|
+
| `listbox`, `menu`, `tree` | up / down |
|
|
205
|
+
| `menubar`, `tablist`, `toolbar` | left / right |
|
|
206
|
+
| open `combobox` / popup (`aria-expanded="true"`) | up / down |
|
|
207
|
+
|
|
208
|
+
`aria-orientation` overrides the default orientation. If a widget doesn't handle arrow keys itself, add `data-arrowtab="enable"` to it.
|
|
209
|
+
|
|
210
|
+
### Lost focus
|
|
211
|
+
|
|
212
|
+
If the focused element is removed or focus is lost otherwise, ArrowTab navigates from the position of the last focused element instead of starting at the top of the page.
|
|
213
|
+
|
|
214
|
+
### Focusable containers
|
|
215
|
+
|
|
216
|
+
If a focusable container (e.g. a card with `tabindex="0"`) is focused, down / right move into it.
|
|
217
|
+
|
|
106
218
|
## data-arrowtab
|
|
107
219
|
|
|
108
220
|
You can control the behavior of ArrowTab by adding a `data-arrowtab` attribute to any element. Separate the keywords with a space. For example: `<div data-arrowtab="disable-left disable-right">`
|
|
@@ -115,6 +227,22 @@ Sometimes your components are listening to arrow keys themselves. In that case,
|
|
|
115
227
|
|
|
116
228
|
If you only want to disable a specific key, then use: `disable-left`, `disable-right`, `disable-up`, `disable-down`.
|
|
117
229
|
|
|
230
|
+
### scope
|
|
231
|
+
|
|
232
|
+
Keeps navigation inside this element while focus is inside it. See [Modals and scopes](#modals-and-scopes).
|
|
233
|
+
|
|
234
|
+
### wrap
|
|
235
|
+
|
|
236
|
+
Like `scope`, but at the edge focus jumps to the opposite side, preferably in the same row / column. Add it to `<body>` to wrap around the whole page.
|
|
237
|
+
|
|
238
|
+
### skip
|
|
239
|
+
|
|
240
|
+
The element (and its children) is skipped by the arrow keys but stays reachable with `Tab`. Useful for e.g. help links next to every field.
|
|
241
|
+
|
|
242
|
+
### enable
|
|
243
|
+
|
|
244
|
+
Turns off the [ARIA widget](#aria-widgets) detection for this element and its children.
|
|
245
|
+
|
|
118
246
|
## Development
|
|
119
247
|
|
|
120
248
|
```sh
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
|
-
export { initArrowTab } from './lib/initArrowTab.js';
|
|
1
|
+
export { initArrowTab, type ArrowTabOptions } from './lib/initArrowTab.js';
|
|
2
|
+
export { grid, spatial, type Candidate, type RankedCandidate, type Strategy, } from './lib/strategies.js';
|
|
3
|
+
export { navigateEventName, type ArrowTabNavigateEvent, } from './lib/navigateEvent.js';
|
|
4
|
+
export type { Direction } from './lib/directions.js';
|
|
5
|
+
export type { Rect } from './lib/geometry.js';
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,MAAM,uBAAuB,CAAA;AAC1E,OAAO,EACL,IAAI,EACJ,OAAO,GAIR,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,iBAAiB,GAElB,MAAM,wBAAwB,CAAA"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { isVertical } from './directions.js';
|
|
2
|
+
const mirroredStyles = [
|
|
3
|
+
'boxSizing',
|
|
4
|
+
'fontFamily',
|
|
5
|
+
'fontSize',
|
|
6
|
+
'fontStyle',
|
|
7
|
+
'fontVariant',
|
|
8
|
+
'fontWeight',
|
|
9
|
+
'fontStretch',
|
|
10
|
+
'letterSpacing',
|
|
11
|
+
'wordSpacing',
|
|
12
|
+
'lineHeight',
|
|
13
|
+
'textTransform',
|
|
14
|
+
'textIndent',
|
|
15
|
+
'tabSize',
|
|
16
|
+
'wordBreak',
|
|
17
|
+
'overflowWrap',
|
|
18
|
+
// e.g. pre for wrap="off"
|
|
19
|
+
'whiteSpace',
|
|
20
|
+
'direction',
|
|
21
|
+
'paddingTop',
|
|
22
|
+
'paddingRight',
|
|
23
|
+
'paddingBottom',
|
|
24
|
+
'paddingLeft',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Whether the caret is in the first / last visual line of a textarea, including soft wrapped lines.
|
|
28
|
+
* Renders the text into a hidden copy of the textarea and compares the line positions of markers.
|
|
29
|
+
*/
|
|
30
|
+
const getCaretLine = ({ textarea, start, end, }) => {
|
|
31
|
+
const style = window.getComputedStyle(textarea);
|
|
32
|
+
const mirror = document.createElement('div');
|
|
33
|
+
for (const property of mirroredStyles) {
|
|
34
|
+
mirror.style[property] = style[property];
|
|
35
|
+
}
|
|
36
|
+
Object.assign(mirror.style, {
|
|
37
|
+
position: 'absolute',
|
|
38
|
+
top: '0',
|
|
39
|
+
left: '-9999px',
|
|
40
|
+
visibility: 'hidden',
|
|
41
|
+
border: '0',
|
|
42
|
+
overflow: 'hidden',
|
|
43
|
+
boxSizing: 'content-box',
|
|
44
|
+
width: `${textarea.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight)}px`,
|
|
45
|
+
});
|
|
46
|
+
const { value } = textarea;
|
|
47
|
+
const markers = [0, start, end, value.length].map(() => document.createElement('span'));
|
|
48
|
+
const [first, caretStart, caretEnd, last] = markers;
|
|
49
|
+
mirror.append(first, value.slice(0, start), caretStart, value.slice(start, end), caretEnd, value.slice(end), last);
|
|
50
|
+
document.body.append(mirror);
|
|
51
|
+
const line = {
|
|
52
|
+
isFirst: caretStart.offsetTop <= first.offsetTop,
|
|
53
|
+
isLast: caretEnd.offsetTop >= last.offsetTop,
|
|
54
|
+
};
|
|
55
|
+
mirror.remove();
|
|
56
|
+
return line;
|
|
57
|
+
};
|
|
58
|
+
const canMoveCaretInField = ({ field, direction, }) => {
|
|
59
|
+
const { selectionStart: start, selectionEnd: end, value } = field;
|
|
60
|
+
// Some input types (e.g. number, email) don't expose the caret position
|
|
61
|
+
if (start === null || end === null)
|
|
62
|
+
return false;
|
|
63
|
+
const hasSelection = start !== end;
|
|
64
|
+
const isTextarea = field instanceof HTMLTextAreaElement;
|
|
65
|
+
switch (direction) {
|
|
66
|
+
case 'left':
|
|
67
|
+
return hasSelection || start > 0;
|
|
68
|
+
case 'right':
|
|
69
|
+
return hasSelection || end < value.length;
|
|
70
|
+
case 'up':
|
|
71
|
+
if (!isTextarea)
|
|
72
|
+
return false;
|
|
73
|
+
if (start > 0 && value.lastIndexOf('\n', start - 1) !== -1)
|
|
74
|
+
return true;
|
|
75
|
+
return !getCaretLine({ textarea: field, start, end }).isFirst;
|
|
76
|
+
case 'down':
|
|
77
|
+
if (!isTextarea)
|
|
78
|
+
return false;
|
|
79
|
+
if (value.indexOf('\n', end) !== -1)
|
|
80
|
+
return true;
|
|
81
|
+
return !getCaretLine({ textarea: field, start, end }).isLast;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
// Collapsed whitespace (e.g. from the HTML source) isn't visible, so the caret can't move over it
|
|
85
|
+
const getRenderedText = ({ element, text, }) => {
|
|
86
|
+
const { whiteSpace } = window.getComputedStyle(element);
|
|
87
|
+
if (['pre', 'pre-wrap', 'break-spaces'].includes(whiteSpace))
|
|
88
|
+
return text;
|
|
89
|
+
if (whiteSpace === 'pre-line')
|
|
90
|
+
return text.replace(/[^\S\n]+/g, '');
|
|
91
|
+
return text.trim();
|
|
92
|
+
};
|
|
93
|
+
const canMoveCaretInContentEditable = ({ element, direction, }) => {
|
|
94
|
+
const selection = window.getSelection();
|
|
95
|
+
if (!selection || selection.rangeCount === 0)
|
|
96
|
+
return false;
|
|
97
|
+
const range = selection.getRangeAt(0);
|
|
98
|
+
const vertical = isVertical({ direction });
|
|
99
|
+
if (!range.collapsed && !vertical)
|
|
100
|
+
return true;
|
|
101
|
+
// Content between the caret and the start / end of the element
|
|
102
|
+
const isBackward = direction === 'left' || direction === 'up';
|
|
103
|
+
const rest = document.createRange();
|
|
104
|
+
rest.selectNodeContents(element);
|
|
105
|
+
if (isBackward) {
|
|
106
|
+
rest.setEnd(range.startContainer, range.startOffset);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
rest.setStart(range.endContainer, range.endOffset);
|
|
110
|
+
}
|
|
111
|
+
const hasText = getRenderedText({ element, text: rest.toString() }).length > 0;
|
|
112
|
+
if (!vertical && hasText)
|
|
113
|
+
return true;
|
|
114
|
+
// Whether the rest of the content has lines besides the caret's line, e.g. empty paragraphs without text.
|
|
115
|
+
// Up / down leave once it hasn't, left / right also need it for content without text.
|
|
116
|
+
const caret = range.cloneRange();
|
|
117
|
+
caret.collapse(isBackward);
|
|
118
|
+
const caretRect = caret.getBoundingClientRect();
|
|
119
|
+
// Without layout information only text counts
|
|
120
|
+
if (caretRect.height === 0)
|
|
121
|
+
return hasText;
|
|
122
|
+
const caretCenter = caretRect.top + caretRect.height / 2;
|
|
123
|
+
return Array.from(rest.getClientRects()).some((rect) => rect.width > 0 && (rect.bottom < caretCenter || rect.top > caretCenter));
|
|
124
|
+
};
|
|
125
|
+
// Whether the arrow key should move the caret inside the active element instead of moving focus
|
|
126
|
+
export const canMoveCaret = ({ activeElement, direction, }) => {
|
|
127
|
+
if (activeElement instanceof HTMLInputElement ||
|
|
128
|
+
activeElement instanceof HTMLTextAreaElement) {
|
|
129
|
+
return canMoveCaretInField({ field: activeElement, direction });
|
|
130
|
+
}
|
|
131
|
+
if (activeElement.isContentEditable) {
|
|
132
|
+
return canMoveCaretInContentEditable({ element: activeElement, direction });
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=caret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caret.js","sourceRoot":"","sources":["../../src/lib/caret.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const inputTypesChangingValue = [
|
|
2
|
+
'number',
|
|
3
|
+
'range',
|
|
4
|
+
'radio',
|
|
5
|
+
'date',
|
|
6
|
+
'time',
|
|
7
|
+
'datetime-local',
|
|
8
|
+
'month',
|
|
9
|
+
'week',
|
|
10
|
+
];
|
|
11
|
+
// Arrow keys change the value of these fields. ArrowTab prevents that even if there is nothing to navigate to.
|
|
12
|
+
export const changesValueOnArrowKeys = ({ element }) => element instanceof HTMLSelectElement ||
|
|
13
|
+
(element instanceof HTMLInputElement &&
|
|
14
|
+
inputTypesChangingValue.includes(element.type));
|
|
15
|
+
//# sourceMappingURL=changesValueOnArrowKeys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesValueOnArrowKeys.js","sourceRoot":"","sources":["../../src/lib/changesValueOnArrowKeys.ts"],"names":[],"mappings":"AAAA,MAAM,uBAAuB,GAAG;IAC9B,QAAQ;IACR,OAAO;IACP,OAAO;IACP,MAAM;IACN,MAAM;IACN,gBAAgB;IAChB,OAAO;IACP,MAAM;CACP,CAAA;AAED,+GAA+G;AAC/G,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,EAAE,OAAO,EAAwB,EAAE,EAAE,CAC3E,OAAO,YAAY,iBAAiB;IACpC,CAAC,OAAO,YAAY,gBAAgB;QAClC,uBAAuB,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA"}
|
package/dist/lib/debugMode.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { Rect } from './geometry.js';
|
|
2
|
+
import type { RankedCandidate } from './strategies.js';
|
|
3
|
+
export declare const activateDebugMode: ({ origin, items, }: {
|
|
4
|
+
origin: Rect | undefined;
|
|
5
|
+
items: RankedCandidate[];
|
|
6
6
|
}) => void;
|
|
7
7
|
export declare const deactivateDebugMode: () => void;
|
|
8
8
|
export declare const isInDebugMode: () => boolean;
|
package/dist/lib/debugMode.js
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
let inDebugMode = false;
|
|
2
|
-
export const activateDebugMode = ({
|
|
2
|
+
export const activateDebugMode = ({ origin, items, }) => {
|
|
3
3
|
inDebugMode = true;
|
|
4
|
-
if (
|
|
4
|
+
if (origin) {
|
|
5
5
|
addDebugElementToDom({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
content: '',
|
|
6
|
+
rect: origin,
|
|
7
|
+
label: '',
|
|
9
8
|
color: 'rgb(33 150 243 / 80%)',
|
|
9
|
+
details: { origin },
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
let withinReachCounter = 0;
|
|
13
|
+
const labeled = items.map((item, index) => {
|
|
14
|
+
if (item.withinReach)
|
|
15
|
+
withinReachCounter++;
|
|
16
|
+
return {
|
|
17
|
+
item,
|
|
18
|
+
label: `${index + 1} ${item.withinReach ? `(${withinReachCounter})` : ''}`,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
// Nearest elements are added last, so they are on top if they overlap
|
|
22
|
+
for (const { item, label } of labeled.reverse()) {
|
|
15
23
|
addDebugElementToDom({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
color:
|
|
24
|
+
rect: item.rect,
|
|
25
|
+
label,
|
|
26
|
+
color: item.withinReach
|
|
19
27
|
? 'rgba(76 175 80 / 80%)'
|
|
20
28
|
: 'rgb(244 67 54 / 80%)',
|
|
29
|
+
details: { item },
|
|
21
30
|
});
|
|
22
31
|
}
|
|
23
32
|
};
|
|
@@ -26,42 +35,29 @@ export const deactivateDebugMode = () => {
|
|
|
26
35
|
div.remove();
|
|
27
36
|
});
|
|
28
37
|
inDebugMode = false;
|
|
29
|
-
return;
|
|
30
38
|
};
|
|
31
39
|
export const isInDebugMode = () => inDebugMode;
|
|
32
|
-
const addDebugElementToDom = (
|
|
33
|
-
if (!(focusable.element instanceof HTMLElement))
|
|
34
|
-
return;
|
|
40
|
+
const addDebugElementToDom = ({ rect, label, color, details, }) => {
|
|
35
41
|
const div = document.createElement('div');
|
|
36
|
-
const rect = focusable.element.getBoundingClientRect();
|
|
37
|
-
const scrollX = document.documentElement.scrollLeft;
|
|
38
|
-
const scrollY = document.documentElement.scrollTop;
|
|
39
42
|
div.style.position = 'absolute';
|
|
40
|
-
div.style.top = `${rect.top
|
|
41
|
-
div.style.left = `${rect.left
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
div.style.width = `${width}px`;
|
|
45
|
-
div.style.height = `${height}px`;
|
|
43
|
+
div.style.top = `${rect.top}px`;
|
|
44
|
+
div.style.left = `${rect.left}px`;
|
|
45
|
+
div.style.width = `${Math.max(rect.right - rect.left, 10)}px`;
|
|
46
|
+
div.style.height = `${Math.max(rect.bottom - rect.top, 10)}px`;
|
|
46
47
|
div.style.color = 'white';
|
|
47
|
-
div.style.backgroundColor =
|
|
48
|
+
div.style.backgroundColor = color;
|
|
48
49
|
div.style.zIndex = '100000';
|
|
49
50
|
div.style.display = 'flex';
|
|
50
51
|
div.style.alignItems = 'center';
|
|
51
52
|
div.style.justifyContent = 'center';
|
|
52
53
|
div.style.cursor = 'pointer';
|
|
53
54
|
div.style.borderRadius = '4px';
|
|
54
|
-
div.
|
|
55
|
+
div.textContent = label;
|
|
55
56
|
div.dataset.arrowtab = 'debug';
|
|
56
|
-
// Move the click event to the focusable element
|
|
57
57
|
div.addEventListener('click', (e) => {
|
|
58
58
|
e.preventDefault();
|
|
59
59
|
e.stopPropagation();
|
|
60
|
-
console.log({
|
|
61
|
-
focusable,
|
|
62
|
-
rect,
|
|
63
|
-
debugElement: div,
|
|
64
|
-
});
|
|
60
|
+
console.log({ ...details, rect, debugElement: div });
|
|
65
61
|
});
|
|
66
62
|
document.body.appendChild(div);
|
|
67
63
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debugMode.js","sourceRoot":"","sources":["../../src/lib/debugMode.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"debugMode.js","sourceRoot":"","sources":["../../src/lib/debugMode.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Direction = 'up' | 'down' | 'left' | 'right';
|
|
2
|
+
export declare const getDirection: ({ key }: {
|
|
3
|
+
key: string;
|
|
4
|
+
}) => Direction | undefined;
|
|
5
|
+
export declare const isVertical: ({ direction }: {
|
|
6
|
+
direction: Direction;
|
|
7
|
+
}) => boolean;
|
|
8
|
+
export declare const isForward: ({ direction }: {
|
|
9
|
+
direction: Direction;
|
|
10
|
+
}) => boolean;
|
|
11
|
+
export declare const getOpposite: ({ direction }: {
|
|
12
|
+
direction: Direction;
|
|
13
|
+
}) => Direction;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const directionsByKey = {
|
|
2
|
+
ArrowUp: 'up',
|
|
3
|
+
ArrowDown: 'down',
|
|
4
|
+
ArrowLeft: 'left',
|
|
5
|
+
ArrowRight: 'right',
|
|
6
|
+
};
|
|
7
|
+
export const getDirection = ({ key }) => directionsByKey[key];
|
|
8
|
+
export const isVertical = ({ direction }) => direction === 'up' || direction === 'down';
|
|
9
|
+
export const isForward = ({ direction }) => direction === 'down' || direction === 'right';
|
|
10
|
+
const opposites = {
|
|
11
|
+
up: 'down',
|
|
12
|
+
down: 'up',
|
|
13
|
+
left: 'right',
|
|
14
|
+
right: 'left',
|
|
15
|
+
};
|
|
16
|
+
export const getOpposite = ({ direction }) => opposites[direction];
|
|
17
|
+
//# sourceMappingURL=directions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directions.js","sourceRoot":"","sources":["../../src/lib/directions.ts"],"names":[],"mappings":"AAEA,MAAM,eAAe,GAA8B;IACjD,OAAO,EAAE,IAAI;IACb,SAAS,EAAE,MAAM;IACjB,SAAS,EAAE,MAAM;IACjB,UAAU,EAAE,OAAO;CACpB,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAAE,GAAG,EAAmB,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAA;AAE9E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAE,SAAS,EAA4B,EAAE,EAAE,CACpE,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,MAAM,CAAA;AAE5C,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EAAE,SAAS,EAA4B,EAAE,EAAE,CACnE,SAAS,KAAK,MAAM,IAAI,SAAS,KAAK,OAAO,CAAA;AAE/C,MAAM,SAAS,GAAiC;IAC9C,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,MAAM;CACd,CAAA;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAAE,SAAS,EAA4B,EAAE,EAAE,CACrE,SAAS,CAAC,SAAS,CAAC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const getComposedParent: ({ element }: {
|
|
2
|
+
element: Element;
|
|
3
|
+
}) => Element | null;
|
|
4
|
+
export declare const closestComposed: ({ element, selector, }: {
|
|
5
|
+
element: Element;
|
|
6
|
+
selector: string;
|
|
7
|
+
}) => Element | null;
|
|
8
|
+
export declare const containsComposed: ({ container, element, }: {
|
|
9
|
+
container: Node;
|
|
10
|
+
element: Element;
|
|
11
|
+
}) => boolean;
|
|
12
|
+
export declare const getDeepActiveElement: () => Element | null;
|
|
13
|
+
export declare const querySelectorAllDeep: ({ root, selector, }: {
|
|
14
|
+
root: ParentNode;
|
|
15
|
+
selector: string;
|
|
16
|
+
}) => Element[];
|
package/dist/lib/dom.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Helpers that walk the flat tree (open shadow roots and slots), so ArrowTab works with web components
|
|
2
|
+
// Parent in the flat tree: the slot for slotted elements, the host for elements at the top of a shadow root
|
|
3
|
+
export const getComposedParent = ({ element }) => {
|
|
4
|
+
if (element.assignedSlot)
|
|
5
|
+
return element.assignedSlot;
|
|
6
|
+
if (element.parentElement)
|
|
7
|
+
return element.parentElement;
|
|
8
|
+
const parent = element.parentNode;
|
|
9
|
+
return parent instanceof ShadowRoot ? parent.host : null;
|
|
10
|
+
};
|
|
11
|
+
export const closestComposed = ({ element, selector, }) => {
|
|
12
|
+
let current = element;
|
|
13
|
+
while (current) {
|
|
14
|
+
if (current.matches(selector))
|
|
15
|
+
return current;
|
|
16
|
+
current = getComposedParent({ element: current });
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
export const containsComposed = ({ container, element, }) => {
|
|
21
|
+
let current = element;
|
|
22
|
+
while (current) {
|
|
23
|
+
if (current === container)
|
|
24
|
+
return true;
|
|
25
|
+
current = getComposedParent({ element: current });
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
// document.activeElement is the shadow host if focus is inside a shadow root
|
|
30
|
+
export const getDeepActiveElement = () => {
|
|
31
|
+
let active = document.activeElement;
|
|
32
|
+
while (active?.shadowRoot?.activeElement) {
|
|
33
|
+
active = active.shadowRoot.activeElement;
|
|
34
|
+
}
|
|
35
|
+
return active;
|
|
36
|
+
};
|
|
37
|
+
// Also searches open shadow roots and the elements assigned to their slots
|
|
38
|
+
export const querySelectorAllDeep = ({ root, selector, }) => {
|
|
39
|
+
const found = new Set();
|
|
40
|
+
const visit = ({ node }) => {
|
|
41
|
+
// The node itself can be a host, e.g. the root or a component assigned to a slot
|
|
42
|
+
if (node instanceof Element && node.shadowRoot) {
|
|
43
|
+
visit({ node: node.shadowRoot });
|
|
44
|
+
}
|
|
45
|
+
for (const element of node.querySelectorAll(selector))
|
|
46
|
+
found.add(element);
|
|
47
|
+
for (const slot of node.querySelectorAll('slot')) {
|
|
48
|
+
for (const assigned of slot.assignedElements({ flatten: true })) {
|
|
49
|
+
if (assigned.matches(selector))
|
|
50
|
+
found.add(assigned);
|
|
51
|
+
visit({ node: assigned });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const element of node.querySelectorAll('*')) {
|
|
55
|
+
if (element.shadowRoot)
|
|
56
|
+
visit({ node: element.shadowRoot });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
visit({ node: root });
|
|
60
|
+
return [...found];
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=dom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom.js","sourceRoot":"","sources":["../../src/lib/dom.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FocusTracker } from './focusTracker.js';
|
|
2
|
+
/**
|
|
3
|
+
* Refocuses the element that was focused before a dialog opened, once the dialog closes.
|
|
4
|
+
* Only kicks in if focus got lost, so dialogs restoring focus themselves are left alone.
|
|
5
|
+
*/
|
|
6
|
+
export declare const startAutoDetectHistory: ({ active, focusTracker, }: {
|
|
7
|
+
active: boolean;
|
|
8
|
+
focusTracker: FocusTracker;
|
|
9
|
+
}) => {
|
|
10
|
+
cleanup: () => void;
|
|
11
|
+
};
|