@teammodlr/ui-kit 2.3.22 → 3.0.0-beta.2

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 +9 -4
  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 +23 -11
  33. package/lib/components/select/style/Select.css.d.ts +2 -0
  34. package/lib/components/switch/Switch.vue.d.ts +2 -0
  35. package/lib/components/tab/Tab.vue.d.ts +5 -2
  36. package/lib/components/tab/TabGroup.vue.d.ts +8 -3
  37. package/lib/components/textarea/Textarea.vue.d.ts +2 -1
  38. package/lib/components/tooltip/Tooltip.vue.d.ts +5 -20
  39. package/lib/components/tooltip/TooltipTrigger.vue.d.ts +1 -10
  40. package/lib/components/tree/Tree.vue.d.ts +3 -3
  41. package/lib/components/tree/TreeNode.vue.d.ts +26 -12
  42. package/lib/components/tree/types.d.ts +5 -5
  43. package/lib/index.cjs.js +5 -14
  44. package/lib/index.es.js +14765 -10217
  45. package/lib/style.css +1 -1
  46. package/package.json +12 -6
  47. package/src/components/avatar/Avatar.vue +32 -24
  48. package/src/components/button/Button.vue +24 -5
  49. package/src/components/cardcontextmenu/CardContextMenu.vue +68 -95
  50. package/src/components/cardcontextmenu/CardContextMenuItem.vue +40 -50
  51. package/src/components/cardcontextmenu/CardContextMenuSeperator.vue +9 -7
  52. package/src/components/cardcontextmenu/CardContextMenuTriggerProvider.vue +15 -0
  53. package/src/components/cardcontextmenu/style/Dropdown.css.ts +155 -2
  54. package/src/components/cardcontextmenu/symbol.ts +3 -0
  55. package/src/components/checkbox/Checkbox.vue +37 -12
  56. package/src/components/checkbox/style/Checkbox.css.ts +6 -6
  57. package/src/components/collapse/CollapseItem.vue +46 -21
  58. package/src/components/collapse/style/Collapse.css.ts +5 -1
  59. package/src/components/colorpicker/ColorPicker.vue +76 -45
  60. package/src/components/confirm/Confirm.vue +15 -8
  61. package/src/components/confirm/props.ts +4 -0
  62. package/src/components/dialog/Dialog.vue +169 -36
  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/radio/Radio.vue +31 -17
  78. package/src/components/radiobutton/RadioButton.vue +15 -2
  79. package/src/components/radiobuttongroup/RadioButtonGroup.vue +11 -4
  80. package/src/components/select/Select.vue +309 -148
  81. package/src/components/select/examples/basic.vue +1 -1
  82. package/src/components/select/style/Select.css.ts +15 -0
  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,62 +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>
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 }}
35
9
  </div>
36
10
 
37
- <Teleport :to="'body'">
38
- <Transition :enter-active-class="scale.enterActive" :leave-active-class="scale.leaveActive"
39
- :enter-from-class="scale.enterFrom" :leave-to-class="scale.leaveTo">
40
- <div :class="style.menu" v-if="isOpen" ref="popper" :style="computedConsistentWidth"
41
- :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
+ >
42
60
  <div v-if="searchable || (multiple && selectAll)" :class="style.search">
43
61
  <div v-if="multiple && selectAll" :class="style.checkAll">
44
62
  <button role="button" type="button" :class="{
45
63
  [style.checkAllButton]: true,
46
- [style.checkAllButtonActive]:
47
- computedOptions.length > 0 &&
48
- computedOptions.length ===
49
- selected.length
64
+ [style.checkAllButtonActive]: computedOptions.length > 0 && computedOptions.length === selected.length
50
65
  }" @click.prevent="toggleAllItems" aria-description="Select all items">
51
- <svg v-if="
52
- computedOptions.length > 0 &&
53
- computedOptions.length ===
54
- selected.length
55
- " xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
56
- aria-hidden="true">
57
- <path fill-rule="evenodd"
58
- 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"
59
- 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>
60
68
  </svg>
61
69
  </button>
62
70
  </div>
@@ -67,42 +75,36 @@
67
75
  <div v-for="(option, index) in computedOptions" :key="index" ref="optionsRef" :class="{
68
76
  [style.option]: true,
69
77
  [style.optionSelected]: option.active,
70
- [style.optionFocused]:
71
- selectedIndex === index
72
- }" @click.prevent="option.onClick" @hover="selectedIndex = index"
73
- :data-element="`select-menu-option-${id}-${option.value}`"
74
- >
78
+ [style.optionFocused]: selectedIndex === index
79
+ }" @click.prevent="option.onClick" @mouseenter="selectedIndex = index"
80
+ :data-element="`select-menu-option-${id}-${option.value}`">
75
81
  <span :class="style.optionIcon">
76
- <svg v-if="option.active" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
77
- fill="currentColor" aria-hidden="true">
78
- <path fill-rule="evenodd"
79
- 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"
80
- 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>
81
84
  </svg>
82
85
  </span>
83
- <span :class="style.optionLabel">{{
84
- option.label
85
- }}</span>
86
+ <span :class="style.optionLabel">{{ option.label }}</span>
86
87
  </div>
87
88
  <template v-if="computedOptions.length === 0">
88
89
  <div :class="style.noResults">No results</div>
89
90
  </template>
90
91
  </div>
91
- </div>
92
+ </PopoverContent>
92
93
  </Transition>
93
- </Teleport>
94
- </div>
95
- <div v-if="inputError" :class="style.errorMessage">
96
- {{ inputError }}
94
+ </PopoverPortal>
95
+
96
+ <div v-if="inputError" :class="style.errorMessage">
97
+ {{ inputError }}
98
+ </div>
97
99
  </div>
98
- </div>
100
+ </PopoverRoot>
99
101
  </template>
100
102
 
101
103
  <script lang="ts">
102
104
  import { useInput } from '../../composables/useInput';
103
- import { usePopper, type Placement } from '../../composables/usePopper';
104
105
  import type { MaybeArray, Size } from '../../types/props';
105
- import { onClickOutside, useEventListener } from '@vueuse/core';
106
+ import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui';
107
+ import { useEventListener } from '@vueuse/core';
106
108
  import {
107
109
  computed,
108
110
  defineComponent,
@@ -110,16 +112,18 @@ import {
110
112
  ref,
111
113
  watch,
112
114
  watchEffect,
113
- type PropType
115
+ type PropType
114
116
  } from 'vue';
115
117
  import MTag from '../tag/Tag.vue';
116
118
  import * as style from './style';
117
- import { scale } from '../../styles/transitions.css';
119
+ import { fade } from '../../styles/transitions.css';
118
120
  import { useInteractive } from '../../composables/useInteractive';
119
121
  import type { OnUpdateValue, OnUpdateValueImpl } from './types';
120
122
  import { call } from "../../utils/call";
121
123
  import {useZIndex} from "../../composables/useZIndex";
122
124
 
125
+ let shouldOpenNextFocusedSelect = false;
126
+
123
127
  export default defineComponent({
124
128
  name: 'MSelect',
125
129
  emits: {
@@ -194,6 +198,10 @@ export default defineComponent({
194
198
  type: Number,
195
199
  default: 5
196
200
  },
201
+ portal: {
202
+ type: Boolean,
203
+ default: false
204
+ },
197
205
  zIndex: {
198
206
  type: Number
199
207
  },
@@ -205,22 +213,28 @@ export default defineComponent({
205
213
  onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>,
206
214
  },
207
215
  components: {
208
- MTag
216
+ MTag,
217
+ PopoverContent,
218
+ PopoverPortal,
219
+ PopoverRoot,
220
+ PopoverTrigger
209
221
  },
210
222
  setup(props, { emit, expose }) {
211
223
  const container = ref<HTMLElement | null>(null);
212
- const wrapper = ref<HTMLElement>();
213
- const optionsRef = ref<HTMLElement[]>();
214
- const searchRef = ref<HTMLElement>();
224
+ const optionsRef = ref<HTMLElement[]>([]);
225
+ const searchRef = ref<HTMLInputElement>();
215
226
  const selectRef = ref<HTMLElement>();
227
+ const popper = ref<HTMLElement>();
216
228
  const selectedIndex = ref<number | undefined>(0);
217
229
  const open = ref<boolean>(false);
218
230
  const focused = ref<boolean>(false);
219
231
  const search = ref<string>('');
220
- const placement = ref<Placement>('bottom-start');
221
- const transfer = ref(false);
222
232
  const width = ref<number>(0);
223
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);
224
238
  const {nextZIndex} = useZIndex();
225
239
  function doUpdateValue(
226
240
  value: string | number | Array<string | number> | null,
@@ -251,14 +265,6 @@ export default defineComponent({
251
265
  formLoading
252
266
  } = useInput(props, { focus, emit, withListeners: false });
253
267
 
254
- const { popper, updatePopper } = usePopper({
255
- placement,
256
- transfer,
257
- reference: container,
258
- wrapper,
259
- offset: [0, 5]
260
- });
261
-
262
268
  const disabled = computed(() => formDisabled || props.disabled);
263
269
  const loading = computed(() => formLoading || props.loading);
264
270
 
@@ -321,7 +327,7 @@ export default defineComponent({
321
327
 
322
328
  return {
323
329
  ...styles,
324
- zIndex: zIndex.value
330
+ zIndex: props.portal ? zIndex.value : 1
325
331
  };
326
332
  });
327
333
 
@@ -434,11 +440,16 @@ export default defineComponent({
434
440
  }
435
441
  }
436
442
 
437
- function openSelect() {
443
+ function openSelect(forceOpen = false) {
438
444
  if (disabled.value || loading.value) {
439
445
  return;
440
446
  }
441
447
 
448
+ if (forceOpen) {
449
+ open.value = true;
450
+ return;
451
+ }
452
+
442
453
  if (open.value) {
443
454
  open.value = false;
444
455
  return;
@@ -473,6 +484,88 @@ export default defineComponent({
473
484
  }
474
485
  }
475
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
+
476
569
  function handleOptionClick(value: string | number) {
477
570
  const option = computedOptions.value.find(
478
571
  item => item.value === value
@@ -486,17 +579,15 @@ export default defineComponent({
486
579
  const index = selected.value.indexOf(value);
487
580
 
488
581
  if (index !== -1) {
489
- selected.value.splice(index, 1);
582
+ selected.value = selected.value.filter(item => item !== value);
490
583
  } else {
491
- selected.value.push(value);
492
- emit('update:modelValue', selected.value);
584
+ selected.value = [...selected.value, value];
493
585
  }
494
586
  } else {
495
587
  selected.value = [value];
496
588
  }
497
-
498
- updatePopper();
499
589
  } else {
590
+ restoreTriggerFocusOnClose.value = true;
500
591
  selected.value = value;
501
592
  open.value = false;
502
593
  }
@@ -510,13 +601,7 @@ export default defineComponent({
510
601
  }
511
602
 
512
603
  if (Array.isArray(selected.value)) {
513
- const index = selected.value.indexOf(value);
514
-
515
- if (index !== -1) {
516
- selected.value.splice(index, 1);
517
- }
518
-
519
- updatePopper();
604
+ selected.value = selected.value.filter(item => item !== value);
520
605
  }
521
606
  }
522
607
 
@@ -524,26 +609,58 @@ export default defineComponent({
524
609
  open.value = false;
525
610
  });
526
611
 
527
- function handleKeyDown(e: Event) {
528
- if (computedOptions.value.length === 0) {
529
- return;
530
- }
612
+ useEventListener('keydown', (event: KeyboardEvent) => {
613
+ focusOpenedByKeyboard.value = event.key === 'Tab';
614
+ });
615
+
616
+ useEventListener('pointerdown', () => {
617
+ focusOpenedByKeyboard.value = false;
618
+ });
531
619
 
620
+ function handleKeyDown(e: Event) {
532
621
  if (props.disabled) {
533
622
  return;
534
623
  }
535
624
 
536
- keyboardNavigation.value = true;
537
-
538
625
  const event: KeyboardEvent = e as KeyboardEvent;
539
626
 
540
- switch (event.code) {
541
- case 'Tab':
542
- 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;
543
651
  open.value = false;
544
652
  }
545
- break;
653
+ }
654
+ return;
655
+ }
656
+
657
+ keyboardNavigation.value = true;
658
+
659
+ switch (event.key) {
546
660
  case 'ArrowDown':
661
+ if (computedOptions.value.length === 0) {
662
+ return;
663
+ }
547
664
  e.preventDefault();
548
665
  if (!open.value) {
549
666
  open.value = true;
@@ -553,6 +670,9 @@ export default defineComponent({
553
670
  getSelectedIndex(selectedIndex.value, 'down');
554
671
  break;
555
672
  case 'ArrowUp':
673
+ if (computedOptions.value.length === 0) {
674
+ return;
675
+ }
556
676
  e.preventDefault();
557
677
  if (!open.value) {
558
678
  return;
@@ -561,6 +681,9 @@ export default defineComponent({
561
681
  getSelectedIndex(selectedIndex.value, 'up');
562
682
  break;
563
683
  case 'Enter':
684
+ if (computedOptions.value.length === 0) {
685
+ return;
686
+ }
564
687
  e.preventDefault();
565
688
  e.stopPropagation();
566
689
 
@@ -569,9 +692,13 @@ export default defineComponent({
569
692
  return;
570
693
  }
571
694
 
572
- if (typeof selectedIndex.value !== 'undefined') {
695
+ const activeIndex = typeof selectedIndex.value === 'undefined'
696
+ ? 0
697
+ : selectedIndex.value;
698
+
699
+ if (computedOptions.value[activeIndex]) {
573
700
  handleOptionClick(
574
- computedOptions.value[selectedIndex.value].value
701
+ computedOptions.value[activeIndex].value
575
702
  );
576
703
  if (!props.multiple) {
577
704
  open.value = false;
@@ -579,13 +706,14 @@ export default defineComponent({
579
706
  }
580
707
  break;
581
708
  case 'Escape':
582
- e.preventDefault();
583
- e.stopPropagation();
584
-
585
709
  if (!open.value) {
586
710
  return;
587
711
  }
588
712
 
713
+ e.preventDefault();
714
+ e.stopPropagation();
715
+
716
+ restoreTriggerFocusOnClose.value = true;
589
717
  open.value = false;
590
718
  break;
591
719
  }
@@ -604,13 +732,17 @@ export default defineComponent({
604
732
  ];
605
733
  });
606
734
 
607
- const computedWrapperClasses = computed(() => {
735
+ const selectComputedArrowClasses = computed(() => {
608
736
  return [
609
- style.wrapper,
737
+ style.arrow,
610
738
  {
611
- [style.focused]: focused.value
739
+ [style.arrowOpen]: open.value
612
740
  }
613
- ];
741
+ ]
742
+ })
743
+
744
+ const computedWrapperClasses = computed(() => {
745
+ return [style.wrapper];
614
746
  });
615
747
 
616
748
  const selectComputedClasses = computed(() => {
@@ -621,10 +753,10 @@ export default defineComponent({
621
753
  props.multiple &&
622
754
  Array.isArray(selected.value) &&
623
755
  selected.value.length > 0,
624
- [style.focused]: open.value,
756
+ [style.focused]: open.value || focused.value,
625
757
  // [`m-select--${props.size}`]: props.size,
626
758
  'm-select--disabled': disabled.value || loading.value,
627
- 'm-select--focused': open.value,
759
+ 'm-select--focused': open.value || focused.value,
628
760
  'm-select--multiple': props.multiple,
629
761
  'm-select--error': formError.value
630
762
  }
@@ -642,17 +774,6 @@ export default defineComponent({
642
774
  return option[props.optionLabel];
643
775
  }
644
776
 
645
- useEventListener(selectRef, 'keydown', handleKeyDown);
646
- useEventListener(searchRef, 'keydown', handleKeyDown);
647
-
648
- onClickOutside(
649
- container,
650
- () => {
651
- open.value = false;
652
- },
653
- { ignore: [searchRef, popper] }
654
- );
655
-
656
777
  watchEffect(() => {
657
778
  if (container.value && open.value) {
658
779
  width.value = container.value.offsetWidth;
@@ -681,22 +802,46 @@ export default defineComponent({
681
802
  zIndex.value = nextZIndex();
682
803
  }
683
804
 
684
- if (props.searchable && searchRef.value) {
805
+ if (value && props.searchable && searchRef.value) {
685
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
+ });
686
817
  }
818
+
687
819
  if (value && optionsRef.value) {
820
+ const selectedValue = Array.isArray(selected.value)
821
+ ? selected.value[0]
822
+ : selected.value;
688
823
  const index = computedOptions.value.findIndex(
689
- i => i.value === selected.value
824
+ i => i.value === selectedValue
690
825
  );
691
826
  if (index !== -1) {
692
827
  optionsRef.value[index].scrollIntoView({
693
828
  block: 'nearest'
694
829
  });
695
830
  selectedIndex.value = index;
831
+ } else if (computedOptions.value.length > 0) {
832
+ selectedIndex.value = 0;
696
833
  }
697
834
  }
698
835
 
699
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
+ }
700
845
  });
701
846
  });
702
847
 
@@ -715,20 +860,33 @@ export default defineComponent({
715
860
  return open.value;
716
861
  });
717
862
 
718
- const focusSelect = (event: FocusEvent) => {
719
- setTimeout(() => {
720
- if (selectRef.value && !open.value) {
721
- openSelect();
722
- }
723
- }, 10);
863
+ const handleOpenChange = (value: boolean) => {
864
+ open.value = value;
724
865
  };
725
866
 
726
- const onSelectClick = (event: MouseEvent) => {
727
- event.preventDefault();
728
- 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
+
729
883
  openSelect();
730
884
  };
731
885
 
886
+ const handleTriggerBlur = () => {
887
+ focused.value = false;
888
+ };
889
+
732
890
  expose({
733
891
  focus,
734
892
  blur
@@ -740,8 +898,10 @@ export default defineComponent({
740
898
  searchRef,
741
899
  selectRef,
742
900
  popper,
743
- focusSelect,
744
- onSelectClick,
901
+ handleTriggerFocus,
902
+ handleTriggerBlur,
903
+ handleKeyDown,
904
+ handleOpenChange,
745
905
 
746
906
  id,
747
907
  isOpen,
@@ -754,6 +914,7 @@ export default defineComponent({
754
914
  search,
755
915
 
756
916
  containerComputedClasses,
917
+ selectComputedArrowClasses,
757
918
  selectComputedClasses,
758
919
  computedWrapperClasses,
759
920
  computedConsistentWidth: computedStyle,
@@ -766,7 +927,7 @@ export default defineComponent({
766
927
  inputListeners,
767
928
  style,
768
929
  focused,
769
- scale,
930
+ fade,
770
931
  toggleAllItems
771
932
  };
772
933
  }