@dimailn/vuetify 2.7.2-alpha57 → 2.7.2-alpha59

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 (77) hide show
  1. package/dist/vuetify.js +128 -86
  2. package/dist/vuetify.js.map +1 -1
  3. package/dist/vuetify.min.css +1 -1
  4. package/dist/vuetify.min.js +2 -2
  5. package/es5/components/VAutocomplete/VAutocomplete.js +1 -2
  6. package/es5/components/VAutocomplete/VAutocomplete.js.map +1 -1
  7. package/es5/components/VColorPicker/VColorPickerEdit.js +8 -3
  8. package/es5/components/VColorPicker/VColorPickerEdit.js.map +1 -1
  9. package/es5/components/VContent/VContent.js +12 -12
  10. package/es5/components/VContent/VContent.js.map +1 -1
  11. package/es5/components/VInput/VInput.js +17 -4
  12. package/es5/components/VInput/VInput.js.map +1 -1
  13. package/es5/components/VLabel/VLabel.js +16 -9
  14. package/es5/components/VLabel/VLabel.js.map +1 -1
  15. package/es5/components/VOverflowBtn/VOverflowBtn.js +8 -3
  16. package/es5/components/VOverflowBtn/VOverflowBtn.js.map +1 -1
  17. package/es5/components/VRadioGroup/VRadioGroup.js +19 -8
  18. package/es5/components/VRadioGroup/VRadioGroup.js.map +1 -1
  19. package/es5/components/VRangeSlider/VRangeSlider.js +4 -4
  20. package/es5/components/VRangeSlider/VRangeSlider.js.map +1 -1
  21. package/es5/components/VSelect/VSelect.js +1 -1
  22. package/es5/components/VSelect/VSelect.js.map +1 -1
  23. package/es5/components/VTextField/VTextField.js +5 -4
  24. package/es5/components/VTextField/VTextField.js.map +1 -1
  25. package/es5/framework.js +1 -1
  26. package/es5/mixins/overlayable/index.js +27 -26
  27. package/es5/mixins/overlayable/index.js.map +1 -1
  28. package/lib/components/VAutocomplete/VAutocomplete.js +1 -2
  29. package/lib/components/VAutocomplete/VAutocomplete.js.map +1 -1
  30. package/lib/components/VColorPicker/VColorPickerEdit.js +6 -3
  31. package/lib/components/VColorPicker/VColorPickerEdit.js.map +1 -1
  32. package/lib/components/VContent/VContent.js +12 -12
  33. package/lib/components/VContent/VContent.js.map +1 -1
  34. package/lib/components/VInput/VInput.js +17 -3
  35. package/lib/components/VInput/VInput.js.map +1 -1
  36. package/lib/components/VLabel/VLabel.js +11 -3
  37. package/lib/components/VLabel/VLabel.js.map +1 -1
  38. package/lib/components/VOverflowBtn/VOverflowBtn.js +8 -3
  39. package/lib/components/VOverflowBtn/VOverflowBtn.js.map +1 -1
  40. package/lib/components/VRadioGroup/VRadioGroup.js +13 -7
  41. package/lib/components/VRadioGroup/VRadioGroup.js.map +1 -1
  42. package/lib/components/VRangeSlider/VRangeSlider.js +4 -4
  43. package/lib/components/VRangeSlider/VRangeSlider.js.map +1 -1
  44. package/lib/components/VSelect/VSelect.js +1 -1
  45. package/lib/components/VSelect/VSelect.js.map +1 -1
  46. package/lib/components/VTextField/VTextField.js +3 -2
  47. package/lib/components/VTextField/VTextField.js.map +1 -1
  48. package/lib/framework.js +1 -1
  49. package/lib/mixins/overlayable/index.js +28 -27
  50. package/lib/mixins/overlayable/index.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/components/VAutocomplete/VAutocomplete.ts +2 -4
  53. package/src/components/VAutocomplete/__tests__/VAutocomplete.spec.ts +17 -0
  54. package/src/components/VColorPicker/VColorPickerEdit.ts +7 -3
  55. package/src/components/VColorPicker/__tests__/VColorPickerEdit.spec.ts +11 -0
  56. package/src/components/VContent/VContent.ts +9 -7
  57. package/src/components/VContent/__tests__/VContent.spec.ts +40 -0
  58. package/src/components/VInput/VInput.ts +13 -4
  59. package/src/components/VInput/__tests__/VInput.spec.ts +28 -1
  60. package/src/components/VInput/__tests__/__snapshots__/VInput.spec.ts.snap +4 -4
  61. package/src/components/VLabel/VLabel.ts +7 -3
  62. package/src/components/VLabel/__tests__/VLabel.spec.ts +10 -0
  63. package/src/components/VOverflowBtn/VOverflowBtn.ts +9 -4
  64. package/src/components/VOverflowBtn/__tests__/VOverflowBtn.spec.ts +15 -0
  65. package/src/components/VOverflowBtn/__tests__/__snapshots__/VOverflowBtn.spec.ts.snap +6 -8
  66. package/src/components/VRadioGroup/VRadioGroup.ts +14 -10
  67. package/src/components/VRadioGroup/__tests__/VRadioGroup.spec.ts +19 -0
  68. package/src/components/VRangeSlider/VRangeSlider.ts +5 -4
  69. package/src/components/VRangeSlider/__tests__/VRangeSlider.spec.ts +4 -1
  70. package/src/components/VRangeSlider/__tests__/__snapshots__/VRangeSlider.spec.ts.snap +8 -8
  71. package/src/components/VSelect/VSelect.ts +1 -1
  72. package/src/components/VSelect/__tests__/VSelect.spec.ts +1 -0
  73. package/src/components/VSlider/__tests__/__snapshots__/VSlider.spec.ts.snap +3 -3
  74. package/src/components/VTextField/VTextField.ts +2 -3
  75. package/src/components/VTextarea/__tests__/VTextarea.spec.ts +15 -0
  76. package/src/mixins/overlayable/__tests__/overlayable.spec.ts +24 -0
  77. package/src/mixins/overlayable/index.ts +25 -23
@@ -36,6 +36,10 @@ export default mixins(Themeable).extend({
36
36
  type: [Number, String],
37
37
  default: 'auto'
38
38
  },
39
+ tag: {
40
+ type: String,
41
+ default: 'label'
42
+ },
39
43
  value: Boolean
40
44
  },
41
45
 
@@ -50,16 +54,16 @@ export default mixins(Themeable).extend({
50
54
  'v-label--is-disabled': this.disabled,
51
55
  ...functionalThemeClasses(this)
52
56
  },
53
- for: props.for,
57
+ ...(props.tag === 'label' ? { for: props.for } : {}),
54
58
  'aria-hidden': !props.for,
55
59
  style: {
56
60
  left: convertToUnit(props.left),
57
61
  right: convertToUnit(props.right),
58
- position: props.absolute ? 'absolute' : 'relative'
62
+ ...(props.absolute ? { position: 'absolute' } : {})
59
63
  },
60
64
  ref: 'label'
61
65
  }, data)
62
66
 
63
- return h('label', Colorable.methods.setTextColor(props.focused && props.color, newData), this.$slots.default?.())
67
+ return h(props.tag, Colorable.methods.setTextColor(props.focused && props.color, newData), this.$slots.default?.())
64
68
  }
65
69
  })
@@ -40,6 +40,16 @@ describe('VLabel', () => {
40
40
  expect(wrapper.element.style.position).toBe('absolute')
41
41
  })
42
42
 
43
+ it('should not set position when absolute is false', () => {
44
+ const wrapper = mountFunction({
45
+ props: {
46
+ absolute: false
47
+ }
48
+ })
49
+
50
+ expect(wrapper.element.style.position).toBe('')
51
+ })
52
+
43
53
  it('should render default slot content', () => {
44
54
  const wrapper = mountFunction({
45
55
  slots: {
@@ -82,10 +82,15 @@ export default defineComponent({
82
82
 
83
83
  if (!label) return label
84
84
 
85
- label.data = label.data || {}
86
-
87
- // Reset previously set styles from parent
88
- label.data.style = {}
85
+ label.props ||= {}
86
+
87
+ // Label must stay in document flow to size the control
88
+ // (.v-select__selections has width: 0).
89
+ // Do not use position:relative — it activates CSS `top` and breaks alignment.
90
+ label.props.absolute = false
91
+ label.props.left = ''
92
+ label.props.right = ''
93
+ label.props.style = {}
89
94
 
90
95
  return label
91
96
  },
@@ -109,6 +109,21 @@ describe('VOverflowBtn.js', () => {
109
109
  expect(callback).toHaveBeenCalled()
110
110
  })
111
111
 
112
+ it('should keep label in document flow without relative positioning', () => {
113
+ const wrapper = mountFunction({
114
+ props: {
115
+ items: ['foo'],
116
+ label: 'Select font'
117
+ }
118
+ })
119
+
120
+ const label = wrapper.find('label')
121
+
122
+ expect(label.exists()).toBe(true)
123
+ expect(label.element.style.position).toBe('')
124
+ expect(label.element.style.left).toBe('')
125
+ })
126
+
112
127
  it('should show label with persistentPlaceholder property set to true', async () => {
113
128
  const wrapper = mountFunction({
114
129
  props: {
@@ -6,19 +6,18 @@ exports[`VOverflowBtn.js should show label with persistentPlaceholder property s
6
6
  <div role="combobox"
7
7
  aria-haspopup="listbox"
8
8
  aria-expanded="false"
9
- aria-owns="list-15"
9
+ aria-owns="list-22"
10
10
  class="v-input__slot"
11
11
  >
12
12
  <div class="v-select__slot">
13
13
  <label class="v-label theme--light"
14
- for="input-15"
14
+ for="input-22"
15
15
  aria-hidden="false"
16
- style="left: 0px; position: absolute;"
17
16
  >
18
17
  Some label
19
18
  </label>
20
19
  <div class="v-select__selections">
21
- <input id="input-15"
20
+ <input id="input-22"
22
21
  readonly
23
22
  type="text"
24
23
  value
@@ -53,19 +52,18 @@ exports[`VOverflowBtn.js should show label with persistentPlaceholder property s
53
52
  <div role="combobox"
54
53
  aria-haspopup="listbox"
55
54
  aria-expanded="true"
56
- aria-owns="list-15"
55
+ aria-owns="list-22"
57
56
  class="v-input__slot"
58
57
  >
59
58
  <div class="v-select__slot">
60
59
  <label class="v-label theme--light"
61
- for="input-15"
60
+ for="input-22"
62
61
  aria-hidden="false"
63
- style="left: 0px; position: absolute;"
64
62
  >
65
63
  Some label
66
64
  </label>
67
65
  <div class="v-select__selections">
68
- <input id="input-15"
66
+ <input id="input-22"
69
67
  readonly
70
68
  type="text"
71
69
  value
@@ -4,12 +4,14 @@ import './VRadioGroup.sass'
4
4
 
5
5
  // Extensions
6
6
  import VInput from '../VInput'
7
+ import VLabel from '../VLabel'
7
8
  import { BaseItemGroup } from '../VItemGroup/VItemGroup'
8
9
 
9
10
  // Utilities
10
11
  import { mergeProps, h } from 'vue'
11
12
  import mixins from '../../util/mixins'
12
13
  import { breaking } from '../../util/console'
14
+ import { getSlot } from '../../util/helpers'
13
15
 
14
16
  // Types
15
17
  import type { PropType } from 'vue'
@@ -87,16 +89,18 @@ export default baseMixins.extend({
87
89
  return render
88
90
  },
89
91
  genLabel () {
90
- const label = VInput.methods.genLabel.call(this)
91
-
92
- if (!label) return null
93
-
94
- label.data!.attrs!.id = this.computedId
95
- // WAI considers this an orphaned label
96
- delete label.data!.attrs!.for
97
- label.tag = 'legend'
98
-
99
- return label
92
+ if (!this.hasLabel) return null
93
+
94
+ return h(VLabel, {
95
+ id: this.computedId,
96
+ color: this.validationState,
97
+ dark: this.dark,
98
+ disabled: this.isDisabled,
99
+ focused: this.hasState,
100
+ for: this.computedId,
101
+ light: this.light,
102
+ tag: 'legend'
103
+ }, () => getSlot(this, 'label') || this.label)
100
104
  },
101
105
  onClick: BaseItemGroup.methods.onClick
102
106
  },
@@ -37,4 +37,23 @@ describe('VRadioGroup.ts', () => {
37
37
 
38
38
  expect(wrapper.html()).toMatchSnapshot()
39
39
  })
40
+
41
+ it('should render group label as legend without for attribute', () => {
42
+ const wrapper = mountFunction({
43
+ props: {
44
+ label: 'Radio group label'
45
+ },
46
+ slots: {
47
+ default: [VRadio]
48
+ }
49
+ })
50
+
51
+ const legend = wrapper.find('legend')
52
+
53
+ expect(legend.exists()).toBe(true)
54
+ expect(legend.text()).toBe('Radio group label')
55
+ expect(legend.attributes('id')).toBeTruthy()
56
+ expect(legend.attributes('for')).toBeUndefined()
57
+ expect(legend.attributes('aria-hidden')).toBe('false')
58
+ })
40
59
  })
@@ -112,10 +112,11 @@ export default defineComponent({
112
112
  return createRange(2).map(i => {
113
113
  const input = VSlider.methods.genInput.call(this)
114
114
 
115
- input.data = input.data || {}
116
- input.data.attrs = input.data.attrs || {}
117
- input.data.attrs.value = this.internalValue[i]
118
- input.data.attrs.id = `input-${i ? 'max' : 'min'}-${this.$.uid}`
115
+ input.props = {
116
+ ...input.props,
117
+ value: this.internalValue[i],
118
+ id: `input-${i ? 'max' : 'min'}-${this.$.uid}`
119
+ }
119
120
 
120
121
  return input
121
122
  })
@@ -191,7 +191,10 @@ describe('VRangeSlider', () => {
191
191
  const wrapper = mountFunction()
192
192
  const [min, max] = wrapper.vm.genInput()
193
193
 
194
- expect(min.data.attrs.id).not.toEqual(max.data.attrs.id)
194
+ expect(min.props!.id).not.toEqual(max.props!.id)
195
+
196
+ const inputs = wrapper.findAll('input')
197
+ expect(inputs[0].attributes('id')).not.toEqual(inputs[1].attributes('id'))
195
198
  })
196
199
 
197
200
  // https://github.com/vuetifyjs/vuetify/issues/12733
@@ -5,17 +5,17 @@ exports[`VRangeSlider should render a vertical slider 1`] = `
5
5
  <div class="v-input__control">
6
6
  <div class="v-input__slot">
7
7
  <div class="v-slider v-slider--vertical theme--light">
8
- <input id="input-25"
8
+ <input id="input-min-25"
9
9
  disabled
10
10
  readonly
11
11
  tabindex="-1"
12
- value="0,0"
12
+ value="0"
13
13
  >
14
- <input id="input-25"
14
+ <input id="input-max-25"
15
15
  disabled
16
16
  readonly
17
17
  tabindex="-1"
18
- value="0,0"
18
+ value="0"
19
19
  >
20
20
  <div class="v-slider__track-container">
21
21
  <div class="v-slider__track-background primary lighten-3">
@@ -66,17 +66,17 @@ exports[`VRangeSlider should render disabled slider 1`] = `
66
66
  <div class="v-input__control">
67
67
  <div class="v-input__slot">
68
68
  <div class="v-slider v-slider--horizontal v-slider--disabled theme--light">
69
- <input id="input-29"
69
+ <input id="input-min-29"
70
70
  disabled
71
71
  readonly
72
72
  tabindex="-1"
73
- value="0,0"
73
+ value="0"
74
74
  >
75
- <input id="input-29"
75
+ <input id="input-max-29"
76
76
  disabled
77
77
  readonly
78
78
  tabindex="-1"
79
- value="0,0"
79
+ value="0"
80
80
  >
81
81
  <div class="v-slider__track-container">
82
82
  <div class="v-slider__track-background">
@@ -505,7 +505,7 @@ export default baseMixins.extend({
505
505
  type: 'text',
506
506
  'aria-readonly': String(this.isReadonly),
507
507
  'aria-activedescendant': getObjectValueByPath(this.$refs.menu, 'activeTile.id'),
508
- autocomplete: getObjectValueByPath(input.data!, 'attrs.autocomplete', 'off'),
508
+ autocomplete: getObjectValueByPath(input.props, 'autocomplete', 'off'),
509
509
  placeholder: (!this.isDirty && (this.persistentPlaceholder || this.isFocused || !this.hasLabel)) ? this.placeholder : undefined,
510
510
  onKeypress: this.onKeyPress
511
511
  })
@@ -490,6 +490,7 @@ describe('VSelect.ts', () => {
490
490
  })
491
491
 
492
492
  expect(wrapper.vm.$attrs.autocomplete).toBe('on')
493
+ expect(wrapper.find('input').attributes('autocomplete')).toBe('on')
493
494
  })
494
495
 
495
496
  // Based on issue: https://github.com/vuetifyjs/vuetify/issues/12769
@@ -317,7 +317,7 @@ exports[`VSlider.ts should display label and have different aria-label 1`] = `
317
317
  <label class="v-label theme--light"
318
318
  for="input-98"
319
319
  aria-hidden="false"
320
- style="left: 0px; position: relative;"
320
+ style="left: 0px;"
321
321
  >
322
322
  foo
323
323
  </label>
@@ -704,7 +704,7 @@ exports[`VSlider.ts should reverse label location when inverse 1`] = `
704
704
  <label class="v-label theme--light"
705
705
  for="input-89"
706
706
  aria-hidden="false"
707
- style="left: 0px; position: relative;"
707
+ style="left: 0px;"
708
708
  >
709
709
  foo
710
710
  </label>
@@ -788,7 +788,7 @@ exports[`VSlider.ts should reverse label location when inverse 2`] = `
788
788
  <label class="v-label theme--light"
789
789
  for="input-89"
790
790
  aria-hidden="false"
791
- style="left: 0px; position: relative;"
791
+ style="left: 0px;"
792
792
  >
793
793
  foo
794
794
  </label>
@@ -216,7 +216,6 @@ export default baseMixins.extend({
216
216
 
217
217
  created () {
218
218
  const breakingProps = [
219
- ['value', 'modelValue'],
220
219
  ['onInput', 'onUpdate:modelValue']
221
220
  ]
222
221
 
@@ -391,12 +390,12 @@ export default baseMixins.extend({
391
390
  genInput () {
392
391
  const listeners = Object.assign({}, this.listeners$)
393
392
  delete listeners.change // Change should not be bound externally
394
- const { title, onChange: _onChange, class: _class, style: _style, ...inputAttrs } = this.attrs$ as Record<string, any>
393
+ const { title, onChange: _onChange, class: _class, style: _style, value: _value, ...inputAttrs } = this.attrs$ as Record<string, any>
395
394
 
396
395
  const node = h('input', {
397
396
  style: {},
398
- value: (this.type === 'number' && Object.is(this.lazyValue, -0)) ? '-0' : this.lazyValue,
399
397
  ...inputAttrs,
398
+ value: (this.type === 'number' && Object.is(this.lazyValue, -0)) ? '-0' : this.lazyValue,
400
399
  autofocus: this.autofocus,
401
400
  disabled: this.isDisabled,
402
401
  id: this.computedId,
@@ -272,4 +272,19 @@ describe('VTextarea.ts', () => {
272
272
  const textarea = wrapper.find('textarea')
273
273
  expect(textarea.attributes('rows')).toBe('5')
274
274
  })
275
+
276
+ it('should activate label when using legacy value prop with auto-grow', () => {
277
+ const wrapper = mountFunction({
278
+ props: {
279
+ autoGrow: true,
280
+ filled: true,
281
+ label: 'Label',
282
+ value: 'The Woodman set to work at once, and so sharp was his axe that the tree was soon chopped nearly through.'
283
+ }
284
+ })
285
+
286
+ expect(wrapper.element.classList).toContain('v-input--is-label-active')
287
+ expect(wrapper.element.classList).toContain('v-input--is-dirty')
288
+ expect(wrapper.find('.v-label').element.classList).toContain('v-label--active')
289
+ })
275
290
  })
@@ -75,6 +75,12 @@ describe('Overlayable.ts', () => {
75
75
  expect(wrapper.vm.overlay).toBeFalsy()
76
76
  })
77
77
 
78
+ it('destroyOverlay безопасен когда оверлея нет', () => {
79
+ const wrapper = mountFunction()
80
+ expect(() => wrapper.vm.destroyOverlay()).not.toThrow()
81
+ expect(wrapper.vm.overlay).toBeFalsy()
82
+ })
83
+
78
84
  it('should remove overlay app container after close', async () => {
79
85
  const wrapper = mountFunction()
80
86
 
@@ -210,6 +216,24 @@ describe('Overlayable.ts', () => {
210
216
  expect(wrapper.vm.overlay!.$el.isConnected).toBe(true)
211
217
  })
212
218
 
219
+ it('beforeUnmount уничтожает оверлей даже при isActive (фикс залипшего overlay)', async () => {
220
+ const wrapper = mountDrawerLike()
221
+
222
+ wrapper.vm.temporary = true
223
+ wrapper.vm.isActive = true
224
+ await nextTick()
225
+ await waitAnimationFrame()
226
+
227
+ const app = document.querySelector('[data-app]') as HTMLElement
228
+ expect(app.querySelector('.v-overlay')).toBeTruthy()
229
+ expect(document.querySelectorAll('[data-v-app]')).toHaveLength(1)
230
+
231
+ wrapper.unmount()
232
+
233
+ expect(app.querySelector('.v-overlay')).toBeNull()
234
+ expect(document.querySelectorAll('[data-v-app]')).toHaveLength(0)
235
+ })
236
+
213
237
  it('showOverlay true→true не вызывает genOverlay после потери [data-app]', async () => {
214
238
  const genOverlay = jest.fn()
215
239
  const removeOverlay = jest.fn()
@@ -57,7 +57,7 @@ export default defineComponent({
57
57
  },
58
58
 
59
59
  beforeUnmount () {
60
- this.removeOverlay()
60
+ this.destroyOverlay()
61
61
  },
62
62
 
63
63
  methods: {
@@ -113,14 +113,7 @@ export default defineComponent({
113
113
  if (this.hideOverlay) return
114
114
 
115
115
  if (!this.overlay?.$el?.isConnected) {
116
- if (this.overlay) {
117
- cancelAnimationFrame(this.animationFrame)
118
- const container = this.overlay.$el?.parentNode as HTMLElement | null
119
- this.overlayApp?.unmount()
120
- this.overlayApp = null
121
- if (container?.parentNode) container.parentNode.removeChild(container)
122
- this.overlay = null
123
- }
116
+ this.teardownOverlay()
124
117
  this.createOverlay()
125
118
  }
126
119
 
@@ -142,10 +135,7 @@ export default defineComponent({
142
135
  removeOverlay (showScroll = true) {
143
136
  if (this.overlay) {
144
137
  if (!this.overlay.$el?.isConnected) {
145
- cancelAnimationFrame(this.animationFrame)
146
- this.overlayApp?.unmount()
147
- this.overlayApp = null
148
- this.overlay = null
138
+ this.teardownOverlay()
149
139
  showScroll && this.showScroll()
150
140
  return
151
141
  }
@@ -159,16 +149,7 @@ export default defineComponent({
159
149
  this.isActive
160
150
  ) return
161
151
 
162
- const overlayContainer = this.overlay.$el.parentNode as HTMLElement | null
163
-
164
- this.overlayApp?.unmount()
165
- this.overlayApp = null
166
-
167
- if (overlayContainer?.parentNode) {
168
- overlayContainer.parentNode.removeChild(overlayContainer)
169
- }
170
-
171
- this.overlay = null
152
+ this.teardownOverlay()
172
153
  })
173
154
 
174
155
  // Cancel animation frame in case
@@ -181,6 +162,27 @@ export default defineComponent({
181
162
 
182
163
  showScroll && this.showScroll()
183
164
  },
165
+ teardownOverlay () {
166
+ cancelAnimationFrame(this.animationFrame)
167
+
168
+ if (!this.overlay) return
169
+
170
+ const container = this.overlay.$el?.parentNode as HTMLElement | null
171
+
172
+ this.overlayApp?.unmount()
173
+ this.overlayApp = null
174
+
175
+ if (container?.parentNode) {
176
+ container.parentNode.removeChild(container)
177
+ }
178
+
179
+ this.overlay = null
180
+ },
181
+ /** Синхронно уничтожает оверлей независимо от isActive/анимации. Для unmount. */
182
+ destroyOverlay () {
183
+ this.teardownOverlay()
184
+ this.showScroll()
185
+ },
184
186
  scrollListener (e: WheelEvent | KeyboardEvent) {
185
187
  if ('key' in e) {
186
188
  if (