@veritree/ui 0.19.2 → 0.20.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.
- package/index.js +64 -68
- package/mixins/floating-ui-content.js +81 -0
- package/mixins/floating-ui-item.js +266 -0
- package/mixins/floating-ui.js +67 -0
- package/mixins/form-control-icon.js +53 -0
- package/mixins/form-control.js +71 -0
- package/nuxt.js +30 -23
- package/package.json +9 -4
- package/src/components/Alert/VTAlert.vue +55 -14
- package/src/components/Avatar/VTAvatar.vue +32 -29
- package/src/components/Button/VTButton.vue +9 -6
- package/src/components/Checkbox/VTCheckbox.vue +134 -0
- package/src/components/Checkbox/VTCheckboxLabel.vue +3 -0
- package/src/components/Checkbox/VTCheckboxText.vue +20 -0
- package/src/components/Dialog/VTDialog.vue +22 -32
- package/src/components/Dialog/VTDialogClose.vue +19 -25
- package/src/components/Dialog/VTDialogContent.vue +24 -19
- package/src/components/Dialog/VTDialogFooter.vue +11 -16
- package/src/components/Dialog/VTDialogHeader.vue +16 -18
- package/src/components/Dialog/VTDialogMain.vue +11 -18
- package/src/components/Dialog/VTDialogOverlay.vue +14 -18
- package/src/components/Dialog/VTDialogTitle.vue +10 -7
- package/src/components/Disclosure/VTDisclosureContent.vue +1 -1
- package/src/components/Disclosure/VTDisclosureDetails.vue +1 -1
- package/src/components/Disclosure/VTDisclosureHeader.vue +2 -2
- package/src/components/Disclosure/VTDisclosureIcon.vue +1 -1
- package/src/components/Drawer/VTDrawer.vue +14 -16
- package/src/components/Drawer/VTDrawerClose.vue +9 -9
- package/src/components/Drawer/VTDrawerContent.vue +8 -8
- package/src/components/Drawer/VTDrawerFooter.vue +3 -3
- package/src/components/Drawer/VTDrawerHeader.vue +4 -4
- package/src/components/Drawer/VTDrawerMain.vue +5 -5
- package/src/components/Drawer/VTDrawerOverlay.vue +6 -6
- package/src/components/Drawer/VTDrawerTitle.vue +5 -5
- package/src/components/DropdownMenu/VTDropdownMenu.vue +45 -28
- package/src/components/DropdownMenu/VTDropdownMenuContent.vue +29 -64
- package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +8 -14
- package/src/components/DropdownMenu/VTDropdownMenuItem.vue +11 -124
- package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +3 -12
- package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +91 -121
- package/src/components/Form/VTFormFeedback.vue +39 -22
- package/src/components/Form/VTFormGroup.vue +5 -7
- package/src/components/Form/VTFormLabel.vue +22 -0
- package/src/components/Form/VTFormRow.vue +5 -0
- package/src/components/Form/VTInput.vue +40 -0
- package/src/components/Form/VTInputIcon.vue +35 -0
- package/src/components/Form/VTInputPassword.vue +55 -0
- package/src/components/Form/VTTextarea.vue +22 -0
- package/src/components/Listbox/VTListbox.vue +122 -50
- package/src/components/Listbox/VTListboxContent.vue +20 -116
- package/src/components/Listbox/VTListboxItem.vue +115 -166
- package/src/components/Listbox/VTListboxLabel.vue +3 -14
- package/src/components/Listbox/VTListboxList.vue +10 -40
- package/src/components/Listbox/VTListboxSearch.vue +76 -68
- package/src/components/Listbox/VTListboxTrigger.vue +75 -86
- package/src/components/Popover/VTPopover.vue +42 -29
- package/src/components/Popover/VTPopoverContent.vue +24 -59
- package/src/components/Popover/VTPopoverDivider.vue +4 -11
- package/src/components/Popover/VTPopoverItem.vue +21 -14
- package/src/components/Popover/VTPopoverTrigger.vue +126 -21
- package/src/components/ProgressBar/VTProgressBar.vue +21 -3
- package/src/components/Skeleton/VTSkeleton.vue +11 -0
- package/src/components/Skeleton/VTSkeletonItem.vue +9 -0
- package/src/components/Tabs/VTTab.vue +4 -3
- package/src/components/Tabs/VTTabGroup.vue +9 -7
- package/src/components/Tabs/VTTabPanel.vue +4 -5
- package/src/components/Tooltip/VTTooltip.vue +65 -0
- package/src/components/Tooltip/VTTooltipContent.vue +59 -0
- package/src/components/Tooltip/VTTooltipTrigger.vue +98 -0
- package/src/components/Transitions/FadeInOut.vue +2 -2
- package/src/components/Utils/FloatingUi.vue +93 -0
- package/package-lock.json +0 -13
- package/src/components/Input/VTInput.vue +0 -82
- package/src/components/Input/VTInputDate.vue +0 -36
- package/src/components/Input/VTInputFile.vue +0 -60
- package/src/components/Input/VTInputUpload.vue +0 -54
- package/src/components/Modal/VTModal.vue +0 -69
- package/src/utils/genId.js +0 -13
|
@@ -1,51 +1,54 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<li
|
|
3
3
|
:id="id"
|
|
4
|
-
:class="
|
|
5
|
-
ListboxItem: headless,
|
|
6
|
-
'relative z-10 flex items-center gap-2 px-3 py-2 no-underline': !headless,
|
|
7
|
-
'hover:bg-secondary-200/10': !dark && !headless,
|
|
8
|
-
'text-white': dark && !headless,
|
|
9
|
-
'pointer-events-none opacity-75': disabled && !headless,
|
|
10
|
-
'bg-secondary-200/10': selected && !headless,
|
|
11
|
-
}"
|
|
4
|
+
:class="classComputed"
|
|
12
5
|
:aria-disabled="disabled"
|
|
13
6
|
:aria-selected="String(selected)"
|
|
14
7
|
:tabindex="tabIndex"
|
|
15
|
-
role="option"
|
|
8
|
+
:role="'option'"
|
|
16
9
|
@click.stop="onClick"
|
|
17
|
-
@keydown.
|
|
18
|
-
@keydown.
|
|
10
|
+
@keydown.prevent="onKeydown"
|
|
11
|
+
@keydown.down.prevent="focusNextItem"
|
|
12
|
+
@keydown.up.prevent="focusPreviousItem"
|
|
19
13
|
@keydown.home.prevent="focusFirstItem"
|
|
20
14
|
@keydown.end.prevent="focusLastItem"
|
|
21
15
|
@keydown.esc.stop="onKeyEsc"
|
|
22
16
|
@keydown.enter.prevent="onClick"
|
|
23
|
-
@mousemove="onMousemove"
|
|
24
|
-
@mouseout="onMouseleave"
|
|
25
17
|
@keydown.tab.prevent
|
|
26
18
|
>
|
|
27
|
-
<span
|
|
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>
|
|
29
|
+
<slot></slot>
|
|
30
|
+
<span v-if="wasSelected" class="ml-auto">
|
|
31
|
+
<IconCheckOutline class="text-secondary-200 h-5 w-5" />
|
|
32
|
+
</span>
|
|
28
33
|
</li>
|
|
29
34
|
</template>
|
|
30
35
|
|
|
31
36
|
<script>
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
37
|
+
import { IconCheckOutline } from '@veritree/icons';
|
|
38
|
+
import { floatingUiItemMixin } from '../../../mixins/floating-ui-item';
|
|
39
|
+
|
|
40
|
+
let keydownSearchTimeout = null;
|
|
34
41
|
|
|
35
42
|
export default {
|
|
36
|
-
name:
|
|
43
|
+
name: 'VTListboxItem',
|
|
44
|
+
|
|
45
|
+
mixins: [floatingUiItemMixin],
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
components: { IconCheckOutline },
|
|
48
|
+
|
|
49
|
+
inject: ['apiListbox'],
|
|
39
50
|
|
|
40
51
|
props: {
|
|
41
|
-
headless: {
|
|
42
|
-
type: Boolean,
|
|
43
|
-
default: false,
|
|
44
|
-
},
|
|
45
|
-
disabled: {
|
|
46
|
-
type: Boolean,
|
|
47
|
-
default: false,
|
|
48
|
-
},
|
|
49
52
|
value: {
|
|
50
53
|
type: [String, Number, Object, Array],
|
|
51
54
|
required: true,
|
|
@@ -54,172 +57,118 @@ export default {
|
|
|
54
57
|
|
|
55
58
|
data() {
|
|
56
59
|
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
tabIndex: 0,
|
|
60
|
+
// apiInjected is used in the mixin floating-ui-item
|
|
61
|
+
apiInjected: this.apiListbox,
|
|
62
|
+
componentName: 'listbox-item',
|
|
61
63
|
};
|
|
62
64
|
},
|
|
63
65
|
|
|
64
66
|
computed: {
|
|
65
|
-
|
|
66
|
-
return this.
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
items() {
|
|
70
|
-
return this.api().items;
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
el() {
|
|
74
|
-
return this.$el;
|
|
75
|
-
},
|
|
76
|
-
|
|
77
|
-
trigger() {
|
|
78
|
-
return this.api().trigger;
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
content() {
|
|
82
|
-
return this.api().content;
|
|
67
|
+
componentContent() {
|
|
68
|
+
return this.apiInjected().componentContent;
|
|
83
69
|
},
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
return this.
|
|
71
|
+
hasComponentSearch() {
|
|
72
|
+
return this.apiListbox().componentSearch !== null;
|
|
87
73
|
},
|
|
88
74
|
|
|
89
75
|
search() {
|
|
90
|
-
return this.
|
|
76
|
+
return this.apiListbox().search;
|
|
91
77
|
},
|
|
92
|
-
},
|
|
93
78
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
scrollElementIntoView(this.el, this.list.el);
|
|
106
|
-
}
|
|
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
|
+
);
|
|
107
90
|
},
|
|
108
91
|
},
|
|
109
92
|
|
|
110
93
|
mounted() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
beforeDestroy() {
|
|
125
|
-
this.api().unregisterItem(this.id);
|
|
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
|
+
}
|
|
126
107
|
},
|
|
127
108
|
|
|
128
109
|
methods: {
|
|
129
|
-
select() {
|
|
130
|
-
this.selected = true;
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
unselect() {
|
|
134
|
-
this.selected = false;
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
focus() {
|
|
138
|
-
if (!this.el) return;
|
|
139
|
-
|
|
140
|
-
this.tabIndex = -1;
|
|
141
|
-
this.selected = true;
|
|
142
|
-
this.el.focus();
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
focusFirstItem() {
|
|
146
|
-
this.setFocusToItem(0);
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
focusLastItem() {
|
|
150
|
-
this.setFocusToItem(this.items.length - 1);
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Focus the previous item in the menu.
|
|
155
|
-
* If is the first item, jump to the last item.
|
|
156
|
-
*/
|
|
157
|
-
focusPreviousItem() {
|
|
158
|
-
const isLast = this.index === this.items.length - 1;
|
|
159
|
-
const goToIndex = isLast ? 0 : this.index + 1;
|
|
160
|
-
|
|
161
|
-
this.setFocusToItem(goToIndex);
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Focus the next item in the menu.
|
|
166
|
-
* If is the last item, jump to the first item.
|
|
167
|
-
*/
|
|
168
|
-
focusNextItem() {
|
|
169
|
-
const isFirst = this.index === 0;
|
|
170
|
-
const goToIndex = isFirst ? this.items.length - 1 : this.index - 1;
|
|
171
|
-
|
|
172
|
-
this.setFocusToItem(goToIndex);
|
|
173
|
-
},
|
|
174
|
-
|
|
175
110
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
111
|
+
* This event is mainly for the search since other keydown
|
|
112
|
+
* events are handled with vue v-on (@) directive
|
|
178
113
|
*
|
|
179
|
-
* @param {
|
|
114
|
+
* @param {Object} event
|
|
180
115
|
*/
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
this.
|
|
189
|
-
this.items.forEach((item) => item.unselect());
|
|
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);
|
|
190
124
|
}
|
|
191
|
-
|
|
192
|
-
this.items[goToIndex].focus();
|
|
193
125
|
},
|
|
194
126
|
|
|
195
127
|
/**
|
|
196
|
-
*
|
|
128
|
+
* Searches and focus the options that matches the
|
|
129
|
+
* value typed when the item is selected.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} event
|
|
197
132
|
*/
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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);
|
|
223
172
|
},
|
|
224
173
|
},
|
|
225
174
|
};
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="as"
|
|
4
|
-
:class="
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'text-inherit': !dark && !headless,
|
|
8
|
-
'text-white': dark && !headless,
|
|
9
|
-
}"
|
|
4
|
+
:class="[
|
|
5
|
+
headless ? 'listbox-label' : 'mb-2 block text-xs font-normal uppercase',
|
|
6
|
+
]"
|
|
10
7
|
>
|
|
11
8
|
<slot></slot>
|
|
12
9
|
</component>
|
|
@@ -16,8 +13,6 @@
|
|
|
16
13
|
export default {
|
|
17
14
|
name: 'VTListboxLabel',
|
|
18
15
|
|
|
19
|
-
inject: ['api'],
|
|
20
|
-
|
|
21
16
|
props: {
|
|
22
17
|
as: {
|
|
23
18
|
type: String,
|
|
@@ -28,11 +23,5 @@ export default {
|
|
|
28
23
|
default: false,
|
|
29
24
|
},
|
|
30
25
|
},
|
|
31
|
-
|
|
32
|
-
computed: {
|
|
33
|
-
dark() {
|
|
34
|
-
return this.api().isDark;
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
26
|
};
|
|
38
27
|
</script>
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<ul
|
|
3
3
|
:id="id"
|
|
4
|
-
:class="
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
:class="[
|
|
5
|
+
headless
|
|
6
|
+
? 'listbox-list'
|
|
7
|
+
: '-mx-3 max-h-[160px] w-auto overflow-y-auto scroll-auto',
|
|
8
|
+
]"
|
|
8
9
|
>
|
|
9
10
|
<slot></slot>
|
|
10
11
|
</ul>
|
|
11
12
|
</template>
|
|
12
13
|
|
|
13
14
|
<script>
|
|
14
|
-
import { genId } from "../../utils/ids";
|
|
15
|
-
|
|
16
15
|
export default {
|
|
17
|
-
name:
|
|
16
|
+
name: 'VTListboxList',
|
|
18
17
|
|
|
19
|
-
inject: [
|
|
18
|
+
inject: ['apiListbox'],
|
|
20
19
|
|
|
21
20
|
props: {
|
|
22
21
|
headless: {
|
|
@@ -25,38 +24,9 @@ export default {
|
|
|
25
24
|
},
|
|
26
25
|
},
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
isMousemove: false,
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
mounted() {
|
|
36
|
-
const list = {
|
|
37
|
-
el: this.$el,
|
|
38
|
-
getMousemove: this.getMousemove,
|
|
39
|
-
setMousemove: this.setMousemove,
|
|
40
|
-
unsetMousemove: this.unsetMousemove,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
this.api().registerList(list);
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
methods: {
|
|
47
|
-
// Mousemove instead of mouseover to support keyboard navigation.
|
|
48
|
-
// The problem with mouseover is that when scrolling (scrollIntoView),
|
|
49
|
-
// mouseover event gets triggered.
|
|
50
|
-
setMousemove() {
|
|
51
|
-
this.isMousemove = true;
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
unsetMousemove() {
|
|
55
|
-
this.isMousemove = false;
|
|
56
|
-
},
|
|
57
|
-
|
|
58
|
-
getMousemove() {
|
|
59
|
-
return this.isMousemove;
|
|
27
|
+
computed: {
|
|
28
|
+
id() {
|
|
29
|
+
return `listbox-list-${this.apiListbox().id}`;
|
|
60
30
|
},
|
|
61
31
|
},
|
|
62
32
|
};
|
|
@@ -1,54 +1,51 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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>
|
|
15
20
|
</template>
|
|
16
21
|
|
|
17
22
|
<script>
|
|
23
|
+
import { formControlMixin } from '../../../mixins/form-control';
|
|
24
|
+
|
|
18
25
|
export default {
|
|
19
26
|
name: 'VTListboxSearch',
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
mixins: [formControlMixin],
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
default: false,
|
|
27
|
-
},
|
|
28
|
-
},
|
|
30
|
+
inheritAttrs: false,
|
|
31
|
+
|
|
32
|
+
inject: ['apiListbox'],
|
|
29
33
|
|
|
30
34
|
data() {
|
|
31
35
|
return {
|
|
36
|
+
name: 'listbox-search',
|
|
37
|
+
index: null,
|
|
32
38
|
search: '',
|
|
33
|
-
index: -1,
|
|
34
39
|
};
|
|
35
40
|
},
|
|
36
41
|
|
|
37
42
|
computed: {
|
|
38
|
-
|
|
39
|
-
return this.
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
trigger() {
|
|
43
|
-
return this.api().trigger;
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
list() {
|
|
47
|
-
return this.api().list;
|
|
43
|
+
componentTrigger() {
|
|
44
|
+
return this.apiListbox().componentTrigger;
|
|
48
45
|
},
|
|
49
46
|
|
|
50
47
|
items() {
|
|
51
|
-
return this.
|
|
48
|
+
return this.apiListbox().items;
|
|
52
49
|
},
|
|
53
50
|
|
|
54
51
|
item() {
|
|
@@ -57,73 +54,81 @@ export default {
|
|
|
57
54
|
},
|
|
58
55
|
|
|
59
56
|
mounted() {
|
|
60
|
-
const
|
|
57
|
+
const componentSearch = {
|
|
61
58
|
el: this.$el,
|
|
62
59
|
};
|
|
63
60
|
|
|
64
|
-
this.
|
|
61
|
+
this.apiListbox().registerSearch(componentSearch);
|
|
62
|
+
this.$nextTick(() => setTimeout(() => this.$refs.search.focus(), 150));
|
|
65
63
|
},
|
|
66
64
|
|
|
67
|
-
|
|
65
|
+
beforeUnmount() {
|
|
68
66
|
this.search = '';
|
|
69
|
-
this.$emit('
|
|
67
|
+
this.$emit('update:modelValue', '');
|
|
70
68
|
},
|
|
71
69
|
|
|
72
70
|
methods: {
|
|
73
71
|
focusNextItem() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
this.index = 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (this.item) this.item.select();
|
|
78
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
85
79
|
},
|
|
86
80
|
|
|
87
81
|
focusPreviousItem() {
|
|
88
|
-
this.
|
|
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;
|
|
89
87
|
|
|
90
|
-
this.
|
|
91
|
-
|
|
92
|
-
if (this.index < 0) {
|
|
93
|
-
this.index = this.items.length - 1;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
this.item.select();
|
|
88
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
97
89
|
},
|
|
98
90
|
|
|
99
91
|
focusFirstItem() {
|
|
100
|
-
this.
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
93
|
+
const newSelectedIndex = 0;
|
|
94
|
+
|
|
95
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
103
96
|
},
|
|
104
97
|
|
|
105
98
|
focusLastItem() {
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
100
|
+
const newSelectedIndex = this.items.length - 1;
|
|
101
|
+
|
|
102
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
109
103
|
},
|
|
110
104
|
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
}
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.
|
|
112
|
+
// select new item
|
|
113
|
+
if (this.items[newSelectedIndex]) {
|
|
114
|
+
this.index = newSelectedIndex;
|
|
115
|
+
this.items[newSelectedIndex].select();
|
|
117
116
|
}
|
|
117
|
+
},
|
|
118
118
|
|
|
119
|
+
unselectItem() {
|
|
119
120
|
if (this.item) this.item.unselect();
|
|
120
121
|
},
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
getItemSelectedIndex() {
|
|
124
|
+
return this.items.findIndex((item) => item.isSelected());
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
onInput(event) {
|
|
123
128
|
this.index = 0;
|
|
124
129
|
if (this.item) this.item.select();
|
|
125
130
|
|
|
126
|
-
this.$emit('
|
|
131
|
+
this.$emit('update:modelValue', event.target.value);
|
|
127
132
|
},
|
|
128
133
|
|
|
129
134
|
onKeyEnter() {
|
|
@@ -131,7 +136,10 @@ export default {
|
|
|
131
136
|
},
|
|
132
137
|
|
|
133
138
|
hide() {
|
|
134
|
-
if (this.
|
|
139
|
+
if (this.componentTrigger) {
|
|
140
|
+
this.componentTrigger.cancel();
|
|
141
|
+
this.componentTrigger.focus();
|
|
142
|
+
}
|
|
135
143
|
},
|
|
136
144
|
},
|
|
137
145
|
};
|