arrowtab 0.0.1 → 0.0.3

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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -3
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.test.d.ts +1 -0
  7. package/dist/index.test.js +6 -0
  8. package/dist/index.test.js.map +1 -0
  9. package/dist/lib/debugMode.d.ts +8 -0
  10. package/dist/lib/debugMode.js +68 -0
  11. package/dist/lib/debugMode.js.map +1 -0
  12. package/dist/lib/distances.d.ts +13 -0
  13. package/dist/lib/distances.js +15 -0
  14. package/dist/lib/distances.js.map +1 -0
  15. package/dist/lib/getAngle.d.ts +4 -0
  16. package/dist/lib/getAngle.js +15 -0
  17. package/dist/lib/getAngle.js.map +1 -0
  18. package/dist/lib/getFocusable.js +29 -0
  19. package/dist/lib/getFocusable.js.map +1 -0
  20. package/dist/lib/getXyDistance.d.ts +7 -0
  21. package/dist/lib/getXyDistance.js +12 -0
  22. package/dist/lib/getXyDistance.js.map +1 -0
  23. package/dist/lib/hasDisabledKeys.d.ts +4 -0
  24. package/dist/lib/hasDisabledKeys.js +26 -0
  25. package/dist/lib/hasDisabledKeys.js.map +1 -0
  26. package/dist/lib/hasTextSelection.d.ts +4 -0
  27. package/dist/lib/hasTextSelection.js +53 -0
  28. package/dist/lib/hasTextSelection.js.map +1 -0
  29. package/dist/lib/initArrowTab.d.ts +5 -0
  30. package/dist/lib/initArrowTab.js +102 -0
  31. package/dist/lib/initArrowTab.js.map +1 -0
  32. package/dist/lib/positions.js +16 -0
  33. package/dist/lib/positions.js.map +1 -0
  34. package/dist/lib/preventNativeArrowKeyPresses.d.ts +4 -0
  35. package/dist/lib/preventNativeArrowKeyPresses.js +24 -0
  36. package/dist/lib/preventNativeArrowKeyPresses.js.map +1 -0
  37. package/dist/lib/strategies.d.ts +9 -23
  38. package/dist/lib/strategies.js +111 -39
  39. package/dist/lib/strategies.js.map +1 -1
  40. package/dist/lib/{sumBy.mjs → sumBy.js} +1 -1
  41. package/dist/lib/sumBy.js.map +1 -0
  42. package/package.json +35 -9
  43. package/.prettierrc +0 -5
  44. package/.vscode/settings.json +0 -4
  45. package/dist/index.d.mts +0 -1
  46. package/dist/index.mjs +0 -177
  47. package/dist/index.mjs.map +0 -1
  48. package/dist/lib/getFocusable.mjs +0 -15
  49. package/dist/lib/getFocusable.mjs.map +0 -1
  50. package/dist/lib/positions.mjs +0 -12
  51. package/dist/lib/positions.mjs.map +0 -1
  52. package/dist/lib/sumBy.mjs.map +0 -1
  53. package/index.html +0 -142
  54. package/src/index.mts +0 -228
  55. package/src/lib/getFocusable.mts +0 -13
  56. package/src/lib/positions.mts +0 -12
  57. package/src/lib/strategies.ts +0 -62
  58. package/src/lib/sumBy.mts +0 -4
  59. package/tsconfig.json +0 -20
  60. /package/dist/lib/{getFocusable.d.mts → getFocusable.d.ts} +0 -0
  61. /package/dist/lib/{positions.d.mts → positions.d.ts} +0 -0
  62. /package/dist/lib/{sumBy.d.mts → sumBy.d.ts} +0 -0
package/index.html DELETED
@@ -1,142 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>arrowTab</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- </head>
9
- <body>
10
- <div class="flex flex-col gap-48">
11
- <div class="grid grid-cols-3 gap-4 p-4 items-baseline">
12
- <input type="text" class="border-2 border-gray-300 p-2" />
13
- <input type="text" class="border-2 border-gray-300 p-2" />
14
- <input type="text" class="border-2 border-gray-300 p-2" />
15
- <input type="text" class="border-2 border-gray-300 p-2" />
16
- <input type="text" class="border-2 border-gray-300 p-2" value="Hello" />
17
- <input type="text" class="border-2 border-gray-300 p-2" />
18
- <input type="text" class="border-2 border-gray-300 p-2" />
19
- <input type="text" class="border-2 border-gray-300 p-2" />
20
- <input type="text" class="border-2 border-gray-300 p-2" />
21
- </div>
22
-
23
- <div class="grid grid-cols-3 gap-4 p-4 items-baseline">
24
- <input type="text" class="border-2 border-gray-300 p-2" />
25
- <input type="text" disabled class="border-2 border-gray-300 p-2" />
26
- <input type="text" class="border-2 border-gray-300 p-2" />
27
- <input type="text" disabled class="border-2 border-gray-300 p-2" />
28
- <input type="text" class="border-2 border-gray-300 p-2" />
29
- <input type="text" disabled class="border-2 border-gray-300 p-2" />
30
- <input type="text" class="border-2 border-gray-300 p-2" />
31
- <input type="text" disabled class="border-2 border-gray-300 p-2" />
32
- <input type="text" class="border-2 border-gray-300 p-2" />
33
- </div>
34
-
35
- <div class="grid grid-cols-3 gap-4 p-4 items-baseline">
36
- <input type="text" class="border-2 border-gray-300 p-2" />
37
- <input type="text" class="border-2 border-gray-300 p-2" />
38
- <input type="text" class="border-2 border-gray-300 p-2" />
39
- <input type="text" class="border-2 border-gray-300 p-2" />
40
- <input
41
- type="text"
42
- class="border-2 border-gray-300 p-2"
43
- value="✅ Input with Text"
44
- />
45
- <input type="text" class="border-2 border-gray-300 p-2" />
46
- <input type="text" class="border-2 border-gray-300 p-2" />
47
- <input type="text" class="border-2 border-gray-300 p-2" />
48
- <input type="text" class="border-2 border-gray-300 p-2" />
49
-
50
- <hr class="col-span-3" />
51
-
52
- <input
53
- type="text"
54
- class="border-2 border-gray-300 p-2"
55
- value="✅ input"
56
- />
57
- <div>
58
- <span>❌ input hidden</span>
59
- <input type="text" hidden class="border-2 border-gray-300 p-2" />
60
- </div>
61
- <input
62
- type="text"
63
- disabled
64
- class="border-2 border-gray-300 p-2"
65
- value="❌ input disabled"
66
- />
67
- <button>✅ Button</button>
68
- <button disabled>❌ button disabled</button>
69
- <button aria-hidden="true">❌ button aria-hidden</button>
70
- <a href="#" class="focus:outline">✅ a tag with href</a>
71
- <a class="focus:outline">❌ only a tag</a>
72
- <div></div>
73
- <div tabindex="10">✅ tabindex="10"</div>
74
- <div tabindex="0">✅ tabindex="0"</div>
75
- <div tabindex="-1">❌ tabindex="-1"</div>
76
-
77
- <hr class="col-span-3" />
78
-
79
- <input type="text" class="border-2 border-gray-300 p-2" />
80
- <input type="text" class="border-2 border-gray-300 p-2" />
81
- <input type="text" class="border-2 border-gray-300 p-2" />
82
- <input type="text" class="border-2 border-gray-300 p-2" />
83
- <textarea class="border-2 border-gray-300 p-2">
84
- ✅ Multiline
85
- Textarea
86
- </textarea>
87
- <input type="text" class="border-2 border-gray-300 p-2" />
88
- <input type="text" class="border-2 border-gray-300 p-2" />
89
- <input type="text" class="border-2 border-gray-300 p-2" />
90
- <input type="text" class="border-2 border-gray-300 p-2" />
91
-
92
- <hr class="col-span-3" />
93
-
94
- <input
95
- type="button"
96
- class="border-2 border-gray-300 p-2"
97
- value="input button"
98
- />
99
- <input
100
- type="submit"
101
- class="border-2 border-gray-300 p-2"
102
- value="input submit"
103
- />
104
- <input
105
- type="reset"
106
- class="border-2 border-gray-300 p-2"
107
- value="input reset"
108
- />
109
-
110
- <input type="password" class="border-2 border-gray-300 p-2" />
111
- <input type="checkbox" class="border-2 border-gray-300 p-2" />
112
- <input type="radio" class="border-2 border-gray-300 p-2" />
113
- <input type="file" class="border-2 border-gray-300 p-2" />
114
- <input type="image" class="border-2 border-gray-300 p-2" />
115
- <input type="email" class="border-2 border-gray-300 p-2" />
116
- <input type="url" class="border-2 border-gray-300 p-2" />
117
- <input type="tel" class="border-2 border-gray-300 p-2" />
118
- <input type="search" class="border-2 border-gray-300 p-2" />
119
- <input type="date" class="border-2 border-gray-300 p-2" />
120
- <input type="time" class="border-2 border-gray-300 p-2" />
121
- <input type="datetime" class="border-2 border-gray-300 p-2" />
122
- <input type="datetime-local" class="border-2 border-gray-300 p-2" />
123
- <input type="month" class="border-2 border-gray-300 p-2" />
124
- <input type="week" class="border-2 border-gray-300 p-2" />
125
- <input type="number" class="border-2 border-gray-300 p-2" />
126
- <input type="range" class="border-2 border-gray-300 p-2" />
127
- <input type="color" class="border-2 border-gray-300 p-2" />
128
-
129
- <input type="text" class="border-2 border-gray-300 p-2" />
130
- <input type="text" class="border-2 border-gray-300 p-2" />
131
- <input type="text" class="border-2 border-gray-300 p-2" />
132
-
133
- <!-- TODO: select, details -->
134
- <!-- TODO: summary, iframe (?) -->
135
- </div>
136
- </div>
137
- <script type="module">
138
- import { initArrowTab } from './dist/index.mjs'
139
- initArrowTab()
140
- </script>
141
- </body>
142
- </html>
package/src/index.mts DELETED
@@ -1,228 +0,0 @@
1
- // TODO: Options
2
- // modifierKey: 'ctrlKey',
3
- // onlyWithModifierKey: false, // TODO: Better name
4
- // strategy: arrowTab.XWalkEuclidean,
5
- // selector: undefined,
6
- // elementFilter: () => boolean,
7
- // getElementPosition: (element) => { x: number, y: number },
8
- // eventTriggerName: 'keydown' | 'keyup',
9
- // earlyReturn: (event) => boolean, // TODO: Better name
10
-
11
- import { getFocusable } from './lib/getFocusable.mjs'
12
- import { getMiddle } from './lib/positions.mjs'
13
- import { strategy } from './lib/strategies.js'
14
- import { sumBy } from './lib/sumBy.mjs'
15
-
16
- export const initArrowTab = () => {
17
- document.addEventListener(
18
- 'keydown',
19
- (event) => {
20
- const activeStrategy = strategy.XWalkEuclidean
21
-
22
- const activeElement =
23
- document.activeElement === document.body
24
- ? getFocusable()?.[0]
25
- : document.activeElement
26
-
27
- if (!activeElement) {
28
- return
29
- }
30
-
31
- if (
32
- event.key !== 'ArrowLeft' &&
33
- event.key !== 'ArrowRight' &&
34
- event.key !== 'ArrowUp' &&
35
- event.key !== 'ArrowDown'
36
- ) {
37
- return
38
- }
39
-
40
- if (
41
- activeElement instanceof HTMLInputElement ||
42
- activeElement instanceof HTMLTextAreaElement
43
- ) {
44
- const selectionStart = activeElement.selectionStart
45
- const selectionEnd = activeElement.selectionEnd
46
-
47
- if (
48
- event.key === 'ArrowLeft' &&
49
- selectionStart !== undefined &&
50
- selectionStart !== null &&
51
- selectionStart !== 0
52
- ) {
53
- return
54
- }
55
-
56
- if (
57
- event.key === 'ArrowRight' &&
58
- selectionStart !== undefined &&
59
- selectionStart !== null &&
60
- selectionStart !== activeElement.value?.length
61
- ) {
62
- return
63
- }
64
-
65
- if (
66
- (event.key === 'ArrowLeft' || event.key === 'ArrowRight') &&
67
- selectionStart !== undefined &&
68
- selectionEnd !== undefined &&
69
- selectionStart !== null &&
70
- selectionEnd !== null &&
71
- selectionStart !== selectionEnd
72
- ) {
73
- return
74
- }
75
-
76
- if (
77
- event.key === 'ArrowUp' &&
78
- selectionStart !== undefined &&
79
- selectionStart !== null &&
80
- activeElement instanceof HTMLTextAreaElement
81
- ) {
82
- const rows = activeElement.value?.split('\n')
83
- const start = selectionStart
84
- const firstRow = rows.at(0)
85
- if (firstRow && start > firstRow.length) {
86
- return
87
- }
88
- }
89
-
90
- if (
91
- event.key === 'ArrowDown' &&
92
- selectionStart !== undefined &&
93
- selectionStart !== null &&
94
- activeElement instanceof HTMLTextAreaElement
95
- ) {
96
- const rows = activeElement.value?.split('\n')
97
- const withoutLastRow = rows.slice(0, -1)
98
- const lengthUntilLastRow = sumBy(withoutLastRow, (row) => row.length)
99
-
100
- const start = selectionStart
101
- if (start <= lengthUntilLastRow + 1) {
102
- return
103
- }
104
- }
105
- }
106
-
107
- const inputTypesThatNeedModifierKey: Record<string, string[]> = {
108
- date: ['ArrowDown', 'ArrowUp'],
109
- time: ['ArrowDown', 'ArrowUp'],
110
- datetime: ['ArrowDown', 'ArrowUp'],
111
- 'datetime-local': ['ArrowDown', 'ArrowUp'],
112
- month: ['ArrowDown', 'ArrowUp'],
113
- week: ['ArrowDown', 'ArrowUp'],
114
- number: ['ArrowDown', 'ArrowUp'],
115
- range: ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'],
116
- }
117
-
118
- if (activeElement instanceof HTMLInputElement) {
119
- const inputType = activeElement.type
120
- if (
121
- inputTypesThatNeedModifierKey[inputType] &&
122
- inputTypesThatNeedModifierKey[inputType].includes(event.key)
123
- ) {
124
- // TODO: Use modifierKey
125
- if (!event.shiftKey) {
126
- return
127
- } else {
128
- event.preventDefault()
129
- }
130
- }
131
- }
132
-
133
- const activePosition = getMiddle(activeElement)
134
-
135
- const allFocusable = getFocusable()
136
- const withoutActiveElement = allFocusable.filter(
137
- (element) => element !== activeElement,
138
- )
139
- const withPosition = withoutActiveElement.map((element) => {
140
- const position = getMiddle(element)
141
-
142
- // Calculate angle between active element and focusable element
143
- const xDistance = position.x - activePosition.x
144
- const yDistance = position.y - activePosition.y
145
- let angle =
146
- Math.acos(yDistance / Math.sqrt(xDistance ** 2 + yDistance ** 2)) *
147
- (180 / Math.PI)
148
- const direction = xDistance > 0 ? 'right' : 'left'
149
- if (direction === 'right') {
150
- angle = 360 - angle
151
- }
152
-
153
- return {
154
- xDistance,
155
- yDistance,
156
- element,
157
- angle,
158
- position,
159
- }
160
- })
161
- const withDistance = withPosition.map((focusable) => {
162
- const distance = activeStrategy.distanceMetric({ ...focusable, event })
163
-
164
- return {
165
- ...focusable,
166
- distance,
167
- }
168
- })
169
- const withWithinReach = withDistance.map((focusable) => {
170
- const withinReach = activeStrategy.withinReachMetric({
171
- ...focusable,
172
- event,
173
- })
174
-
175
- return {
176
- ...focusable,
177
- withinReach,
178
- }
179
- })
180
-
181
- const reversed = [...withWithinReach].reverse() // Prefer down / right if same distance
182
-
183
- const sorted = reversed.sort((a, b) => {
184
- return a.distance - b.distance
185
- })
186
-
187
- const withinReach = sorted.filter(({ withinReach }) => withinReach)
188
-
189
- let nearest = withinReach.at(0)
190
-
191
- if (!nearest) {
192
- return
193
- }
194
-
195
- // const nearlySameDistance = _.filter(candidates, (candidate) => {
196
- // return candidate.distance - nearest.distance < 10
197
- // })
198
-
199
- // console.log('nearlySameDistance', nearlySameDistance)
200
-
201
- // if (nearlySameDistance.length > 1) {
202
- // const ordered = _.orderBy(nearlySameDistance, (candidate) => {
203
- // if (event.key === 'ArrowDown') {
204
- // console.log('ArrowDown', candidate.xDistance)
205
- // return -candidate.xDistance
206
- // }
207
- // if (event.key === 'ArrowUp') {
208
- // return candidate.xDistance
209
- // }
210
- // if (event.key === 'ArrowLeft') {
211
- // return candidate.yDistance
212
- // }
213
- // if (event.key === 'ArrowRight') {
214
- // console.log('ArrowRight', candidate.yDistance)
215
- // return -candidate.yDistance
216
- // }
217
- // })
218
- // console.log('ordered', ordered)
219
- // nearest = _.first(ordered)
220
- // }
221
-
222
- if (nearest.element instanceof HTMLElement) {
223
- nearest.element.focus()
224
- }
225
- },
226
- { capture: true },
227
- )
228
- }
@@ -1,13 +0,0 @@
1
- export const getFocusable = () => {
2
- // Selectors from https://zellwk.com/blog/keyboard-focusable-elements/
3
- const allFocusable = document.querySelectorAll(
4
- 'a[href], button, input, textarea, select, details, [tabindex]:not([tabindex^="-"])',
5
- )
6
- const focusable = Array.from(allFocusable).filter((element) => {
7
- if ('disabled' in element && element.disabled) return false
8
- if ('hidden' in element && element.hidden) return false
9
- if (element.getAttribute('aria-hidden') === 'true') return false
10
- return true
11
- })
12
- return focusable
13
- }
@@ -1,12 +0,0 @@
1
- export const getMiddle = (element: Element) => {
2
- const { x, y, width, height } = element.getBoundingClientRect()
3
- return {
4
- x: x + width / 2,
5
- y: y + height / 2,
6
- }
7
- }
8
-
9
- export const getTopLeft = (element: Element) => {
10
- const { x, y } = element.getBoundingClientRect()
11
- return { x, y }
12
- }
@@ -1,62 +0,0 @@
1
- type StrategyInput = {
2
- event: KeyboardEvent
3
- xDistance: number
4
- yDistance: number
5
- element: Element
6
- angle: number
7
- position: { x: number; y: number }
8
- }
9
-
10
- const withinReachMetrics = {
11
- XWalk: ({ angle, event }: StrategyInput) => {
12
- if (event.key === 'ArrowDown' && (angle >= 315 || angle <= 45)) {
13
- return true
14
- }
15
-
16
- if (event.key === 'ArrowLeft' && angle > 45 && angle < 135) {
17
- return true
18
- }
19
-
20
- if (event.key === 'ArrowUp' && angle >= 135 && angle <= 225) {
21
- return true
22
- }
23
-
24
- if (event.key === 'ArrowRight' && angle > 225 && angle < 315) {
25
- return true
26
- }
27
-
28
- return false
29
- },
30
- }
31
-
32
- const distanceMetrics = {
33
- euclidean: ({ xDistance, yDistance }: StrategyInput) => {
34
- return Math.sqrt(xDistance ** 2 + yDistance ** 2)
35
- },
36
- manhattan: ({ xDistance, yDistance }: StrategyInput) => {
37
- return Math.abs(xDistance) + Math.abs(yDistance)
38
- },
39
- manhattanWeighted: ({ xDistance, yDistance, event }: StrategyInput) => {
40
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
41
- return Math.abs(xDistance) + Math.abs(yDistance) * 0.1
42
- }
43
- if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
44
- return Math.abs(xDistance) * 0.1 + Math.abs(yDistance)
45
- }
46
- },
47
- }
48
-
49
- export const strategy = {
50
- XWalkEuclidean: {
51
- withinReachMetric: withinReachMetrics.XWalk,
52
- distanceMetric: distanceMetrics.euclidean,
53
- },
54
- XWalkManhattan: {
55
- withinReachMetric: withinReachMetrics.XWalk,
56
- distanceMetric: distanceMetrics.manhattan,
57
- },
58
- XWalkManhattanWeighted: {
59
- withinReachMetric: withinReachMetrics.XWalk,
60
- distanceMetric: distanceMetrics.manhattanWeighted,
61
- },
62
- }
package/src/lib/sumBy.mts DELETED
@@ -1,4 +0,0 @@
1
- export const sumBy = <T extends unknown>(
2
- arr: Array<T>,
3
- func: (item: T) => number,
4
- ) => arr.reduce((acc, item) => acc + func(item), 0)
package/tsconfig.json DELETED
@@ -1,20 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "esModuleInterop": true,
4
- "skipLibCheck": true,
5
- "target": "es2022",
6
- "allowJs": true,
7
- "resolveJsonModule": true,
8
- "moduleDetection": "force",
9
- "isolatedModules": true,
10
- "verbatimModuleSyntax": true,
11
- "strict": true,
12
- "noUncheckedIndexedAccess": true,
13
- "noImplicitOverride": true,
14
- "module": "NodeNext",
15
- "outDir": "dist",
16
- "sourceMap": true,
17
- "declaration": true,
18
- "lib": ["es2022", "dom", "dom.iterable"]
19
- }
20
- }
File without changes
File without changes