@veritree/ui 0.21.0 → 0.21.1-1
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.js +68 -0
- package/package.json +2 -1
- package/src/components/Avatar/VTAvatar.vue +32 -29
- package/src/components/Button/VTButton.vue +1 -1
- package/src/components/DropdownMenu/VTDropdownMenu.vue +23 -14
- package/src/components/DropdownMenu/VTDropdownMenuContent.vue +46 -41
- package/src/components/DropdownMenu/VTDropdownMenuItem.vue +33 -12
- package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +70 -62
- package/src/components/Listbox/VTListbox.vue +22 -36
- package/src/components/Listbox/VTListboxContent.vue +38 -65
- package/src/components/Listbox/VTListboxItem.vue +12 -12
- package/src/components/Listbox/VTListboxSearch.vue +4 -8
- package/src/components/Listbox/VTListboxTrigger.vue +40 -18
- package/src/components/Popover/VTPopover.vue +23 -16
- package/src/components/Popover/VTPopoverContent.vue +52 -36
- package/src/components/Popover/VTPopoverTrigger.vue +16 -8
- package/src/components/Utils/FloatingUi.vue +56 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { computePosition, flip, shift, offset, size } from '@floating-ui/dom';
|
|
2
|
+
|
|
3
|
+
export const floatingUiMixin = {
|
|
4
|
+
props: {
|
|
5
|
+
placement: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: 'bottom-start'
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
data() {
|
|
12
|
+
return {
|
|
13
|
+
component: null,
|
|
14
|
+
componentTrigger: null,
|
|
15
|
+
componentContent: null,
|
|
16
|
+
active: false,
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
watch: {
|
|
21
|
+
active(newVal) {
|
|
22
|
+
if (newVal) this.$nextTick(() => this.positionContentToTrigger());
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
mounted() {
|
|
27
|
+
this.component = {
|
|
28
|
+
setActive: this.setActive,
|
|
29
|
+
clearActive: this.clearActive,
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
methods: {
|
|
34
|
+
setActive() {
|
|
35
|
+
this.active = true;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
clearActive() {
|
|
39
|
+
this.active = null;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
positionContentToTrigger() {
|
|
43
|
+
const trigger = document.getElementById(this.componentTrigger.id);
|
|
44
|
+
const content = document.getElementById(this.componentContent.id);
|
|
45
|
+
|
|
46
|
+
computePosition(trigger, content, {
|
|
47
|
+
placement: this.placement,
|
|
48
|
+
middleware: [
|
|
49
|
+
offset(6),
|
|
50
|
+
flip(),
|
|
51
|
+
shift({ padding: 5 }),
|
|
52
|
+
size({
|
|
53
|
+
apply({ rects }) {
|
|
54
|
+
Object.assign(content.style, {
|
|
55
|
+
width: `${rects.reference.width}px`,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
}).then(({ x, y }) => {
|
|
61
|
+
Object.assign(content.style, {
|
|
62
|
+
left: `${x}px`,
|
|
63
|
+
top: `${y}px`,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veritree/ui",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.1-1",
|
|
4
4
|
"description": "veritree ui library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@floating-ui/dom": "^1.0.4",
|
|
14
15
|
"@linusborg/vue-simple-portal": "^0.1.5",
|
|
15
16
|
"@veritree/icons": "^0.19.0"
|
|
16
17
|
},
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
:class="
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
:class="[
|
|
4
|
+
// default styles
|
|
5
|
+
headless
|
|
6
|
+
? 'avatar'
|
|
7
|
+
: 'flex items-center justify-center overflow-hidden rounded-full bg-white border border-solid',
|
|
8
|
+
// variant styles
|
|
9
|
+
headless ? `avatar--${variant}` : null,
|
|
10
|
+
// sizes styles
|
|
11
|
+
headless
|
|
12
|
+
? `avatar--${size}`
|
|
13
|
+
: isSmall
|
|
14
|
+
? 'h-8 w-8'
|
|
15
|
+
: isLarge
|
|
16
|
+
? 'h-10 w-10'
|
|
17
|
+
: null,
|
|
18
|
+
]"
|
|
12
19
|
>
|
|
13
20
|
<slot></slot>
|
|
14
21
|
</div>
|
|
@@ -18,32 +25,28 @@
|
|
|
18
25
|
export default {
|
|
19
26
|
name: 'VTAvatar',
|
|
20
27
|
|
|
21
|
-
provide() {
|
|
22
|
-
return {
|
|
23
|
-
api: () => {
|
|
24
|
-
const { dark: isDark, headless: isHeadless, light: isLight } = this;
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
isDark,
|
|
28
|
-
isHeadless,
|
|
29
|
-
isLight,
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
|
|
35
28
|
props: {
|
|
36
29
|
headless: {
|
|
37
30
|
type: Boolean,
|
|
38
31
|
default: false,
|
|
39
32
|
},
|
|
40
|
-
|
|
41
|
-
type:
|
|
42
|
-
default:
|
|
33
|
+
variant: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: 'primary',
|
|
43
36
|
},
|
|
44
|
-
|
|
45
|
-
type:
|
|
46
|
-
default:
|
|
37
|
+
size: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: 'large',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
computed: {
|
|
44
|
+
isLarge() {
|
|
45
|
+
return this.size === 'large';
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
isSmall() {
|
|
49
|
+
return this.size === 'small';
|
|
47
50
|
},
|
|
48
51
|
},
|
|
49
52
|
};
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
? 'button'
|
|
12
12
|
: isIcon
|
|
13
13
|
? 'inline-flex items-center justify-center rounded-full [&_svg]:max-h-full [&_svg]:max-w-full'
|
|
14
|
-
: '
|
|
14
|
+
: 'inline-flex rounded border border-solid px-4 text-sm font-semibold leading-none no-underline transition-all',
|
|
15
15
|
// variant styles
|
|
16
16
|
headless
|
|
17
17
|
? `button--${variant}`
|
|
@@ -5,26 +5,31 @@
|
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script>
|
|
8
|
-
import {
|
|
8
|
+
import { floatingUiMixin } from '../../../mixins/floating-ui';
|
|
9
|
+
import { genId } from '../../utils/ids';
|
|
9
10
|
|
|
10
11
|
export default {
|
|
11
|
-
name:
|
|
12
|
+
name: 'VTDropdownMenu',
|
|
13
|
+
|
|
14
|
+
mixins: [floatingUiMixin],
|
|
12
15
|
|
|
13
16
|
provide() {
|
|
14
17
|
return {
|
|
15
18
|
api: () => {
|
|
16
|
-
const { dark: isDark, headless: isHeadless
|
|
17
|
-
const { id, trigger, content, items } = this;
|
|
19
|
+
const { dark: isDark, headless: isHeadless } = this;
|
|
18
20
|
|
|
19
21
|
const registerTrigger = (trigger) => {
|
|
20
|
-
|
|
22
|
+
if (!trigger) return;
|
|
23
|
+
this.componentTrigger = trigger;
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
const registerContent = (content) => {
|
|
24
|
-
|
|
27
|
+
if (!content) return;
|
|
28
|
+
this.componentContent = content;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
const registerItem = (item) => {
|
|
32
|
+
if (!item) return;
|
|
28
33
|
this.items.push(item);
|
|
29
34
|
};
|
|
30
35
|
|
|
@@ -33,13 +38,13 @@ export default {
|
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
return {
|
|
36
|
-
id,
|
|
41
|
+
id: this.componentId,
|
|
37
42
|
isDark,
|
|
38
43
|
isHeadless,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
items,
|
|
44
|
+
component: this.component,
|
|
45
|
+
componentTrigger: this.componentTrigger,
|
|
46
|
+
componentContent: this.componentContent,
|
|
47
|
+
items: this.items,
|
|
43
48
|
registerTrigger,
|
|
44
49
|
registerContent,
|
|
45
50
|
registerItem,
|
|
@@ -66,11 +71,15 @@ export default {
|
|
|
66
71
|
|
|
67
72
|
data() {
|
|
68
73
|
return {
|
|
69
|
-
|
|
70
|
-
trigger: null,
|
|
71
|
-
content: null,
|
|
74
|
+
componentId: genId(),
|
|
72
75
|
items: [],
|
|
73
76
|
};
|
|
74
77
|
},
|
|
78
|
+
|
|
79
|
+
computed: {
|
|
80
|
+
id() {
|
|
81
|
+
return `dropdown-menu-${this.componentId}`;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
75
84
|
};
|
|
76
85
|
</script>
|
|
@@ -1,47 +1,37 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
leave-class="translate-y-0 opacity-100"
|
|
8
|
-
leave-to-class="translate-y-[15px] opacity-0"
|
|
9
|
-
@after-leave="hide"
|
|
2
|
+
<FloatingUi
|
|
3
|
+
:visible="visible"
|
|
4
|
+
:id="id"
|
|
5
|
+
:headless="headless"
|
|
6
|
+
:class="{ 'dropdown-menu-content': headless }"
|
|
10
7
|
>
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
:id="id"
|
|
14
|
-
:class="{
|
|
15
|
-
MenuList: headless,
|
|
16
|
-
'absolute top-full mt-3 grid min-w-[222px] overflow-hidden rounded border border-solid py-2 px-3 transition-all':
|
|
17
|
-
!headless,
|
|
18
|
-
'border-gray-100 bg-white shadow-300': !dark,
|
|
19
|
-
'bg-forest-default border-gray-700 shadow-gray-700': dark,
|
|
20
|
-
'left-0': !right,
|
|
21
|
-
'right-0': right,
|
|
22
|
-
}"
|
|
23
|
-
>
|
|
24
|
-
<slot></slot>
|
|
25
|
-
</div>
|
|
26
|
-
</transition>
|
|
8
|
+
<slot></slot>
|
|
9
|
+
</FloatingUi>
|
|
27
10
|
</template>
|
|
28
11
|
|
|
29
12
|
<script>
|
|
30
|
-
import
|
|
13
|
+
import FloatingUi from '../Utils/FloatingUi.vue';
|
|
31
14
|
|
|
32
15
|
export default {
|
|
33
16
|
name: 'VTDropdownMenuContent',
|
|
34
17
|
|
|
18
|
+
components: {
|
|
19
|
+
FloatingUi,
|
|
20
|
+
},
|
|
21
|
+
|
|
35
22
|
inject: ['api'],
|
|
36
23
|
|
|
37
24
|
data() {
|
|
38
25
|
return {
|
|
39
|
-
id: `menucontent-${genId()}`,
|
|
40
26
|
visible: false,
|
|
41
27
|
};
|
|
42
28
|
},
|
|
43
29
|
|
|
44
30
|
computed: {
|
|
31
|
+
id() {
|
|
32
|
+
return `dropdown-menu-content-${this.api().id}`;
|
|
33
|
+
},
|
|
34
|
+
|
|
45
35
|
dark() {
|
|
46
36
|
return this.api().isDark;
|
|
47
37
|
},
|
|
@@ -50,44 +40,59 @@ export default {
|
|
|
50
40
|
return this.api().isHeadless;
|
|
51
41
|
},
|
|
52
42
|
|
|
53
|
-
|
|
54
|
-
return this.api().
|
|
43
|
+
component() {
|
|
44
|
+
return this.api().component;
|
|
55
45
|
},
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
return this.api().
|
|
47
|
+
componentTrigger() {
|
|
48
|
+
return this.api().componentTrigger;
|
|
59
49
|
},
|
|
60
50
|
},
|
|
61
51
|
|
|
62
52
|
mounted() {
|
|
63
53
|
this.api().registerContent(this);
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
if (this.trigger) this.trigger.toggleHasPopup();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// TODO: Create a directive or mixin for this
|
|
55
|
+
// T-218 Create a directive or mixin for this
|
|
70
56
|
document.addEventListener('click', (e) => {
|
|
57
|
+
if (!e) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
71
61
|
e.stopPropagation();
|
|
72
|
-
|
|
62
|
+
|
|
63
|
+
if (this.visible && !this.$el.contains(e.target)) {
|
|
64
|
+
this.componentTrigger.onClick();
|
|
65
|
+
}
|
|
73
66
|
});
|
|
74
67
|
},
|
|
75
68
|
|
|
76
69
|
destroyed() {
|
|
77
|
-
|
|
78
|
-
document.removeEventListener('click', this.trigger.onClick());
|
|
70
|
+
document.removeEventListener('click', this.componentTrigger.onClick);
|
|
79
71
|
},
|
|
80
72
|
|
|
81
73
|
methods: {
|
|
82
74
|
show() {
|
|
75
|
+
if (this.visible) return;
|
|
76
|
+
|
|
83
77
|
this.visible = true;
|
|
84
|
-
|
|
78
|
+
|
|
79
|
+
this.$nextTick(() => {
|
|
80
|
+
this.component.setActive();
|
|
81
|
+
this.$emit('shown');
|
|
82
|
+
});
|
|
85
83
|
},
|
|
86
84
|
|
|
87
85
|
hide() {
|
|
86
|
+
if (!this.visible) return;
|
|
87
|
+
|
|
88
88
|
this.visible = false;
|
|
89
|
-
|
|
90
|
-
this
|
|
89
|
+
|
|
90
|
+
this.$nextTick(() => {
|
|
91
|
+
this.componentTrigger.focus();
|
|
92
|
+
this.componentTrigger.hideExpanded();
|
|
93
|
+
this.component.clearActive();
|
|
94
|
+
this.$emit('hidden');
|
|
95
|
+
});
|
|
91
96
|
},
|
|
92
97
|
},
|
|
93
98
|
};
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
:to="to"
|
|
6
6
|
:class="{
|
|
7
7
|
MenuItem: headless,
|
|
8
|
-
'-mx-3 flex min-w-max items-center gap-
|
|
8
|
+
'-mx-3 flex min-w-max items-center gap-2 px-3 py-2 text-inherit no-underline':
|
|
9
9
|
!headless,
|
|
10
|
-
'hover:bg-secondary-200/10': !dark,
|
|
11
|
-
'text-white
|
|
12
|
-
'pointer-events-none opacity-75': disabled,
|
|
10
|
+
'hover:bg-secondary-200/10': !dark && !headless,
|
|
11
|
+
'text-white': dark && !headless,
|
|
12
|
+
'pointer-events-none opacity-75': disabled && !headless,
|
|
13
|
+
'bg-secondary-200/10': selected && !headless,
|
|
13
14
|
}"
|
|
14
15
|
:tabindex="tabIndex"
|
|
15
16
|
:aria-disabled="disabled"
|
|
@@ -52,13 +53,17 @@ export default {
|
|
|
52
53
|
|
|
53
54
|
data() {
|
|
54
55
|
return {
|
|
55
|
-
id: `menuitem-${genId()}`,
|
|
56
56
|
index: null,
|
|
57
|
+
selected: false,
|
|
57
58
|
tabIndex: 0,
|
|
58
59
|
};
|
|
59
60
|
},
|
|
60
61
|
|
|
61
62
|
computed: {
|
|
63
|
+
id() {
|
|
64
|
+
return `dropdown-menu-item-${this.api().id}-${genId()}`;
|
|
65
|
+
},
|
|
66
|
+
|
|
62
67
|
dark() {
|
|
63
68
|
return this.api().isDark;
|
|
64
69
|
},
|
|
@@ -79,19 +84,20 @@ export default {
|
|
|
79
84
|
return this.$el;
|
|
80
85
|
},
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
return this.api().
|
|
87
|
+
componentTrigger() {
|
|
88
|
+
return this.api().componentTrigger;
|
|
84
89
|
},
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
return this.api().
|
|
91
|
+
componentContent() {
|
|
92
|
+
return this.api().componentContent;
|
|
88
93
|
},
|
|
89
94
|
},
|
|
90
95
|
|
|
91
96
|
mounted() {
|
|
92
97
|
const item = {
|
|
98
|
+
select: this.select,
|
|
99
|
+
unselect: this.unselect,
|
|
93
100
|
focus: this.focus,
|
|
94
|
-
el: this.el,
|
|
95
101
|
};
|
|
96
102
|
|
|
97
103
|
this.api().registerItem(item);
|
|
@@ -100,10 +106,19 @@ export default {
|
|
|
100
106
|
},
|
|
101
107
|
|
|
102
108
|
methods: {
|
|
109
|
+
select() {
|
|
110
|
+
this.selected = true;
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
unselect() {
|
|
114
|
+
this.selected = false;
|
|
115
|
+
},
|
|
116
|
+
|
|
103
117
|
focus() {
|
|
104
118
|
if (!this.el) return;
|
|
105
119
|
|
|
106
120
|
this.tabIndex = -1;
|
|
121
|
+
this.selected = true;
|
|
107
122
|
this.el.focus();
|
|
108
123
|
},
|
|
109
124
|
|
|
@@ -145,6 +160,12 @@ export default {
|
|
|
145
160
|
*/
|
|
146
161
|
setFocusToItem(goToIndex) {
|
|
147
162
|
this.tabIndex = 0;
|
|
163
|
+
this.selected = false;
|
|
164
|
+
|
|
165
|
+
// set all selected to false
|
|
166
|
+
this.items.forEach((item) => item.unselect());
|
|
167
|
+
|
|
168
|
+
// focus item
|
|
148
169
|
this.items[goToIndex].focus();
|
|
149
170
|
},
|
|
150
171
|
|
|
@@ -152,8 +173,8 @@ export default {
|
|
|
152
173
|
* Hides content/menu and focus on trigger
|
|
153
174
|
*/
|
|
154
175
|
leaveMenu() {
|
|
155
|
-
if (this.
|
|
156
|
-
if (this.
|
|
176
|
+
if (this.componentContent) this.componentContent.hide();
|
|
177
|
+
if (this.componentTrigger) this.componentTrigger.focus();
|
|
157
178
|
},
|
|
158
179
|
|
|
159
180
|
onKeyEsc() {
|