@rokkit/actions 1.0.0-next.37 → 1.0.0-next.39

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.37",
3
+ "version": "1.0.0-next.39",
4
4
  "description": "Contains generic actions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "validators": "latest",
24
24
  "vite": "^4.4.7",
25
25
  "vitest": "~0.33.0",
26
- "shared-config": "1.0.0-next.37"
26
+ "shared-config": "1.0.0-next.39"
27
27
  },
28
28
  "files": [
29
29
  "src/**/*.js",
package/src/hierarchy.js CHANGED
@@ -1,47 +1,4 @@
1
- /**
2
- * Check if the current item is a parent
3
- *
4
- * @param {*} item
5
- * @param {import('@rokkit/core').FieldMapping} fields
6
- * @returns {boolean}
7
- */
8
- export function hasChildren(item, fields) {
9
- return (
10
- typeof item === 'object' &&
11
- fields.children in item &&
12
- Array.isArray(item[fields.children])
13
- )
14
- }
15
-
16
- /**
17
- * Check if the current item is a parent and is expanded
18
- *
19
- * @param {*} item
20
- * @param {import('@rokkit/core').FieldMapping} fields
21
- * @returns {boolean}
22
- */
23
- export function isExpanded(item, fields) {
24
- if (item == null) return false
25
- if (!hasChildren(item, fields)) return false
26
- if (fields.isOpen in item) {
27
- return item[fields.isOpen]
28
- }
29
- return false
30
- }
31
-
32
- /**
33
- * Verify if at least one item has children
34
- *
35
- * @param {Array<*>} items
36
- * @param {import('@rokkit/core').FieldMapping} fields
37
- * @returns {boolean}
38
- */
39
- export function isNested(items, fields) {
40
- for (let i = 0; i < items.length; i++) {
41
- if (hasChildren(items[i], fields)) return true
42
- }
43
- return false
44
- }
1
+ import { isExpanded } from '@rokkit/core'
45
2
 
46
3
  /**
47
4
  * Navigate to last visible child in the hirarchy starting with the provided path
@@ -0,0 +1 @@
1
+ export * from './internal'
@@ -0,0 +1,89 @@
1
+ import { compact } from '@rokkit/core'
2
+
3
+ /**
4
+ * Emits a custom event with the given data.
5
+ *
6
+ * @param {HTMLElement} element
7
+ * @param {string} event
8
+ * @param {*} data
9
+ * @returns {void}
10
+ */
11
+ export function emit(element, event, data) {
12
+ element.dispatchEvent(new CustomEvent(event, { detail: data }))
13
+ }
14
+
15
+ /**
16
+ * Maps keyboard events to actions based on the given handlers and options.
17
+ *
18
+ * @param {import('../types').ActionHandlers} handlers
19
+ * @param {import('../types').NavigationOptions} options
20
+ * @returns {import('../types').KeyboardActions}
21
+ */
22
+ export function mapKeyboardEventsToActions(handlers, options) {
23
+ const { next, previous, select, escape } = handlers
24
+ const { horizontal, nested } = {
25
+ horizontal: false,
26
+ nested: false,
27
+ ...options
28
+ }
29
+ let expand = nested ? handlers.expand : null
30
+ let collapse = nested ? handlers.collapse : null
31
+
32
+ return compact({
33
+ ArrowDown: horizontal ? expand : next,
34
+ ArrowUp: horizontal ? collapse : previous,
35
+ ArrowRight: horizontal ? next : expand,
36
+ ArrowLeft: horizontal ? previous : collapse,
37
+ Enter: select,
38
+ Escape: escape,
39
+ ' ': select
40
+ })
41
+ }
42
+
43
+ /**
44
+ * Finds the closest ancestor of the given element that has the given attribute.
45
+ *
46
+ * @param {HTMLElement} element
47
+ * @param {string} attribute
48
+ * @returns {HTMLElement|null}
49
+ */
50
+ export function getClosestAncestorWithAttribute(element, attribute) {
51
+ if (!element) return null
52
+ if (element.getAttribute(attribute)) return element
53
+ return getClosestAncestorWithAttribute(element.parentElement, attribute)
54
+ }
55
+
56
+ /**
57
+ * Sets up event handlers based on the given options.
58
+ * Returns whether or not the event handlers are listening.
59
+ *
60
+ * @param {HTMLElement} element
61
+ * @param {import('../types').EventHandlers} listeners
62
+ * @param {import('../types').TraversableOptions} options
63
+ * @returns {void}
64
+ */
65
+ export function setupListeners(element, listeners, options) {
66
+ const { enabled } = { enabled: true, ...options }
67
+
68
+ if (enabled) {
69
+ Object.entries(listeners).forEach(([event, listener]) =>
70
+ element.addEventListener(event, listener)
71
+ )
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Removes event handlers based on the given options.
77
+ * Returns whether or not the event handlers are listening.
78
+ *
79
+ * @param {HTMLElement} element
80
+ * @param {import('../types').EventHandlers} listeners
81
+ * @returns {void}
82
+ */
83
+ export function removeListeners(element, listeners) {
84
+ if (listeners) {
85
+ Object.entries(listeners).forEach(([event, listener]) => {
86
+ element.removeEventListener(event, listener)
87
+ })
88
+ }
89
+ }
package/src/navigable.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { handleAction, getKeyboardActions } from './utils'
2
+
3
+ const defaultOptions = { horizontal: true, nested: false, enabled: true }
1
4
  /**
2
5
  * A svelte action function that captures keyboard evvents and emits event for corresponding movements.
3
6
  *
@@ -5,45 +8,40 @@
5
8
  * @param {import('./types').NavigableOptions} options
6
9
  * @returns {import('./types').SvelteActionReturn}
7
10
  */
8
- export function navigable(
9
- node,
10
- { horizontal = true, nested = false, enabled = true } = {}
11
- ) {
12
- if (!enabled) return { destroy() {} }
13
- const previous = () => node.dispatchEvent(new CustomEvent('previous'))
14
- const next = () => node.dispatchEvent(new CustomEvent('next'))
15
- const collapse = () => node.dispatchEvent(new CustomEvent('collapse'))
16
- const expand = () => node.dispatchEvent(new CustomEvent('expand'))
17
- const select = () => node.dispatchEvent(new CustomEvent('select'))
11
+ export function navigable(node, options) {
12
+ options = { ...defaultOptions, ...options }
18
13
 
19
- const movement = horizontal
20
- ? { ArrowLeft: previous, ArrowRight: next }
21
- : { ArrowUp: previous, ArrowDown: next }
22
- const change = nested
23
- ? horizontal
24
- ? { ArrowUp: collapse, ArrowDown: expand }
25
- : { ArrowLeft: collapse, ArrowRight: expand }
26
- : {}
27
- const actions = {
28
- Enter: select,
29
- ' ': select,
30
- ...movement,
31
- ...change
14
+ let listening = false
15
+ const handlers = {
16
+ previous: () => node.dispatchEvent(new CustomEvent('previous')),
17
+ next: () => node.dispatchEvent(new CustomEvent('next')),
18
+ collapse: () => node.dispatchEvent(new CustomEvent('collapse')),
19
+ expand: () => node.dispatchEvent(new CustomEvent('expand')),
20
+ select: () => node.dispatchEvent(new CustomEvent('select'))
32
21
  }
33
22
 
34
- function handleKeydown(event) {
35
- if (actions[event.key]) {
36
- event.preventDefault()
37
- event.stopPropagation()
38
- actions[event.key]()
39
- }
23
+ let actions = {} //getKeyboardActions(node, { horizontal, nested })
24
+ const handleKeydown = (event) => handleAction(actions, event)
25
+
26
+ function updateListeners(options) {
27
+ if (options.enabled) actions = getKeyboardActions(node, options, handlers)
28
+
29
+ if (options.enabled && !listening)
30
+ node.addEventListener('keydown', handleKeydown)
31
+ else if (!options.enabled && listening)
32
+ node.removeEventListener('keydown', handleKeydown)
33
+ listening = options.enabled
40
34
  }
41
35
 
42
- node.addEventListener('keydown', handleKeydown)
36
+ updateListeners(options)
43
37
 
44
38
  return {
45
- destroy() {
46
- node.removeEventListener('keydown', handleKeydown)
39
+ update: (config) => {
40
+ options = { ...options, ...config }
41
+ updateListeners(options)
42
+ },
43
+ destroy: () => {
44
+ updateListeners({ enabled: false })
47
45
  }
48
46
  }
49
47
  }
package/src/navigator.js CHANGED
@@ -1,13 +1,11 @@
1
- // import { tick } from 'svelte'
1
+ import { handleAction } from './utils'
2
+ import { isNested, hasChildren, isExpanded } from '@rokkit/core'
2
3
  import {
3
4
  moveNext,
4
5
  movePrevious,
5
- isNested,
6
- hasChildren,
7
6
  pathFromIndices,
8
7
  indicesFromPath,
9
- getCurrentNode,
10
- isExpanded
8
+ getCurrentNode
11
9
  } from './hierarchy'
12
10
 
13
11
  /**
@@ -80,27 +78,14 @@ export function navigator(element, options) {
80
78
  emit('expand', element, indicesFromPath(path), currentNode)
81
79
  }
82
80
  }
81
+ const handlers = { next, previous, select, collapse, expand }
83
82
 
84
83
  update(options)
85
84
 
86
85
  const nested = isNested(items, fields)
87
- const movement = vertical
88
- ? { ArrowDown: next, ArrowUp: previous }
89
- : { ArrowRight: next, ArrowLeft: previous }
90
- const states = !nested
91
- ? {}
92
- : vertical
93
- ? { ArrowRight: expand, ArrowLeft: collapse }
94
- : { ArrowDown: expand, ArrowUp: collapse }
95
- const actions = { ...movement, Enter: select, ...states }
96
-
97
- const handleKeyDown = (event) => {
98
- if (actions[event.key]) {
99
- event.preventDefault()
100
- event.stopPropagation()
101
- actions[event.key]()
102
- }
103
- }
86
+ const actions = mapKeyboardEventsToActions(vertical, nested, handlers)
87
+
88
+ const handleKeyDown = (event) => handleAction(actions, event)
104
89
 
105
90
  const handleClick = (event) => {
106
91
  let target = findParentWithDataPath(event.target, element)
@@ -189,3 +174,27 @@ function emit(event, element, indices, node) {
189
174
  })
190
175
  )
191
176
  }
177
+
178
+ function mapKeyboardEventsToActions(vertical, nested, handlers) {
179
+ let actions = { Enter: handlers.select }
180
+
181
+ if (vertical) {
182
+ actions = {
183
+ ...actions,
184
+ ...{ ArrowDown: handlers.next, ArrowUp: handlers.previous },
185
+ ...(nested
186
+ ? { ArrowRight: handlers.expand, ArrowLeft: handlers.collapse }
187
+ : {})
188
+ }
189
+ } else {
190
+ actions = {
191
+ ...actions,
192
+ ...{ ArrowRight: handlers.next, ArrowLeft: handlers.previous },
193
+ ...(nested
194
+ ? { ArrowDown: handlers.expand, ArrowUp: handlers.collapse }
195
+ : {})
196
+ }
197
+ }
198
+
199
+ return actions
200
+ }
package/src/pannable.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { removeListeners, setupListeners } from './lib'
2
+
1
3
  /**
2
4
  * Handle drag and move events
3
5
  *
@@ -7,6 +9,18 @@
7
9
  export function pannable(node) {
8
10
  let x
9
11
  let y
12
+ let listeners = {
13
+ primary: {
14
+ mousedown: start,
15
+ touchstart: start
16
+ },
17
+ secondary: {
18
+ mousemove: move,
19
+ mouseup: stop,
20
+ touchmove: move,
21
+ touchend: stop
22
+ }
23
+ }
10
24
 
11
25
  function track(event, name, delta = {}) {
12
26
  x = event.clientX || event.touches[0].clientX
@@ -20,37 +34,26 @@ export function pannable(node) {
20
34
  )
21
35
  }
22
36
 
23
- function handleMousedown(event) {
37
+ function start(event) {
24
38
  track(event, 'panstart')
25
- window.addEventListener('mousemove', handleMousemove)
26
- window.addEventListener('mouseup', handleMouseup)
27
- window.addEventListener('touchmove', handleMousemove, { passive: false })
28
- window.addEventListener('touchend', handleMouseup)
39
+ setupListeners(window, listeners.secondary)
29
40
  }
30
41
 
31
- function handleMousemove(event) {
42
+ function move(event) {
32
43
  const dx = (event.clientX || event.touches[0].clientX) - x
33
44
  const dy = (event.clientY || event.touches[0].clientY) - y
34
45
 
35
46
  track(event, 'panmove', { dx, dy })
36
47
  }
37
48
 
38
- function handleMouseup(event) {
49
+ function stop(event) {
39
50
  track(event, 'panend')
40
-
41
- window.removeEventListener('mousemove', handleMousemove)
42
- window.removeEventListener('mouseup', handleMouseup)
43
- window.removeEventListener('touchmove', handleMousemove)
44
- window.removeEventListener('touchend', handleMouseup)
51
+ removeListeners(window, listeners.secondary)
45
52
  }
46
53
 
47
- node.addEventListener('mousedown', handleMousedown)
48
- node.addEventListener('touchstart', handleMousedown, { passive: false })
54
+ setupListeners(node, listeners.primary)
49
55
 
50
56
  return {
51
- destroy() {
52
- node.removeEventListener('mousedown', handleMousedown)
53
- node.removeEventListener('touchstart', handleMousedown)
54
- }
57
+ destroy: () => removeListeners(node, listeners.primary)
55
58
  }
56
59
  }
package/src/swipeable.js CHANGED
@@ -6,68 +6,82 @@
6
6
  * @returns {import('./types').SvelteActionReturn}
7
7
  */
8
8
 
9
- export function swipeable(
10
- node,
11
- {
12
- horizontal = true,
13
- vertical = false,
14
- threshold = 100,
15
- enabled = true,
16
- minSpeed = 300
17
- } = {}
18
- ) {
19
- if (!enabled) return { destroy() {} }
20
-
21
- let startX
22
- let startY
23
- let startTime
9
+ import { removeListeners, setupListeners } from './lib'
10
+ const defaultOptions = {
11
+ horizontal: true,
12
+ vertical: false,
13
+ threshold: 100,
14
+ enabled: true,
15
+ minSpeed: 300
16
+ }
17
+ export function swipeable(node, options = defaultOptions) {
18
+ let track = {}
19
+ let listeners = {}
24
20
 
25
- function touchStart(event) {
26
- const touch = event.touches ? event.touches[0] : event
27
- startX = touch.clientX
28
- startY = touch.clientY
29
- startTime = new Date().getTime()
21
+ const updateListeners = (options) => {
22
+ removeListeners(node, listeners)
23
+ listeners = getListeners(node, options, track)
24
+ setupListeners(node, listeners, options)
30
25
  }
31
26
 
32
- function touchEnd(event) {
33
- const touch = event.changedTouches ? event.changedTouches[0] : event
34
- const distX = touch.clientX - startX
35
- const distY = touch.clientY - startY
36
- const duration = (new Date().getTime() - startTime) / 1000
37
- const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
27
+ options = { ...defaultOptions, ...options }
28
+ updateListeners(options)
38
29
 
39
- if (horizontal && speed > minSpeed) {
40
- if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) >= threshold) {
41
- if (distX > 0 && distX / duration > minSpeed) {
42
- node.dispatchEvent(new CustomEvent('swipeRight'))
43
- } else {
44
- node.dispatchEvent(new CustomEvent('swipeLeft'))
45
- }
46
- }
30
+ return {
31
+ update: (data) => {
32
+ options = { ...options, ...data }
33
+ updateListeners(options)
34
+ },
35
+ destroy() {
36
+ removeListeners(node, listeners)
47
37
  }
38
+ }
39
+ }
40
+
41
+ function getListeners(node, options, track) {
42
+ if (!options.enabled) return {}
43
+
44
+ let listeners = {
45
+ touchend: (e) => touchEnd(e, node, options, track),
46
+ touchstart: (e) => touchStart(e, track),
47
+ mousedown: (e) => touchStart(e, track),
48
+ mouseup: (e) => touchEnd(e, node, options, track)
49
+ }
50
+ return listeners
51
+ }
52
+
53
+ function touchStart(event, track) {
54
+ const touch = event.touches ? event.touches[0] : event
55
+ track.startX = touch.clientX
56
+ track.startY = touch.clientY
57
+ track.startTime = new Date().getTime()
58
+ }
59
+
60
+ function touchEnd(event, node, options, track) {
61
+ const { horizontal, vertical, threshold, minSpeed } = options
62
+ const touch = event.changedTouches ? event.changedTouches[0] : event
63
+ const distX = touch.clientX - track.startX
64
+ const distY = touch.clientY - track.startY
65
+ const duration = (new Date().getTime() - track.startTime) / 1000
66
+ const speed = Math.max(Math.abs(distX), Math.abs(distY)) / duration
48
67
 
49
- if (vertical && speed > minSpeed) {
50
- if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
51
- if (distY > 0) {
52
- node.dispatchEvent(new CustomEvent('swipeDown'))
53
- } else {
54
- node.dispatchEvent(new CustomEvent('swipeUp'))
55
- }
68
+ if (horizontal && speed > minSpeed) {
69
+ if (Math.abs(distX) > Math.abs(distY) && Math.abs(distX) >= threshold) {
70
+ if (distX > 0 && distX / duration > minSpeed) {
71
+ node.dispatchEvent(new CustomEvent('swipeRight'))
72
+ } else {
73
+ node.dispatchEvent(new CustomEvent('swipeLeft'))
56
74
  }
57
75
  }
58
76
  }
59
77
 
60
- node.addEventListener('touchstart', touchStart)
61
- node.addEventListener('touchend', touchEnd)
62
- node.addEventListener('mousedown', touchStart)
63
- node.addEventListener('mouseup', touchEnd)
64
-
65
- return {
66
- destroy() {
67
- node.removeEventListener('touchstart', touchStart)
68
- node.removeEventListener('touchend', touchEnd)
69
- node.removeEventListener('mousedown', touchStart)
70
- node.removeEventListener('mouseup', touchEnd)
78
+ if (vertical && speed > minSpeed) {
79
+ if (Math.abs(distY) > Math.abs(distX) && Math.abs(distY) >= threshold) {
80
+ if (distY > 0) {
81
+ node.dispatchEvent(new CustomEvent('swipeDown'))
82
+ } else {
83
+ node.dispatchEvent(new CustomEvent('swipeUp'))
84
+ }
71
85
  }
72
86
  }
73
87
  }
@@ -0,0 +1,90 @@
1
+ import {
2
+ getClosestAncestorWithAttribute,
3
+ mapKeyboardEventsToActions,
4
+ setupListeners,
5
+ removeListeners,
6
+ emit
7
+ } from './lib'
8
+
9
+ const defaultOptions = {
10
+ horizontal: false,
11
+ nested: false,
12
+ enabled: true
13
+ }
14
+
15
+ /**
16
+ * An action that can be used to traverse a nested list of items using keyboard and mouse.
17
+ *
18
+ * @param {HTMLElement} element
19
+ * @param {import('./types').TraversableOptions} data
20
+ * @returns
21
+ */
22
+ export function traversable(element, data) {
23
+ let options = {}
24
+ let tracker = {}
25
+ let handlers
26
+ let actions
27
+ let listeners
28
+
29
+ const configure = (data) => {
30
+ options = { ...options, ...data }
31
+
32
+ removeListeners(element, listeners)
33
+ actions = getActions(element, tracker)
34
+ handlers = mapKeyboardEventsToActions(actions, options)
35
+ listeners = getListeners(handlers, actions, tracker)
36
+ setupListeners(element, listeners, options)
37
+ }
38
+
39
+ configure({ ...defaultOptions, ...data })
40
+
41
+ return {
42
+ update: configure,
43
+ destroy: () => removeListeners(element, listeners)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Returns the listeners for the given handlers and actions.
49
+ *
50
+ * @param {import('./types').KeyboardActions} handlers
51
+ * @param {import('./types').ActionHandlers} actions
52
+ * @param {import('./types').PositionTracker} tracker
53
+ */
54
+ function getListeners(handlers, actions, tracker) {
55
+ return {
56
+ keydown: (event) => {
57
+ const action = handlers[event.key]
58
+ if (action) action(event)
59
+ },
60
+ click: (event) => {
61
+ const target = getClosestAncestorWithAttribute(event.target, 'data-index')
62
+
63
+ if (target) {
64
+ const index = parseInt(target.getAttribute('data-index'))
65
+ tracker.index = index
66
+ actions.select()
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ *
74
+ * @param {HTMLElement} element
75
+ * @param {import('./types').PositionTracker} tracker
76
+ * @returns {import('./types').ActionHandlers}
77
+ */
78
+ function getActions(element, tracker) {
79
+ const actions = {
80
+ next: () => emit(element, 'move', tracker),
81
+ previous: () => emit(element, 'move', tracker),
82
+ select: () => emit(element, 'select', tracker),
83
+ escape: () => emit(element, 'escape', tracker),
84
+ collapse: () => emit(element, 'collapse', tracker),
85
+ expand: () => emit(element, 'expand', tracker)
86
+ }
87
+ return actions
88
+ }
89
+
90
+ // function handleValueChange(element, options) {}
package/src/types.js CHANGED
@@ -51,3 +51,61 @@
51
51
  * @property {number} threshold - Threshold for swipe
52
52
  * @property {number} minSpeed - Minimum speed for swipe
53
53
  */
54
+
55
+ /**
56
+ * @typedef TraversableOptions
57
+ * @property {boolean} horizontal - Traverse horizontally
58
+ * @property {boolean} nested - Traverse nested items
59
+ * @property {boolean} enabled - Enable traversal
60
+ * @property {string} value - Value to be used for traversal
61
+ * @property {Array<*>} items - An array containing the data set to traverse
62
+ * @property {Array<integer} [indices] - Indices of the items to be traversed
63
+ */
64
+
65
+ /**
66
+ * @typedef PositionTracker
67
+ * @property {integer} index
68
+ * @property {integer} previousIndex
69
+ */
70
+
71
+ /**
72
+ * @typedef EventHandlers
73
+ * @property {function} [keydown]
74
+ * @property {function} [keyup]
75
+ * @property {function} [click]
76
+ * @property {function} [touchstart]
77
+ * @property {function} [touchmove]
78
+ * @property {function} [touchend]
79
+ * @property {function} [touchcancel]
80
+ * @property {function} [mousedown]
81
+ * @property {function} [mouseup]
82
+ * @property {function} [mousemove]
83
+ */
84
+
85
+ /**
86
+ * @typedef {Object} ActionHandlers
87
+ * @property {Function} [next]
88
+ * @property {Function} [previous]
89
+ * @property {Function} [select]
90
+ * @property {Function} [escape]
91
+ * @property {Function} [collapse]
92
+ * @property {Function} [expand]
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} NavigationOptions
97
+ * @property {Boolean} [horizontal]
98
+ * @property {Boolean} [nested]
99
+ * @property {Boolean} [enabled]
100
+ */
101
+
102
+ /**
103
+ * @typedef {Object} KeyboardActions
104
+ * @property {Function} [ArrowDown]
105
+ * @property {Function} [ArrowUp]
106
+ * @property {Function} [ArrowRight]
107
+ * @property {Function} [ArrowLeft]
108
+ * @property {Function} [Enter]
109
+ * @property {Function} [Escape]
110
+ * @property {Function} [" "]
111
+ */
package/src/utils.js ADDED
@@ -0,0 +1,24 @@
1
+ export function handleAction(actions, event) {
2
+ if (event.key in actions) {
3
+ event.preventDefault()
4
+ event.stopPropagation()
5
+ actions[event.key]()
6
+ }
7
+ }
8
+
9
+ export function getKeyboardActions(node, options, handlers) {
10
+ const movement = options.horizontal
11
+ ? { ArrowLeft: handlers.previous, ArrowRight: handlers.next }
12
+ : { ArrowUp: handlers.previous, ArrowDown: handlers.next }
13
+ const change = options.nested
14
+ ? options.horizontal
15
+ ? { ArrowUp: handlers.collapse, ArrowDown: handlers.expand }
16
+ : { ArrowLeft: handlers.collapse, ArrowRight: handlers.expand }
17
+ : {}
18
+ return {
19
+ Enter: handlers.select,
20
+ ' ': handlers.select,
21
+ ...movement,
22
+ ...change
23
+ }
24
+ }