@teammodlr/ui-kit 2.3.23 → 3.0.0-beta.4

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 (95) hide show
  1. package/lib/components/avatar/Avatar.vue.d.ts +2 -2
  2. package/lib/components/badge/style/Badge.css.d.ts +2 -2
  3. package/lib/components/button/Button.vue.d.ts +7 -4
  4. package/lib/components/button/style/Button.css.d.ts +3 -3
  5. package/lib/components/cardcontextmenu/CardContextMenu.vue.d.ts +5 -8
  6. package/lib/components/cardcontextmenu/CardContextMenuItem.vue.d.ts +11 -2
  7. package/lib/components/cardcontextmenu/CardContextMenuTriggerProvider.vue.d.ts +2 -0
  8. package/lib/components/cardcontextmenu/style/Dropdown.css.d.ts +2 -1
  9. package/lib/components/cardcontextmenu/symbol.d.ts +1 -0
  10. package/lib/components/checkbox/Checkbox.vue.d.ts +15 -4
  11. package/lib/components/collapse/CollapseItem.vue.d.ts +3 -2
  12. package/lib/components/colorpicker/ColorPicker.vue.d.ts +8 -9
  13. package/lib/components/confirm/Confirm.vue.d.ts +9 -0
  14. package/lib/components/confirm/props.d.ts +4 -0
  15. package/lib/components/dialog/Dialog.vue.d.ts +13 -5
  16. package/lib/components/dialogprovider/DialogProvider.vue.d.ts +1 -0
  17. package/lib/components/dialogprovider/types.d.ts +1 -0
  18. package/lib/components/dropdown/Dropdown.vue.d.ts +3 -1
  19. package/lib/components/dropdown/DropdownItem.vue.d.ts +11 -1
  20. package/lib/components/dropdown/DropdownSeperator.vue.d.ts +1 -0
  21. package/lib/components/dropdown/DropdownTriggerProvider.vue.d.ts +2 -0
  22. package/lib/components/dropdown/style/Dropdown.css.d.ts +2 -1
  23. package/lib/components/dropdown/symbol.d.ts +2 -0
  24. package/lib/components/image/Image.vue.d.ts +1 -1
  25. package/lib/components/messageprovider/Message.vue.d.ts +1 -1
  26. package/lib/components/messageprovider/MessageProvider.vue.d.ts +22 -24
  27. package/lib/components/messageprovider/style/Message.css.d.ts +1 -1
  28. package/lib/components/messageprovider/types.d.ts +5 -0
  29. package/lib/components/modal/Modal.vue.d.ts +18 -6
  30. package/lib/components/radio/Radio.vue.d.ts +2 -0
  31. package/lib/components/radiobuttongroup/RadioButtonGroup.vue.d.ts +1 -0
  32. package/lib/components/select/Select.vue.d.ts +20 -11
  33. package/lib/components/switch/Switch.vue.d.ts +2 -0
  34. package/lib/components/tab/Tab.vue.d.ts +5 -2
  35. package/lib/components/tab/TabGroup.vue.d.ts +8 -3
  36. package/lib/components/textarea/Textarea.vue.d.ts +2 -1
  37. package/lib/components/tooltip/Tooltip.vue.d.ts +5 -20
  38. package/lib/components/tooltip/TooltipTrigger.vue.d.ts +1 -10
  39. package/lib/components/tree/Tree.vue.d.ts +3 -3
  40. package/lib/components/tree/TreeNode.vue.d.ts +26 -12
  41. package/lib/components/tree/types.d.ts +5 -5
  42. package/lib/index.cjs.js +5 -14
  43. package/lib/index.es.js +15444 -10914
  44. package/lib/style.css +1 -1
  45. package/package.json +12 -6
  46. package/src/components/avatar/Avatar.vue +32 -24
  47. package/src/components/button/Button.vue +24 -5
  48. package/src/components/cardcontextmenu/CardContextMenu.vue +68 -95
  49. package/src/components/cardcontextmenu/CardContextMenuItem.vue +40 -50
  50. package/src/components/cardcontextmenu/CardContextMenuSeperator.vue +9 -7
  51. package/src/components/cardcontextmenu/CardContextMenuTriggerProvider.vue +15 -0
  52. package/src/components/cardcontextmenu/style/Dropdown.css.ts +155 -2
  53. package/src/components/cardcontextmenu/symbol.ts +3 -0
  54. package/src/components/checkbox/Checkbox.vue +37 -12
  55. package/src/components/checkbox/style/Checkbox.css.ts +6 -6
  56. package/src/components/collapse/CollapseItem.vue +46 -21
  57. package/src/components/collapse/style/Collapse.css.ts +5 -1
  58. package/src/components/colorpicker/ColorPicker.vue +76 -45
  59. package/src/components/confirm/Confirm.vue +15 -8
  60. package/src/components/confirm/props.ts +4 -0
  61. package/src/components/dialog/Dialog.vue +183 -37
  62. package/src/components/dialog/style/Dialog.css.ts +15 -1
  63. package/src/components/dialogprovider/types.ts +1 -0
  64. package/src/components/dropdown/Dropdown.vue +46 -3
  65. package/src/components/dropdown/DropdownItem.vue +55 -25
  66. package/src/components/dropdown/DropdownSeperator.vue +19 -11
  67. package/src/components/dropdown/DropdownTriggerProvider.vue +15 -0
  68. package/src/components/dropdown/examples/context.vue +2 -2
  69. package/src/components/dropdown/style/Dropdown.css.ts +157 -4
  70. package/src/components/dropdown/symbol.ts +4 -0
  71. package/src/components/form/examples/basic.vue +19 -8
  72. package/src/components/messageprovider/MessageProvider.vue +136 -90
  73. package/src/components/messageprovider/examples/basic.vue +3 -1
  74. package/src/components/messageprovider/examples/index.stories.ts +37 -1
  75. package/src/components/messageprovider/types.ts +10 -0
  76. package/src/components/modal/Modal.vue +142 -67
  77. package/src/components/modal/style/Modal.css.ts +26 -2
  78. package/src/components/radio/Radio.vue +31 -17
  79. package/src/components/radiobutton/RadioButton.vue +15 -2
  80. package/src/components/radiobuttongroup/RadioButtonGroup.vue +11 -4
  81. package/src/components/select/Select.vue +301 -153
  82. package/src/components/select/examples/basic.vue +1 -1
  83. package/src/components/slider/Slider.vue +1 -1
  84. package/src/components/switch/Switch.vue +30 -10
  85. package/src/components/switch/style/Switch.css.ts +3 -5
  86. package/src/components/tab/Tab.vue +29 -20
  87. package/src/components/tab/TabGroup.vue +29 -38
  88. package/src/components/textarea/Textarea.vue +8 -4
  89. package/src/components/tooltip/Tooltip.vue +52 -141
  90. package/src/components/tooltip/TooltipTrigger.vue +14 -17
  91. package/src/components/tooltip/style/Tooltip.css.ts +148 -29
  92. package/src/components/tree/Tree.vue +138 -100
  93. package/src/components/tree/TreeNode.vue +105 -98
  94. package/src/components/tree/types.ts +4 -4
  95. package/src/styles/transitions.css.ts +36 -16
@@ -1,65 +1,70 @@
1
1
  <template>
2
- <div ref="selectRef" tabindex="-1">
3
- <label v-if="label" :for="id" :class="style.label" :id="`${id}-label`">
4
- {{ label }}
5
- </label>
6
- <div v-if="description" :class="style.description">
7
- {{ description }}
8
- </div>
9
-
10
- <div :class="computedWrapperClasses" v-on="inputListeners" :id="id" ref="container"
11
- :aria-disabled="disabled ? 'true' : 'false'" :aria-labelledby="`${id}-label`" tabindex="0"
12
- @mousedown.prevent="onSelectClick" @focusin.stop="focusSelect" :data-value="selected">
13
- <div :class="selectComputedClasses">
14
- <template v-if="
15
- multiple &&
16
- Array.isArray(selected) &&
17
- selected.length > 0
18
- ">
19
- <MTag v-for="value in selectedMax" :key="value" :closeable="true"
20
- @close="event => removeSelected(event, value)" :size="size">
21
- {{ getSelectedLabel(value) }}
22
- </MTag>
23
- <template v-if="selectedCountNotShown > 0">
24
- <MTag :size="size">
25
- +{{ selectedCountNotShown }} more
26
- </MTag>
27
- </template>
28
- </template>
29
- <template v-else-if="!multiple && selected">
30
- {{ getSelectedLabel(selected) }}
31
- </template>
32
- <template v-else>
33
- <div :class="style.placeholder">{{ placeholder }}</div>
34
- </template>
35
- <span :class="selectComputedArrowClasses">
36
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
37
- </span>
2
+ <PopoverRoot :open="isOpen" @update:open="handleOpenChange">
3
+ <div ref="selectRef" tabindex="-1">
4
+ <label v-if="label" :for="id" :class="style.label" :id="`${id}-label`">
5
+ {{ label }}
6
+ </label>
7
+ <div v-if="description" :class="style.description">
8
+ {{ description }}
38
9
  </div>
39
10
 
40
- <Teleport :to="'body'">
41
- <Transition :enter-active-class="scale.enterActive" :leave-active-class="scale.leaveActive"
42
- :enter-from-class="scale.enterFrom" :leave-to-class="scale.leaveTo">
43
- <div :class="style.menu" v-if="isOpen" ref="popper" :style="computedConsistentWidth"
44
- :aria-hidden="isOpen ? 'false' : 'true'" :data-element="`select-menu-overlay-${id}`">
11
+ <PopoverTrigger as-child :disabled="disabled || loading">
12
+ <div :class="computedWrapperClasses" v-on="inputListeners" :id="id" ref="container"
13
+ :aria-disabled="disabled ? 'true' : 'false'" :aria-labelledby="`${id}-label`" tabindex="0"
14
+ data-m-select-trigger="true"
15
+ @focus="handleTriggerFocus" @blur="handleTriggerBlur" @keydown="handleKeyDown" :data-value="selected">
16
+ <div :class="selectComputedClasses">
17
+ <template v-if="multiple && Array.isArray(selected) && selected.length > 0">
18
+ <MTag v-for="value in selectedMax" :key="value" :closeable="true"
19
+ @close="event => removeSelected(event, value)" :size="size">
20
+ {{ getSelectedLabel(value) }}
21
+ </MTag>
22
+ <template v-if="selectedCountNotShown > 0">
23
+ <MTag :size="size">
24
+ +{{ selectedCountNotShown }} more
25
+ </MTag>
26
+ </template>
27
+ </template>
28
+ <template v-else-if="!multiple && selected">
29
+ {{ getSelectedLabel(selected) }}
30
+ </template>
31
+ <template v-else>
32
+ <div :class="style.placeholder">{{ placeholder }}</div>
33
+ </template>
34
+ <span :class="selectComputedArrowClasses">
35
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>
36
+ </span>
37
+ </div>
38
+ </div>
39
+ </PopoverTrigger>
40
+
41
+ <PopoverPortal :disabled="!portal">
42
+ <Transition type="animation" :enter-active-class="fade.enterActive" :leave-active-class="fade.leaveActive"
43
+ :enter-from-class="fade.enterFrom" :leave-to-class="fade.leaveTo">
44
+ <PopoverContent
45
+ v-if="isOpen"
46
+ ref="popper"
47
+ :class="style.menu"
48
+ :style="computedConsistentWidth"
49
+ side="bottom"
50
+ align="start"
51
+ :side-offset="5"
52
+ :collision-padding="8"
53
+ :disable-outside-pointer-events="false"
54
+ :aria-hidden="isOpen ? 'false' : 'true'"
55
+ :data-element="`select-menu-overlay-${id}`"
56
+ @keydown.capture="handleKeyDown"
57
+ @open-auto-focus.prevent
58
+ @close-auto-focus.prevent
59
+ >
45
60
  <div v-if="searchable || (multiple && selectAll)" :class="style.search">
46
61
  <div v-if="multiple && selectAll" :class="style.checkAll">
47
62
  <button role="button" type="button" :class="{
48
63
  [style.checkAllButton]: true,
49
- [style.checkAllButtonActive]:
50
- computedOptions.length > 0 &&
51
- computedOptions.length ===
52
- selected.length
64
+ [style.checkAllButtonActive]: computedOptions.length > 0 && computedOptions.length === selected.length
53
65
  }" @click.prevent="toggleAllItems" aria-description="Select all items">
54
- <svg v-if="
55
- computedOptions.length > 0 &&
56
- computedOptions.length ===
57
- selected.length
58
- " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
59
- aria-hidden="true">
60
- <path fill-rule="evenodd"
61
- d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
62
- clip-rule="evenodd"></path>
66
+ <svg v-if="computedOptions.length > 0 && computedOptions.length === selected.length" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
67
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
63
68
  </svg>
64
69
  </button>
65
70
  </div>
@@ -70,42 +75,36 @@
70
75
  <div v-for="(option, index) in computedOptions" :key="index" ref="optionsRef" :class="{
71
76
  [style.option]: true,
72
77
  [style.optionSelected]: option.active,
73
- [style.optionFocused]:
74
- selectedIndex === index
75
- }" @click.prevent="option.onClick" @hover="selectedIndex = index"
76
- :data-element="`select-menu-option-${id}-${option.value}`"
77
- >
78
+ [style.optionFocused]: selectedIndex === index
79
+ }" @click.prevent="option.onClick" @mouseenter="selectedIndex = index"
80
+ :data-element="`select-menu-option-${id}-${option.value}`">
78
81
  <span :class="style.optionIcon">
79
- <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
80
- fill="currentColor" aria-hidden="true">
81
- <path fill-rule="evenodd"
82
- d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
83
- clip-rule="evenodd"></path>
82
+ <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
83
+ <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" clip-rule="evenodd"></path>
84
84
  </svg>
85
85
  </span>
86
- <span :class="style.optionLabel">{{
87
- option.label
88
- }}</span>
86
+ <span :class="style.optionLabel">{{ option.label }}</span>
89
87
  </div>
90
88
  <template v-if="computedOptions.length === 0">
91
89
  <div :class="style.noResults">No results</div>
92
90
  </template>
93
91
  </div>
94
- </div>
92
+ </PopoverContent>
95
93
  </Transition>
96
- </Teleport>
97
- </div>
98
- <div v-if="inputError" :class="style.errorMessage">
99
- {{ inputError }}
94
+ </PopoverPortal>
95
+
96
+ <div v-if="inputError" :class="style.errorMessage">
97
+ {{ inputError }}
98
+ </div>
100
99
  </div>
101
- </div>
100
+ </PopoverRoot>
102
101
  </template>
103
102
 
104
103
  <script lang="ts">
105
104
  import { useInput } from '../../composables/useInput';
106
- import { usePopper, type Placement } from '../../composables/usePopper';
107
105
  import type { MaybeArray, Size } from '../../types/props';
108
- import { onClickOutside, useEventListener } from '@vueuse/core';
106
+ import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui';
107
+ import { useEventListener } from '@vueuse/core';
109
108
  import {
110
109
  computed,
111
110
  defineComponent,
@@ -113,16 +112,18 @@ import {
113
112
  ref,
114
113
  watch,
115
114
  watchEffect,
116
- type PropType
115
+ type PropType
117
116
  } from 'vue';
118
117
  import MTag from '../tag/Tag.vue';
119
118
  import * as style from './style';
120
- import { scale } from '../../styles/transitions.css';
119
+ import { fade } from '../../styles/transitions.css';
121
120
  import { useInteractive } from '../../composables/useInteractive';
122
121
  import type { OnUpdateValue, OnUpdateValueImpl } from './types';
123
122
  import { call } from "../../utils/call";
124
123
  import {useZIndex} from "../../composables/useZIndex";
125
124
 
125
+ let shouldOpenNextFocusedSelect = false;
126
+
126
127
  export default defineComponent({
127
128
  name: 'MSelect',
128
129
  emits: {
@@ -197,6 +198,10 @@ export default defineComponent({
197
198
  type: Number,
198
199
  default: 5
199
200
  },
201
+ portal: {
202
+ type: Boolean,
203
+ default: false
204
+ },
200
205
  zIndex: {
201
206
  type: Number
202
207
  },
@@ -208,22 +213,28 @@ export default defineComponent({
208
213
  onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
209
214
  },
210
215
  components: {
211
- MTag
216
+ MTag,
217
+ PopoverContent,
218
+ PopoverPortal,
219
+ PopoverRoot,
220
+ PopoverTrigger
212
221
  },
213
222
  setup(props, { emit, expose }) {
214
223
  const container = ref<HTMLElement | null>(null);
215
- const wrapper = ref<HTMLElement>();
216
- const optionsRef = ref<HTMLElement[]>();
217
- const searchRef = ref<HTMLElement>();
224
+ const optionsRef = ref<HTMLElement[]>([]);
225
+ const searchRef = ref<HTMLInputElement>();
218
226
  const selectRef = ref<HTMLElement>();
227
+ const popper = ref<HTMLElement>();
219
228
  const selectedIndex = ref<number | undefined>(0);
220
229
  const open = ref<boolean>(false);
221
230
  const focused = ref<boolean>(false);
222
231
  const search = ref<string>('');
223
- const placement = ref<Placement>('bottom-start');
224
- const transfer = ref(false);
225
232
  const width = ref<number>(0);
226
233
  const keyboardNavigation = ref<boolean>(false);
234
+ const focusOpenedByKeyboard = ref<boolean>(false);
235
+ const pendingFocusDirection = ref<boolean | null>(null);
236
+ const restoreTriggerFocusOnClose = ref<boolean>(false);
237
+ const suppressOpenOnFocusOnce = ref<boolean>(false);
227
238
  const {nextZIndex} = useZIndex();
228
239
  function doUpdateValue(
229
240
  value: string | number | Array<string | number> | null,
@@ -254,14 +265,6 @@ export default defineComponent({
254
265
  formLoading
255
266
  } = useInput(props, { focus, emit, withListeners: false });
256
267
 
257
- const { popper, updatePopper } = usePopper({
258
- placement,
259
- transfer,
260
- reference: container,
261
- wrapper,
262
- offset: [0, 5]
263
- });
264
-
265
268
  const disabled = computed(() => formDisabled || props.disabled);
266
269
  const loading = computed(() => formLoading || props.loading);
267
270
 
@@ -324,7 +327,7 @@ export default defineComponent({
324
327
 
325
328
  return {
326
329
  ...styles,
327
- zIndex: zIndex.value
330
+ zIndex: props.portal ? zIndex.value : 1
328
331
  };
329
332
  });
330
333
 
@@ -437,11 +440,16 @@ export default defineComponent({
437
440
  }
438
441
  }
439
442
 
440
- function openSelect() {
443
+ function openSelect(forceOpen = false) {
441
444
  if (disabled.value || loading.value) {
442
445
  return;
443
446
  }
444
447
 
448
+ if (forceOpen) {
449
+ open.value = true;
450
+ return;
451
+ }
452
+
445
453
  if (open.value) {
446
454
  open.value = false;
447
455
  return;
@@ -476,6 +484,88 @@ export default defineComponent({
476
484
  }
477
485
  }
478
486
 
487
+ function getFocusableElements() {
488
+ return Array.from(
489
+ document.querySelectorAll<HTMLElement>(
490
+ 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
491
+ )
492
+ ).filter(element => {
493
+ return !element.matches(':disabled')
494
+ && element.getAttribute('aria-disabled') !== 'true'
495
+ && element.getAttribute('aria-hidden') !== 'true'
496
+ && element.tabIndex >= 0
497
+ && element.getClientRects().length > 0;
498
+ });
499
+ }
500
+
501
+ function getElementRef<T extends HTMLElement>(value: T | { $el?: T } | null | undefined) {
502
+ if (!value) {
503
+ return null;
504
+ }
505
+
506
+ if (value instanceof HTMLElement) {
507
+ return value;
508
+ }
509
+
510
+ if ('$el' in value && value.$el instanceof HTMLElement) {
511
+ return value.$el;
512
+ }
513
+
514
+ return null;
515
+ }
516
+
517
+ function getAdjacentFocusableElement(backwards = false) {
518
+ if (!container.value) {
519
+ return null;
520
+ }
521
+
522
+ const popperElement = getElementRef(popper.value);
523
+
524
+ const focusableElements = getFocusableElements();
525
+ const currentIndex = focusableElements.indexOf(container.value);
526
+
527
+ if (currentIndex === -1) {
528
+ return null;
529
+ }
530
+
531
+ const isInsideCurrentSelect = (element: HTMLElement) => {
532
+ return container.value?.contains(element)
533
+ || popperElement?.contains(element)
534
+ || selectRef.value?.contains(element);
535
+ };
536
+
537
+ let nextIndex = backwards ? currentIndex - 1 : currentIndex + 1;
538
+
539
+ while (nextIndex >= 0 && nextIndex < focusableElements.length) {
540
+ const nextElement = focusableElements[nextIndex];
541
+
542
+ if (!isInsideCurrentSelect(nextElement)) {
543
+ return nextElement;
544
+ }
545
+
546
+ nextIndex += backwards ? -1 : 1;
547
+ }
548
+
549
+ return null;
550
+ }
551
+
552
+ function focusAdjacentElement(backwards = false) {
553
+ const nextElement = getAdjacentFocusableElement(backwards);
554
+
555
+ if (!nextElement) {
556
+ return;
557
+ }
558
+
559
+ shouldOpenNextFocusedSelect = nextElement.getAttribute('data-m-select-trigger') === 'true';
560
+
561
+ nextElement.focus();
562
+ }
563
+
564
+ function closeAndMoveFocus(backwards = false) {
565
+ pendingFocusDirection.value = backwards;
566
+ open.value = false;
567
+ }
568
+
479
569
  function handleOptionClick(value: string | number) {
480
570
  const option = computedOptions.value.find(
481
571
  item => item.value === value
@@ -489,17 +579,15 @@ export default defineComponent({
489
579
  const index = selected.value.indexOf(value);
490
580
 
491
581
  if (index !== -1) {
492
- selected.value.splice(index, 1);
582
+ selected.value = selected.value.filter(item => item !== value);
493
583
  } else {
494
- selected.value.push(value);
495
- emit('update:modelValue', selected.value);
584
+ selected.value = [...selected.value, value];
496
585
  }
497
586
  } else {
498
587
  selected.value = [value];
499
588
  }
500
-
501
- updatePopper();
502
589
  } else {
590
+ restoreTriggerFocusOnClose.value = true;
503
591
  selected.value = value;
504
592
  open.value = false;
505
593
  }
@@ -513,13 +601,7 @@ export default defineComponent({
513
601
  }
514
602
 
515
603
  if (Array.isArray(selected.value)) {
516
- const index = selected.value.indexOf(value);
517
-
518
- if (index !== -1) {
519
- selected.value.splice(index, 1);
520
- }
521
-
522
- updatePopper();
604
+ selected.value = selected.value.filter(item => item !== value);
523
605
  }
524
606
  }
525
607
 
@@ -527,26 +609,58 @@ export default defineComponent({
527
609
  open.value = false;
528
610
  });
529
611
 
530
- function handleKeyDown(e: Event) {
531
- if (computedOptions.value.length === 0) {
532
- return;
533
- }
612
+ useEventListener('keydown', (event: KeyboardEvent) => {
613
+ focusOpenedByKeyboard.value = event.key === 'Tab';
614
+ });
615
+
616
+ useEventListener('pointerdown', () => {
617
+ focusOpenedByKeyboard.value = false;
618
+ });
534
619
 
620
+ function handleKeyDown(e: Event) {
535
621
  if (props.disabled) {
536
622
  return;
537
623
  }
538
624
 
539
- keyboardNavigation.value = true;
540
-
541
625
  const event: KeyboardEvent = e as KeyboardEvent;
542
626
 
543
- switch (event.code) {
544
- case 'Tab':
545
- if (open.value) {
627
+ if (event.key === 'Tab') {
628
+ if (open.value) {
629
+ const activeElement = document.activeElement as HTMLElement | null;
630
+ const popperElement = getElementRef(popper.value);
631
+ const isFocusInsidePopup = !!activeElement && !!popperElement?.contains(activeElement);
632
+
633
+ if (isFocusInsidePopup) {
634
+ event.preventDefault();
635
+ const nextElement = getAdjacentFocusableElement(event.shiftKey);
636
+
637
+ pendingFocusDirection.value = null;
638
+ restoreTriggerFocusOnClose.value = false;
639
+ open.value = false;
640
+
641
+ if (nextElement) {
642
+ requestAnimationFrame(() => {
643
+ shouldOpenNextFocusedSelect = nextElement.getAttribute('data-m-select-trigger') === 'true';
644
+
645
+ nextElement.focus();
646
+ });
647
+ }
648
+ } else {
649
+ pendingFocusDirection.value = null;
650
+ restoreTriggerFocusOnClose.value = false;
546
651
  open.value = false;
547
652
  }
548
- break;
653
+ }
654
+ return;
655
+ }
656
+
657
+ keyboardNavigation.value = true;
658
+
659
+ switch (event.key) {
549
660
  case 'ArrowDown':
661
+ if (computedOptions.value.length === 0) {
662
+ return;
663
+ }
550
664
  e.preventDefault();
551
665
  if (!open.value) {
552
666
  open.value = true;
@@ -556,6 +670,9 @@ export default defineComponent({
556
670
  getSelectedIndex(selectedIndex.value, 'down');
557
671
  break;
558
672
  case 'ArrowUp':
673
+ if (computedOptions.value.length === 0) {
674
+ return;
675
+ }
559
676
  e.preventDefault();
560
677
  if (!open.value) {
561
678
  return;
@@ -564,6 +681,9 @@ export default defineComponent({
564
681
  getSelectedIndex(selectedIndex.value, 'up');
565
682
  break;
566
683
  case 'Enter':
684
+ if (computedOptions.value.length === 0) {
685
+ return;
686
+ }
567
687
  e.preventDefault();
568
688
  e.stopPropagation();
569
689
 
@@ -572,9 +692,13 @@ export default defineComponent({
572
692
  return;
573
693
  }
574
694
 
575
- if (typeof selectedIndex.value !== 'undefined') {
695
+ const activeIndex = typeof selectedIndex.value === 'undefined'
696
+ ? 0
697
+ : selectedIndex.value;
698
+
699
+ if (computedOptions.value[activeIndex]) {
576
700
  handleOptionClick(
577
- computedOptions.value[selectedIndex.value].value
701
+ computedOptions.value[activeIndex].value
578
702
  );
579
703
  if (!props.multiple) {
580
704
  open.value = false;
@@ -582,13 +706,14 @@ export default defineComponent({
582
706
  }
583
707
  break;
584
708
  case 'Escape':
585
- e.preventDefault();
586
- e.stopPropagation();
587
-
588
709
  if (!open.value) {
589
710
  return;
590
711
  }
591
712
 
713
+ e.preventDefault();
714
+ e.stopPropagation();
715
+
716
+ restoreTriggerFocusOnClose.value = true;
592
717
  open.value = false;
593
718
  break;
594
719
  }
@@ -617,12 +742,7 @@ export default defineComponent({
617
742
  })
618
743
 
619
744
  const computedWrapperClasses = computed(() => {
620
- return [
621
- style.wrapper,
622
- {
623
- [style.focused]: focused.value
624
- }
625
- ];
745
+ return [style.wrapper];
626
746
  });
627
747
 
628
748
  const selectComputedClasses = computed(() => {
@@ -633,10 +753,10 @@ export default defineComponent({
633
753
  props.multiple &&
634
754
  Array.isArray(selected.value) &&
635
755
  selected.value.length > 0,
636
- [style.focused]: open.value,
756
+ [style.focused]: open.value || focused.value,
637
757
  // [`m-select--${props.size}`]: props.size,
638
758
  'm-select--disabled': disabled.value || loading.value,
639
- 'm-select--focused': open.value,
759
+ 'm-select--focused': open.value || focused.value,
640
760
  'm-select--multiple': props.multiple,
641
761
  'm-select--error': formError.value
642
762
  }
@@ -654,17 +774,6 @@ export default defineComponent({
654
774
  return option[props.optionLabel];
655
775
  }
656
776
 
657
- useEventListener(selectRef, 'keydown', handleKeyDown);
658
- useEventListener(searchRef, 'keydown', handleKeyDown);
659
-
660
- onClickOutside(
661
- container,
662
- () => {
663
- open.value = false;
664
- },
665
- { ignore: [searchRef, popper] }
666
- );
667
-
668
777
  watchEffect(() => {
669
778
  if (container.value && open.value) {
670
779
  width.value = container.value.offsetWidth;
@@ -693,22 +802,46 @@ export default defineComponent({
693
802
  zIndex.value = nextZIndex();
694
803
  }
695
804
 
696
- if (props.searchable && searchRef.value) {
805
+ if (value && props.searchable && searchRef.value) {
697
806
  searchRef.value.focus();
807
+ } else if (value && container.value) {
808
+ container.value.focus();
809
+ }
810
+
811
+ if (!value && restoreTriggerFocusOnClose.value && container.value) {
812
+ restoreTriggerFocusOnClose.value = false;
813
+ suppressOpenOnFocusOnce.value = true;
814
+ requestAnimationFrame(() => {
815
+ container.value?.focus();
816
+ });
698
817
  }
818
+
699
819
  if (value && optionsRef.value) {
820
+ const selectedValue = Array.isArray(selected.value)
821
+ ? selected.value[0]
822
+ : selected.value;
700
823
  const index = computedOptions.value.findIndex(
701
- i => i.value === selected.value
824
+ i => i.value === selectedValue
702
825
  );
703
826
  if (index !== -1) {
704
827
  optionsRef.value[index].scrollIntoView({
705
828
  block: 'nearest'
706
829
  });
707
830
  selectedIndex.value = index;
831
+ } else if (computedOptions.value.length > 0) {
832
+ selectedIndex.value = 0;
708
833
  }
709
834
  }
710
835
 
711
836
  keyboardNavigation.value = false;
837
+
838
+ if (!value && pendingFocusDirection.value !== null) {
839
+ const backwards = pendingFocusDirection.value;
840
+ pendingFocusDirection.value = null;
841
+ requestAnimationFrame(() => {
842
+ focusAdjacentElement(backwards);
843
+ });
844
+ }
712
845
  });
713
846
  });
714
847
 
@@ -727,20 +860,33 @@ export default defineComponent({
727
860
  return open.value;
728
861
  });
729
862
 
730
- const focusSelect = (event: FocusEvent) => {
731
- setTimeout(() => {
732
- if (selectRef.value && !open.value) {
733
- openSelect();
734
- }
735
- }, 10);
863
+ const handleOpenChange = (value: boolean) => {
864
+ open.value = value;
736
865
  };
737
866
 
738
- const onSelectClick = (event: MouseEvent) => {
739
- event.preventDefault();
740
- event.stopPropagation();
867
+ const handleTriggerFocus = () => {
868
+ focused.value = true;
869
+
870
+ if (suppressOpenOnFocusOnce.value) {
871
+ suppressOpenOnFocusOnce.value = false;
872
+ return;
873
+ }
874
+
875
+ const shouldOpenFromFocus = focusOpenedByKeyboard.value || shouldOpenNextFocusedSelect;
876
+
877
+ shouldOpenNextFocusedSelect = false;
878
+
879
+ if (!shouldOpenFromFocus || open.value) {
880
+ return;
881
+ }
882
+
741
883
  openSelect();
742
884
  };
743
885
 
886
+ const handleTriggerBlur = () => {
887
+ focused.value = false;
888
+ };
889
+
744
890
  expose({
745
891
  focus,
746
892
  blur
@@ -752,8 +898,10 @@ export default defineComponent({
752
898
  searchRef,
753
899
  selectRef,
754
900
  popper,
755
- focusSelect,
756
- onSelectClick,
901
+ handleTriggerFocus,
902
+ handleTriggerBlur,
903
+ handleKeyDown,
904
+ handleOpenChange,
757
905
 
758
906
  id,
759
907
  isOpen,
@@ -779,7 +927,7 @@ export default defineComponent({
779
927
  inputListeners,
780
928
  style,
781
929
  focused,
782
- scale,
930
+ fade,
783
931
  toggleAllItems
784
932
  };
785
933
  }