@veritree/ui 0.22.3 → 0.23.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/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
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
<component
|
|
3
3
|
:is="as"
|
|
4
4
|
:id="id"
|
|
5
|
-
:class="
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
:class="[
|
|
6
|
+
headless
|
|
7
|
+
? 'dialog-header'
|
|
8
|
+
: 'mb-8 flex w-full items-center justify-between gap-x-3',
|
|
9
|
+
]"
|
|
9
10
|
>
|
|
10
11
|
<slot></slot>
|
|
11
12
|
</component>
|
|
@@ -13,28 +14,24 @@
|
|
|
13
14
|
|
|
14
15
|
<script>
|
|
15
16
|
export default {
|
|
16
|
-
name:
|
|
17
|
+
name: 'VTDialogHeader',
|
|
17
18
|
|
|
18
|
-
inject: [
|
|
19
|
+
inject: ['apiDialog'],
|
|
19
20
|
|
|
20
21
|
props: {
|
|
22
|
+
headless: {
|
|
23
|
+
type: Boolean,
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
21
26
|
as: {
|
|
22
27
|
type: String,
|
|
23
|
-
default:
|
|
28
|
+
default: 'header',
|
|
24
29
|
},
|
|
25
30
|
},
|
|
26
31
|
|
|
27
32
|
computed: {
|
|
28
|
-
dark() {
|
|
29
|
-
return this.api().isDark;
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
headless() {
|
|
33
|
-
return this.api().isHeadless;
|
|
34
|
-
},
|
|
35
|
-
|
|
36
33
|
id() {
|
|
37
|
-
return
|
|
34
|
+
return `dialog-header-${this.apiDialog().componentId}`;
|
|
38
35
|
},
|
|
39
36
|
},
|
|
40
37
|
|
|
@@ -45,10 +42,10 @@ export default {
|
|
|
45
42
|
methods: {
|
|
46
43
|
// In here because if there is no header, the dialog will not be labelled by
|
|
47
44
|
setDialogLabelledby() {
|
|
48
|
-
const dialog = document.getElementById(this.
|
|
45
|
+
const dialog = document.getElementById(this.apiDialog().id);
|
|
49
46
|
|
|
50
47
|
if (dialog) {
|
|
51
|
-
dialog.setAttribute(
|
|
48
|
+
dialog.setAttribute('aria-labelledby', this.id);
|
|
52
49
|
}
|
|
53
50
|
},
|
|
54
51
|
},
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
<component
|
|
3
3
|
:is="as"
|
|
4
4
|
:id="id"
|
|
5
|
-
:class="
|
|
6
|
-
'Dialog-body': headless,
|
|
7
|
-
'flex-1 w-full h-full overflow-y-auto': !headless,
|
|
8
|
-
}"
|
|
5
|
+
:class="[headless ? 'dialog-body' : 'h-full w-full flex-1 overflow-y-auto']"
|
|
9
6
|
>
|
|
10
7
|
<slot></slot>
|
|
11
8
|
</component>
|
|
@@ -13,28 +10,24 @@
|
|
|
13
10
|
|
|
14
11
|
<script>
|
|
15
12
|
export default {
|
|
16
|
-
name:
|
|
13
|
+
name: 'VTDialogMain',
|
|
17
14
|
|
|
18
|
-
inject: [
|
|
15
|
+
inject: ['apiDialog'],
|
|
19
16
|
|
|
20
17
|
props: {
|
|
18
|
+
headless: {
|
|
19
|
+
type: Boolean,
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
21
22
|
as: {
|
|
22
23
|
type: String,
|
|
23
|
-
default:
|
|
24
|
+
default: 'main',
|
|
24
25
|
},
|
|
25
26
|
},
|
|
26
27
|
|
|
27
28
|
computed: {
|
|
28
|
-
dark() {
|
|
29
|
-
return this.api().isDark;
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
headless() {
|
|
33
|
-
return this.api().isHeadless;
|
|
34
|
-
},
|
|
35
|
-
|
|
36
29
|
id() {
|
|
37
|
-
return
|
|
30
|
+
return `dialog-main-${this.apiDialog().componentId}`;
|
|
38
31
|
},
|
|
39
32
|
},
|
|
40
33
|
|
|
@@ -45,10 +38,10 @@ export default {
|
|
|
45
38
|
methods: {
|
|
46
39
|
// In here because if there is no body, the dialog will not be described by
|
|
47
40
|
setDialogDescribedby() {
|
|
48
|
-
const dialog = document.getElementById(this.
|
|
41
|
+
const dialog = document.getElementById(this.apiDialog().id);
|
|
49
42
|
|
|
50
43
|
if (dialog) {
|
|
51
|
-
dialog.setAttribute(
|
|
44
|
+
dialog.setAttribute('aria-describedby', this.id);
|
|
52
45
|
}
|
|
53
46
|
},
|
|
54
47
|
},
|
|
@@ -3,25 +3,29 @@
|
|
|
3
3
|
<div
|
|
4
4
|
v-if="visible"
|
|
5
5
|
:id="id"
|
|
6
|
-
:class="
|
|
7
|
-
|
|
8
|
-
'fixed inset-0 bg-fd-450/80': !headless,
|
|
9
|
-
}"
|
|
10
|
-
></div>
|
|
6
|
+
:class="[headless ? 'dialog-overlay' : 'bg-primary-200/80 fixed inset-0']"
|
|
7
|
+
/>
|
|
11
8
|
</FadeInOut>
|
|
12
9
|
</template>
|
|
13
10
|
|
|
14
11
|
<script>
|
|
15
|
-
import FadeInOut from
|
|
12
|
+
import FadeInOut from '../Transitions/FadeInOut.vue';
|
|
16
13
|
|
|
17
14
|
export default {
|
|
18
|
-
name:
|
|
15
|
+
name: 'VTDialogOverlay',
|
|
19
16
|
|
|
20
17
|
components: {
|
|
21
18
|
FadeInOut,
|
|
22
19
|
},
|
|
23
20
|
|
|
24
|
-
inject: [
|
|
21
|
+
inject: ['apiDialog'],
|
|
22
|
+
|
|
23
|
+
props: {
|
|
24
|
+
headless: {
|
|
25
|
+
type: Boolean,
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
25
29
|
|
|
26
30
|
data() {
|
|
27
31
|
return {
|
|
@@ -31,21 +35,13 @@ export default {
|
|
|
31
35
|
|
|
32
36
|
computed: {
|
|
33
37
|
id() {
|
|
34
|
-
return `dialog-overlay-${this.
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
dark() {
|
|
38
|
-
return this.api().isDark;
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
headless() {
|
|
42
|
-
return this.api().isHeadless;
|
|
38
|
+
return `dialog-overlay-${this.apiDialog().componentId}`;
|
|
43
39
|
},
|
|
44
40
|
},
|
|
45
41
|
|
|
46
42
|
mounted() {
|
|
47
43
|
this.visible = true;
|
|
48
|
-
this.
|
|
44
|
+
this.apiDialog().registerOverlay(this);
|
|
49
45
|
},
|
|
50
46
|
|
|
51
47
|
methods: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<span
|
|
3
3
|
:id="id"
|
|
4
|
-
:class="
|
|
4
|
+
:class="[headless ? 'dialog-header' : 'text-2xl font-semibold']"
|
|
5
5
|
>
|
|
6
6
|
<slot></slot
|
|
7
7
|
></span>
|
|
@@ -9,17 +9,20 @@
|
|
|
9
9
|
|
|
10
10
|
<script>
|
|
11
11
|
export default {
|
|
12
|
-
name:
|
|
12
|
+
name: 'VTDialogTitle',
|
|
13
13
|
|
|
14
|
-
inject: [
|
|
14
|
+
inject: ['apiDialog'],
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
headless
|
|
18
|
-
|
|
16
|
+
props: {
|
|
17
|
+
headless: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false,
|
|
19
20
|
},
|
|
21
|
+
},
|
|
20
22
|
|
|
23
|
+
computed: {
|
|
21
24
|
id() {
|
|
22
|
-
return
|
|
25
|
+
return `dialog-overlay-${this.apiDialog().componentId}`;
|
|
23
26
|
},
|
|
24
27
|
},
|
|
25
28
|
};
|
|
@@ -56,12 +56,31 @@ export default {
|
|
|
56
56
|
type: Boolean,
|
|
57
57
|
default: false,
|
|
58
58
|
},
|
|
59
|
+
placement: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: 'bottom-start',
|
|
62
|
+
},
|
|
59
63
|
},
|
|
60
64
|
|
|
61
65
|
data() {
|
|
62
66
|
return {
|
|
63
67
|
componentId: genId(),
|
|
64
68
|
items: [],
|
|
69
|
+
/**
|
|
70
|
+
* Explaining the need for the floatingUiMinWidth data
|
|
71
|
+
*
|
|
72
|
+
* The floating ui is a result of two items:
|
|
73
|
+
*
|
|
74
|
+
* 1. Trigger: the action button
|
|
75
|
+
* 2. Content: the popper/wrapper that appears after triggering the action button
|
|
76
|
+
*
|
|
77
|
+
* By default, the content will match the triggers width.
|
|
78
|
+
* The problem with this is that the trigger width
|
|
79
|
+
* might be too small causing the content to not fit
|
|
80
|
+
* what is inside it properly. So, to avoid this,
|
|
81
|
+
* a min width is needed.
|
|
82
|
+
*/
|
|
83
|
+
floatingUiMinWidth: 200,
|
|
65
84
|
};
|
|
66
85
|
},
|
|
67
86
|
|
|
@@ -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="dropdown"
|
|
8
8
|
>
|
|
9
9
|
<slot></slot>
|
|
10
10
|
</FloatingUi>
|
|
@@ -16,6 +16,8 @@ import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
|
|
|
16
16
|
export default {
|
|
17
17
|
name: 'VTDropdownMenuContent',
|
|
18
18
|
|
|
19
|
+
inheritAttrs: false,
|
|
20
|
+
|
|
19
21
|
mixins: [floatingUiContentMixin],
|
|
20
22
|
|
|
21
23
|
inject: ['apiDropdownMenu'],
|
|
@@ -39,9 +41,6 @@ export default {
|
|
|
39
41
|
id: this.id,
|
|
40
42
|
hide: this.hide,
|
|
41
43
|
show: this.show,
|
|
42
|
-
getMousemove: this.getMousemove,
|
|
43
|
-
setMousemove: this.setMousemove,
|
|
44
|
-
unsetMousemove: this.unsetMousemove,
|
|
45
44
|
setActiveDescedant: this.setActiveDescedant,
|
|
46
45
|
};
|
|
47
46
|
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
class="-mx-3"
|
|
10
10
|
role="menuitem"
|
|
11
11
|
@click.stop.prevent="onClick"
|
|
12
|
-
@keydown.down.prevent="
|
|
13
|
-
@keydown.up.prevent="
|
|
12
|
+
@keydown.down.prevent="focusNextItem"
|
|
13
|
+
@keydown.up.prevent="focusPreviousItem"
|
|
14
14
|
@keydown.home.prevent="focusFirstItem"
|
|
15
15
|
@keydown.end.prevent="focusLastItem"
|
|
16
16
|
@keydown.esc.prevent="onKeyEsc"
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
:class="[
|
|
4
|
-
headless ? 'form-feedback' : '
|
|
4
|
+
headless ? 'form-feedback' : 'mt-1 flex items-baseline gap-2',
|
|
5
5
|
// variant styles
|
|
6
|
-
headless ? `form-feedback
|
|
6
|
+
headless ? `form-feedback--${variant}` : null,
|
|
7
7
|
]"
|
|
8
8
|
>
|
|
9
9
|
<component
|
|
10
10
|
v-if="showIcon"
|
|
11
11
|
:is="icon"
|
|
12
12
|
:class="[
|
|
13
|
-
headless ? 'form-feedback__icon' : 'relative top-1
|
|
13
|
+
headless ? 'form-feedback__icon' : 'relative top-1 h-4 w-4 shrink-0',
|
|
14
14
|
// variant styles
|
|
15
15
|
headless
|
|
16
|
-
? `form-feedback__icon
|
|
16
|
+
? `form-feedback__icon--${variant}`
|
|
17
17
|
: isError
|
|
18
18
|
? 'text-error-500'
|
|
19
19
|
: null,
|
|
20
20
|
]"
|
|
21
21
|
/>
|
|
22
|
-
<span :class="[headless ? 'form-feedback--text' : 'text-gray-500
|
|
22
|
+
<span :class="[headless ? 'form-feedback--text' : 'text-sm text-gray-500']">
|
|
23
23
|
<slot />
|
|
24
24
|
</span>
|
|
25
25
|
</div>
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
</template>
|
|
9
9
|
|
|
10
10
|
<script>
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
formControlMixin,
|
|
13
|
+
formControlStyleMixin,
|
|
14
|
+
} from '../../../mixins/form-control';
|
|
12
15
|
|
|
13
16
|
export default {
|
|
14
|
-
mixins: [formControlMixin],
|
|
17
|
+
mixins: [formControlMixin, formControlStyleMixin],
|
|
15
18
|
|
|
16
19
|
data() {
|
|
17
20
|
return {
|
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
</template>
|
|
9
9
|
|
|
10
10
|
<script>
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
formControlMixin,
|
|
13
|
+
formControlStyleMixin,
|
|
14
|
+
} from '../../../mixins/form-control';
|
|
12
15
|
|
|
13
16
|
export default {
|
|
14
|
-
mixins: [formControlMixin],
|
|
17
|
+
mixins: [formControlMixin, formControlStyleMixin],
|
|
15
18
|
|
|
16
19
|
data() {
|
|
17
20
|
return {
|
|
@@ -28,14 +28,9 @@ export default {
|
|
|
28
28
|
|
|
29
29
|
const registerSearch = (search) => {
|
|
30
30
|
if (!search) return;
|
|
31
|
-
this.
|
|
31
|
+
this.componentSearch = search;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
// const registerList = (list) => {
|
|
35
|
-
// if (!list) return;
|
|
36
|
-
// this.list = list;
|
|
37
|
-
// };
|
|
38
|
-
|
|
39
34
|
const registerItem = (item) => {
|
|
40
35
|
if (!item) return;
|
|
41
36
|
this.items.push(item);
|
|
@@ -46,6 +41,16 @@ export default {
|
|
|
46
41
|
this.items.splice(index, 1);
|
|
47
42
|
};
|
|
48
43
|
|
|
44
|
+
const pushSearch = (key) => {
|
|
45
|
+
key === 'Backspace' && this.search.length
|
|
46
|
+
? (this.search = this.search.slice(0, -1))
|
|
47
|
+
: (this.search += key);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const clearSearch = () => {
|
|
51
|
+
this.search = '';
|
|
52
|
+
};
|
|
53
|
+
|
|
49
54
|
const emit = (value) => {
|
|
50
55
|
this.$emit('input', value);
|
|
51
56
|
this.$emit('change', value);
|
|
@@ -56,14 +61,17 @@ export default {
|
|
|
56
61
|
component: this.component,
|
|
57
62
|
componentTrigger: this.componentTrigger,
|
|
58
63
|
componentContent: this.componentContent,
|
|
64
|
+
componentSearch: this.componentSearch,
|
|
59
65
|
items: this.items,
|
|
60
66
|
search: this.search,
|
|
67
|
+
selectedValue: this.value,
|
|
61
68
|
registerTrigger,
|
|
62
69
|
registerContent,
|
|
63
70
|
registerSearch,
|
|
64
|
-
// registerList,
|
|
65
71
|
registerItem,
|
|
66
72
|
unregisterItem,
|
|
73
|
+
pushSearch,
|
|
74
|
+
clearSearch,
|
|
67
75
|
emit,
|
|
68
76
|
};
|
|
69
77
|
},
|
|
@@ -79,9 +87,9 @@ export default {
|
|
|
79
87
|
type: Boolean,
|
|
80
88
|
default: false,
|
|
81
89
|
},
|
|
82
|
-
|
|
83
|
-
type:
|
|
84
|
-
default:
|
|
90
|
+
placement: {
|
|
91
|
+
type: String,
|
|
92
|
+
default: 'bottom-start',
|
|
85
93
|
},
|
|
86
94
|
right: {
|
|
87
95
|
type: Boolean,
|
|
@@ -92,8 +100,24 @@ export default {
|
|
|
92
100
|
data() {
|
|
93
101
|
return {
|
|
94
102
|
componentId: genId(),
|
|
95
|
-
|
|
103
|
+
componentSearch: null,
|
|
104
|
+
search: '',
|
|
96
105
|
items: [],
|
|
106
|
+
/**
|
|
107
|
+
* Explaining the need for the floatingUiMinWidth data
|
|
108
|
+
*
|
|
109
|
+
* The floating ui is a result of two items:
|
|
110
|
+
*
|
|
111
|
+
* 1. Trigger: the action button
|
|
112
|
+
* 2. Content: the popper/wrapper that appears after triggering the action button
|
|
113
|
+
*
|
|
114
|
+
* By default, the content will match the triggers width.
|
|
115
|
+
* The problem with this is that the trigger width
|
|
116
|
+
* might be too small causing the content to not fit
|
|
117
|
+
* what is inside it properly. So, to avoid this,
|
|
118
|
+
* a min width is needed.
|
|
119
|
+
*/
|
|
120
|
+
floatingUiMinWidth: 200,
|
|
97
121
|
};
|
|
98
122
|
},
|
|
99
123
|
|
|
@@ -1,11 +1,11 @@
|
|
|
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
|
+
:portal-class="$vnode.data.staticClass"
|
|
8
|
+
component="listbox"
|
|
9
9
|
role="listbox"
|
|
10
10
|
>
|
|
11
11
|
<slot></slot>
|
|
@@ -47,9 +47,6 @@ export default {
|
|
|
47
47
|
id: this.id,
|
|
48
48
|
show: this.show,
|
|
49
49
|
hide: this.hide,
|
|
50
|
-
getMousemove: this.getMousemove,
|
|
51
|
-
setMousemove: this.setMousemove,
|
|
52
|
-
unsetMousemove: this.unsetMousemove,
|
|
53
50
|
setActiveDescedant: this.setActiveDescedant,
|
|
54
51
|
};
|
|
55
52
|
|
|
@@ -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
|
};
|