mockaton 12.2.2 → 12.3.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/src/client/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createElement as r,
3
3
  createSvgElement as s,
4
- className, restoreFocus, Fragment, adoptCSS
4
+ classNames, restoreFocus, Fragment, adoptCSS
5
5
  } from './dom-utils.js'
6
6
 
7
7
  import { store } from './app-store.js'
@@ -11,29 +11,25 @@ import { HEADER_502 } from './ApiConstants.js'
11
11
  import CSS from './styles.css' with { type: 'css' }
12
12
  adoptCSS(CSS)
13
13
 
14
- const FocusGroup = {
15
- ProxyToggler: 0,
16
- DelayToggler: 1,
17
- StatusToggler: 2,
18
- PreviewLink: 3
19
- }
20
14
 
21
15
  const t = translation => translation[0]
22
16
 
23
17
  store.onError = onError
24
18
  store.render = render
25
19
  store.renderRow = renderRow
26
- store.fetchState()
20
+
27
21
  initRealTimeUpdates()
28
22
  initKeyboardNavigation()
29
23
 
24
+ let mounted = false
30
25
  function render() {
31
- render.count++
32
- restoreFocus(() => document.body.replaceChildren(...App()))
26
+ restoreFocus(() => {
27
+ document.body.replaceChildren(...App())
28
+ })
33
29
  if (store.hasChosenLink)
34
30
  previewMock()
31
+ mounted = true
35
32
  }
36
- render.count = 0
37
33
 
38
34
 
39
35
  const leftSideRef = {}
@@ -47,10 +43,10 @@ function App() {
47
43
  style: { width: leftSideRef.width },
48
44
  className: CSS.leftSide
49
45
  },
50
- r('div', className(CSS.SubToolbar),
46
+ r('div', classNames(CSS.SubToolbar),
51
47
  GroupByMethod(),
52
48
  BulkSelector()),
53
- r('div', className(CSS.Table),
49
+ r('div', classNames(CSS.Table),
54
50
  MockList(),
55
51
  StaticFilesList())),
56
52
  r('div', { className: CSS.rightSide },
@@ -64,11 +60,12 @@ function Header() {
64
60
  r('header', null,
65
61
  r('a', {
66
62
  className: CSS.Logo,
67
- href: 'https://mockaton.com'
63
+ href: 'https://mockaton.com',
64
+ alt: t`Documentation`
68
65
  },
69
66
  Logo()),
70
67
  r('div', null,
71
- r('div', className(CSS.GlobalDelayWrap),
68
+ r('div', classNames(CSS.GlobalDelayWrap),
72
69
  GlobalDelayField(),
73
70
  GlobalDelayJitterField()),
74
71
  CookieSelector(),
@@ -92,10 +89,9 @@ function GlobalDelayField() {
92
89
  onWheel.timer = setTimeout(onChange.bind(this), 300)
93
90
  }
94
91
  return (
95
- r('label', className(CSS.Field, CSS.GlobalDelayField),
92
+ r('label', classNames(CSS.Field, CSS.GlobalDelayField),
96
93
  r('span', null, t`Delay (ms)`),
97
94
  r('input', {
98
- name: 'delay',
99
95
  type: 'number',
100
96
  min: 0,
101
97
  step: 100,
@@ -122,10 +118,9 @@ function GlobalDelayJitterField() {
122
118
  onWheel.timer = setTimeout(onChange.bind(this), 300)
123
119
  }
124
120
  return (
125
- r('label', className(CSS.Field, CSS.GlobalDelayJitterField),
121
+ r('label', classNames(CSS.Field, CSS.GlobalDelayJitterField),
126
122
  r('span', null, t`Max Jitter %`),
127
123
  r('input', {
128
- name: 'delay-jitter',
129
124
  type: 'number',
130
125
  min: 0,
131
126
  max: 300,
@@ -143,12 +138,14 @@ function CookieSelector() {
143
138
  const disabled = cookies.length <= 1
144
139
  const list = cookies.length ? cookies : [[t`None`, true]]
145
140
  return (
146
- r('label', className(CSS.Field, CSS.CookieSelector),
141
+ r('label', classNames(CSS.Field, CSS.CookieSelector),
147
142
  r('span', null, t`Cookie`),
148
143
  r('select', {
149
144
  autocomplete: 'off',
150
145
  disabled,
151
- title: disabled ? t`No cookies specified in config.cookies` : '',
146
+ title: disabled
147
+ ? t`No cookies specified in config.cookies`
148
+ : undefined,
152
149
  onChange() { store.selectCookie(this.value) }
153
150
  }, list.map(([value, selected]) =>
154
151
  r('option', { value, selected }, value)))))
@@ -165,13 +162,12 @@ function ProxyFallbackField() {
165
162
  store.setProxyFallback(this.value.trim())
166
163
  }
167
164
  return (
168
- r('div', className(CSS.Field, CSS.FallbackBackend),
165
+ r('div', classNames(CSS.Field, CSS.FallbackBackend),
169
166
  r('label', null,
170
167
  r('span', null, t`Fallback`),
171
168
  r('input', {
172
- name: 'fallback',
173
169
  type: 'url',
174
- autocomplete: 'none',
170
+ name: 'fallback',
175
171
  placeholder: t`Type backend address`,
176
172
  value: store.proxyFallback,
177
173
  onChange
@@ -181,16 +177,15 @@ function ProxyFallbackField() {
181
177
 
182
178
  function SaveProxiedCheckbox(ref) {
183
179
  return (
184
- r('label', className(CSS.SaveProxiedCheckbox),
180
+ r('label', classNames(CSS.SaveProxiedCheckbox),
185
181
  r('input', {
186
- name: 'save-proxied',
187
182
  ref,
188
183
  type: 'checkbox',
189
184
  disabled: !store.canProxy,
190
185
  checked: store.collectProxied,
191
186
  onChange() { store.setCollectProxied(this.checked) }
192
187
  }),
193
- r('span', className(CSS.checkboxBody), t`Save Mocks`)))
188
+ r('span', classNames(CSS.checkboxBody), t`Save Mocks`)))
194
189
  }
195
190
 
196
191
 
@@ -204,13 +199,13 @@ function ResetButton() {
204
199
 
205
200
 
206
201
  function HelpLink() {
207
- return r('a', {
202
+ return (
203
+ r('a', {
208
204
  target: '_blank',
209
205
  href: 'https://mockaton.com',
210
206
  title: t`Documentation`,
211
207
  className: CSS.HelpLink
212
- },
213
- HelpIcon())
208
+ }, HelpIcon()))
214
209
  }
215
210
 
216
211
 
@@ -227,12 +222,14 @@ function BulkSelector() {
227
222
  }
228
223
  const disabled = !comments.length
229
224
  return (
230
- r('label', className(CSS.BulkSelector),
225
+ r('label', classNames(CSS.BulkSelector),
231
226
  r('span', null, t`Bulk Select`),
232
227
  r('select', {
233
228
  autocomplete: 'off',
234
229
  disabled,
235
- title: disabled ? t`No mock files have comments which are anything within parentheses on the filename.` : '',
230
+ title: disabled
231
+ ? t`No mock files have comments which are anything within parentheses on the filename.`
232
+ : undefined,
236
233
  onChange
237
234
  },
238
235
  r('option', { value: firstOption }, firstOption),
@@ -244,14 +241,13 @@ function BulkSelector() {
244
241
 
245
242
  function GroupByMethod() {
246
243
  return (
247
- r('label', className(CSS.GroupByMethod),
244
+ r('label', classNames(CSS.GroupByMethod),
248
245
  r('input', {
249
- name: 'group-by-method',
250
246
  type: 'checkbox',
251
247
  checked: store.groupByMethod,
252
248
  onChange: store.toggleGroupByMethod
253
249
  }),
254
- r('span', className(CSS.checkboxBody), t`Group by Method`)))
250
+ r('span', classNames(CSS.checkboxBody), t`Group by Method`)))
255
251
  }
256
252
 
257
253
 
@@ -262,10 +258,10 @@ function MockList() {
262
258
  return r('div', null, t`No mocks found`)
263
259
 
264
260
  if (store.groupByMethod)
265
- return Object.keys(store.brokersByMethod).map(method => Fragment(
266
- r('div', className(CSS.TableHeading, store.canProxy && CSS.canProxy),
267
- method),
268
- store.brokersAsRowsByMethod(method).map(Row)))
261
+ return Object.keys(store.brokersByMethod).map(method =>
262
+ Fragment(
263
+ r('div', classNames(CSS.TableHeading, store.canProxy && CSS.canProxy), method),
264
+ store.brokersAsRowsByMethod(method).map(Row)))
269
265
 
270
266
  return store.brokersAsRowsByMethod('*').map(Row)
271
267
  }
@@ -279,18 +275,29 @@ function Row(row, i) {
279
275
  return (
280
276
  r('div', {
281
277
  key: row.key,
282
- ...className(CSS.TableRow,
283
- render.count > 1 && row.isNew && CSS.animIn)
278
+ ...classNames(CSS.TableRow,
279
+ mounted && row.isNew && CSS.animIn)
284
280
  },
285
281
  store.canProxy && ProxyToggler(method, urlMask, row.proxied),
286
282
 
287
- DelayRouteToggler(method, urlMask, row.delayed),
283
+ DelayToggler({
284
+ checked: row.delayed,
285
+ commit(checked) {
286
+ store.setDelayed(method, urlMask, checked)
287
+ },
288
+ }),
288
289
 
289
- InternalServerErrorToggler(method, urlMask,
290
- !row.proxied && row.status === 500, // checked
291
- row.opts.length === 1 && row.status === 500), // disabled
290
+ StatusCodeToggler({
291
+ title: t`Internal Server Error`,
292
+ body: t`500`,
293
+ disabled: row.opts.length === 1 && row.status === 500,
294
+ checked: !row.proxied && row.status === 500,
295
+ commit() {
296
+ store.toggle500(method, urlMask)
297
+ }
298
+ }),
292
299
 
293
- !store.groupByMethod && r('span', className(CSS.Method), method),
300
+ !store.groupByMethod && r('span', classNames(CSS.Method), method),
294
301
 
295
302
  PreviewLink(method, urlMask, row.urlMaskDittoed, i === 0),
296
303
 
@@ -298,39 +305,58 @@ function Row(row, i) {
298
305
  }
299
306
 
300
307
  function renderRow(method, urlMask) {
301
- restoreFocus(() => {
302
- unChooseOld()
303
- const row = store.brokerAsRow(method, urlMask)
304
- trFor(row.key).replaceWith(Row(row))
305
- previewMock()
306
- })
307
-
308
- function trFor(key) {
309
- return leftSideRef.elem.querySelector(`.${CSS.TableRow}[key="${key}"]`)
310
- }
311
- function unChooseOld() {
312
- return leftSideRef.elem.querySelector(`a.${CSS.chosen}`)
313
- ?.classList.remove(CSS.chosen)
308
+ const row = store.brokerAsRow(method, urlMask)
309
+ const tr = leftSideRef.elem.querySelector(`.${CSS.TableRow}[key="${row.key}"]`)
310
+ mergeTableRow(tr, Row(row))
311
+ }
312
+
313
+ function mergeTableRow(oldRow, newRow) {
314
+ for (let i = 0; i < newRow.children.length; i++) {
315
+ const oldEl = oldRow.children[i]
316
+ const newEl = newRow.children[i]
317
+ switch (newEl.tagName) {
318
+ case 'LABEL': {
319
+ const oldInput = oldEl.querySelector('[type="checkbox"]')
320
+ const newInput = newEl.querySelector('[type="checkbox"]')
321
+ oldInput.checked = newInput.checked
322
+ oldInput.disabled = newInput.disabled
323
+ break
324
+ }
325
+ case 'A':
326
+ oldEl.className = newEl.className
327
+ break
328
+ case 'SELECT':
329
+ oldEl.disabled = newEl.disabled
330
+ oldEl.replaceChildren(...newEl.cloneNode(true).children)
331
+ break
332
+ }
314
333
  }
315
334
  }
316
335
 
317
336
 
337
+
318
338
  function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
319
339
  function onClick(event) {
340
+ unChooseOld()
320
341
  event.preventDefault()
321
342
  store.previewLink(method, urlMask)
343
+ previewMock()
344
+ }
345
+ function unChooseOld() {
346
+ return leftSideRef.elem.querySelector(`a.${CSS.chosen}`)
347
+ ?.classList.remove(CSS.chosen)
322
348
  }
349
+
323
350
  const isChosen = store.chosenLink.method === method && store.chosenLink.urlMask === urlMask
324
351
  const [ditto, tail] = urlMaskDittoed
325
352
  return (
326
353
  r('a', {
327
- ...className(CSS.PreviewLink, isChosen && CSS.chosen),
354
+ ...classNames(CSS.PreviewLink, isChosen && CSS.chosen),
328
355
  href: urlMask,
329
356
  autofocus,
330
- 'data-focus-group': FocusGroup.PreviewLink,
331
357
  onClick
332
358
  }, ditto
333
- ? [r('span', className(CSS.dittoDir), ditto), tail]
359
+ ? [r('span', classNames(CSS.dittoDir), ditto), tail]
334
360
  : tail))
335
361
  }
336
362
 
@@ -339,11 +365,18 @@ function PreviewLink(method, urlMask, urlMaskDittoed, autofocus) {
339
365
  function MockSelector(row) {
340
366
  return (
341
367
  r('select', {
342
- onChange() { store.selectFile(this.value) },
343
- autocomplete: 'off',
368
+ onChange() {
369
+ store.selectFile(this.value)
370
+ },
371
+ onKeyDown(event) {
372
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft')
373
+ event.preventDefault()
374
+ // Because in Firefox they change the select.option, and
375
+ // we use those keys for spreadsheet-like navigation.
376
+ },
344
377
  'aria-label': t`Mock Selector`,
345
378
  disabled: row.opts.length < 2,
346
- ...className(
379
+ ...classNames(
347
380
  CSS.MockSelector,
348
381
  row.selectedIdx > 0 && CSS.nonDefault,
349
382
  row.selectedFileIs4xx && CSS.status4xx)
@@ -352,45 +385,18 @@ function MockSelector(row) {
352
385
  }
353
386
 
354
387
 
355
- function DelayRouteToggler(method, urlMask, checked) {
388
+ function ProxyToggler(method, urlMask, checked) {
356
389
  return ClickDragToggler({
390
+ className: CSS.ProxyToggler,
391
+ title: t`Proxy Toggler`,
357
392
  checked,
358
- commit(checked) { store.setDelayed(method, urlMask, checked) },
359
- focusGroup: FocusGroup.DelayToggler
393
+ commit(checked) {
394
+ store.setProxied(method, urlMask, checked)
395
+ },
396
+ body: CloudIcon()
360
397
  })
361
398
  }
362
399
 
363
- function InternalServerErrorToggler(method, urlMask, checked, disabled) {
364
- return (
365
- r('label', {
366
- className: CSS.InternalServerErrorToggler,
367
- title: t`Internal Server Error`
368
- },
369
- r('input', {
370
- type: 'checkbox',
371
- disabled,
372
- checked,
373
- onChange() { store.toggle500(method, urlMask) },
374
- 'data-focus-group': FocusGroup.StatusToggler
375
- }),
376
- r('span', className(CSS.checkboxBody), t`500`)))
377
- }
378
-
379
- function ProxyToggler(method, urlMask, checked) {
380
- return (
381
- r('label', {
382
- className: CSS.ProxyToggler,
383
- title: t`Proxy Toggler`
384
- },
385
- r('input', {
386
- type: 'checkbox',
387
- checked,
388
- onChange() { store.setProxied(method, urlMask, this.checked) },
389
- 'data-focus-group': FocusGroup.ProxyToggler
390
- }),
391
- CloudIcon()))
392
- }
393
-
394
400
 
395
401
 
396
402
  /** # StaticFilesList */
@@ -401,10 +407,12 @@ function StaticFilesList() {
401
407
  ? null
402
408
  : Fragment(
403
409
  r('div',
404
- className(CSS.TableHeading,
410
+ classNames(CSS.TableHeading,
405
411
  store.canProxy && CSS.canProxy,
406
412
  !store.groupByMethod && CSS.nonGroupedByMethod),
407
- store.groupByMethod ? t`Static GET` : t`Static`),
413
+ store.groupByMethod
414
+ ? t`Static GET`
415
+ : t`Static`),
408
416
  rows.map(StaticRow))
409
417
  }
410
418
 
@@ -415,55 +423,62 @@ function StaticRow(row) {
415
423
  return (
416
424
  r('div', {
417
425
  key: row.key,
418
- ...className(CSS.TableRow,
419
- render.count > 1 && row.isNew && CSS.animIn)
426
+ ...classNames(CSS.TableRow,
427
+ mounted && row.isNew && CSS.animIn)
420
428
  },
421
- DelayStaticRouteToggler(row.urlMask, row.delayed),
422
429
 
423
- NotFoundToggler(row.urlMask, row.status === 404),
430
+ DelayToggler({
431
+ optClassName: store.canProxy && CSS.canProxy,
432
+ checked: row.delayed,
433
+ commit(checked) {
434
+ store.setDelayedStatic(row.urlMask, checked)
435
+ }
436
+ }),
437
+
438
+ StatusCodeToggler({
439
+ title: t`Not Found`,
440
+ body: t`404`,
441
+ checked: row.status === 404,
442
+ commit(checked) {
443
+ store.setStaticRouteStatus(row.urlMask, checked
444
+ ? 404
445
+ : 200)
446
+ }
447
+ }),
424
448
 
425
- !groupByMethod && r('span', className(CSS.Method), 'GET'),
449
+ !groupByMethod && r('span', classNames(CSS.Method), 'GET'),
426
450
 
427
451
  r('a', {
428
452
  href: row.urlMask,
429
453
  target: '_blank',
430
454
  className: CSS.PreviewLink,
431
- 'data-focus-group': FocusGroup.PreviewLink
432
455
  }, ditto
433
- ? [r('span', className(CSS.dittoDir), ditto), tail]
456
+ ? [r('span', classNames(CSS.dittoDir), ditto), tail]
434
457
  : tail)))
435
458
  }
436
459
 
437
- function DelayStaticRouteToggler(route, checked) {
460
+ function StatusCodeToggler({ title, body, commit, checked, disabled }) {
438
461
  return ClickDragToggler({
439
- optClassName: store.canProxy && CSS.canProxy,
462
+ title,
463
+ disabled,
464
+ className: CSS.StatusCodeToggler,
465
+ commit,
440
466
  checked,
441
- focusGroup: FocusGroup.DelayToggler,
442
- commit(checked) {
443
- store.setDelayedStatic(route, checked)
444
- }
467
+ body
445
468
  })
446
469
  }
447
470
 
448
- function NotFoundToggler(route, checked) {
449
- return (
450
- r('label', {
451
- className: CSS.NotFoundToggler,
452
- title: t`Not Found`
453
- },
454
- r('input', {
455
- type: 'checkbox',
456
- checked,
457
- 'data-focus-group': FocusGroup.StatusToggler,
458
- onChange() {
459
- store.setStaticRouteStatus(route, this.checked ? 404 : 200)
460
- }
461
- }),
462
- r('span', className(CSS.checkboxBody), t`404`)))
471
+ function DelayToggler({ checked, commit, optClassName }) {
472
+ return ClickDragToggler({
473
+ checked,
474
+ commit,
475
+ ...classNames(CSS.DelayToggler, optClassName),
476
+ title: t`Delay`,
477
+ body: TimerIcon()
478
+ })
463
479
  }
464
480
 
465
-
466
- function ClickDragToggler({ checked, commit, focusGroup, optClassName }) {
481
+ function ClickDragToggler({ checked, commit, className, title, body }) {
467
482
  function onPointerEnter(event) {
468
483
  if (event.buttons === 1)
469
484
  onPointerDown.call(this)
@@ -481,20 +496,16 @@ function ClickDragToggler({ checked, commit, focusGroup, optClassName }) {
481
496
  commit(this.checked)
482
497
  }
483
498
  return (
484
- r('label', {
485
- ...className(CSS.DelayToggler, optClassName),
486
- title: t`Delay`
487
- },
499
+ r('label', { ...classNames(CSS.Toggler, className), title },
488
500
  r('input', {
489
501
  type: 'checkbox',
490
- 'data-focus-group': focusGroup,
491
502
  checked,
492
503
  onPointerEnter,
493
504
  onPointerDown,
494
505
  onClick,
495
506
  onChange
496
507
  }),
497
- TimerIcon()))
508
+ r('span', classNames(CSS.checkboxBody), body)))
498
509
  }
499
510
 
500
511
  function Resizer(ref) {
@@ -549,7 +560,7 @@ const payloadViewerCodeRef = {}
549
560
 
550
561
  function PayloadViewer() {
551
562
  return (
552
- r('div', className(CSS.PayloadViewer),
563
+ r('div', classNames(CSS.PayloadViewer),
553
564
  RightToolbar(),
554
565
  r('pre', null,
555
566
  r('code', { ref: payloadViewerCodeRef },
@@ -557,9 +568,10 @@ function PayloadViewer() {
557
568
  }
558
569
 
559
570
  function RightToolbar() {
560
- return r('div', className(CSS.SubToolbar),
561
- r('h2', { ref: payloadViewerTitleRef },
562
- !store.hasChosenLink && t`Preview`))
571
+ return (
572
+ r('div', classNames(CSS.SubToolbar),
573
+ r('h2', { ref: payloadViewerTitleRef },
574
+ !store.hasChosenLink && t`Preview`)))
563
575
  }
564
576
 
565
577
 
@@ -588,8 +600,12 @@ function PayloadViewerTitleWhenProxied(response) {
588
600
  const SPINNER_DELAY = 80
589
601
  function PayloadViewerProgressBar() {
590
602
  return (
591
- r('div', className(CSS.ProgressBar),
592
- r('div', { style: { animationDuration: store.delay - SPINNER_DELAY + 'ms' } })))
603
+ r('div', classNames(CSS.ProgressBar),
604
+ r('div', {
605
+ style: {
606
+ animationDuration: store.delay - SPINNER_DELAY + 'ms'
607
+ }
608
+ })))
593
609
  }
594
610
 
595
611
  async function previewMock() {
@@ -724,7 +740,7 @@ function HelpIcon() {
724
740
  * The version increments when a mock file is added, removed, or renamed.
725
741
  */
726
742
  function initRealTimeUpdates() {
727
- let oldVersion = undefined // undefined so it waits until next event or timeout
743
+ let oldVersion = -1
728
744
  let controller = new AbortController()
729
745
 
730
746
  longPoll()
@@ -740,21 +756,18 @@ function initRealTimeUpdates() {
740
756
  async function longPoll() {
741
757
  try {
742
758
  const response = await store.getSyncVersion(oldVersion, controller.signal)
743
- if (response.ok) {
744
- if (ErrorToast.isOffline)
745
- ErrorToast.close()
746
-
747
- const version = await response.json()
748
- const shouldSkip = oldVersion === undefined
749
- if (oldVersion !== version) { // because it could be < or >
750
- oldVersion = version
751
- if (!shouldSkip)
752
- store.fetchState()
753
- }
754
- longPoll()
755
- }
756
- else
759
+ if (!response.ok)
757
760
  throw response.status
761
+
762
+ if (ErrorToast.isOffline)
763
+ ErrorToast.close()
764
+
765
+ const version = await response.json()
766
+ if (oldVersion !== version) { // because it could be < or >
767
+ oldVersion = version
768
+ store.fetchState()
769
+ }
770
+ longPoll()
758
771
  }
759
772
  catch (error) {
760
773
  if (error !== '_hidden_tab_')
@@ -765,45 +778,48 @@ function initRealTimeUpdates() {
765
778
 
766
779
 
767
780
  function initKeyboardNavigation() {
768
- addEventListener('keydown', onKeyDown)
781
+ const columnSelectors = [
782
+ `.${CSS.TableRow} .${CSS.ProxyToggler} input`,
783
+ `.${CSS.TableRow} .${CSS.DelayToggler} input`,
784
+ `.${CSS.TableRow} .${CSS.StatusCodeToggler} input`,
785
+ `.${CSS.TableRow} .${CSS.PreviewLink}`,
786
+ // No .MockSelector because down/up arrows have native behavior on them
787
+ ]
769
788
 
770
- function onKeyDown(event) {
771
- const pivot = document.activeElement
772
- switch (event.key) {
789
+ const rowSelectors = [
790
+ ...columnSelectors,
791
+ `.${CSS.TableRow} .${CSS.MockSelector}:enabled`,
792
+ ]
793
+
794
+ addEventListener('keydown', function ({ key }) {
795
+ switch (key) {
773
796
  case 'ArrowDown':
774
797
  case 'ArrowUp': {
775
- let fg = pivot.getAttribute('data-focus-group')
776
- if (fg !== null) {
777
- const offset = event.key === 'ArrowDown' ? +1 : -1
778
- circularAdjacent(offset, allInFocusGroup(+fg), pivot).focus()
798
+ const pivot = document.activeElement
799
+ const sel = columnSelectors.find(s => pivot?.matches(s))
800
+ if (sel) {
801
+ const offset = key === 'ArrowDown' ? +1 : -1
802
+ const siblings = leftSideRef.elem.querySelectorAll(sel)
803
+ circularAdjacent(offset, siblings, pivot).focus()
779
804
  }
780
805
  break
781
806
  }
782
807
  case 'ArrowRight':
783
808
  case 'ArrowLeft': {
784
- if (pivot.hasAttribute('data-focus-group') || pivot.classList.contains(CSS.MockSelector)) {
785
- const offset = event.key === 'ArrowRight' ? +1 : -1
786
- rowFocusable(pivot, offset).focus()
809
+ const pivot = document.activeElement
810
+ const sel = rowSelectors.find(s => pivot?.matches(s))
811
+ if (sel) {
812
+ const offset = key === 'ArrowRight' ? +1 : -1
813
+ const siblings = pivot.closest(`.${CSS.TableRow}`).querySelectorAll(rowSelectors.join(','))
814
+ circularAdjacent(offset, siblings, pivot).focus()
787
815
  }
788
816
  break
789
817
  }
790
818
  }
791
- }
792
-
793
- function rowFocusable(el, step) {
794
- const row = el.closest(`.${CSS.TableRow}`)
795
- if (row) {
796
- const focusables = Array.from(row.querySelectorAll('a, input, select:not(:disabled)'))
797
- return circularAdjacent(step, focusables, el)
798
- }
799
- }
800
-
801
- function allInFocusGroup(focusGroup) {
802
- return Array.from(leftSideRef.elem.querySelectorAll(
803
- `.${CSS.TableRow} [data-focus-group="${focusGroup}"]:is(input, a)`))
804
- }
819
+ })
805
820
 
806
- function circularAdjacent(step = 1, arr, pivot) {
821
+ function circularAdjacent(step, siblings, pivot) {
822
+ const arr = Array.from(siblings)
807
823
  return arr[(arr.indexOf(pivot) + step + arr.length) % arr.length]
808
824
  }
809
825
  }