@pocketprep/ui-kit 3.8.4 → 3.9.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 (159) hide show
  1. package/dist/@pocketprep/ui-kit.css +1 -1
  2. package/dist/@pocketprep/ui-kit.js +14469 -17731
  3. package/dist/@pocketprep/ui-kit.js.map +1 -1
  4. package/dist/@pocketprep/ui-kit.umd.cjs +19 -29
  5. package/dist/@pocketprep/ui-kit.umd.cjs.map +1 -1
  6. package/eslint.config.ts +38 -11
  7. package/lib/SVGDefinitions.vue +32 -35
  8. package/lib/components/Banners/Banner.vue +10 -14
  9. package/lib/components/Blobs/Blob.vue +6 -14
  10. package/lib/components/Blobs/BlobEmptyState.vue +9 -8
  11. package/lib/components/Blobs/blob.d.ts +1 -1
  12. package/lib/components/BundleIcons/BundleIcon.vue +36 -63
  13. package/lib/components/BundleIcons/bundleIcon.d.ts +1 -1
  14. package/lib/components/Bundles/BundleList.vue +71 -59
  15. package/lib/components/Bundles/BundleSearch.vue +93 -117
  16. package/lib/components/Bundles/PremiumPill.vue +6 -12
  17. package/lib/components/Buttons/Button.vue +32 -35
  18. package/lib/components/Buttons/Link.vue +32 -31
  19. package/lib/components/Buttons/Tab.vue +14 -17
  20. package/lib/components/Calendar/Calendar.vue +87 -85
  21. package/lib/components/Charts/Bar.vue +192 -263
  22. package/lib/components/Charts/Pie.vue +55 -61
  23. package/lib/components/Charts/highcharts-wrap.ts +81 -0
  24. package/lib/components/Controls/SegmentControl.vue +26 -24
  25. package/lib/components/Controls/Slider.vue +51 -47
  26. package/lib/components/Controls/ToggleSwitch.vue +33 -31
  27. package/lib/components/EmptyStates/EmptyState.vue +69 -73
  28. package/lib/components/Exams/ExamCard.vue +59 -47
  29. package/lib/components/Exams/ExamMenuCard.vue +30 -28
  30. package/lib/components/Filters/FilterDropdown.vue +83 -86
  31. package/lib/components/Filters/FilterOptions.vue +83 -88
  32. package/lib/components/Forms/Checkbox.vue +27 -27
  33. package/lib/components/Forms/CheckboxOption.vue +30 -30
  34. package/lib/components/Forms/Errors.vue +21 -24
  35. package/lib/components/Forms/Input.vue +71 -59
  36. package/lib/components/Forms/Radio.vue +2 -2
  37. package/lib/components/Forms/RadioButton.vue +8 -8
  38. package/lib/components/Forms/Select.vue +265 -257
  39. package/lib/components/Forms/Textarea.vue +49 -35
  40. package/lib/components/Icons/IconAccordionArrow.vue +7 -9
  41. package/lib/components/Icons/IconActivity.vue +7 -9
  42. package/lib/components/Icons/IconAdd.vue +7 -11
  43. package/lib/components/Icons/IconAddCircle.vue +7 -9
  44. package/lib/components/Icons/IconArrow.vue +7 -9
  45. package/lib/components/Icons/IconBarChart.vue +7 -9
  46. package/lib/components/Icons/IconCalendar.vue +7 -9
  47. package/lib/components/Icons/IconCalendarPicker.vue +7 -9
  48. package/lib/components/Icons/IconChat.vue +7 -9
  49. package/lib/components/Icons/IconCheck.vue +7 -9
  50. package/lib/components/Icons/IconClose.vue +7 -9
  51. package/lib/components/Icons/IconConcept.vue +1 -1
  52. package/lib/components/Icons/IconCorrect.vue +7 -9
  53. package/lib/components/Icons/IconEdit.vue +7 -11
  54. package/lib/components/Icons/IconExam.vue +7 -9
  55. package/lib/components/Icons/IconExternalLink.vue +7 -9
  56. package/lib/components/Icons/IconEyeHide.vue +7 -9
  57. package/lib/components/Icons/IconEyeShow.vue +7 -9
  58. package/lib/components/Icons/IconFilter.vue +7 -9
  59. package/lib/components/Icons/IconFilterActive.vue +7 -9
  60. package/lib/components/Icons/IconFlag.vue +7 -9
  61. package/lib/components/Icons/IconFlagContent.vue +8 -9
  62. package/lib/components/Icons/IconFlagFeedback.vue +8 -10
  63. package/lib/components/Icons/IconFlagFilled.vue +7 -9
  64. package/lib/components/Icons/IconFullView.vue +7 -9
  65. package/lib/components/Icons/IconFullViewActive.vue +7 -9
  66. package/lib/components/Icons/IconGridDrag.vue +2 -2
  67. package/lib/components/Icons/IconHandle.vue +7 -9
  68. package/lib/components/Icons/IconHeart.vue +7 -9
  69. package/lib/components/Icons/IconHelp.vue +7 -9
  70. package/lib/components/Icons/IconHighlight.vue +2 -2
  71. package/lib/components/Icons/IconHourglass.vue +7 -9
  72. package/lib/components/Icons/IconImage.vue +7 -9
  73. package/lib/components/Icons/IconIncorrect.vue +7 -9
  74. package/lib/components/Icons/IconInfo.vue +7 -9
  75. package/lib/components/Icons/IconKeyboard.vue +7 -9
  76. package/lib/components/Icons/IconLaunch.vue +7 -9
  77. package/lib/components/Icons/IconLevelUp.vue +7 -9
  78. package/lib/components/Icons/IconLightbulb.vue +7 -9
  79. package/lib/components/Icons/IconLightning.vue +7 -9
  80. package/lib/components/Icons/IconLink.vue +7 -9
  81. package/lib/components/Icons/IconList.vue +7 -9
  82. package/lib/components/Icons/IconLoading.vue +7 -9
  83. package/lib/components/Icons/IconLoading2.vue +11 -11
  84. package/lib/components/Icons/IconLock.vue +7 -9
  85. package/lib/components/Icons/IconMissedQuestions.vue +7 -9
  86. package/lib/components/Icons/IconMoon.vue +7 -9
  87. package/lib/components/Icons/IconPaginationArrow.vue +7 -9
  88. package/lib/components/Icons/IconPaginationArrowDouble.vue +7 -9
  89. package/lib/components/Icons/IconPassage.vue +7 -9
  90. package/lib/components/Icons/IconPencil.vue +7 -9
  91. package/lib/components/Icons/IconPeople.vue +7 -9
  92. package/lib/components/Icons/IconPercent.vue +7 -9
  93. package/lib/components/Icons/IconPerson.vue +8 -9
  94. package/lib/components/Icons/IconPresent.vue +7 -9
  95. package/lib/components/Icons/IconPreview.vue +7 -9
  96. package/lib/components/Icons/IconQuestions.vue +7 -9
  97. package/lib/components/Icons/IconQuick10.vue +7 -9
  98. package/lib/components/Icons/IconQuickActions.vue +2 -2
  99. package/lib/components/Icons/IconRecommendedFilter.vue +1 -1
  100. package/lib/components/Icons/IconRemoveCircle.vue +7 -9
  101. package/lib/components/Icons/IconReviewFlag.vue +7 -9
  102. package/lib/components/Icons/IconSearch.vue +7 -9
  103. package/lib/components/Icons/IconShare.vue +7 -9
  104. package/lib/components/Icons/IconSideBar.vue +7 -9
  105. package/lib/components/Icons/IconSideBarActive.vue +7 -9
  106. package/lib/components/Icons/IconStar.vue +1 -1
  107. package/lib/components/Icons/IconStopwatch.vue +7 -9
  108. package/lib/components/Icons/IconStrike.vue +7 -9
  109. package/lib/components/Icons/IconSubject.vue +7 -9
  110. package/lib/components/Icons/IconText.vue +7 -9
  111. package/lib/components/Icons/IconTimer.vue +8 -9
  112. package/lib/components/Icons/IconWarning.vue +7 -9
  113. package/lib/components/Icons/icon.d.ts +1 -1
  114. package/lib/components/Loaders/SkeletonLoader.vue +1 -5
  115. package/lib/components/Modal/Modal.vue +23 -29
  116. package/lib/components/Modal/ModalContainer.vue +135 -133
  117. package/lib/components/Onboarding/EmailAuth.vue +66 -70
  118. package/lib/components/Onboarding/MagicCodeEntry.vue +88 -83
  119. package/lib/components/Pagination/QuestionReviewPagination.vue +3 -3
  120. package/lib/components/Pagination/TablePagination.vue +47 -44
  121. package/lib/components/PhonePerson/PhonePerson.vue +18 -18
  122. package/lib/components/PhonePerson/phonePerson.d.ts +1 -1
  123. package/lib/components/Quiz/FlagToggle.vue +45 -44
  124. package/lib/components/Quiz/GlobalMetricsToggle.vue +29 -28
  125. package/lib/components/Quiz/KeyboardShortcutsButton.vue +16 -23
  126. package/lib/components/Quiz/KeyboardShortcutsModal.vue +36 -37
  127. package/lib/components/Quiz/Question/BuildListChoicesContainer.vue +65 -65
  128. package/lib/components/Quiz/Question/ChoicesContainer.vue +5 -5
  129. package/lib/components/Quiz/Question/DropdownExplanation.vue +5 -5
  130. package/lib/components/Quiz/Question/Explanation.vue +6 -6
  131. package/lib/components/Quiz/Question/MPMCChoicesContainer.vue +17 -17
  132. package/lib/components/Quiz/Question/MPMCRadioGroup.vue +2 -2
  133. package/lib/components/Quiz/Question/MatrixChoicesContainer.vue +39 -39
  134. package/lib/components/Quiz/Question/MatrixRadioGroup.vue +6 -6
  135. package/lib/components/Quiz/Question/MobileMatrixChoicesContainer.vue +27 -28
  136. package/lib/components/Quiz/Question/MobileMatrixRadioGroup.vue +2 -2
  137. package/lib/components/Quiz/Question/PassageAndImage.vue +3 -3
  138. package/lib/components/Quiz/Question/PassageAndImageDropdown.vue +7 -7
  139. package/lib/components/Quiz/Question/Paywall.vue +2 -2
  140. package/lib/components/Quiz/Question/QuestionContext.vue +1 -1
  141. package/lib/components/Quiz/Question/StatsSummary.vue +2 -2
  142. package/lib/components/Quiz/Question/Summary.vue +11 -11
  143. package/lib/components/Quiz/Question.vue +90 -82
  144. package/lib/components/Quiz/QuizContainer.vue +1 -1
  145. package/lib/components/Quiz/QuizProgressBar.vue +23 -23
  146. package/lib/components/Quiz/question.d.ts +3 -3
  147. package/lib/components/Search/Pill.vue +16 -19
  148. package/lib/components/Search/Search.vue +52 -47
  149. package/lib/components/SidePanels/SidePanel.vue +168 -174
  150. package/lib/components/Tables/Table.vue +135 -122
  151. package/lib/components/Tables/TableActions.vue +81 -76
  152. package/lib/components/Tables/table.d.ts +1 -1
  153. package/lib/components/Tags/Tag.vue +49 -39
  154. package/lib/components/Toasts/Toast.vue +44 -42
  155. package/lib/components/Tooltips/OverflowTooltip.vue +39 -45
  156. package/lib/components/Tooltips/Tooltip.vue +69 -70
  157. package/lib/directives.ts +4 -4
  158. package/lib/utils.ts +13 -12
  159. package/package.json +27 -28
@@ -16,7 +16,7 @@
16
16
  }"
17
17
  >{{ label }}</label>
18
18
  <div
19
- ref="uikit-select__input-container"
19
+ ref="uikit-select__input-container"
20
20
  class="uikit-select__input-container"
21
21
  :tabindex="disabled || typeahead ? -1 : 0"
22
22
  role="combobox"
@@ -41,7 +41,7 @@
41
41
  >
42
42
  <slot name="selectValue" :item="modelValue">
43
43
  {{ modelValue ? modelValue.label : placeholder }}
44
-
44
+
45
45
  <div
46
46
  v-if="subtext"
47
47
  v-dark="isDarkMode"
@@ -148,10 +148,10 @@
148
148
  </div>
149
149
  </template>
150
150
 
151
- <script lang="ts">
152
- import { Component, Vue, Prop, Watch, Emit } from 'vue-facing-decorator'
151
+ <script setup lang="ts">
153
152
  import Icon from '../Icons/Icon.vue'
154
- import { dark } from '../../directives'
153
+ import { dark as vDark } from '../../directives'
154
+ import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'
155
155
 
156
156
  interface IItem {
157
157
  value?: string | null
@@ -160,304 +160,312 @@ interface IItem {
160
160
  subtext?: string
161
161
  }
162
162
 
163
- /**
164
- * @see [Designs for Inputs](https://marvelapp.com/adf8ab3/screen/70331421)
165
- */
166
- @Component({
167
- name: 'PocketSelect',
168
- components: {
169
- Icon,
170
- },
171
- directives: {
172
- dark,
173
- },
174
- })
175
- export default class Select extends Vue {
176
- @Prop() label?: string
177
- @Prop() modelValue?: IItem
178
- @Prop() subtext?: string
179
- @Prop() placeholder?: string
180
- @Prop() data!: IItem[]
181
- @Prop() disabled?: boolean
182
- @Prop() autoFocus?: boolean
183
- @Prop() showEmptyOption?: boolean
184
- @Prop() emptyOptionLabel?: string
185
- @Prop() typeahead?: boolean
186
- @Prop() error?: boolean
187
- @Prop() openMenuAbove?: boolean
188
- @Prop() customDropdownTop?: number
189
- @Prop({ default: false }) isDarkMode!: boolean
190
- @Prop({ default: false }) dropdownOverride!: boolean // Can override the icon to be an X instead
191
- @Prop({ default: false }) showTypeaheadClear!: boolean
192
-
193
- hover = false
194
- focus = false
195
- showDropdown = false
196
- searchText = ''
197
- menuHeight: number | null = null
198
-
199
- get filteredData () {
200
- if (this.typeahead && this.searchText) {
201
- return this.data.filter(item => item.label.toLowerCase().includes(this.searchText.toLowerCase()))
202
- } else {
203
- if (this.showEmptyOption && this.modelValue) {
204
- return [{ value: null, label: this.emptyOptionLabel || '' }, ...this.data ]
205
- }
206
- return this.data
207
- }
163
+ const {
164
+ label = '',
165
+ modelValue = null,
166
+ subtext = '',
167
+ placeholder = '',
168
+ data,
169
+ disabled = false,
170
+ autoFocus = false,
171
+ showEmptyOption = false,
172
+ emptyOptionLabel = '',
173
+ typeahead = false,
174
+ error = false,
175
+ openMenuAbove = false,
176
+ customDropdownTop = 0,
177
+ isDarkMode = false,
178
+ dropdownOverride = false,
179
+ showTypeaheadClear = false,
180
+ } = defineProps<{
181
+ label?: string
182
+ modelValue?: IItem | null
183
+ subtext?: string
184
+ placeholder?: string
185
+ data: IItem[]
186
+ disabled?: boolean
187
+ autoFocus?: boolean
188
+ showEmptyOption?: boolean
189
+ emptyOptionLabel?: string
190
+ typeahead?: boolean
191
+ error?: boolean
192
+ openMenuAbove?: boolean
193
+ customDropdownTop?: number
194
+ isDarkMode?: boolean
195
+ dropdownOverride?: boolean // Can override the icon to be an X instead
196
+ showTypeaheadClear?: boolean
197
+ }>()
198
+
199
+ const emit = defineEmits<{
200
+ 'update:modelValue': [modelVal: IItem | null]
201
+ 'linkClick': [item: IItem | null]
202
+ 'openDropdown': [open: boolean]
203
+ 'close': []
204
+ }>()
205
+
206
+ const hover = ref(false)
207
+ const focus = ref(false)
208
+ const showDropdown = ref(false)
209
+ const searchText = ref('')
210
+ const menuHeight = ref<number | null>(null)
211
+
212
+ const inputRef = useTemplateRef<HTMLElement>('uikit-select__input')
213
+ const inputContainerRef = useTemplateRef<HTMLElement>('uikit-select__input-container')
214
+ const menuRef = useTemplateRef<HTMLElement>('menu')
215
+ const itemRefs = useTemplateRef<HTMLElement[]>('uikit-select__items')
216
+
217
+ const filteredData = computed(() => {
218
+ if (typeahead && searchText.value) {
219
+ return data.filter(item => item.label.toLowerCase().includes(searchText.value.toLowerCase()))
220
+ } else {
221
+ if (showEmptyOption && modelValue) {
222
+ return [{ value: null, label: emptyOptionLabel || '' }, ...data ]
223
+ }
224
+ return data
208
225
  }
226
+ })
209
227
 
210
- get menuPositionTop () {
211
- if (this.customDropdownTop) return this.customDropdownTop
212
-
213
- if (this.menuHeight) return `-${this.menuHeight}`
228
+ const menuPositionTop = computed(() => {
229
+ if (customDropdownTop) return customDropdownTop
214
230
 
215
- if (this.subtext) return 60
231
+ if (menuHeight.value) return `-${menuHeight.value}`
216
232
 
217
- return 36
218
- }
233
+ if (subtext) return 60
219
234
 
220
- mounted () {
221
- if (this.autoFocus) {
222
- if (this.typeahead) {
223
- (this.$refs['uikit-select__input'] as HTMLElement).focus()
224
- } else {
225
- (this.$refs['uikit-select__input-container'] as HTMLElement).focus()
226
- }
227
- }
235
+ return 36
236
+ })
228
237
 
229
- if (this.typeahead && this.modelValue?.label) {
230
- this.searchText = this.modelValue.label
238
+ onMounted(() => {
239
+ if (autoFocus) {
240
+ if (typeahead) {
241
+ inputRef.value?.focus()
242
+ } else {
243
+ inputContainerRef.value?.focus()
231
244
  }
232
-
233
- this.updateMenuHeight()
234
245
  }
235
246
 
236
- keydownListener (e: Event | KeyboardEvent) {
237
- if (this.focus && 'key' in e && e.key.match(/^[A-Za-z0-9\s\-_@]$/)) {
238
- e.stopPropagation()
239
- }
247
+ if (typeahead && modelValue?.label) {
248
+ searchText.value = modelValue.label
240
249
  }
241
250
 
242
- blurMenu (e?: FocusEvent) {
243
- if ((e?.relatedTarget as Element)?.tagName === 'LI') {
244
- return
245
- }
251
+ updateMenuHeight()
252
+ })
246
253
 
247
- this.focus = false
248
- this.showDropdown = false
254
+ const keydownListener = (e: Event | KeyboardEvent) => {
255
+ if (focus.value && 'key' in e && e.key.match(/^[A-Za-z0-9\s\-_@]$/)) {
256
+ e.stopPropagation()
257
+ }
258
+ }
249
259
 
250
- if (this.typeahead && !this.searchText) {
251
- this.emitUpdateModelValue(null)
252
- } else if (this.typeahead && this.searchText && this.filteredData.length < 1 && !this.dropdownOverride) {
253
- this.emitUpdateModelValue(null)
254
- this.searchText = ''
255
- } else if (this.typeahead && this.searchText && this.filteredData.length === 1 && this.filteredData[0]) {
256
- this.emitUpdateModelValue(this.filteredData[0])
257
- }
260
+ const blurMenu = (e?: FocusEvent) => {
261
+ if ((e?.relatedTarget as Element)?.tagName === 'LI') {
262
+ return
258
263
  }
259
264
 
260
- updateMenuHeight () {
261
- if (this.openMenuAbove) {
262
- const originalShowDropdownValue = this.showDropdown
263
- if (!this.showDropdown) {
264
- this.showDropdown = true
265
- }
266
- this.$nextTick(() => {
267
- const menu = this.$refs['menu'] as HTMLElement
268
-
269
- if (menu) {
270
- this.menuHeight = menu.getBoundingClientRect().height
271
- }
272
- if (!originalShowDropdownValue) {
273
- this.showDropdown = false
274
- }
275
- })
276
- }
277
-
278
- this.menuHeight = null
265
+ focus.value = false
266
+ showDropdown.value = false
267
+
268
+ if (typeahead && !searchText.value) {
269
+ emitUpdateModelValue(null)
270
+ } else if (typeahead && searchText.value && filteredData.value.length < 1 && !dropdownOverride) {
271
+ emitUpdateModelValue(null)
272
+ searchText.value = ''
273
+ } else if (typeahead && searchText.value && filteredData.value.length === 1 && filteredData.value[0]) {
274
+ emitUpdateModelValue(filteredData.value[0])
279
275
  }
276
+ }
280
277
 
281
- keyPressedItem (e: KeyboardEvent) {
282
- // select option on enter or space or tab (but not shift tab)
283
- if (e.key === 'Enter' || e.key === ' ') {
284
- e.preventDefault()
285
- const itemValue = (e.target as HTMLElement).getAttribute('data-value')
286
- const item = this.filteredData.find(i => itemValue ? String(i.value) === itemValue : i.value === null);
287
- (this.$refs['uikit-select__input-container'] as HTMLElement).focus()
288
- item && this.selectItem(item)
289
- }
290
- // close menu on tab
291
- if (e.key === 'Tab' && !e.shiftKey) {
292
- this.blurMenu()
293
- }
294
- // navigate items with up key
295
- if (e.key === 'ArrowUp') {
296
- e.preventDefault()
297
- const itemValue = (e.target as HTMLElement).getAttribute('data-value')
298
- const itemIndex = this.filteredData.findIndex(
299
- i => itemValue ? String(i.value) === itemValue : i.value === null
300
- )
301
- const prevIndex = itemIndex < 1 ? 0 : itemIndex - 1
302
- const prevValue = this.filteredData[prevIndex]?.value
303
- const items = this.$refs['uikit-select__items'] as HTMLElement[]
304
- const prevItem = items.find(
305
- item => prevValue
306
- ? String(prevValue) === item.getAttribute('data-value')
307
- : item.getAttribute('data-value') === null
308
- )
309
- if (prevItem) {
310
- prevItem.focus()
311
- this.showDropdown = true
312
- this.focus = true
313
- }
278
+ const updateMenuHeight = () => {
279
+ if (openMenuAbove) {
280
+ const originalShowDropdownValue = showDropdown.value
281
+ if (!showDropdown.value) {
282
+ showDropdown.value = true
314
283
  }
315
- // navigate items with down key
316
- if (e.key === 'ArrowDown') {
317
- e.preventDefault()
318
- const data = this.filteredData
319
- const itemValue = (e.target as HTMLElement).getAttribute('data-value')
320
- const itemIndex = data.findIndex(i => itemValue ? String(i.value) === itemValue : i.value === null)
321
- const nextIndex = itemIndex >= data.length - 1 ? data.length - 1 : itemIndex + 1
322
- const nextValue = this.filteredData[nextIndex]?.value
323
- const items = this.$refs['uikit-select__items'] as HTMLElement[]
324
- const nextItem = items.find(
325
- item => nextValue
326
- ? String(nextValue) === item.getAttribute('data-value')
327
- : item.getAttribute('data-value') === null
328
- )
329
- if (nextItem) {
330
- nextItem.focus()
331
- this.showDropdown = true
332
- this.focus = true
284
+ nextTick(() => {
285
+ if (menuRef.value) {
286
+ menuHeight.value = menuRef.value.getBoundingClientRect().height || null
333
287
  }
334
- }
335
- }
336
-
337
- keyPressedContainer (e: KeyboardEvent) {
338
- if (!(e.target as HTMLElement).className.includes('uikit-select__input-container')
339
- && !(e.target as HTMLElement).className.includes('uikit-select__input')
340
- ) {
341
- return
342
- }
343
-
344
- // open showDropdown on down arrow and select first item
345
- if (e.key === 'ArrowDown'
346
- || (e.key === 'Tab' && !e.shiftKey && this.typeahead && this.searchText && this.showDropdown)) {
347
- e.preventDefault()
348
- const firstValue = this.filteredData[0]?.value
349
- const items = this.$refs['uikit-select__items'] as HTMLElement[]
350
- const firstItem = items.find(
351
- item => firstValue
352
- ? String(firstValue) === item.getAttribute('data-value')
353
- : item.getAttribute('data-value') === null
354
- )
355
- if (firstItem) {
356
- firstItem.focus()
357
- this.showDropdown = true
358
- this.focus = true
288
+ if (!originalShowDropdownValue) {
289
+ showDropdown.value = false
359
290
  }
360
- }
291
+ })
292
+ }
361
293
 
362
- // toggle showDropdown on enter or space + not typeahead
363
- if (e.key === 'Enter' || (e.key === ' ' && !this.typeahead)) {
364
- e.preventDefault()
365
- this.showDropdown = !this.showDropdown
366
- }
294
+ menuHeight.value = null
295
+ }
367
296
 
368
- // escape to close dropdown
369
- if (e.key === 'Escape') {
370
- e.preventDefault()
371
- e.stopPropagation()
372
- this.showDropdown = false
373
- }
297
+ const keyPressedItem = (e: KeyboardEvent) => {
298
+ // select option on enter or space or tab (but not shift tab)
299
+ if (e.key === 'Enter' || e.key === ' ') {
300
+ e.preventDefault()
301
+ const itemValue = (e.target as HTMLElement).getAttribute('data-value')
302
+ const item = filteredData.value.find(i => itemValue ? String(i.value) === itemValue : i.value === null)
303
+ inputContainerRef.value?.focus()
304
+ item && selectItem(item)
374
305
  }
375
-
376
- mouseOverSelect () {
377
- this.hover = this.disabled ? false : true
306
+ // close menu on tab
307
+ if (e.key === 'Tab' && !e.shiftKey) {
308
+ blurMenu()
378
309
  }
379
-
380
- focusSelect () {
381
- this.focus = this.disabled ? false : true
382
- if (this.typeahead && !this.searchText) {
383
- this.showDropdown = true
310
+ // navigate items with up key
311
+ if (e.key === 'ArrowUp') {
312
+ e.preventDefault()
313
+ const itemValue = (e.target as HTMLElement).getAttribute('data-value')
314
+ const itemIndex = filteredData.value.findIndex(
315
+ i => itemValue ? String(i.value) === itemValue : i.value === null
316
+ )
317
+ const prevIndex = itemIndex < 1 ? 0 : itemIndex - 1
318
+ const prevValue = filteredData.value[prevIndex]?.value
319
+ const prevItem = itemRefs.value?.find(
320
+ item => prevValue
321
+ ? prevValue === item.getAttribute('data-value')
322
+ : item.getAttribute('data-value') === null
323
+ )
324
+ if (prevItem) {
325
+ prevItem.focus()
326
+ showDropdown.value = true
327
+ focus.value = true
384
328
  }
385
329
  }
386
-
387
- selectItem (item: IItem) {
388
- if (item) {
389
- this.showDropdown = false
330
+ // navigate items with down key
331
+ if (e.key === 'ArrowDown') {
332
+ e.preventDefault()
333
+ const itemValue = (e.target as HTMLElement).getAttribute('data-value')
334
+ const itemIndex = filteredData.value.findIndex(i =>
335
+ itemValue
336
+ ? String(i.value) === itemValue
337
+ : i.value === null
338
+ )
339
+ const nextIndex = itemIndex >= filteredData.value.length - 1 ? filteredData.value.length - 1 : itemIndex + 1
340
+ const nextValue = filteredData.value[nextIndex]?.value
341
+ const nextItem = itemRefs.value?.find(
342
+ item => nextValue
343
+ ? nextValue === item.getAttribute('data-value')
344
+ : item.getAttribute('data-value') === null
345
+ )
346
+ if (nextItem) {
347
+ nextItem.focus()
348
+ showDropdown.value = true
349
+ focus.value = true
390
350
  }
351
+ }
352
+ }
391
353
 
392
- if (item.type === 'link') {
393
- this.emitLinkClick(item.value ? item : null)
394
- } else {
395
- this.emitUpdateModelValue(item.value ? item : null)
396
- }
354
+ const keyPressedContainer = (e: KeyboardEvent) => {
355
+ if (!(e.target as HTMLElement).className.includes('uikit-select__input-container')
356
+ && !(e.target as HTMLElement).className.includes('uikit-select__input')
357
+ ) {
358
+ return
397
359
  }
398
360
 
399
- performCloseActions () {
400
- /**
401
- * If the dropdown is showing an X, there is some logic we need...
402
- * The first time, it should clear out search text -- the second time it should collapse dropdown
403
- * */
404
- if (this.searchText) {
405
- this.emitUpdateModelValue(null)
406
- this.searchText = ''
407
- } else {
408
- this.showDropdown = false
409
- this.emitClose()
361
+ // open showDropdown on down arrow and select first item
362
+ if (e.key === 'ArrowDown'
363
+ || (e.key === 'Tab' && !e.shiftKey && typeahead && searchText.value && showDropdown.value)) {
364
+ e.preventDefault()
365
+ const firstValue = filteredData.value[0]?.value
366
+ const firstItem = itemRefs.value?.find(
367
+ item => firstValue
368
+ ? firstValue === item.getAttribute('data-value')
369
+ : item.getAttribute('data-value') === null
370
+ )
371
+ if (firstItem) {
372
+ firstItem.focus()
373
+ showDropdown.value = true
374
+ focus.value = true
410
375
  }
411
376
  }
412
377
 
413
- @Watch('showDropdown')
414
- showDropdownChanged (newVal: boolean) {
415
- this.emitOpenDropdown(newVal)
378
+ // toggle showDropdown on enter or space + not typeahead
379
+ if (e.key === 'Enter' || (e.key === ' ' && !typeahead)) {
380
+ e.preventDefault()
381
+ showDropdown.value = !showDropdown.value
416
382
  }
417
383
 
418
- @Watch('modelValue')
419
- valueChanged (newVal: IItem) {
420
- if (this.typeahead) {
421
- this.searchText = newVal?.label || ''
422
- }
384
+ // escape to close dropdown
385
+ if (e.key === 'Escape') {
386
+ e.preventDefault()
387
+ e.stopPropagation()
388
+ showDropdown.value = false
423
389
  }
390
+ }
424
391
 
425
- @Watch('searchText')
426
- searchTextChanged (newVal: string) {
427
- if (this.typeahead) {
428
- const matchedItem = this.filteredData.find(item =>
429
- item.label.toLowerCase() === newVal.toLowerCase()
430
- )
431
- if (!matchedItem) {
432
- this.showDropdown = true
433
- }
434
- if (this.filteredData.length === 1 && matchedItem && this.modelValue?.label !== matchedItem.label) {
435
- this.selectItem(matchedItem)
436
- }
392
+ const mouseOverSelect = () => {
393
+ hover.value = disabled ? false : true
394
+ }
437
395
 
438
- this.updateMenuHeight()
439
- }
396
+ const focusSelect = () => {
397
+ focus.value = disabled ? false : true
398
+ if (typeahead && !searchText.value) {
399
+ showDropdown.value = true
400
+ }
401
+ }
402
+
403
+ const selectItem = (item: IItem) => {
404
+ if (item) {
405
+ showDropdown.value = false
440
406
  }
441
407
 
442
- @Emit('update:modelValue')
443
- emitUpdateModelValue (item: IItem | null) {
444
- return item
408
+ if (item.type === 'link') {
409
+ emitLinkClick(item.value ? item : null)
410
+ } else {
411
+ emitUpdateModelValue(item.value ? item : null)
445
412
  }
413
+ }
446
414
 
447
- @Emit('linkClick')
448
- emitLinkClick (item: IItem | null) {
449
- return item
415
+ const performCloseActions = () => {
416
+ /**
417
+ * If the dropdown is showing an X, there is some logic we need...
418
+ * The first time, it should clear out search text -- the second time it should collapse dropdown
419
+ * */
420
+ if (searchText.value) {
421
+ emitUpdateModelValue(null)
422
+ searchText.value = ''
423
+ } else {
424
+ showDropdown.value = false
425
+ emitClose()
450
426
  }
427
+ }
428
+
429
+ watch(showDropdown, (newVal: boolean) => {
430
+ emitOpenDropdown(newVal)
431
+ })
451
432
 
452
- @Emit('openDropdown')
453
- emitOpenDropdown (open: boolean) {
454
- return open
433
+ watch(() => modelValue, (newVal) => {
434
+ if (typeahead) {
435
+ searchText.value = newVal?.label || ''
455
436
  }
437
+ })
456
438
 
457
- @Emit('close')
458
- emitClose () {
459
- return true
439
+ watch(searchText, (newVal: string) => {
440
+ if (typeahead) {
441
+ const matchedItem = filteredData.value.find(item =>
442
+ item.label.toLowerCase() === newVal.toLowerCase()
443
+ )
444
+ if (!matchedItem) {
445
+ showDropdown.value = true
446
+ }
447
+ if (filteredData.value.length === 1 && matchedItem && modelValue?.label !== matchedItem.label) {
448
+ selectItem(matchedItem)
449
+ }
450
+
451
+ updateMenuHeight()
460
452
  }
453
+ })
454
+
455
+ const emitUpdateModelValue = (item: IItem | null) => {
456
+ emit('update:modelValue', item)
457
+ }
458
+
459
+ const emitLinkClick = (item: IItem | null) => {
460
+ emit('linkClick', item)
461
+ }
462
+
463
+ const emitOpenDropdown = (open: boolean) => {
464
+ emit('openDropdown', open)
465
+ }
466
+
467
+ const emitClose = () => {
468
+ emit('close')
461
469
  }
462
470
  </script>
463
471
 
@@ -613,7 +621,7 @@ export default class Select extends Vue {
613
621
  border-color: $pewter;
614
622
  color: $fog;
615
623
  caret-color: $banana-bread;
616
-
624
+
617
625
  &--hover {
618
626
  background-color: $charcoal;
619
627
  }