@veritree/ui 0.19.2-20 → 0.19.2-22
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/Alert/VTAlert.vue +55 -14
- package/src/components/Dialog/VTDialog.vue +13 -11
- package/src/components/Dialog/VTDialogClose.vue +13 -19
- package/src/components/Dialog/VTDialogContent.vue +19 -14
- package/src/components/Dialog/VTDialogFooter.vue +9 -14
- package/src/components/Dialog/VTDialogHeader.vue +11 -13
- package/src/components/Dialog/VTDialogMain.vue +6 -13
- package/src/components/Dialog/VTDialogOverlay.vue +9 -13
- package/src/components/Dialog/VTDialogTitle.vue +8 -5
- package/src/components/Disclosure/VTDisclosureHeader.vue +1 -1
- package/src/components/DropdownMenu/VTDropdownMenu.vue +19 -0
- package/src/components/DropdownMenu/VTDropdownMenuContent.vue +14 -6
- package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +7 -16
- package/src/components/DropdownMenu/VTDropdownMenuItem.vue +3 -7
- package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +1 -10
- package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +4 -9
- 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 +60 -53
- package/src/components/Listbox/VTListboxTrigger.vue +12 -6
- package/src/components/Popover/VTPopover.vue +19 -0
- 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 +13 -11
- 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/Utils/FloatingUi.vue +52 -17
- 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
|
@@ -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,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) {
|
|
@@ -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
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
]"
|
|
8
8
|
role="progressbar"
|
|
9
9
|
aria-valuemin="0"
|
|
10
|
-
aria-valuemax="
|
|
10
|
+
:aria-valuemax="max"
|
|
11
11
|
:aria-valuenow="value"
|
|
12
12
|
:aria-label="label"
|
|
13
13
|
>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
:class="[
|
|
16
16
|
headless
|
|
17
17
|
? 'progress-bar__indicator'
|
|
18
|
-
: 'absolute left-0 h-full
|
|
18
|
+
: 'bg-secondary-200 absolute left-0 h-full transition-all',
|
|
19
19
|
]"
|
|
20
|
-
:style="{ width: `${
|
|
20
|
+
:style="{ width: `${percentageComputed}%` }"
|
|
21
21
|
></div>
|
|
22
22
|
</div>
|
|
23
23
|
</template>
|
|
@@ -37,6 +37,24 @@ export default {
|
|
|
37
37
|
type: [String, Number],
|
|
38
38
|
default: 0,
|
|
39
39
|
},
|
|
40
|
+
max: {
|
|
41
|
+
type: [String, Number],
|
|
42
|
+
default: 100,
|
|
43
|
+
},
|
|
44
|
+
percentage: {
|
|
45
|
+
type: [String, Number],
|
|
46
|
+
default: null,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
computed: {
|
|
51
|
+
percentageComputed() {
|
|
52
|
+
if (typeof this.percentage !== 'undefined' && this.percentage !== null) {
|
|
53
|
+
return this.percentage;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (this.value / this.max) * 100;
|
|
57
|
+
},
|
|
40
58
|
},
|
|
41
59
|
};
|
|
42
60
|
</script>
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
]"
|
|
19
19
|
role="tab"
|
|
20
20
|
type="button"
|
|
21
|
-
@click.stop="onClick"
|
|
21
|
+
@click.prevent.stop="onClick"
|
|
22
22
|
@keydown="onKeyDown"
|
|
23
23
|
@blur="onBlur"
|
|
24
24
|
>
|
|
25
|
-
<slot></slot>
|
|
25
|
+
<slot :selected="selected"></slot>
|
|
26
26
|
</button>
|
|
27
27
|
</template>
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ import { keys } from '../../utils/keyboard';
|
|
|
32
32
|
export default {
|
|
33
33
|
name: 'VTTabItem',
|
|
34
34
|
|
|
35
|
-
inject: ['
|
|
35
|
+
inject: ['api'],
|
|
36
36
|
|
|
37
37
|
props: {
|
|
38
38
|
headless: {
|
|
@@ -43,6 +43,7 @@ export default {
|
|
|
43
43
|
|
|
44
44
|
data() {
|
|
45
45
|
return {
|
|
46
|
+
api: this.api(),
|
|
46
47
|
index: null,
|
|
47
48
|
indexFocus: null,
|
|
48
49
|
selected: false,
|
|
@@ -56,11 +57,11 @@ export default {
|
|
|
56
57
|
},
|
|
57
58
|
|
|
58
59
|
mounted() {
|
|
59
|
-
this.
|
|
60
|
+
this.api.registerTab(this);
|
|
60
61
|
},
|
|
61
62
|
|
|
62
63
|
beforeUnmount() {
|
|
63
|
-
this.
|
|
64
|
+
this.api.unregisterTab(this.index);
|
|
64
65
|
},
|
|
65
66
|
|
|
66
67
|
methods: {
|
|
@@ -77,11 +78,11 @@ export default {
|
|
|
77
78
|
},
|
|
78
79
|
|
|
79
80
|
focusFirstTab() {
|
|
80
|
-
this.
|
|
81
|
+
this.api.focusTab(0);
|
|
81
82
|
},
|
|
82
83
|
|
|
83
84
|
focusLastTab(lastTabIndex) {
|
|
84
|
-
this.
|
|
85
|
+
this.api.focusTab(lastTabIndex);
|
|
85
86
|
},
|
|
86
87
|
|
|
87
88
|
focusPreviousTab(lastTabIndex) {
|
|
@@ -90,7 +91,7 @@ export default {
|
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
this.
|
|
94
|
+
this.api.focusTab(this.indexFocus - 1);
|
|
94
95
|
},
|
|
95
96
|
|
|
96
97
|
focusNextTab(lastTabIndex) {
|
|
@@ -99,17 +100,18 @@ export default {
|
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
this.
|
|
103
|
+
this.api.focusTab(this.indexFocus + 1);
|
|
103
104
|
},
|
|
104
105
|
|
|
105
106
|
onClick() {
|
|
106
|
-
this.
|
|
107
|
+
this.api.selectTab(this.index);
|
|
107
108
|
this.$emit('change');
|
|
109
|
+
this.$emit('click');
|
|
108
110
|
},
|
|
109
111
|
|
|
110
112
|
onKeyDown(event) {
|
|
111
113
|
const key = event.key;
|
|
112
|
-
const lastTabIndex = this.
|
|
114
|
+
const lastTabIndex = this.api.getTabsLength() - 1;
|
|
113
115
|
|
|
114
116
|
if (this.indexFocus === null) {
|
|
115
117
|
this.indexFocus = this.index;
|