@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,374 @@
1
+ import { beforeEach, afterEach, describe, it, expect } from 'vitest'
2
+ import { userEvent } from 'vitest/browser'
3
+ import {
4
+ getByLabelText,
5
+ getByText,
6
+ getByTestId,
7
+ queryByTestId,
8
+ // Tip: all queries are also exposed on an object
9
+ // called "queries" which you could import here as well
10
+ waitFor
11
+ } from '@testing-library/dom'
12
+
13
+
14
+
15
+ import '../test-helpers/style.css'
16
+ import Block from '../test-helpers/Block.jsx'
17
+ import VisaulController from '@peter.naydenov/visual-controller-for-react'
18
+ import wait from '../test-helpers/wait.js'
19
+ import {
20
+ shortcuts
21
+ , pluginKey
22
+ , pluginClick
23
+ , pluginForm
24
+ , pluginScroll
25
+ } from '../src/main.js'
26
+
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
+ , scroll : {
45
+ ' scroll: up': () => b = true,
46
+ 'scroll : down': () => c = 'down',
47
+ 'scroll:left': () => c = 'left',
48
+ 'scroll:right': () => c = 'right'
49
+ }
50
+ , extra : {
51
+ 'key : p,r,o,b,a': () => b = true
52
+ }
53
+ }
54
+
55
+
56
+ const short = shortcuts ();
57
+
58
+
59
+
60
+ describe ( 'Scroll plugin', () => {
61
+
62
+
63
+
64
+ beforeEach ( async () => {
65
+ short.load ( contextDefinition )
66
+ const container = document.createElement ( 'div' );
67
+ container.id = 'app'
68
+ document.body.appendChild ( container )
69
+ // Make page scrollable both vertically and horizontally
70
+ document.body.style.height = '2000px'
71
+ document.body.style.width = '2000px'
72
+ document.body.style.margin = '0'
73
+ document.documentElement.style.height = '2000px'
74
+ document.documentElement.style.width = '2000px'
75
+ await html.publish ( Block, {}, 'app' )
76
+ a = false, b = false, c = null
77
+ }) // beforeEach
78
+
79
+
80
+
81
+ afterEach ( async () => {
82
+ short.reset ();
83
+ html.destroy ()
84
+ a = false, b = false, c = null;
85
+ document.body.querySelector ( '#app' ).remove ()
86
+ }) // afterEach
87
+
88
+
89
+
90
+ it ( 'No "scroll" plugin installed', async () => {
91
+ const r = short.listShortcuts ('scroll');
92
+ // Shortcuts are untouched if plugin is not installed
93
+ expect ( r[0]).to.equal ( ' scroll: up' )
94
+ expect ( r[1]).to.equal ( 'scroll : down' )
95
+ expect ( r[2]).to.equal ( 'scroll:left' )
96
+ expect ( r[3]).to.equal ( 'scroll:right' )
97
+ }) // it no 'scroll' plugin installed
98
+
99
+
100
+
101
+ it ( 'Enable plugin when context has no shortcuts for this plugin', () => {
102
+ short.changeContext ( 'general' ) // 'general' has no scroll shortcuts
103
+ short.enablePlugin ( pluginScroll )
104
+ expect ( short.listPlugins () ).to.include ( 'scroll' )
105
+ // Since no scroll shortcuts in 'general', listener should not start
106
+ }) // it enable plugin when context has no shortcuts
107
+
108
+
109
+
110
+ it ( 'Scroll plugin installed', async () => {
111
+ short.enablePlugin ( pluginScroll )
112
+ const r = short.listShortcuts ( 'scroll' );
113
+ // Shortcuts are normalized
114
+ expect ( r[0]).to.equal ( 'SCROLL:UP' )
115
+ expect ( r[1]).to.equal ( 'SCROLL:DOWN' )
116
+ expect ( r[2]).to.equal ( 'SCROLL:LEFT' )
117
+ expect ( r[3]).to.equal ( 'SCROLL:RIGHT' )
118
+ }) // it scroll plugin installed
119
+
120
+
121
+
122
+ it ( 'Scroll down event', async () => {
123
+ expect ( c ).to.equal ( null )
124
+ short.changeContext ( 'scroll' )
125
+ short.enablePlugin ( pluginScroll )
126
+
127
+ // Scroll down by setting scroll position
128
+ window.scrollTo ( 0, 100 )
129
+ await wait ( 50 )
130
+ await waitFor ( () => {
131
+ expect ( c ).to.equal ( 'down' )
132
+ }, { timeout: 1000, interval: 12 })
133
+ }) // it scroll down event
134
+
135
+
136
+
137
+ it ( 'Scroll up event', async () => {
138
+ expect ( b ).to.equal ( false )
139
+ short.changeContext ( 'scroll' )
140
+ short.enablePlugin ( pluginScroll )
141
+
142
+ // First scroll down to establish baseline
143
+ window.scrollTo ( 0, 100)
144
+ await wait ( 50 )
145
+
146
+ // Then scroll up
147
+ window.scrollTo ( 0, 50)
148
+ await wait ( 50 )
149
+
150
+ await waitFor ( () => {
151
+ expect ( b ).to.equal ( true )
152
+ }, { timeout: 1000, interval: 12 })
153
+ }) // it scroll up event
154
+
155
+
156
+
157
+ it ( 'Scroll left event', async () => {
158
+ expect ( c ).to.equal ( null )
159
+ short.changeContext ( 'scroll' )
160
+ short.enablePlugin ( pluginScroll )
161
+
162
+ // First scroll right to establish baseline
163
+ window.scrollTo ( 100, 0 )
164
+ await wait ( 50 )
165
+
166
+ // Then scroll left
167
+ window.scrollTo ( 50, 0 )
168
+ await wait ( 50 )
169
+
170
+ await waitFor ( () => {
171
+ expect ( c ).to.equal ( 'left' )
172
+ }, { timeout: 1000, interval: 12 })
173
+ }) // it scroll left event
174
+
175
+
176
+
177
+ it ( 'Scroll right event', async () => {
178
+ expect ( c ).to.equal ( null )
179
+ short.changeContext ( 'scroll' )
180
+ short.enablePlugin ( pluginScroll )
181
+
182
+ // Scroll right by setting scroll position
183
+ window.scrollTo ( 100, 0 )
184
+ await wait ( 50 )
185
+
186
+ await waitFor ( () => {
187
+ expect ( c ).to.equal ( 'right' )
188
+ }, { timeout: 1000, interval: 12 })
189
+ }) // it scroll right event
190
+
191
+
192
+
193
+ it ( 'Combined horizontal and vertical scroll', async () => {
194
+ const scrollEvents = []
195
+ short.setDependencies ({ scrollEvents })
196
+ short.changeContext ( 'scroll' )
197
+ short.enablePlugin ( pluginScroll )
198
+
199
+ // Reset scroll position to (0, 0) first
200
+ window.scrollTo ( 0, 0 )
201
+ await wait ( 200 ) // Wait longer for any pending scroll events
202
+
203
+ // Clear any events that might have been triggered by reset
204
+ scrollEvents.length = 0
205
+
206
+ // Create a temporary context to capture scroll events
207
+ short.load ({
208
+ 'testScroll': {
209
+ 'scroll:up': ({ direction }) => scrollEvents.push(direction),
210
+ 'scroll:down': ({ direction }) => scrollEvents.push(direction),
211
+ 'scroll:left': ({ direction }) => scrollEvents.push(direction),
212
+ 'scroll:right': ({ direction }) => scrollEvents.push(direction)
213
+ }
214
+ })
215
+ short.changeContext ( 'testScroll' )
216
+
217
+ // Scroll diagonally (right and down)
218
+ window.scrollTo ( 100, 100 )
219
+ await wait ( 50 )
220
+
221
+ // Scroll left
222
+ window.scrollTo ( 50, 100 )
223
+ await wait ( 50 )
224
+
225
+ // Scroll up
226
+ window.scrollTo ( 50, 50 )
227
+ await wait ( 50 )
228
+
229
+ await waitFor ( () => {
230
+ expect ( scrollEvents ).to.include.members ( ['right', 'down', 'left', 'up'] )
231
+ expect ( scrollEvents ).to.have.length ( 4 )
232
+ }, { timeout: 1000, interval: 12 })
233
+ }) // it combined horizontal and vertical scroll
234
+
235
+
236
+
237
+ it ( 'Mute and unmute scroll plugin', async () => {
238
+ short.changeContext ( 'scroll' )
239
+ short.enablePlugin ( pluginScroll )
240
+ c = null // Reset c variable
241
+
242
+ // Mute the plugin
243
+ short.mutePlugin ( 'scroll' )
244
+
245
+ // Scroll should not trigger
246
+ window.scrollTo ( 0, 100)
247
+ await wait ( 50 )
248
+ expect ( c ).to.equal ( null )
249
+
250
+ // Unmute the plugin
251
+ short.unmutePlugin ( 'scroll' )
252
+
253
+ // Scroll should trigger now
254
+ window.scrollTo ( 0, 200)
255
+ await wait ( 50 )
256
+
257
+ await waitFor ( () => {
258
+ expect ( c ).to.equal ( 'down' )
259
+ }, { timeout: 1000, interval: 12 })
260
+ }) // it mute and unmute scroll plugin
261
+
262
+
263
+
264
+ it ( 'Pause and resume', async () => {
265
+ short.changeContext ( 'scroll' )
266
+ short.enablePlugin ( pluginScroll )
267
+
268
+ // Pause the shortcut
269
+ short.pause ( 'scroll : down' )
270
+
271
+ // Scroll should not trigger
272
+ window.scrollTo(0, 100)
273
+ await wait ( 50 )
274
+ expect ( c ).to.equal ( null )
275
+
276
+ // Resume the shortcut
277
+ short.resume ( ' scroll : down' )
278
+
279
+ // Scroll should trigger now
280
+ window.scrollTo(0, 200)
281
+ await wait ( 50 )
282
+
283
+ await waitFor ( () => {
284
+ expect ( c ).to.equal ( 'down' )
285
+ }, { timeout: 1000, interval: 12 })
286
+ }) // it pause and resume
287
+
288
+
289
+
290
+ it ( 'Enable already enabled plugin', () => {
291
+ // Enable plugin first time
292
+ short.enablePlugin ( pluginScroll )
293
+ let plugins = short.listPlugins ()
294
+ expect ( plugins ).to.include ( 'scroll' )
295
+ expect ( plugins ).to.have.lengthOf ( 1 )
296
+
297
+ // Try to enable the same plugin again
298
+ short.enablePlugin ( pluginScroll )
299
+ plugins = short.listPlugins ()
300
+ // Should still have only one instance
301
+ expect ( plugins ).to.include ( 'scroll' )
302
+ expect ( plugins ).to.have.lengthOf ( 1 )
303
+ }) // it enable already enabled plugin
304
+
305
+
306
+
307
+ it ( 'Disable a plugin', () => {
308
+ // Enable plugin
309
+ short.enablePlugin ( pluginScroll )
310
+ expect ( short.listPlugins () ).to.include ( 'scroll' )
311
+
312
+ // Disable plugin
313
+ short.disablePlugin ( 'scroll' )
314
+ expect ( short.listPlugins () ).to.not.include ( 'scroll' )
315
+ }) // it disable a plugin
316
+
317
+
318
+ it ( 'Extra parameters to plugin options', async () => {
319
+ short.enablePlugin ( pluginScroll )
320
+ const emit = [];
321
+ const setupContext = {
322
+ 'scroll:setup' : () => {
323
+ emit.push ( 'setup' )
324
+ return { minSpace: 10, customParam: 'scroll-test', emit }
325
+ },
326
+ 'scroll:down' : ({options}) => {
327
+ expect ( options.minSpace ).to.equal ( 10 )
328
+ expect ( options.customParam ).to.equal ( 'scroll-test' )
329
+ options.emit.push ( 'down' )
330
+ },
331
+ 'scroll:up' : ({options}) => {
332
+ expect ( options.minSpace ).to.equal ( 10 )
333
+ expect ( options.customParam ).to.equal ( 'scroll-test' )
334
+ options.emit.push ( 'up' )
335
+ },
336
+ 'scroll:right' : ({options}) => {
337
+ expect ( options.minSpace ).to.equal ( 10 )
338
+ expect ( options.customParam ).to.equal ( 'scroll-test' )
339
+ options.emit.push ( 'right' )
340
+ },
341
+ 'scroll:left' : ({options}) => {
342
+ expect ( options.minSpace ).to.equal ( 10 )
343
+ expect ( options.customParam ).to.equal ( 'scroll-test' )
344
+ options.emit.push ( 'left' )
345
+ }
346
+ } // setupContext
347
+
348
+ short.load ({ setupContext })
349
+ short.changeContext ( 'setupContext' )
350
+
351
+ // Setup event execution is on change context:
352
+ expect ( emit[0] ).to.equal ( 'setup' )
353
+
354
+ // Reset scroll position to (0, 0) first
355
+ window.scrollTo ( 0, 0 )
356
+ await wait ( 200 ) // Wait for any pending scroll events
357
+
358
+ // Test scroll with modified minSpace (smaller movements should trigger)
359
+ // We just need to verify that the options are accessible and contain our custom parameters
360
+ // The exact scroll behavior may vary in test environment
361
+ window.scrollTo ( 0, 50 ) // 50px movement, should definitely trigger with minSpace=10
362
+ await wait ( 100 )
363
+
364
+ // At least one scroll event should have been triggered with our custom options
365
+ expect ( emit.length ).to.be.greaterThan ( 1 )
366
+ expect ( emit[0] ).to.equal ( 'setup' )
367
+
368
+ // Verify that the scroll events have access to the custom parameters
369
+ // We don't need to assert exact scroll directions since test environment may behave differently
370
+ const scrollEvents = emit.filter(e => e !== 'setup')
371
+ expect ( scrollEvents.length ).to.be.greaterThan ( 0 )
372
+ }) // it extra parameters to plugin options
373
+
374
+ }) // describe Scroll plugin
@@ -1,10 +1,11 @@
1
1
  function Block () {
2
2
  return <>
3
- <div className="block" data-click="red"><span id='rspan'>Red</span> </div>
4
- <button className="big-btn" data-click="mega">Mega button</button>
3
+ <div className="block" data-click="red" data-hover="red"><span id='rspan'>Red</span> </div>
4
+ <button className="big-btn" data-click="mega" data-hover="blue">Mega button</button>
5
5
  <p>Some text with <a href="#" target="_blank">link <span id="anchor">in</span></a> it</p>
6
6
  <p><input id="name" type="text" /></p>
7
7
  <p><input type="text" id="age" /></p>
8
+ <p><button id="hidden" className="hide" data-click="hidden">Hidden button</button></p>
8
9
  </>
9
10
  }
10
11
 
@@ -18,4 +18,9 @@
18
18
  margin-left: 230px;
19
19
  background-color: skyblue;
20
20
  border-radius: 10px;
21
- }
21
+ }
22
+
23
+
24
+ .hide {
25
+ display: none;
26
+ }
package/vitest.config.js CHANGED
@@ -1,21 +1,23 @@
1
1
  import { defineConfig } from 'vitest/config'
2
2
  import react from '@vitejs/plugin-react'
3
+ import { playwright } from '@vitest/browser-playwright'
3
4
 
4
5
  export default defineConfig ({
5
6
  plugins: [react()],
6
7
  test: {
7
8
  coverage: {
8
- reporter: ['lcov', 'text-summary']
9
+ provider: 'v8',
10
+ reporter: ['lcov', 'html', 'text-summary' ]
9
11
  },
10
- browser: {
11
- enabled: true,
12
- headless: true,
13
- provider: 'playwright',
14
- instances: [
15
- {
16
- browser: 'chromium'
17
- }
18
- ]
19
- }
12
+ browser: {
13
+ enabled: true,
14
+ headless: true,
15
+ provider: playwright(),
16
+ instances: [
17
+ {
18
+ browser: 'chromium'
19
+ }
20
+ ]
21
+ }
20
22
  }
21
23
  })
@@ -1,41 +0,0 @@
1
- # Shortcut plugins
2
-
3
- Shortcut plugin should be a function that receives 3 arguments: dependencies, state and options. Shoud return an shortcut plugin API object.
4
-
5
- ```js
6
- function plugin ( dependencies, state, options ) {
7
- // Normalize all shortcuts related to this plugin
8
- // Setup some internal state
9
- // Start listening to DOM events if current context has shortcuts related to this plugin
10
- // ...
11
- return {
12
- // API
13
- getPrefix: function () {
14
- // return a plugin prefix
15
- },
16
- shortcutName : function ( shortcutName ) {
17
- // normalize shortcut name
18
- // return normalized shortcut name
19
- },
20
- contextChange: function ( context ) {
21
- // How plugin should react to context change
22
- // return void
23
- },
24
- mute: function () {
25
- // Function that will stop DOM events from being triggered
26
- },
27
- unmute: function () {
28
- // Function that will resume DOM events
29
- },
30
- destroy: function () {
31
- // Destroy plugin instance
32
- }
33
- }
34
- } // plugin
35
- ```
36
-
37
- State and dependencies are objects coming from the main library. **Dependencies** contains the library event emitter ( dependencies.ev). From state object you have access to the current context object state and loaded shortcuts contextes. Object **options** contains a plugin options provided by the user.
38
-
39
- If you need see an example of a shortcut plugin, check the available plugins in the library: key and click.
40
-
41
-