@veritree/ui 0.19.2-20 → 0.19.2-21
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/mixins/floating-ui-content.js +1 -22
- package/mixins/floating-ui-item.js +93 -46
- package/mixins/floating-ui.js +5 -12
- package/package.json +1 -1
- package/src/components/Listbox/VTListbox.vue +105 -14
- package/src/components/Listbox/VTListboxContent.vue +3 -7
- package/src/components/Listbox/VTListboxItem.vue +128 -6
- package/src/components/Listbox/VTListboxLabel.vue +3 -4
- package/src/components/Listbox/VTListboxList.vue +3 -24
- package/src/components/Listbox/VTListboxSearch.vue +60 -53
- package/src/components/Listbox/VTListboxTrigger.vue +12 -6
- package/src/components/Utils/FloatingUi.vue +48 -19
|
@@ -10,22 +10,17 @@ export const floatingUiContentMixin = {
|
|
|
10
10
|
type: Boolean,
|
|
11
11
|
default: false,
|
|
12
12
|
},
|
|
13
|
-
floatingUiClass: {
|
|
14
|
-
type: [String, Function],
|
|
15
|
-
default: null,
|
|
16
|
-
},
|
|
17
13
|
},
|
|
18
14
|
|
|
19
15
|
data() {
|
|
20
16
|
return {
|
|
21
17
|
el: null,
|
|
22
|
-
isMousemove: false,
|
|
23
18
|
visible: false,
|
|
24
19
|
};
|
|
25
20
|
},
|
|
26
21
|
|
|
27
22
|
mounted() {
|
|
28
|
-
document.addEventListener('click',
|
|
23
|
+
document.addEventListener('click', this.onDocumentClick);
|
|
29
24
|
},
|
|
30
25
|
|
|
31
26
|
destroyed() {
|
|
@@ -79,22 +74,6 @@ export const floatingUiContentMixin = {
|
|
|
79
74
|
}
|
|
80
75
|
},
|
|
81
76
|
|
|
82
|
-
// Mousemove instead of mouseover to support keyboard navigation.
|
|
83
|
-
// The problem with mouseover is that when scrolling (scrollIntoView),
|
|
84
|
-
// mouseover event gets triggered.
|
|
85
|
-
setMousemove() {
|
|
86
|
-
this.isMousemove = true;
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
unsetMousemove() {
|
|
90
|
-
this.isMousemove = false;
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
getMousemove() {
|
|
94
|
-
return this.isMousemove;
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
//
|
|
98
77
|
setActiveDescedant(id) {
|
|
99
78
|
this.activeDescedant = id;
|
|
100
79
|
},
|
|
@@ -19,10 +19,6 @@ export const floatingUiItemMixin = {
|
|
|
19
19
|
return this.apiInjected().items;
|
|
20
20
|
},
|
|
21
21
|
|
|
22
|
-
el() {
|
|
23
|
-
return this.$el;
|
|
24
|
-
},
|
|
25
|
-
|
|
26
22
|
componentContent() {
|
|
27
23
|
return this.apiInjected().componentContent;
|
|
28
24
|
},
|
|
@@ -36,7 +32,7 @@ export const floatingUiItemMixin = {
|
|
|
36
32
|
// default styles
|
|
37
33
|
this.headless
|
|
38
34
|
? `${this.componentName}`
|
|
39
|
-
: 'relative z-10 flex items-center gap-2 px-3 py-2 text-inherit no-underline',
|
|
35
|
+
: 'relative z-10 flex items-center gap-2 px-3 py-2 text-inherit no-underline cursor-pointer',
|
|
40
36
|
// disabled state styles
|
|
41
37
|
this.headless
|
|
42
38
|
? this.disabled
|
|
@@ -51,7 +47,7 @@ export const floatingUiItemMixin = {
|
|
|
51
47
|
? `${this.componentName}--selected`
|
|
52
48
|
: null
|
|
53
49
|
: this.selected
|
|
54
|
-
? 'bg-
|
|
50
|
+
? 'bg-gray-200'
|
|
55
51
|
: null,
|
|
56
52
|
];
|
|
57
53
|
},
|
|
@@ -62,20 +58,24 @@ export const floatingUiItemMixin = {
|
|
|
62
58
|
index: null,
|
|
63
59
|
selected: false,
|
|
64
60
|
tabIndex: 0,
|
|
61
|
+
eventType: null,
|
|
65
62
|
};
|
|
66
63
|
},
|
|
67
64
|
|
|
68
65
|
watch: {
|
|
69
66
|
selected(newValue) {
|
|
70
|
-
if (!newValue || !this.componentContent)
|
|
67
|
+
if (!newValue || !this.componentContent) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
71
70
|
|
|
72
71
|
this.componentContent.setActiveDescedant(this.id);
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.el.scrollIntoView({ block: 'nearest' });
|
|
73
|
+
// do not scroll into view if it's a mouse event
|
|
74
|
+
if (this.eventType && this.eventType.includes('mouse')) {
|
|
75
|
+
return;
|
|
78
76
|
}
|
|
77
|
+
|
|
78
|
+
this.$el.scrollIntoView({ block: 'nearest' });
|
|
79
79
|
},
|
|
80
80
|
},
|
|
81
81
|
|
|
@@ -95,8 +95,10 @@ export const floatingUiItemMixin = {
|
|
|
95
95
|
|
|
96
96
|
const item = {
|
|
97
97
|
id: this.id,
|
|
98
|
+
text: '',
|
|
98
99
|
select: this.select,
|
|
99
100
|
unselect: this.unselect,
|
|
101
|
+
isSelected: this.isSelected,
|
|
100
102
|
focus: this.focus,
|
|
101
103
|
onClick: this.onClick,
|
|
102
104
|
};
|
|
@@ -104,17 +106,30 @@ export const floatingUiItemMixin = {
|
|
|
104
106
|
this.apiInjected().registerItem(item);
|
|
105
107
|
},
|
|
106
108
|
|
|
109
|
+
/**
|
|
110
|
+
* mousemove and mouseleave events will be attached to
|
|
111
|
+
* the element here instead of using vue @mousemove
|
|
112
|
+
* and @mouseleave in the element since they did
|
|
113
|
+
* not worker properly
|
|
114
|
+
*/
|
|
107
115
|
addMouseEventListeners() {
|
|
108
|
-
this
|
|
109
|
-
this
|
|
116
|
+
this.$el.addEventListener('mousemove', this.onMousemove);
|
|
117
|
+
this.$el.addEventListener('mouseleave', this.onMouseleave);
|
|
110
118
|
},
|
|
111
119
|
|
|
120
|
+
/**
|
|
121
|
+
* remove the mousemove and mouseleave added for
|
|
122
|
+
* avoiding memory leaks, if @mousemove and
|
|
123
|
+
* @mouseleave were in the element, this
|
|
124
|
+
* wound't be needed
|
|
125
|
+
*/
|
|
112
126
|
removeMouseEventListeners() {
|
|
113
|
-
this
|
|
114
|
-
this
|
|
127
|
+
this.$el.removeEventListener('mousemove', this.onMousemove);
|
|
128
|
+
this.$el.removeEventListener('mouseleave', this.onMouseleave);
|
|
115
129
|
},
|
|
116
130
|
|
|
117
|
-
select() {
|
|
131
|
+
select(eventType) {
|
|
132
|
+
this.eventType = eventType;
|
|
118
133
|
this.selected = true;
|
|
119
134
|
},
|
|
120
135
|
|
|
@@ -122,59 +137,84 @@ export const floatingUiItemMixin = {
|
|
|
122
137
|
this.selected = false;
|
|
123
138
|
},
|
|
124
139
|
|
|
140
|
+
isSelected() {
|
|
141
|
+
return this.selected;
|
|
142
|
+
},
|
|
143
|
+
|
|
125
144
|
focus() {
|
|
126
|
-
if (!this
|
|
145
|
+
if (!this.$el) return;
|
|
127
146
|
|
|
128
147
|
this.tabIndex = -1;
|
|
129
148
|
this.selected = true;
|
|
130
|
-
this
|
|
149
|
+
this.$el.focus();
|
|
131
150
|
},
|
|
132
151
|
|
|
133
152
|
focusFirstItem() {
|
|
134
|
-
this.
|
|
153
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
154
|
+
const newSelectedIndex = 0;
|
|
155
|
+
|
|
156
|
+
this.setFocusToItem(selectedIndex, newSelectedIndex);
|
|
135
157
|
},
|
|
136
158
|
|
|
137
159
|
focusLastItem() {
|
|
138
|
-
this.
|
|
160
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
161
|
+
const newSelectedIndex = this.items.length - 1;
|
|
162
|
+
|
|
163
|
+
this.setFocusToItem(selectedIndex, newSelectedIndex);
|
|
139
164
|
},
|
|
140
165
|
|
|
141
166
|
/**
|
|
142
|
-
* Focus the
|
|
143
|
-
* If is the
|
|
167
|
+
* Focus the next item in the menu.
|
|
168
|
+
* If is the last item, jump to the first item.
|
|
144
169
|
*/
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
const
|
|
170
|
+
focusNextItem() {
|
|
171
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
172
|
+
const isLast = selectedIndex === this.items.length - 1;
|
|
173
|
+
const firstItemIndex = 0;
|
|
174
|
+
const nextItemIndex = selectedIndex + 1;
|
|
175
|
+
const newSelectedIndex = isLast ? firstItemIndex : nextItemIndex;
|
|
148
176
|
|
|
149
|
-
this.setFocusToItem(
|
|
177
|
+
this.setFocusToItem(selectedIndex, newSelectedIndex);
|
|
150
178
|
},
|
|
151
179
|
|
|
152
180
|
/**
|
|
153
|
-
* Focus the
|
|
154
|
-
* If is the
|
|
181
|
+
* Focus the previous item in the menu.
|
|
182
|
+
* If is the first item, jump to the last item.
|
|
155
183
|
*/
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
184
|
+
focusPreviousItem() {
|
|
185
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
186
|
+
const isFirst = selectedIndex === 0;
|
|
187
|
+
const lastItemIndex = this.items.length - 1;
|
|
188
|
+
const previousItemIndex = selectedIndex - 1;
|
|
189
|
+
const newSelectedIndex = isFirst ? lastItemIndex : previousItemIndex;
|
|
159
190
|
|
|
160
|
-
this.setFocusToItem(
|
|
191
|
+
this.setFocusToItem(selectedIndex, newSelectedIndex);
|
|
161
192
|
},
|
|
162
193
|
|
|
163
194
|
/**
|
|
164
|
-
* Focus item by
|
|
195
|
+
* Focus/select item by removing its tabindex and calling
|
|
165
196
|
* focus to the element.
|
|
166
197
|
*
|
|
167
|
-
* @param {Number, String}
|
|
198
|
+
* @param {Number, String} selectedIndex
|
|
199
|
+
* @param {Number, String} newSelectedIndex
|
|
168
200
|
*/
|
|
169
|
-
setFocusToItem(
|
|
201
|
+
setFocusToItem(selectedIndex, newSelectedIndex) {
|
|
170
202
|
this.tabIndex = 0;
|
|
171
|
-
this.selected = false;
|
|
172
203
|
|
|
173
|
-
//
|
|
174
|
-
|
|
204
|
+
// before focusing, let's unselect selected
|
|
205
|
+
// item that were previously focused
|
|
206
|
+
if (selectedIndex >= 0) {
|
|
207
|
+
this.items[selectedIndex].unselect();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// focus new item
|
|
211
|
+
if (this.items[newSelectedIndex]) {
|
|
212
|
+
this.items[newSelectedIndex].focus();
|
|
213
|
+
}
|
|
214
|
+
},
|
|
175
215
|
|
|
176
|
-
|
|
177
|
-
this.items
|
|
216
|
+
getItemSelectedIndex() {
|
|
217
|
+
return this.items.findIndex((item) => item.isSelected());
|
|
178
218
|
},
|
|
179
219
|
|
|
180
220
|
leaveMenu() {
|
|
@@ -200,20 +240,27 @@ export const floatingUiItemMixin = {
|
|
|
200
240
|
this.leaveMenu();
|
|
201
241
|
},
|
|
202
242
|
|
|
203
|
-
onMousemove() {
|
|
243
|
+
onMousemove(event) {
|
|
204
244
|
if (this.selected) {
|
|
205
245
|
return;
|
|
206
246
|
}
|
|
207
247
|
|
|
208
|
-
|
|
248
|
+
// unselect all items before selecting new one
|
|
249
|
+
for (const item of this.items) {
|
|
250
|
+
item.unselect();
|
|
251
|
+
}
|
|
209
252
|
|
|
210
|
-
|
|
211
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Select item passing the event type to decide if
|
|
255
|
+
* scrolling will be disabled or not. It is
|
|
256
|
+
* expected that on mouse move event, the scroll
|
|
257
|
+
* doesn't happen automatically.
|
|
258
|
+
*/
|
|
259
|
+
this.select(event.type);
|
|
212
260
|
},
|
|
213
261
|
|
|
214
262
|
onMouseleave() {
|
|
215
263
|
this.unselect();
|
|
216
|
-
this.componentContent.unsetMousemove();
|
|
217
264
|
},
|
|
218
265
|
},
|
|
219
|
-
};
|
|
266
|
+
};
|
package/mixins/floating-ui.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { computePosition, flip, shift, offset, size } from '@floating-ui/dom';
|
|
2
2
|
|
|
3
3
|
export const floatingUiMixin = {
|
|
4
|
-
props: {
|
|
5
|
-
placement: {
|
|
6
|
-
type: String,
|
|
7
|
-
default: 'bottom-start',
|
|
8
|
-
},
|
|
9
|
-
},
|
|
10
|
-
|
|
11
4
|
data() {
|
|
12
5
|
return {
|
|
13
6
|
component: null,
|
|
@@ -42,18 +35,18 @@ export const floatingUiMixin = {
|
|
|
42
35
|
positionContentToTrigger() {
|
|
43
36
|
const trigger = document.getElementById(this.componentTrigger.id);
|
|
44
37
|
const content = document.getElementById(this.componentContent.id);
|
|
38
|
+
const minWidthLimit = Number(this.floatingUiMinWidth);
|
|
45
39
|
|
|
46
40
|
computePosition(trigger, content, {
|
|
47
41
|
placement: this.placement,
|
|
48
42
|
middleware: [
|
|
49
|
-
offset(
|
|
43
|
+
offset(5),
|
|
50
44
|
flip(),
|
|
51
45
|
shift({ padding: 5 }),
|
|
52
46
|
size({
|
|
53
47
|
apply({ rects }) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const minWidthLimit = 200;
|
|
48
|
+
if (!minWidthLimit) return;
|
|
49
|
+
|
|
57
50
|
const width = rects.reference.width;
|
|
58
51
|
const minWidth = width < minWidthLimit ? minWidthLimit : width;
|
|
59
52
|
|
|
@@ -71,4 +64,4 @@ export const floatingUiMixin = {
|
|
|
71
64
|
});
|
|
72
65
|
},
|
|
73
66
|
},
|
|
74
|
-
};
|
|
67
|
+
};
|
package/package.json
CHANGED
|
@@ -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,119 @@ 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
|
+
console.log(this.apiListbox().$mutable);
|
|
95
|
+
if (this.wasSelected) {
|
|
96
|
+
/**
|
|
97
|
+
* There are some conflicts between the portal and the interaction
|
|
98
|
+
* with the element. It's unclear why it happens but if no delay,
|
|
99
|
+
* the floating ui content, that is placed in the body by the
|
|
100
|
+
* vue portal plugin, will not appear close to the trigger.
|
|
101
|
+
*
|
|
102
|
+
* Kind hacky but no time do find real solution.
|
|
103
|
+
*/
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
this.hasComponentSearch ? this.select() : this.focus();
|
|
106
|
+
}, 1);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
methods: {
|
|
111
|
+
/**
|
|
112
|
+
* This event is mainly for the search since other keydown
|
|
113
|
+
* events are handled with vue v-on (@) directive
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} event
|
|
116
|
+
*/
|
|
117
|
+
onKeydown(e) {
|
|
118
|
+
const isLetter = e.code.includes('Key');
|
|
119
|
+
const isNumber = e.code.includes('Numpad') || e.code.includes('Digit');
|
|
120
|
+
const isBackspace = e.code === 'Backspace';
|
|
121
|
+
const isSpace = e.code === 'Space';
|
|
122
|
+
|
|
123
|
+
if (isLetter || isNumber || isBackspace || isSpace) {
|
|
124
|
+
this.doSearch(e);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Searches and focus the options that matches the
|
|
130
|
+
* value typed when the item is selected.
|
|
131
|
+
*
|
|
132
|
+
* @param {Object} event
|
|
133
|
+
*/
|
|
134
|
+
doSearch(e) {
|
|
135
|
+
clearTimeout(keydownSearchTimeout);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Add letter or number to search data in component
|
|
139
|
+
* root, so it is available for all children
|
|
140
|
+
*/
|
|
141
|
+
this.apiListbox().pushSearch(e.key);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Let's get the item selected index at the moment the search
|
|
145
|
+
* starts to later unselect it if necessary
|
|
146
|
+
*/
|
|
147
|
+
const itemSelectedIndex = this.getItemSelectedIndex();
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* As we have now the search data with value, let's go
|
|
151
|
+
* through all items and check if the search matches
|
|
152
|
+
* with any of the items to get its index value
|
|
153
|
+
*/
|
|
154
|
+
const newItemSelectedIndex = this.items.findIndex((item) =>
|
|
155
|
+
item.text.toLowerCase().includes(this.search.toLowerCase())
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Focus the item that matches the search, and if no match
|
|
160
|
+
* we will just remove the selected state from item
|
|
161
|
+
*/
|
|
162
|
+
newItemSelectedIndex >= 0
|
|
163
|
+
? this.setFocusToItem(itemSelectedIndex, newItemSelectedIndex)
|
|
164
|
+
: this.unselect();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Clear the search data value after some time
|
|
168
|
+
* to allow other searches to be performed
|
|
169
|
+
*/
|
|
170
|
+
keydownSearchTimeout = setTimeout(() => {
|
|
171
|
+
this.apiListbox().clearSearch();
|
|
172
|
+
}, 1000);
|
|
51
173
|
},
|
|
52
174
|
},
|
|
53
175
|
};
|
|
@@ -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,12 +54,12 @@ 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() {
|
|
@@ -67,56 +69,61 @@ export default {
|
|
|
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) {
|
|
81
|
-
this.item.select();
|
|
82
|
-
}
|
|
78
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
83
79
|
},
|
|
84
80
|
|
|
85
81
|
focusPreviousItem() {
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.index = this.items.length - 1;
|
|
92
|
-
}
|
|
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;
|
|
93
87
|
|
|
94
|
-
this.
|
|
88
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
95
89
|
},
|
|
96
90
|
|
|
97
91
|
focusFirstItem() {
|
|
98
|
-
this.
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
93
|
+
const newSelectedIndex = 0;
|
|
94
|
+
|
|
95
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
101
96
|
},
|
|
102
97
|
|
|
103
98
|
focusLastItem() {
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
|
|
99
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
100
|
+
const newSelectedIndex = this.items.length - 1;
|
|
101
|
+
|
|
102
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
107
103
|
},
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
|
|
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
|
+
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.
|
|
112
|
+
// select new item
|
|
113
|
+
if (this.items[newSelectedIndex]) {
|
|
114
|
+
this.index = newSelectedIndex;
|
|
115
|
+
this.items[newSelectedIndex].select();
|
|
115
116
|
}
|
|
117
|
+
},
|
|
116
118
|
|
|
119
|
+
unselectItem() {
|
|
117
120
|
if (this.item) this.item.unselect();
|
|
118
121
|
},
|
|
119
122
|
|
|
123
|
+
getItemSelectedIndex() {
|
|
124
|
+
return this.items.findIndex((item) => item.isSelected());
|
|
125
|
+
},
|
|
126
|
+
|
|
120
127
|
onInput(event) {
|
|
121
128
|
this.index = 0;
|
|
122
129
|
if (this.item) this.item.select();
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
</template>
|
|
25
25
|
|
|
26
26
|
<script>
|
|
27
|
-
import {
|
|
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,20 +35,23 @@ 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-
|
|
44
|
+
name: 'listbox-trigger',
|
|
42
45
|
expanded: false,
|
|
43
46
|
hasPopup: false,
|
|
44
|
-
id: null,
|
|
45
47
|
};
|
|
46
48
|
},
|
|
47
49
|
|
|
48
50
|
computed: {
|
|
51
|
+
id() {
|
|
52
|
+
return `listbox-trigger-${this.apiListbox().id}`;
|
|
53
|
+
},
|
|
54
|
+
|
|
49
55
|
componentContent() {
|
|
50
56
|
return this.apiListbox().componentContent;
|
|
51
57
|
},
|
|
@@ -64,8 +70,6 @@ export default {
|
|
|
64
70
|
},
|
|
65
71
|
|
|
66
72
|
mounted() {
|
|
67
|
-
this.id = `listbox-trigger-${this.apiListbox().id}`;
|
|
68
|
-
|
|
69
73
|
const trigger = {
|
|
70
74
|
el: this.$el,
|
|
71
75
|
cancel: this.cancel,
|
|
@@ -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) {
|
|
@@ -2,25 +2,24 @@
|
|
|
2
2
|
<Teleport to="body">
|
|
3
3
|
<transition
|
|
4
4
|
enter-active-class="duration-200 ease-out"
|
|
5
|
-
enter-from-class="
|
|
6
|
-
enter-to-class="
|
|
5
|
+
:enter-from-class="transitionEnterClass"
|
|
6
|
+
:enter-to-class="transitionEnterToClass"
|
|
7
7
|
leave-active-class="duration-200 ease-in"
|
|
8
|
-
leave-from-class="
|
|
9
|
-
leave-to-class="
|
|
10
|
-
@after-leave="
|
|
8
|
+
:leave-from-class="transitionLeaveClass"
|
|
9
|
+
:leave-to-class="transitionLeaveToClass"
|
|
10
|
+
@after-leave="hidden"
|
|
11
|
+
@after-enter="shown"
|
|
11
12
|
>
|
|
12
13
|
<div
|
|
13
14
|
v-if="visible"
|
|
14
|
-
:id="id"
|
|
15
15
|
:class="[
|
|
16
16
|
headless
|
|
17
|
-
?
|
|
18
|
-
:
|
|
19
|
-
floatingUiClass ? floatingUiClass : null,
|
|
17
|
+
? `${this.component}-content`
|
|
18
|
+
: `absolute z-50 grid overflow-hidden rounded-md shadow-md ${this.classes} ${this.portalClass}`,
|
|
20
19
|
]"
|
|
21
20
|
v-bind="$attrs"
|
|
22
21
|
>
|
|
23
|
-
<slot
|
|
22
|
+
<slot />
|
|
24
23
|
</div>
|
|
25
24
|
</transition>
|
|
26
25
|
</Teleport>
|
|
@@ -28,30 +27,60 @@
|
|
|
28
27
|
|
|
29
28
|
<script>
|
|
30
29
|
export default {
|
|
31
|
-
inheritAttrs: false,
|
|
32
|
-
|
|
33
30
|
props: {
|
|
31
|
+
component: {
|
|
32
|
+
type: String,
|
|
33
|
+
default: null,
|
|
34
|
+
},
|
|
34
35
|
headless: {
|
|
35
36
|
type: Boolean,
|
|
36
37
|
default: false,
|
|
37
38
|
},
|
|
38
|
-
id: {
|
|
39
|
-
type: [String, Number],
|
|
40
|
-
default: null,
|
|
41
|
-
},
|
|
42
39
|
visible: {
|
|
43
40
|
type: Boolean,
|
|
44
41
|
default: false,
|
|
45
42
|
},
|
|
46
|
-
|
|
43
|
+
portalClass: {
|
|
47
44
|
type: [String, Function],
|
|
48
45
|
default: null,
|
|
49
46
|
},
|
|
50
47
|
},
|
|
51
48
|
|
|
49
|
+
computed: {
|
|
50
|
+
isTooltip() {
|
|
51
|
+
return this.component === 'tooltip';
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
classes() {
|
|
55
|
+
return this.isTooltip
|
|
56
|
+
? 'bg-gray-800 text-sm text-white py-1 px-2'
|
|
57
|
+
: 'bg-white py-2 px-3';
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
transitionEnterClass() {
|
|
61
|
+
return this.isTooltip ? 'opacity-0' : 'translate-y-[15px] opacity-0';
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
transitionEnterToClass() {
|
|
65
|
+
return this.isTooltip ? 'opacity-100' : 'translate-y-0 opacity-100';
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
transitionLeaveClass() {
|
|
69
|
+
return this.transitionEnterToClass;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
transitionLeaveToClass() {
|
|
73
|
+
return this.transitionEnterClass;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
|
|
52
77
|
methods: {
|
|
53
|
-
|
|
54
|
-
this.$emit('
|
|
78
|
+
shown() {
|
|
79
|
+
this.$emit('shown');
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
hidden() {
|
|
83
|
+
this.$emit('hidden');
|
|
55
84
|
},
|
|
56
85
|
},
|
|
57
86
|
};
|