@veritree/ui 0.22.3 → 0.24.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/mixins/floating-ui-content.js +0 -21
- package/mixins/floating-ui-item.js +88 -46
- package/mixins/floating-ui.js +4 -13
- package/mixins/form-control-icon.js +2 -2
- package/mixins/form-control.js +9 -5
- package/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/Avatar/VTAvatarImage.vue +26 -6
- package/src/components/Button/VTButton.vue +5 -5
- package/src/components/Dialog/VTDialog.vue +6 -15
- package/src/components/Dialog/VTDialogClose.vue +19 -25
- package/src/components/Dialog/VTDialogContent.vue +18 -21
- package/src/components/Dialog/VTDialogFooter.vue +7 -18
- package/src/components/Dialog/VTDialogHeader.vue +15 -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/Drawer/VTDrawerContent.vue +1 -1
- package/src/components/Drawer/VTDrawerFooter.vue +1 -1
- package/src/components/DropdownMenu/VTDropdownMenu.vue +19 -0
- package/src/components/DropdownMenu/VTDropdownMenuContent.vue +5 -6
- package/src/components/DropdownMenu/VTDropdownMenuItem.vue +2 -2
- package/src/components/Form/VTFormFeedback.vue +5 -5
- package/src/components/Form/VTInput.vue +5 -2
- package/src/components/Form/VTTextarea.vue +5 -2
- package/src/components/Listbox/VTListbox.vue +35 -11
- package/src/components/Listbox/VTListboxContent.vue +4 -7
- package/src/components/Listbox/VTListboxItem.vue +117 -6
- package/src/components/Listbox/VTListboxList.vue +1 -24
- package/src/components/Listbox/VTListboxSearch.vue +58 -52
- package/src/components/Listbox/VTListboxTrigger.vue +7 -4
- package/src/components/Modal/VTModal.vue +1 -1
- package/src/components/Popover/VTPopover.vue +19 -0
- package/src/components/Popover/VTPopoverContent.vue +3 -3
- package/src/components/Tooltip/VTTooltip.vue +65 -0
- package/src/components/Tooltip/VTTooltipContent.vue +59 -0
- package/src/components/Tooltip/VTTooltipTrigger.vue +100 -0
- package/src/components/Utils/FloatingUi.vue +27 -13
|
@@ -5,30 +5,37 @@
|
|
|
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
|
>
|
|
20
19
|
<slot></slot>
|
|
20
|
+
<span v-if="wasSelected" class="ml-auto">
|
|
21
|
+
<IconCheckOutline class="text-secondary-200 h-5 w-5" />
|
|
22
|
+
</span>
|
|
21
23
|
</li>
|
|
22
24
|
</template>
|
|
23
25
|
|
|
24
26
|
<script>
|
|
27
|
+
import { IconCheckOutline } from '@veritree/icons';
|
|
25
28
|
import { floatingUiItemMixin } from '../../../mixins/floating-ui-item';
|
|
26
29
|
|
|
30
|
+
let keydownSearchTimeout = null;
|
|
31
|
+
|
|
27
32
|
export default {
|
|
28
33
|
name: 'VTListboxItem',
|
|
29
34
|
|
|
30
35
|
mixins: [floatingUiItemMixin],
|
|
31
36
|
|
|
37
|
+
components: { IconCheckOutline },
|
|
38
|
+
|
|
32
39
|
inject: ['apiListbox'],
|
|
33
40
|
|
|
34
41
|
props: {
|
|
@@ -40,14 +47,118 @@ export default {
|
|
|
40
47
|
|
|
41
48
|
data() {
|
|
42
49
|
return {
|
|
50
|
+
// apiInjected is used in the mixin floating-ui-item
|
|
43
51
|
apiInjected: this.apiListbox,
|
|
44
52
|
componentName: 'listbox-item',
|
|
45
53
|
};
|
|
46
54
|
},
|
|
47
55
|
|
|
48
56
|
computed: {
|
|
57
|
+
componentContent() {
|
|
58
|
+
return this.apiInjected().componentContent;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
hasComponentSearch() {
|
|
62
|
+
return this.apiListbox().componentSearch !== null;
|
|
63
|
+
},
|
|
64
|
+
|
|
49
65
|
search() {
|
|
50
|
-
return this.
|
|
66
|
+
return this.apiListbox().search;
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The difference between selected (in the mixin) and
|
|
71
|
+
* wasSelected is that selected is more related to
|
|
72
|
+
* the aria-selected while wasSelected is the
|
|
73
|
+
* real selected option for the listbox
|
|
74
|
+
*/
|
|
75
|
+
wasSelected() {
|
|
76
|
+
return (
|
|
77
|
+
JSON.stringify(this.value) ===
|
|
78
|
+
JSON.stringify(this.apiListbox().selectedValue)
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
mounted() {
|
|
84
|
+
if (this.wasSelected) {
|
|
85
|
+
/**
|
|
86
|
+
* There are some conflicts between the portal and the interaction
|
|
87
|
+
* with the element. It's unclear why it happens but if no delay,
|
|
88
|
+
* the floating ui content, that is placed in the body by the
|
|
89
|
+
* vue portal plugin, will not appear close to the trigger.
|
|
90
|
+
*
|
|
91
|
+
* Kind hacky but no time do find real solution.
|
|
92
|
+
*/
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
this.hasComponentSearch ? this.select() : this.focus();
|
|
95
|
+
}, 1);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
methods: {
|
|
100
|
+
/**
|
|
101
|
+
* This event is mainly for the search since other keydown
|
|
102
|
+
* events are handled with vue v-on (@) directive
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} event
|
|
105
|
+
*/
|
|
106
|
+
onKeydown(e) {
|
|
107
|
+
const isLetter = e.code.includes('Key');
|
|
108
|
+
const isNumber = e.code.includes('Numpad') || e.code.includes('Digit');
|
|
109
|
+
const isBackspace = e.code === 'Backspace';
|
|
110
|
+
const isSpace = e.code === 'Space';
|
|
111
|
+
|
|
112
|
+
if (isLetter || isNumber || isBackspace || isSpace) {
|
|
113
|
+
this.doSearch(e);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Searches and focus the options that matches the
|
|
119
|
+
* value typed when the item is selected.
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} event
|
|
122
|
+
*/
|
|
123
|
+
doSearch(e) {
|
|
124
|
+
clearTimeout(keydownSearchTimeout);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Add letter or number to search data in component
|
|
128
|
+
* root, so it is available for all children
|
|
129
|
+
*/
|
|
130
|
+
this.apiListbox().pushSearch(e.key);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Let's get the item selected index at the moment the search
|
|
134
|
+
* starts to later unselect it if necessary
|
|
135
|
+
*/
|
|
136
|
+
const itemSelectedIndex = this.getItemSelectedIndex();
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* As we have now the search data with value, let's go
|
|
140
|
+
* through all items and check if the search matches
|
|
141
|
+
* with any of the items to get its index value
|
|
142
|
+
*/
|
|
143
|
+
const newItemSelectedIndex = this.items.findIndex((item) =>
|
|
144
|
+
item.text.toLowerCase().includes(this.search.toLowerCase())
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Focus the item that matches the search, and if no match
|
|
149
|
+
* we will just remove the selected state from item
|
|
150
|
+
*/
|
|
151
|
+
newItemSelectedIndex >= 0
|
|
152
|
+
? this.setFocusToItem(itemSelectedIndex, newItemSelectedIndex)
|
|
153
|
+
: this.unselect();
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Clear the search data value after some time
|
|
157
|
+
* to allow other searches to be performed
|
|
158
|
+
*/
|
|
159
|
+
keydownSearchTimeout = setTimeout(() => {
|
|
160
|
+
this.apiListbox().clearSearch();
|
|
161
|
+
}, 1000);
|
|
51
162
|
},
|
|
52
163
|
},
|
|
53
164
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<ul
|
|
3
3
|
:id="id"
|
|
4
4
|
:class="[
|
|
5
|
-
headless ? 'listbox-list' : 'max-h-[160px] w-auto overflow-y-auto
|
|
5
|
+
headless ? 'listbox-list' : '-mx-3 max-h-[160px] w-auto overflow-y-auto',
|
|
6
6
|
]"
|
|
7
7
|
>
|
|
8
8
|
<slot></slot>
|
|
@@ -27,28 +27,5 @@ export default {
|
|
|
27
27
|
return `listbox-list-${this.apiListbox().id}`;
|
|
28
28
|
},
|
|
29
29
|
},
|
|
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
30
|
};
|
|
54
31
|
</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
|
+
v-model="search"
|
|
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="onChange"
|
|
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
|
beforeDestroy() {
|
|
@@ -67,54 +69,58 @@ 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) 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
|
-
this.item.select();
|
|
105
|
-
},
|
|
99
|
+
const selectedIndex = this.getItemSelectedIndex();
|
|
100
|
+
const newSelectedIndex = this.items.length - 1;
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
this.selectItem(selectedIndex, newSelectedIndex);
|
|
103
|
+
},
|
|
109
104
|
|
|
110
|
-
|
|
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();
|
|
113
110
|
}
|
|
114
111
|
|
|
112
|
+
// select new item
|
|
113
|
+
this.items[newSelectedIndex].select();
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
unselectItem() {
|
|
115
117
|
if (this.item) this.item.unselect();
|
|
116
118
|
},
|
|
117
119
|
|
|
120
|
+
getItemSelectedIndex() {
|
|
121
|
+
return this.items.findIndex((item) => item.isSelected());
|
|
122
|
+
},
|
|
123
|
+
|
|
118
124
|
onChange() {
|
|
119
125
|
this.index = 0;
|
|
120
126
|
if (this.item) this.item.select();
|
|
@@ -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
|
|
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 {
|
|
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,12 @@ 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',
|
|
42
44
|
expanded: false,
|
|
43
45
|
hasPopup: false,
|
|
44
46
|
};
|
|
@@ -101,6 +103,7 @@ export default {
|
|
|
101
103
|
if (!this.componentContent) {
|
|
102
104
|
return;
|
|
103
105
|
}
|
|
106
|
+
|
|
104
107
|
this.expanded = false;
|
|
105
108
|
|
|
106
109
|
this.hideComponentContent();
|
|
@@ -48,11 +48,30 @@ export default {
|
|
|
48
48
|
type: Boolean,
|
|
49
49
|
default: false,
|
|
50
50
|
},
|
|
51
|
+
placement: {
|
|
52
|
+
type: String,
|
|
53
|
+
default: 'bottom-start',
|
|
54
|
+
},
|
|
51
55
|
},
|
|
52
56
|
|
|
53
57
|
data() {
|
|
54
58
|
return {
|
|
55
59
|
componentId: genId(),
|
|
60
|
+
/**
|
|
61
|
+
* Explaining the need for the floatingUiMinWidth data
|
|
62
|
+
*
|
|
63
|
+
* The floating ui is a result of two items:
|
|
64
|
+
*
|
|
65
|
+
* 1. Trigger: the action button
|
|
66
|
+
* 2. Content: the popper/wrapper that appears after triggering the action button
|
|
67
|
+
*
|
|
68
|
+
* By default, the content will match the triggers width.
|
|
69
|
+
* The problem with this is that the trigger width
|
|
70
|
+
* might be too small causing the content to not fit
|
|
71
|
+
* what is inside it properly. So, to avoid this,
|
|
72
|
+
* a min width is needed.
|
|
73
|
+
*/
|
|
74
|
+
floatingUiMinWidth: 200,
|
|
56
75
|
};
|
|
57
76
|
},
|
|
58
77
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<FloatingUi
|
|
3
|
-
:visible="visible"
|
|
4
3
|
:id="id"
|
|
4
|
+
:visible="visible"
|
|
5
5
|
:headless="headless"
|
|
6
|
-
:class="
|
|
7
|
-
|
|
6
|
+
:portal-class="$vnode.data.staticClass"
|
|
7
|
+
component="popover"
|
|
8
8
|
>
|
|
9
9
|
<slot></slot>
|
|
10
10
|
</FloatingUi>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import { floatingUiMixin } from '../../../mixins/floating-ui';
|
|
9
|
+
import { genId } from '../../utils/ids';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
name: 'VTTooltip',
|
|
13
|
+
|
|
14
|
+
mixins: [floatingUiMixin],
|
|
15
|
+
|
|
16
|
+
props: {
|
|
17
|
+
delayDuration: {
|
|
18
|
+
type: [String, Number],
|
|
19
|
+
default: 500,
|
|
20
|
+
},
|
|
21
|
+
placement: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: 'bottom',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
data() {
|
|
28
|
+
return {
|
|
29
|
+
floatingUiMinWidth: 0,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
provide() {
|
|
34
|
+
return {
|
|
35
|
+
apiTooltip: () => {
|
|
36
|
+
const registerTrigger = (trigger) => {
|
|
37
|
+
if (!trigger) return;
|
|
38
|
+
this.componentTrigger = trigger;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const registerContent = (content) => {
|
|
42
|
+
if (!content) return;
|
|
43
|
+
this.componentContent = content;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id: this.componentId,
|
|
48
|
+
component: this.component,
|
|
49
|
+
componentTrigger: this.componentTrigger,
|
|
50
|
+
componentContent: this.componentContent,
|
|
51
|
+
delayDuration: this.delayDuration,
|
|
52
|
+
registerTrigger,
|
|
53
|
+
registerContent,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
data() {
|
|
60
|
+
return {
|
|
61
|
+
componentId: genId(),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<FloatingUi
|
|
3
|
+
:id="id"
|
|
4
|
+
:visible="visible"
|
|
5
|
+
:headless="headless"
|
|
6
|
+
component="tooltip"
|
|
7
|
+
>
|
|
8
|
+
<slot></slot>
|
|
9
|
+
</FloatingUi>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
name: 'VTTooltipContent',
|
|
17
|
+
|
|
18
|
+
inheritAttrs: false,
|
|
19
|
+
|
|
20
|
+
mixins: [floatingUiContentMixin],
|
|
21
|
+
|
|
22
|
+
inject: ['apiTooltip'],
|
|
23
|
+
|
|
24
|
+
data() {
|
|
25
|
+
return {
|
|
26
|
+
visible: false,
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
computed: {
|
|
31
|
+
id() {
|
|
32
|
+
return `tooltip-content-${this.apiTooltip().id}`;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
component() {
|
|
36
|
+
return this.apiTooltip().component;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
componentTrigger() {
|
|
40
|
+
return this.apiTooltip().componentTrigger;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
ariaLabelledby() {
|
|
44
|
+
if (!this.componentTrigger) return null;
|
|
45
|
+
return this.visible ? this.componentTrigger.id : null;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
mounted() {
|
|
50
|
+
const content = {
|
|
51
|
+
id: this.id,
|
|
52
|
+
hide: this.hide,
|
|
53
|
+
show: this.show,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
this.apiTooltip().registerContent(content);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:id="id"
|
|
4
|
+
:aria-describedby="ariaDescribedBy"
|
|
5
|
+
class="inline-flex"
|
|
6
|
+
@mouseenter="onMouseenter"
|
|
7
|
+
@mouseleave="onMouseleave"
|
|
8
|
+
>
|
|
9
|
+
<slot />
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
let tooltipTriggerTimeout = null;
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
name: 'VTTooltipTrigger',
|
|
18
|
+
|
|
19
|
+
inject: ['apiTooltip'],
|
|
20
|
+
|
|
21
|
+
data() {
|
|
22
|
+
return {
|
|
23
|
+
expanded: false,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
computed: {
|
|
28
|
+
id() {
|
|
29
|
+
return `tooltip-trigger-${this.apiTooltip().id}`;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
componentContent() {
|
|
33
|
+
return this.apiTooltip().componentContent;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
ariaDescribedBy() {
|
|
37
|
+
if (!this.componentContent) return null;
|
|
38
|
+
return this.expanded ? this.componentContent.id : null;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
mounted() {
|
|
43
|
+
const trigger = {
|
|
44
|
+
cancel: this.cancel,
|
|
45
|
+
id: this.id,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.apiTooltip().registerTrigger(trigger);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
methods: {
|
|
52
|
+
onMouseenter(e) {
|
|
53
|
+
tooltipTriggerTimeout = setTimeout(() => {
|
|
54
|
+
this.init(e);
|
|
55
|
+
}, this.apiTooltip().delayDuration);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
onMouseleave() {
|
|
59
|
+
clearTimeout(tooltipTriggerTimeout);
|
|
60
|
+
this.cancel();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
init(e) {
|
|
64
|
+
if (!this.componentContent) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.expanded) {
|
|
69
|
+
this.cancel();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.expanded = true;
|
|
74
|
+
|
|
75
|
+
// delay stop propagation to close other visible
|
|
76
|
+
// dropdowns and delay click event to control
|
|
77
|
+
// this dropdown visibility
|
|
78
|
+
setTimeout(() => e.stopImmediatePropagation(), 50);
|
|
79
|
+
setTimeout(() => this.showComponentContent(), 100);
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
cancel() {
|
|
83
|
+
if (!this.componentContent) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.expanded = false;
|
|
88
|
+
this.hideComponentContent();
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
showComponentContent() {
|
|
92
|
+
this.componentContent.show();
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
hideComponentContent() {
|
|
96
|
+
this.componentContent.hide();
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
</script>
|