@veritree/ui 0.22.3 → 0.23.0-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 (75) hide show
  1. package/index.js +64 -72
  2. package/mixins/floating-ui-content.js +1 -22
  3. package/mixins/floating-ui-item.js +96 -50
  4. package/mixins/floating-ui.js +7 -9
  5. package/mixins/form-control-icon.js +5 -5
  6. package/mixins/form-control.js +17 -18
  7. package/nuxt.js +30 -23
  8. package/package.json +14 -6
  9. package/src/components/Alert/VTAlert.vue +55 -14
  10. package/src/components/Button/VTButton.vue +20 -12
  11. package/src/components/Checkbox/VTCheckbox.vue +134 -0
  12. package/src/components/Checkbox/VTCheckboxLabel.vue +3 -0
  13. package/src/components/Checkbox/VTCheckboxText.vue +20 -0
  14. package/src/components/Dialog/VTDialog.vue +22 -32
  15. package/src/components/Dialog/VTDialogClose.vue +19 -25
  16. package/src/components/Dialog/VTDialogContent.vue +24 -19
  17. package/src/components/Dialog/VTDialogFooter.vue +11 -16
  18. package/src/components/Dialog/VTDialogHeader.vue +16 -18
  19. package/src/components/Dialog/VTDialogMain.vue +11 -18
  20. package/src/components/Dialog/VTDialogOverlay.vue +18 -18
  21. package/src/components/Dialog/VTDialogTitle.vue +10 -7
  22. package/src/components/Disclosure/VTDisclosureContent.vue +1 -1
  23. package/src/components/Disclosure/VTDisclosureDetails.vue +1 -1
  24. package/src/components/Disclosure/VTDisclosureHeader.vue +2 -2
  25. package/src/components/Disclosure/VTDisclosureIcon.vue +1 -1
  26. package/src/components/Drawer/VTDrawer.vue +6 -15
  27. package/src/components/Drawer/VTDrawerClose.vue +5 -5
  28. package/src/components/Drawer/VTDrawerContent.vue +9 -9
  29. package/src/components/Drawer/VTDrawerFooter.vue +3 -3
  30. package/src/components/Drawer/VTDrawerHeader.vue +4 -4
  31. package/src/components/Drawer/VTDrawerMain.vue +5 -5
  32. package/src/components/Drawer/VTDrawerOverlay.vue +6 -6
  33. package/src/components/Drawer/VTDrawerTitle.vue +5 -5
  34. package/src/components/DropdownMenu/VTDropdownMenu.vue +19 -6
  35. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +14 -6
  36. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +7 -16
  37. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +7 -3
  38. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +1 -10
  39. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +2 -4
  40. package/src/components/Form/VTFormFeedback.vue +11 -5
  41. package/src/components/Form/VTFormGroup.vue +5 -7
  42. package/src/components/Form/VTFormLabel.vue +22 -0
  43. package/src/components/Form/VTFormRow.vue +5 -0
  44. package/src/components/Form/VTInput.vue +7 -7
  45. package/src/components/Form/VTInputIcon.vue +1 -2
  46. package/src/components/Form/VTInputPassword.vue +14 -5
  47. package/src/components/Form/VTTextarea.vue +1 -1
  48. package/src/components/Image/VTImage.vue +33 -4
  49. package/src/components/Listbox/VTListbox.vue +105 -14
  50. package/src/components/Listbox/VTListboxContent.vue +3 -7
  51. package/src/components/Listbox/VTListboxItem.vue +127 -6
  52. package/src/components/Listbox/VTListboxLabel.vue +3 -4
  53. package/src/components/Listbox/VTListboxList.vue +3 -24
  54. package/src/components/Listbox/VTListboxSearch.vue +64 -55
  55. package/src/components/Listbox/VTListboxTrigger.vue +9 -4
  56. package/src/components/Popover/VTPopover.vue +19 -0
  57. package/src/components/Popover/VTPopoverItem.vue +6 -2
  58. package/src/components/ProgressBar/VTProgressBar.vue +21 -3
  59. package/src/components/Skeleton/VTSkeleton.vue +11 -0
  60. package/src/components/Skeleton/VTSkeletonItem.vue +9 -0
  61. package/src/components/Tabs/VTTab.vue +6 -5
  62. package/src/components/Tabs/VTTabGroup.vue +9 -7
  63. package/src/components/Tabs/VTTabPanel.vue +4 -5
  64. package/src/components/Tooltip/VTTooltip.vue +65 -0
  65. package/src/components/Tooltip/VTTooltipContent.vue +59 -0
  66. package/src/components/Tooltip/VTTooltipTrigger.vue +98 -0
  67. package/src/components/Transitions/FadeInOut.vue +2 -2
  68. package/src/components/Utils/FloatingUi.vue +56 -24
  69. package/src/utils/components.js +18 -0
  70. package/src/components/Input/VTInput.vue +0 -82
  71. package/src/components/Input/VTInputDate.vue +0 -36
  72. package/src/components/Input/VTInputFile.vue +0 -60
  73. package/src/components/Input/VTInputUpload.vue +0 -54
  74. package/src/components/Modal/VTModal.vue +0 -69
  75. package/src/utils/genId.js +0 -13
@@ -16,26 +16,44 @@ export default {
16
16
  provide() {
17
17
  return {
18
18
  apiListbox: () => {
19
+ const $mutable = {};
20
+
21
+ // Used to get and update the value computed
22
+ // that will then emit the value up to the
23
+ // parent component
24
+ Object.defineProperty($mutable, 'valueComputed', {
25
+ enumerable: true,
26
+ get: () => this.valueComputed,
27
+ set: (value) => (this.valueComputed = value),
28
+ });
29
+
30
+ /**
31
+ * This function registers a trigger by setting its value to the componentTrigger property of the current object.
32
+ * @param {VueComponent} trigger - The trigger to be registered.
33
+ */
19
34
  const registerTrigger = (trigger) => {
20
35
  if (!trigger) return;
21
36
  this.componentTrigger = trigger;
22
37
  };
23
38
 
39
+ /**
40
+ * Registers content to be used in the children components by setting its value to the componentContent property of the current object.
41
+ * @param {any} content - The content to be registered.
42
+ */
24
43
  const registerContent = (content) => {
25
44
  if (!content) return;
26
45
  this.componentContent = content;
27
46
  };
28
47
 
48
+ /**
49
+ * Registers search to be used in the children components by setting its value to the componentSearch property of the current object.
50
+ * @param {any} search - The search to be registered.
51
+ */
29
52
  const registerSearch = (search) => {
30
53
  if (!search) return;
31
- this.search = search;
54
+ this.componentSearch = search;
32
55
  };
33
56
 
34
- // const registerList = (list) => {
35
- // if (!list) return;
36
- // this.list = list;
37
- // };
38
-
39
57
  const registerItem = (item) => {
40
58
  if (!item) return;
41
59
  this.items.push(item);
@@ -46,9 +64,18 @@ export default {
46
64
  this.items.splice(index, 1);
47
65
  };
48
66
 
67
+ const pushSearch = (key) => {
68
+ key === 'Backspace' && this.search.length
69
+ ? (this.search = this.search.slice(0, -1))
70
+ : (this.search += key);
71
+ };
72
+
73
+ const clearSearch = () => {
74
+ this.search = '';
75
+ };
76
+
49
77
  const emit = (value) => {
50
- this.$emit('input', value);
51
- this.$emit('change', value);
78
+ this.valueComputed = value;
52
79
  };
53
80
 
54
81
  return {
@@ -56,14 +83,18 @@ export default {
56
83
  component: this.component,
57
84
  componentTrigger: this.componentTrigger,
58
85
  componentContent: this.componentContent,
86
+ componentSearch: this.componentSearch,
59
87
  items: this.items,
88
+ multiple: this.multiple,
60
89
  search: this.search,
90
+ $mutable,
61
91
  registerTrigger,
62
92
  registerContent,
63
93
  registerSearch,
64
- // registerList,
65
94
  registerItem,
66
95
  unregisterItem,
96
+ pushSearch,
97
+ clearSearch,
67
98
  emit,
68
99
  };
69
100
  },
@@ -71,19 +102,52 @@ export default {
71
102
  },
72
103
 
73
104
  props: {
74
- value: {
75
- type: [String, Number, Object],
105
+ /**
106
+ * The value of the component. Can be a string, number, object, or array.
107
+ * @type {string|number|object|array}
108
+ * @default null
109
+ */
110
+ modelValue: {
111
+ type: [String, Number, Object, Array],
76
112
  default: null,
77
113
  },
114
+ /**
115
+ * Determines whether the button will use its default atomic style (tailwind) or its default class
116
+ * @type {boolean}
117
+ * @values
118
+ * - true: The button will have no default style and can be fully customized with a custom class
119
+ * - false: The button will use its default atomic style (tailwind) and can be further customized with additional classes
120
+ * @default null
121
+ */
78
122
  headless: {
79
123
  type: Boolean,
80
124
  default: false,
81
125
  },
82
- dark: {
126
+ /**
127
+ * The placement of the component relative to its trigger element.
128
+ * @type {string}
129
+ * @values 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end'
130
+ * @default 'bottom-start'
131
+ */
132
+ placement: {
133
+ type: String,
134
+ default: 'bottom-start',
135
+ },
136
+ /**
137
+ * Determines whether the component should be aligned to the right of its trigger element.
138
+ * @type {boolean}
139
+ * @default false
140
+ */
141
+ right: {
83
142
  type: Boolean,
84
143
  default: false,
85
144
  },
86
- right: {
145
+ /**
146
+ * Determines whether the component should allow multiple selections.
147
+ * @type {boolean}
148
+ * @default false
149
+ */
150
+ multiple: {
87
151
  type: Boolean,
88
152
  default: false,
89
153
  },
@@ -92,8 +156,25 @@ export default {
92
156
  data() {
93
157
  return {
94
158
  componentId: genId(),
95
- search: null,
159
+ componentSearch: null,
160
+ search: '',
96
161
  items: [],
162
+ itemsChecked: [],
163
+ /**
164
+ * Explaining the need for the floatingUiMinWidth data
165
+ *
166
+ * The floating ui is a result of two items:
167
+ *
168
+ * 1. Trigger: the action button
169
+ * 2. Content: the popper/wrapper that appears after triggering the action button
170
+ *
171
+ * By default, the content will match the triggers width.
172
+ * The problem with this is that the trigger width
173
+ * might be too small causing the content to not fit
174
+ * what is inside it properly. So, to avoid this,
175
+ * a min width is needed.
176
+ */
177
+ floatingUiMinWidth: 200,
97
178
  };
98
179
  },
99
180
 
@@ -101,6 +182,16 @@ export default {
101
182
  id() {
102
183
  return `listbox-${this.componentId}`;
103
184
  },
185
+
186
+ valueComputed: {
187
+ get() {
188
+ return this.modelValue;
189
+ },
190
+ set(newValue) {
191
+ this.$emit('update:modelValue', newValue);
192
+ this.$emit('change', newValue);
193
+ },
194
+ },
104
195
  },
105
196
  };
106
197
  </script>
@@ -1,11 +1,10 @@
1
1
  <template>
2
2
  <FloatingUi
3
- :visible="visible"
4
3
  :id="id"
5
- :aria-activedescendant="activeDescedant"
4
+ :visible="visible"
6
5
  :headless="headless"
7
- :class="{ 'listbox-content': headless }"
8
- :floating-ui-class="floatingUiClass"
6
+ :aria-activedescendant="activeDescedant"
7
+ component="listbox"
9
8
  role="listbox"
10
9
  >
11
10
  <slot></slot>
@@ -47,9 +46,6 @@ export default {
47
46
  id: this.id,
48
47
  show: this.show,
49
48
  hide: this.hide,
50
- getMousemove: this.getMousemove,
51
- setMousemove: this.setMousemove,
52
- unsetMousemove: this.unsetMousemove,
53
49
  setActiveDescedant: this.setActiveDescedant,
54
50
  };
55
51
 
@@ -5,30 +5,47 @@
5
5
  :aria-disabled="disabled"
6
6
  :aria-selected="String(selected)"
7
7
  :tabindex="tabIndex"
8
- role="option"
8
+ :role="'option'"
9
9
  @click.stop="onClick"
10
- @keydown.down.prevent="focusPreviousItem"
11
- @keydown.up.prevent="focusNextItem"
10
+ @keydown.prevent="onKeydown"
11
+ @keydown.down.prevent="focusNextItem"
12
+ @keydown.up.prevent="focusPreviousItem"
12
13
  @keydown.home.prevent="focusFirstItem"
13
14
  @keydown.end.prevent="focusLastItem"
14
15
  @keydown.esc.stop="onKeyEsc"
15
16
  @keydown.enter.prevent="onClick"
16
- @mousemove="onMousemove"
17
- @mouseout="onMouseleave"
18
17
  @keydown.tab.prevent
19
18
  >
19
+ <span v-if="apiListbox().multiple">
20
+ <input
21
+ v-model="apiListbox().$mutable.valueComputed"
22
+ :id="`${id}-checkbox`"
23
+ :value="value"
24
+ type="checkbox"
25
+ @click.stop
26
+ @change="onChange"
27
+ />
28
+ </span>
20
29
  <slot></slot>
30
+ <span v-if="wasSelected" class="ml-auto">
31
+ <IconCheckOutline class="text-secondary-200 h-5 w-5" />
32
+ </span>
21
33
  </li>
22
34
  </template>
23
35
 
24
36
  <script>
37
+ import { IconCheckOutline } from '@veritree/icons';
25
38
  import { floatingUiItemMixin } from '../../../mixins/floating-ui-item';
26
39
 
40
+ let keydownSearchTimeout = null;
41
+
27
42
  export default {
28
43
  name: 'VTListboxItem',
29
44
 
30
45
  mixins: [floatingUiItemMixin],
31
46
 
47
+ components: { IconCheckOutline },
48
+
32
49
  inject: ['apiListbox'],
33
50
 
34
51
  props: {
@@ -40,14 +57,118 @@ export default {
40
57
 
41
58
  data() {
42
59
  return {
60
+ // apiInjected is used in the mixin floating-ui-item
43
61
  apiInjected: this.apiListbox,
44
62
  componentName: 'listbox-item',
45
63
  };
46
64
  },
47
65
 
48
66
  computed: {
67
+ componentContent() {
68
+ return this.apiInjected().componentContent;
69
+ },
70
+
71
+ hasComponentSearch() {
72
+ return this.apiListbox().componentSearch !== null;
73
+ },
74
+
49
75
  search() {
50
- return this.apiInjected().search;
76
+ return this.apiListbox().search;
77
+ },
78
+
79
+ /**
80
+ * The difference between selected (in the mixin) and
81
+ * wasSelected is that selected is more related to
82
+ * the aria-selected while wasSelected is the
83
+ * real selected option for the listbox
84
+ */
85
+ wasSelected() {
86
+ return (
87
+ JSON.stringify(this.value) ===
88
+ JSON.stringify(this.apiListbox().$mutable.valueComputed)
89
+ );
90
+ },
91
+ },
92
+
93
+ mounted() {
94
+ if (this.wasSelected) {
95
+ /**
96
+ * There are some conflicts between the portal and the interaction
97
+ * with the element. It's unclear why it happens but if no delay,
98
+ * the floating ui content, that is placed in the body by the
99
+ * vue portal plugin, will not appear close to the trigger.
100
+ *
101
+ * Kind hacky but no time do find real solution.
102
+ */
103
+ setTimeout(() => {
104
+ this.hasComponentSearch ? this.select() : this.focus();
105
+ }, 1);
106
+ }
107
+ },
108
+
109
+ methods: {
110
+ /**
111
+ * This event is mainly for the search since other keydown
112
+ * events are handled with vue v-on (@) directive
113
+ *
114
+ * @param {Object} event
115
+ */
116
+ onKeydown(e) {
117
+ const isLetter = e.code.includes('Key');
118
+ const isNumber = e.code.includes('Numpad') || e.code.includes('Digit');
119
+ const isBackspace = e.code === 'Backspace';
120
+ const isSpace = e.code === 'Space';
121
+
122
+ if (isLetter || isNumber || isBackspace || isSpace) {
123
+ this.doSearch(e);
124
+ }
125
+ },
126
+
127
+ /**
128
+ * Searches and focus the options that matches the
129
+ * value typed when the item is selected.
130
+ *
131
+ * @param {Object} event
132
+ */
133
+ doSearch(e) {
134
+ clearTimeout(keydownSearchTimeout);
135
+
136
+ /**
137
+ * Add letter or number to search data in component
138
+ * root, so it is available for all children
139
+ */
140
+ this.apiListbox().pushSearch(e.key);
141
+
142
+ /**
143
+ * Let's get the item selected index at the moment the search
144
+ * starts to later unselect it if necessary
145
+ */
146
+ const itemSelectedIndex = this.getItemSelectedIndex();
147
+
148
+ /**
149
+ * As we have now the search data with value, let's go
150
+ * through all items and check if the search matches
151
+ * with any of the items to get its index value
152
+ */
153
+ const newItemSelectedIndex = this.items.findIndex((item) =>
154
+ item.text.toLowerCase().includes(this.search.toLowerCase())
155
+ );
156
+
157
+ /**
158
+ * Focus the item that matches the search, and if no match
159
+ * we will just remove the selected state from item
160
+ */
161
+ newItemSelectedIndex >= 0
162
+ ? this.setFocusToItem(itemSelectedIndex, newItemSelectedIndex)
163
+ : this.unselect();
164
+
165
+ /**
166
+ * Clear the search data value after some time
167
+ * to allow other searches to be performed
168
+ */
169
+ keydownSearchTimeout = setTimeout(() => {
170
+ this.apiListbox().clearSearch();
171
+ }, 1000);
51
172
  },
52
173
  },
53
174
  };
@@ -1,10 +1,9 @@
1
1
  <template>
2
2
  <component
3
3
  :is="as"
4
- :class="{
5
- ListboxLabel: headless,
6
- 'mb-2 block text-xs font-normal uppercase': !headless,
7
- }"
4
+ :class="[
5
+ headless ? 'listbox-label' : 'mb-2 block text-xs font-normal uppercase',
6
+ ]"
8
7
  >
9
8
  <slot></slot>
10
9
  </component>
@@ -2,7 +2,9 @@
2
2
  <ul
3
3
  :id="id"
4
4
  :class="[
5
- headless ? 'listbox-list' : 'max-h-[160px] w-auto overflow-y-auto -mx-3',
5
+ headless
6
+ ? 'listbox-list'
7
+ : '-mx-3 max-h-[160px] w-auto overflow-y-auto scroll-auto',
6
8
  ]"
7
9
  >
8
10
  <slot></slot>
@@ -27,28 +29,5 @@ export default {
27
29
  return `listbox-list-${this.apiListbox().id}`;
28
30
  },
29
31
  },
30
-
31
- // mounted() {
32
- // const list = {
33
- // el: this.$el,
34
- // };
35
-
36
- // this.apiListbox().registerList(list);
37
- // },
38
-
39
- methods: {
40
- // Mousemove instead of mouseover to support keyboard navigation.
41
- // The problem with mouseover is that when scrolling (scrollIntoView),
42
- // mouseover event gets triggered.
43
- // setMousemove() {
44
- // this.isMousemove = true;
45
- // },
46
- // unsetMousemove() {
47
- // this.isMousemove = false;
48
- // },
49
- // getMousemove() {
50
- // return this.isMousemove;
51
- // },
52
- },
53
32
  };
54
33
  </script>
@@ -1,18 +1,22 @@
1
1
  <template>
2
- <input
3
- v-model="search"
4
- type="text"
5
- :class="classComputed"
6
- @input="onChange"
7
- @click.stop
8
- @keydown.down.prevent="focusNextItem"
9
- @keydown.up.prevent="focusPreviousItem"
10
- @keydown.home.prevent="focusFirstItem"
11
- @keydown.end.prevent="focusLastItem"
12
- @keydown.enter.prevent="onKeyEnter"
13
- @keydown.esc.stop="hide"
14
- @keydown.tab.prevent="hide"
15
- />
2
+ <div class="-mx-3 -mt-2">
3
+ <input
4
+ ref="search"
5
+ :value="modelValue"
6
+ v-bind="$attrs"
7
+ type="text"
8
+ class="leading-0 font-inherit w-full max-w-full appearance-none border-b border-solid border-gray-300 py-2 px-3 text-base text-inherit"
9
+ @input="onInput"
10
+ @click.stop
11
+ @keydown.down.prevent="focusNextItem"
12
+ @keydown.up.prevent="focusPreviousItem"
13
+ @keydown.home.prevent="focusFirstItem"
14
+ @keydown.end.prevent="focusLastItem"
15
+ @keydown.enter.prevent="onKeyEnter"
16
+ @keydown.esc.stop="hide"
17
+ @keydown.tab.prevent="hide"
18
+ />
19
+ </div>
16
20
  </template>
17
21
 
18
22
  <script>
@@ -23,12 +27,14 @@ export default {
23
27
 
24
28
  mixins: [formControlMixin],
25
29
 
30
+ inheritAttrs: false,
31
+
26
32
  inject: ['apiListbox'],
27
33
 
28
34
  data() {
29
35
  return {
30
36
  name: 'listbox-search',
31
- index: -1,
37
+ index: null,
32
38
  search: '',
33
39
  };
34
40
  },
@@ -38,10 +44,6 @@ export default {
38
44
  return this.apiListbox().componentTrigger;
39
45
  },
40
46
 
41
- componentContent() {
42
- return this.apiListbox().componentContent;
43
- },
44
-
45
47
  items() {
46
48
  return this.apiListbox().items;
47
49
  },
@@ -52,74 +54,81 @@ export default {
52
54
  },
53
55
 
54
56
  mounted() {
55
- const search = {
57
+ const componentSearch = {
56
58
  el: this.$el,
57
59
  };
58
60
 
59
- this.apiListbox().registerSearch(search);
60
- this.$nextTick(() => setTimeout(() => this.$el.focus(), 150));
61
+ this.apiListbox().registerSearch(componentSearch);
62
+ this.$nextTick(() => setTimeout(() => this.$refs.search.focus(), 150));
61
63
  },
62
64
 
63
- beforeDestroy() {
65
+ beforeUnmount() {
64
66
  this.search = '';
65
- this.$emit('change', this.search.trim());
67
+ this.$emit('update:modelValue', '');
66
68
  },
67
69
 
68
70
  methods: {
69
71
  focusNextItem() {
70
- if (this.index !== -1) {
71
- this.unselectItem();
72
- }
73
-
74
- this.index++;
72
+ const selectedIndex = this.getItemSelectedIndex();
73
+ const isLast = selectedIndex === this.items.length - 1;
74
+ const firstItemIndex = 0;
75
+ const nextItemIndex = selectedIndex + 1;
76
+ const newSelectedIndex = isLast ? firstItemIndex : nextItemIndex;
75
77
 
76
- if (this.index > this.items.length - 1) {
77
- this.index = 0;
78
- }
79
-
80
- if (this.item) this.item.select();
78
+ this.selectItem(selectedIndex, newSelectedIndex);
81
79
  },
82
80
 
83
81
  focusPreviousItem() {
84
- this.unselectItem();
85
-
86
- this.index--;
87
-
88
- if (this.index < 0) {
89
- this.index = this.items.length - 1;
90
- }
82
+ const selectedIndex = this.getItemSelectedIndex();
83
+ const isFirst = selectedIndex === 0;
84
+ const lastItemIndex = this.items.length - 1;
85
+ const previousItemIndex = selectedIndex - 1;
86
+ const newSelectedIndex = isFirst ? lastItemIndex : previousItemIndex;
91
87
 
92
- this.item.select();
88
+ this.selectItem(selectedIndex, newSelectedIndex);
93
89
  },
94
90
 
95
91
  focusFirstItem() {
96
- this.unselectItem();
97
- this.index = 0;
98
- this.item.select();
92
+ const selectedIndex = this.getItemSelectedIndex();
93
+ const newSelectedIndex = 0;
94
+
95
+ this.selectItem(selectedIndex, newSelectedIndex);
99
96
  },
100
97
 
101
98
  focusLastItem() {
102
- this.unselectItem();
103
- this.index = this.items.length - 1;
104
- this.item.select();
99
+ const selectedIndex = this.getItemSelectedIndex();
100
+ const newSelectedIndex = this.items.length - 1;
101
+
102
+ this.selectItem(selectedIndex, newSelectedIndex);
105
103
  },
106
104
 
107
- unselectItem() {
108
- const isMousemove = this.componentContent.getMousemove();
105
+ selectItem(selectedIndex, newSelectedIndex) {
106
+ // before selecting, let's unselect selected
107
+ // item that were previously focused
108
+ if (selectedIndex >= 0) {
109
+ this.items[selectedIndex].unselect();
110
+ }
109
111
 
110
- if (isMousemove) {
111
- this.componentContent.unsetMousemove();
112
- this.items.forEach((item) => item.unselect());
112
+ // select new item
113
+ if (this.items[newSelectedIndex]) {
114
+ this.index = newSelectedIndex;
115
+ this.items[newSelectedIndex].select();
113
116
  }
117
+ },
114
118
 
119
+ unselectItem() {
115
120
  if (this.item) this.item.unselect();
116
121
  },
117
122
 
118
- onChange() {
123
+ getItemSelectedIndex() {
124
+ return this.items.findIndex((item) => item.isSelected());
125
+ },
126
+
127
+ onInput(event) {
119
128
  this.index = 0;
120
129
  if (this.item) this.item.select();
121
130
 
122
- this.$emit('change', this.search.trim());
131
+ this.$emit('update:modelValue', event.target.value);
123
132
  },
124
133
 
125
134
  onKeyEnter() {
@@ -11,7 +11,7 @@
11
11
  @keydown.up.prevent="onKeyDownOrUp"
12
12
  @keydown.esc.stop="onKeyEsc"
13
13
  >
14
- <span :class="[headless ? 'listbox-button__text' : 'text-left truncate']">
14
+ <span :class="[headless ? 'listbox-button__text' : 'truncate text-left']">
15
15
  <slot></slot>
16
16
  </span>
17
17
  <span :class="[headless ? 'listbox-button__icon' : 'shrink-0']">
@@ -24,7 +24,10 @@
24
24
  </template>
25
25
 
26
26
  <script>
27
- import { formControlMixin } from '../../../mixins/form-control';
27
+ import {
28
+ formControlMixin,
29
+ formControlStyleMixin,
30
+ } from '../../../mixins/form-control';
28
31
  import { IconChevronDown } from '@veritree/icons';
29
32
 
30
33
  export default {
@@ -32,13 +35,13 @@ export default {
32
35
 
33
36
  components: { IconChevronDown },
34
37
 
35
- mixins: [formControlMixin],
38
+ mixins: [formControlMixin, formControlStyleMixin],
36
39
 
37
40
  inject: ['apiListbox'],
38
41
 
39
42
  data() {
40
43
  return {
41
- name: 'listbox-button',
44
+ name: 'listbox-trigger',
42
45
  expanded: false,
43
46
  hasPopup: false,
44
47
  };
@@ -101,6 +104,7 @@ export default {
101
104
  if (!this.componentContent) {
102
105
  return;
103
106
  }
107
+
104
108
  this.expanded = false;
105
109
 
106
110
  this.hideComponentContent();
@@ -120,6 +124,7 @@ export default {
120
124
 
121
125
  onClick(e) {
122
126
  this.init(e);
127
+ this.$emit('click');
123
128
  },
124
129
 
125
130
  onKeyDownOrUp(e) {