@peter.naydenov/shortcuts 3.5.2 → 4.0.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.
Files changed (98) hide show
  1. package/API.md +939 -0
  2. package/CODE_OF_CONDUCT.md +84 -0
  3. package/CONTRIBUTING.md +476 -0
  4. package/Changelog.md +26 -2
  5. package/How.to.create.plugins.md +929 -0
  6. package/Migration.guide.md +48 -0
  7. package/README.md +396 -24
  8. package/dist/main.d.ts +54 -2
  9. package/dist/methods/_normalizeWithPlugins.d.ts +63 -1
  10. package/dist/methods/_readShortcutWithPlugins.d.ts +8 -1
  11. package/dist/methods/_setupPlugin.d.ts +9 -0
  12. package/dist/methods/_systemAction.d.ts +8 -1
  13. package/dist/methods/changeContext.d.ts +8 -1
  14. package/dist/methods/index.d.ts +2 -0
  15. package/dist/methods/listShortcuts.d.ts +1 -16
  16. package/dist/methods/load.d.ts +8 -1
  17. package/dist/methods/unload.d.ts +8 -1
  18. package/dist/plugins/click/_findTarget.d.ts +9 -1
  19. package/dist/plugins/click/_listenDOM.d.ts +76 -3
  20. package/dist/plugins/click/_normalizeShortcutName.d.ts +7 -1
  21. package/dist/plugins/click/_registerShortcutEvents.d.ts +26 -0
  22. package/dist/plugins/click/index.d.ts +6 -31
  23. package/dist/plugins/form/_defaults.d.ts +13 -1
  24. package/dist/plugins/form/_listenDOM.d.ts +66 -3
  25. package/dist/plugins/form/_registerShortcutEvents.d.ts +95 -1
  26. package/dist/plugins/form/index.d.ts +2 -29
  27. package/dist/plugins/hover/_findTarget.d.ts +10 -0
  28. package/dist/plugins/hover/_listenDOM.d.ts +68 -0
  29. package/dist/plugins/hover/_normalizeShortcutName.d.ts +2 -0
  30. package/dist/plugins/hover/_registerShortcutEvents.d.ts +28 -0
  31. package/dist/plugins/hover/index.d.ts +14 -0
  32. package/dist/plugins/key/_listenDOM.d.ts +61 -3
  33. package/dist/plugins/key/_registerShortcutEvents.d.ts +26 -0
  34. package/dist/plugins/key/_specialChars.d.ts +6 -31
  35. package/dist/plugins/key/index.d.ts +2 -29
  36. package/dist/plugins/scroll/_listenDOM.d.ts +58 -0
  37. package/dist/plugins/scroll/_normalizeShortcutName.d.ts +2 -0
  38. package/dist/plugins/scroll/_registerShortcutEvents.d.ts +28 -0
  39. package/dist/plugins/scroll/index.d.ts +16 -0
  40. package/dist/shortcuts.cjs +1 -1
  41. package/dist/shortcuts.esm.mjs +1 -1
  42. package/dist/shortcuts.umd.js +1 -1
  43. package/eslint.config.js +80 -0
  44. package/html/assets/index-COTh6lXR.css +1 -0
  45. package/html/assets/index-DOkKC3NI.js +53 -0
  46. package/html/bg.png +0 -0
  47. package/html/favicon.ico +0 -0
  48. package/html/favicon.svg +5 -0
  49. package/html/html.meta.json.gz +0 -0
  50. package/html/index.html +32 -0
  51. package/package.json +16 -12
  52. package/shortcuts.png +0 -0
  53. package/src/main.js +52 -22
  54. package/src/methods/_normalizeWithPlugins.js +26 -2
  55. package/src/methods/_readShortcutWithPlugins.js +9 -2
  56. package/src/methods/_setupPlugin.js +93 -0
  57. package/src/methods/_systemAction.js +12 -4
  58. package/src/methods/changeContext.js +11 -3
  59. package/src/methods/index.js +2 -0
  60. package/src/methods/listShortcuts.js +5 -12
  61. package/src/methods/load.js +11 -4
  62. package/src/methods/unload.js +8 -1
  63. package/src/plugins/click/_findTarget.js +11 -5
  64. package/src/plugins/click/_listenDOM.js +58 -20
  65. package/src/plugins/click/_normalizeShortcutName.js +11 -4
  66. package/src/plugins/click/_readClickEvent.js +1 -1
  67. package/src/plugins/click/_registerShortcutEvents.js +33 -5
  68. package/src/plugins/click/index.js +33 -60
  69. package/src/plugins/form/_defaults.js +13 -3
  70. package/src/plugins/form/_listenDOM.js +46 -9
  71. package/src/plugins/form/_normalizeShortcutName.js +2 -2
  72. package/src/plugins/form/_registerShortcutEvents.js +93 -17
  73. package/src/plugins/form/index.js +25 -56
  74. package/src/plugins/hover/_findTarget.js +26 -0
  75. package/src/plugins/hover/_listenDOM.js +154 -0
  76. package/src/plugins/hover/_normalizeShortcutName.js +21 -0
  77. package/src/plugins/hover/_registerShortcutEvents.js +51 -0
  78. package/src/plugins/hover/index.js +71 -0
  79. package/src/plugins/key/_listenDOM.js +67 -33
  80. package/src/plugins/key/_normalizeShortcutName.js +4 -3
  81. package/src/plugins/key/_readKeyEvent.js +1 -1
  82. package/src/plugins/key/_registerShortcutEvents.js +34 -5
  83. package/src/plugins/key/_specialChars.js +5 -0
  84. package/src/plugins/key/index.js +34 -59
  85. package/src/plugins/scroll/_listenDOM.js +141 -0
  86. package/src/plugins/scroll/_normalizeShortcutName.js +21 -0
  87. package/src/plugins/scroll/_registerShortcutEvents.js +50 -0
  88. package/src/plugins/scroll/index.js +61 -0
  89. package/test/01-general.test.js +92 -23
  90. package/test/02-key.test.js +241 -40
  91. package/test/03-click.test.js +291 -47
  92. package/test/04-form.test.js +241 -47
  93. package/test/05-hover.test.js +463 -0
  94. package/test/06-scroll.test.js +374 -0
  95. package/test-helpers/Block.jsx +3 -2
  96. package/test-helpers/style.css +6 -1
  97. package/vitest.config.js +13 -11
  98. package/How..to.make.plugins.md +0 -41
@@ -0,0 +1,463 @@
1
+ import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { userEvent } from 'vitest/browser'
3
+ import {
4
+ getByLabelText,
5
+ getByText,
6
+ getByTestId,
7
+ queryByTestId,
8
+ fireEvent,
9
+ // Tip: all queries are also exposed on an object
10
+ // called "queries" which you could import here as well
11
+ waitFor
12
+ } from '@testing-library/dom'
13
+
14
+
15
+
16
+ import '../test-helpers/style.css'
17
+ import Block from '../test-helpers/Block.jsx'
18
+ import VisaulController from '@peter.naydenov/visual-controller-for-react'
19
+ import wait from '../test-helpers/wait.js'
20
+ import {
21
+ shortcuts
22
+ , pluginKey
23
+ , pluginClick
24
+ , pluginForm
25
+ , pluginHover
26
+ } from '../src/main.js'
27
+
28
+
29
+
30
+ const html = new VisaulController ();
31
+ let
32
+ a = false
33
+ , b = false
34
+ , c = null
35
+ ;
36
+
37
+ const contextDefinition = {
38
+ general : {
39
+ ' key : shift+a': [
40
+ () => a = true,
41
+ () => c = 'triggered'
42
+ ]
43
+ }
44
+ , touch : {
45
+ // Mouse on
46
+ ' hover : on': ({ target }) => {
47
+ b = true
48
+ // Named argument 'target' should be available
49
+ if ( target?.dataset?.hover ) c = target?.dataset?.hover
50
+ },
51
+ // Mouse off
52
+ 'hover: off': ({ target }) => {
53
+ b = false
54
+ if ( target?.dataset?.hover ) c = target.dataset.hover
55
+ }
56
+ }
57
+ , extra : {
58
+ 'key : p,r,o,b,a': () => b = true
59
+ }
60
+ , extend : {
61
+ 'form : watch' : () => 'input'
62
+ , 'form : define' : () => 'input'
63
+ , 'form : action' : () => [
64
+ {
65
+ fn : (e) => e.target
66
+ , type : 'input'
67
+ , timing : 'in'
68
+ }
69
+ ]
70
+ }
71
+ }
72
+
73
+
74
+ const short = shortcuts ();
75
+
76
+
77
+
78
+ describe ( 'Hover plugin', () => {
79
+
80
+
81
+
82
+ beforeEach ( async () => {
83
+ short.load ( contextDefinition )
84
+ const container = document.createElement ( 'div' );
85
+ container.id = 'app'
86
+ document.body.appendChild ( container )
87
+ await html.publish ( Block, {}, 'app' )
88
+ a = false, b = false
89
+ }) // beforeEach
90
+
91
+
92
+ afterEach ( async () => {
93
+ short.reset ();
94
+ short.disablePlugin ( 'hover' )
95
+ html.destroy ()
96
+ a = false, b = false, c = null;
97
+ document.body.querySelector ( '#app' ).remove ()
98
+ }) // afterEach
99
+
100
+
101
+
102
+ afterEach ( async () => {
103
+ short.reset ();
104
+ a = false, b = false, c = null;
105
+ }) // afterEach
106
+
107
+
108
+
109
+ it ( 'No "hover" plugin installed', async () => {
110
+ const r = short.listShortcuts ('touch');
111
+ // Shortcuts are untouched if plugin is not installed
112
+ expect ( r[0]).to.equal ( ' hover : on' )
113
+ }) // it no 'hover' plugin installed
114
+
115
+
116
+
117
+ it ( 'Hover plugin installed', async () => {
118
+ short.enablePlugin ( pluginHover )
119
+ const r = short.listShortcuts ( 'touch' );
120
+ // Shortcuts are normalized
121
+ expect ( r[0]).to.equal ( 'HOVER:ON' )
122
+ }) // it hover plugin installed
123
+
124
+
125
+
126
+ it ( 'Mouse on', async () => {
127
+ expect ( b ).to.equal ( false )
128
+ short.enablePlugin ( pluginHover )
129
+ short.changeContext ( 'touch' )
130
+ await userEvent.hover ( document.querySelector ( '#rspan' ) )
131
+ await wait ( 320 )
132
+ await waitFor ( () => {
133
+ expect ( b ).to.equal ( true )
134
+ // Target is a element that contains data-hover property!
135
+ expect ( c ).to.equal ( 'red' )
136
+ // We hovered on span, but target is the parent element
137
+ // that contains data-hover property
138
+ }, { timeout: 1000, interval: 12 })
139
+ }) // it mouse on
140
+
141
+
142
+
143
+ it ( 'Mouse off', async () => {
144
+ short.enablePlugin ( pluginHover )
145
+ short.changeContext ( 'touch' )
146
+ const hoverElement = document.querySelector ( '#rspan' );
147
+ await userEvent.hover ( hoverElement )
148
+ await wait ( 320 )
149
+ await waitFor ( () => {
150
+ expect ( b ).to.equal ( true )
151
+ expect ( c ).to.equal ( 'red' )
152
+ }, { timeout: 1000, interval: 12 })
153
+ // Simulate off by hovering another element
154
+ // await userEvent.hover ( document.body )
155
+ const unhoverToInput = document.querySelector ( '#name' );
156
+ await userEvent.hover ( unhoverToInput )
157
+ await wait ( 320 )
158
+ await waitFor ( () => {
159
+ expect ( b ).to.equal ( false )
160
+ expect ( c ).to.equal ( 'red' )
161
+ }, { timeout: 1000, interval: 12 })
162
+ }) // it mouse off
163
+
164
+
165
+
166
+ it ( 'Arguments of hover handler', async () => {
167
+ /**
168
+ * Need to know arguments for 'hover' handler
169
+ * function myHoverHandler ({
170
+ * context // (string) Name of the current context;
171
+ * , note // (string) Name of the note or null if note isn't set;
172
+ * , dependencies // (object) Object with dependencies that you have set by calling `setDependencies` method;
173
+ * , target // (DOM element). Target element of the hover event;
174
+ * , targetProps // (object). Coordinates of the target element (top, left, right, bottom, width, height) or null if target element is not available;
175
+ * , x // (number). X coordinate of the target element;
176
+ * , y // (number). Y coordinate of the target element;
177
+ * , event // (object). Original hover event object;
178
+ * }) {
179
+ * // Body of the handler. Do something...
180
+ * }
181
+ */
182
+ // Ensure clean state for this test
183
+ const megaBtn = document.querySelector ( '[data-click="mega"]' )
184
+ const test = [];
185
+ let i = 0;
186
+ short.enablePlugin ( pluginHover )
187
+ short.setDependencies ({ test })
188
+ short.load ({
189
+ 'local' : {
190
+ 'hover : on' : ({
191
+ dependencies
192
+ , target
193
+ , x
194
+ , y
195
+ , targetProps
196
+ , context
197
+ }) => {
198
+ const
199
+ { test } = dependencies
200
+ , result = {
201
+ x
202
+ , y
203
+ , targetProps
204
+ , context
205
+ }
206
+ ;
207
+ result.target = target.dataset.hover
208
+ test.push ( result )
209
+ i++
210
+ }
211
+ } // local
212
+ })
213
+ short.changeContext ( 'local' )
214
+ expect ( megaBtn ).to.not.be.null
215
+ await userEvent.hover ( megaBtn )
216
+ await wait ( 340 ) // Wait for hover processing
217
+ await waitFor ( () => {
218
+ expect ( i ).to.be.equal ( 1 )
219
+ const result = test[0];
220
+ expect ( result.target ).to.be.equal ( 'blue' )
221
+ expect ( result.context ).to.be.equal ( 'local' )
222
+ }, { timeout: 1000, interval: 12 })
223
+ }) // it arguments of hover handler
224
+
225
+
226
+
227
+ it ( 'Hover on anchor', async () => {
228
+ // Hover on anchor that don't have hover-data attribute.
229
+ let result = 'none';
230
+ short.enablePlugin ( pluginHover )
231
+ short.load ({ 'extra' : {
232
+ 'hover : on' : ({target, context, event }) => {
233
+ expect ( context ).to.be.equal ( 'extra' )
234
+ expect ( target.nodeName ).to.be.equal ( 'DIV' )
235
+ result = target.nodeName
236
+ }
237
+ }
238
+ })
239
+ short.changeContext ( 'extra' )
240
+ const loc = document.querySelector ( '#rspan' ) || false;
241
+ if ( loc ) await userEvent.hover ( loc )
242
+ await waitFor ( () => {
243
+ expect ( result ).to.be.equal ( 'DIV' )
244
+ }, { timeout: 1000, interval: 12 })
245
+ }) // it hover on anchor
246
+
247
+
248
+
249
+ it ( 'Mute and unmute hover plugin', async () => {
250
+ const
251
+ result = []
252
+ , trg = document.querySelector ( '#rspan' )
253
+ , offTarget = document.querySelector ( '#name' )
254
+ ;
255
+
256
+ let i = 0;
257
+ short.setDependencies ({ result })
258
+ result.push ( 'init' )
259
+
260
+
261
+ short.load ({
262
+ 'local' : {
263
+ 'hover : on' : ({dependencies}) => {
264
+ const { result } = dependencies;
265
+ result.push ( i++ )
266
+ }
267
+ }
268
+ })
269
+
270
+ short.enablePlugin ( pluginHover )
271
+ short.changeContext ( 'local' )
272
+
273
+
274
+ await userEvent.hover ( trg )
275
+ await wait( 320 )
276
+ await waitFor ( () => {
277
+ // We checking if the shortcut works
278
+ expect ( result ).to.have.lengthOf ( 2 )
279
+ expect ( i ).to.equal ( 1 )
280
+ }, { timeout: 1000, interval: 12 })
281
+
282
+ short.mutePlugin ( 'hover' )
283
+ await userEvent.hover ( offTarget )
284
+ await userEvent.hover ( trg )
285
+
286
+ await wait ( 320 )
287
+ await waitFor ( () => {
288
+ // Plugin is muted, so we don't expect any changes
289
+ expect ( result ).to.have.lengthOf ( 2 )
290
+ expect ( i ).to.equal ( 1 )
291
+ }, { timeout: 1000, interval: 12 })
292
+
293
+
294
+ await userEvent.hover ( offTarget )
295
+ short.unmutePlugin ( 'hover' )
296
+ await userEvent.hover ( trg )
297
+
298
+ await wait ( 320 )
299
+ await waitFor ( () => {
300
+ // Plugin is unmuted, should work again
301
+ expect ( result ).to.have.lengthOf ( 3 )
302
+ expect ( i ).to.equal ( 2 )
303
+ }, { timeout: 1000, interval: 12 })
304
+ }) // it mute and unmute hover plugin
305
+
306
+
307
+
308
+ it ( 'Pause and resume', async () => {
309
+ const
310
+ target = document.querySelector ( '#rspan' )
311
+ , targetOff = document.querySelector ( '#name' )
312
+ ;
313
+
314
+ short.enablePlugin ( pluginHover )
315
+ expect ( b ).to.be.equal ( false )
316
+
317
+ short.changeContext ( 'touch' )
318
+ short.pause ( 'hover : on' )
319
+
320
+ await userEvent.hover ( target )
321
+ await wait ( 100 )
322
+ await waitFor ( () => {
323
+ expect ( b ).to.be.equal ( false )
324
+ }, { timeout: 1000, interval: 30 })
325
+
326
+
327
+ await userEvent.hover ( targetOff )
328
+ short.resume ( 'hover : on' )
329
+
330
+ await userEvent.hover ( target )
331
+ await wait ( 100 )
332
+ await waitFor ( () => {
333
+ expect ( b ).to.be.equal ( true )
334
+ expect ( c ).to.be.equal ( 'red' )
335
+ }, { timeout: 1000, interval: 30 })
336
+ }) // it pause and resume
337
+
338
+
339
+
340
+ it ( 'Fast move over hover target', async () => {
341
+ const
342
+ target = document.querySelector ( '#rspan' )
343
+ , targetOff = document.querySelector ( '#name' )
344
+ ;
345
+
346
+ short.enablePlugin ( pluginHover )
347
+ expect ( b ).to.be.equal ( false )
348
+
349
+ short.changeContext ( 'touch' )
350
+ await userEvent.hover ( target )
351
+ await wait ( 200 )
352
+ await waitFor ( () => {
353
+ expect ( b ).to.be.equal ( false )
354
+ }, { timeout: 1000, interval: 30 })
355
+
356
+ await userEvent.hover ( targetOff )
357
+ await wait ( 320 )
358
+ await waitFor ( () => {
359
+ expect ( b ).to.be.equal ( false )
360
+ }, { timeout: 1000, interval: 30 })
361
+ }) // it fast move over hover target
362
+
363
+
364
+
365
+ it ( 'Immediate hover switch between elements', async () => {
366
+ // Test lines 91-93: immediate hover switch without delay
367
+ const events = [];
368
+ short.setDependencies ({ events })
369
+ short.load ({
370
+ 'immediate' : {
371
+ 'hover : on' : ({ target, dependencies }) => {
372
+ const { events } = dependencies;
373
+ events.push ( { type: 'on', target: target.dataset.hover || target.id } )
374
+ }
375
+ , 'hover: off' : ({ target, dependencies }) => {
376
+ const { events } = dependencies;
377
+ events.push ( { type: 'off', target: target.dataset.hover || target.id } )
378
+ }
379
+ }
380
+ })
381
+
382
+ short.enablePlugin ( pluginHover )
383
+ short.changeContext ( 'immediate' )
384
+
385
+ const
386
+ firstTarget = document.querySelector ( '#rspan' ) // Has data-hover="red"
387
+ , secondTarget = document.querySelector ( '[data-click="mega"]' ) // Has data-hover="blue"
388
+ ;
389
+
390
+ // Hover on first element
391
+ await userEvent.hover ( firstTarget )
392
+ await wait ( 320 )
393
+ await waitFor ( () => {
394
+ expect ( events ).to.have.length ( 1 )
395
+ expect ( events[0] ).to.deep.equal ( { type: 'on', target: 'red' } )
396
+ }, { timeout: 1000, interval: 12 })
397
+
398
+ // Immediately hover on second element (tests lines 91-93)
399
+ // This should trigger immediate HOVER:OFF for first element
400
+ events.length = 0 // Reset events array
401
+ await userEvent.hover ( secondTarget )
402
+ await wait ( 50 ) // Very short wait to capture immediate events
403
+
404
+ // Should have immediate OFF event for first element
405
+ await waitFor ( () => {
406
+ expect ( events ).to.have.length ( 1 )
407
+ expect ( events[0] ).to.deep.equal ( { type: 'off', target: 'red' } )
408
+ }, { timeout: 500, interval: 12 })
409
+
410
+ // Wait for ON event for second element
411
+ await wait ( 270 ) // Total 320ms wait
412
+ await waitFor ( () => {
413
+ expect ( events ).to.have.length ( 2 )
414
+ expect ( events[1] ).to.deep.equal ( { type: 'on', target: 'blue' } )
415
+ }, { timeout: 500, interval: 12 })
416
+ }) // it immediate hover switch between elements
417
+
418
+
419
+ it ( 'Extra parameters to plugin options', async () => {
420
+ short.enablePlugin ( pluginHover )
421
+ const emit = [];
422
+ const setupContext = {
423
+ 'hover:setup' : () => {
424
+ emit.push ( 'setup' )
425
+ return { wait: 100, customParam: 'test-value', emit }
426
+ },
427
+ 'hover:on' : ({options}) => {
428
+ expect ( options.wait ).to.equal ( 100 )
429
+ expect ( options.customParam ).to.equal ( 'test-value' )
430
+ options.emit.push ( 'on' )
431
+ },
432
+ 'hover:off' : ({options}) => {
433
+ expect ( options.wait ).to.equal ( 100 )
434
+ expect ( options.customParam ).to.equal ( 'test-value' )
435
+ options.emit.push ( 'off' )
436
+ }
437
+ } // setupContext
438
+
439
+ short.load ({ setupContext })
440
+ short.changeContext ( 'setupContext' )
441
+
442
+ // Setup event execution is on change context:
443
+ expect ( emit[0] ).to.equal ( 'setup' )
444
+
445
+ // Test hover on and off with modified options
446
+ const target = document.querySelector ( '#rspan' )
447
+ await userEvent.hover ( target )
448
+ await wait ( 150 ) // Wait for modified hover time (100ms + buffer)
449
+ await waitFor ( () => {
450
+ expect ( emit ).to.deep.equal ( [ 'setup', 'on' ] )
451
+ }, { timeout: 1000, interval: 12 })
452
+
453
+ // Test hover off
454
+ await userEvent.hover ( document.querySelector ( '#name' ) )
455
+ await wait ( 150 )
456
+ await waitFor ( () => {
457
+ expect ( emit ).to.deep.equal ( [ 'setup', 'on', 'off' ] )
458
+ }, { timeout: 1000, interval: 12 })
459
+ }) // it extra parameters to plugin options
460
+
461
+
462
+
463
+ }) // describe