@veritree/ui 0.22.1 → 0.22.2-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 -72
- package/mixins/floating-ui-content.js +1 -22
- package/mixins/floating-ui-item.js +101 -51
- package/mixins/floating-ui.js +7 -9
- package/mixins/form-control-icon.js +5 -5
- package/mixins/form-control.js +17 -18
- package/nuxt.js +30 -23
- package/package.json +14 -6
- package/src/components/Alert/VTAlert.vue +55 -14
- package/src/components/Button/VTButton.vue +20 -12
- 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 +6 -15
- package/src/components/Drawer/VTDrawerClose.vue +5 -5
- package/src/components/Drawer/VTDrawerContent.vue +9 -9
- 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 +19 -6
- package/src/components/DropdownMenu/VTDropdownMenuContent.vue +14 -6
- package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +7 -16
- package/src/components/DropdownMenu/VTDropdownMenuItem.vue +7 -3
- package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +1 -10
- package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +2 -4
- package/src/components/Form/VTFormFeedback.vue +11 -5
- 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 +7 -7
- package/src/components/Form/VTInputIcon.vue +1 -2
- package/src/components/Form/VTInputPassword.vue +14 -5
- package/src/components/Form/VTTextarea.vue +1 -1
- package/src/components/Image/VTImage.vue +33 -4
- package/src/components/Listbox/VTListbox.vue +105 -14
- package/src/components/Listbox/VTListboxContent.vue +3 -7
- package/src/components/Listbox/VTListboxItem.vue +127 -6
- package/src/components/Listbox/VTListboxLabel.vue +3 -4
- package/src/components/Listbox/VTListboxList.vue +3 -24
- package/src/components/Listbox/VTListboxSearch.vue +64 -55
- package/src/components/Listbox/VTListboxTrigger.vue +10 -4
- package/src/components/Popover/VTPopover.vue +19 -0
- package/src/components/Popover/VTPopoverItem.vue +6 -2
- 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 +6 -5
- 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 +56 -24
- package/src/utils/components.js +18 -0
- 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
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
4
|
+
:visible="visible"
|
|
6
5
|
:headless="headless"
|
|
7
|
-
:
|
|
8
|
-
|
|
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.
|
|
11
|
-
@keydown.
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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:
|
|
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
|
|
57
|
+
const componentSearch = {
|
|
56
58
|
el: this.$el,
|
|
57
59
|
};
|
|
58
60
|
|
|
59
|
-
this.apiListbox().registerSearch(
|
|
60
|
-
this.$nextTick(() => setTimeout(() => this.$
|
|
61
|
+
this.apiListbox().registerSearch(componentSearch);
|
|
62
|
+
this.$nextTick(() => setTimeout(() => this.$refs.search.focus(), 150));
|
|
61
63
|
},
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
beforeUnmount() {
|
|
64
66
|
this.search = '';
|
|
65
|
-
this.$emit('
|
|
67
|
+
this.$emit('update:modelValue', '');
|
|
66
68
|
},
|
|
67
69
|
|
|
68
70
|
methods: {
|
|
69
71
|
focusNextItem() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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.
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
88
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
93
89
|
},
|
|
94
90
|
|
|
95
91
|
focusFirstItem() {
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
93
|
+
const newSelectedIndex = 0;
|
|
94
|
+
|
|
95
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
99
96
|
},
|
|
100
97
|
|
|
101
98
|
focusLastItem() {
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
100
|
+
const newSelectedIndex = this.items.length - 1;
|
|
101
|
+
|
|
102
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
105
103
|
},
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
this.
|
|
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
|
-
|
|
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('
|
|
131
|
+
this.$emit('update:modelValue', event.target.value);
|
|
123
132
|
},
|
|
124
133
|
|
|
125
134
|
onKeyEnter() {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<button
|
|
3
3
|
:id="id"
|
|
4
4
|
:class="classComputed"
|
|
5
|
+
:disabled="disabled"
|
|
5
6
|
:aria-expanded="expanded"
|
|
6
7
|
:aria-haspopup="hasPopup"
|
|
7
8
|
type="button"
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
@keydown.up.prevent="onKeyDownOrUp"
|
|
11
12
|
@keydown.esc.stop="onKeyEsc"
|
|
12
13
|
>
|
|
13
|
-
<span :class="[headless ? 'listbox-button__text' : 'text-left
|
|
14
|
+
<span :class="[headless ? 'listbox-button__text' : 'truncate text-left']">
|
|
14
15
|
<slot></slot>
|
|
15
16
|
</span>
|
|
16
17
|
<span :class="[headless ? 'listbox-button__icon' : 'shrink-0']">
|
|
@@ -23,7 +24,10 @@
|
|
|
23
24
|
</template>
|
|
24
25
|
|
|
25
26
|
<script>
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
formControlMixin,
|
|
29
|
+
formControlStyleMixin,
|
|
30
|
+
} from '../../../mixins/form-control';
|
|
27
31
|
import { IconChevronDown } from '@veritree/icons';
|
|
28
32
|
|
|
29
33
|
export default {
|
|
@@ -31,13 +35,13 @@ export default {
|
|
|
31
35
|
|
|
32
36
|
components: { IconChevronDown },
|
|
33
37
|
|
|
34
|
-
mixins: [formControlMixin],
|
|
38
|
+
mixins: [formControlMixin, formControlStyleMixin],
|
|
35
39
|
|
|
36
40
|
inject: ['apiListbox'],
|
|
37
41
|
|
|
38
42
|
data() {
|
|
39
43
|
return {
|
|
40
|
-
name: 'listbox-
|
|
44
|
+
name: 'listbox-trigger',
|
|
41
45
|
expanded: false,
|
|
42
46
|
hasPopup: false,
|
|
43
47
|
};
|
|
@@ -100,6 +104,7 @@ export default {
|
|
|
100
104
|
if (!this.componentContent) {
|
|
101
105
|
return;
|
|
102
106
|
}
|
|
107
|
+
|
|
103
108
|
this.expanded = false;
|
|
104
109
|
|
|
105
110
|
this.hideComponentContent();
|
|
@@ -119,6 +124,7 @@ export default {
|
|
|
119
124
|
|
|
120
125
|
onClick(e) {
|
|
121
126
|
this.init(e);
|
|
127
|
+
this.$emit('click');
|
|
122
128
|
},
|
|
123
129
|
|
|
124
130
|
onKeyDownOrUp(e) {
|