@veritree/ui 0.19.2-16 → 0.19.2-17

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.
Files changed (27) hide show
  1. package/mixins/floating-ui-content.js +102 -0
  2. package/mixins/floating-ui-item.js +219 -0
  3. package/mixins/floating-ui.js +74 -0
  4. package/package.json +6 -2
  5. package/src/components/Dialog/VTDialog.vue +3 -0
  6. package/src/components/Dialog/VTDialogOverlay.vue +1 -1
  7. package/src/components/DropdownMenu/VTDropdownMenu.vue +24 -26
  8. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +27 -70
  9. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +5 -2
  10. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +8 -128
  11. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +3 -3
  12. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +99 -114
  13. package/src/components/Form/VTFormLabel.vue +3 -1
  14. package/src/components/Form/VTFormRow.vue +5 -0
  15. package/src/components/Listbox/VTListbox.vue +22 -41
  16. package/src/components/Listbox/VTListboxContent.vue +22 -114
  17. package/src/components/Listbox/VTListboxItem.vue +10 -183
  18. package/src/components/Listbox/VTListboxLabel.vue +0 -10
  19. package/src/components/Listbox/VTListboxList.vue +22 -31
  20. package/src/components/Listbox/VTListboxSearch.vue +28 -29
  21. package/src/components/Listbox/VTListboxTrigger.vue +69 -86
  22. package/src/components/Popover/VTPopover.vue +21 -27
  23. package/src/components/Popover/VTPopoverContent.vue +23 -58
  24. package/src/components/Popover/VTPopoverDivider.vue +4 -11
  25. package/src/components/Popover/VTPopoverItem.vue +13 -10
  26. package/src/components/Popover/VTPopoverTrigger.vue +120 -20
  27. package/src/components/Utils/FloatingUi.vue +58 -0
@@ -0,0 +1,102 @@
1
+ import FloatingUi from '../src/components/Utils/FloatingUi.vue';
2
+
3
+ export const floatingUiContentMixin = {
4
+ components: {
5
+ FloatingUi,
6
+ },
7
+
8
+ props: {
9
+ headless: {
10
+ type: Boolean,
11
+ default: false,
12
+ },
13
+ floatingUiClass: {
14
+ type: [String, Function],
15
+ default: null,
16
+ },
17
+ },
18
+
19
+ data() {
20
+ return {
21
+ el: null,
22
+ isMousemove: false,
23
+ visible: false,
24
+ };
25
+ },
26
+
27
+ mounted() {
28
+ document.addEventListener('click', (e) => this.onDocumentClick(e));
29
+ },
30
+
31
+ destroyed() {
32
+ document.removeEventListener('click', this.onDocumentClick);
33
+ },
34
+
35
+ methods: {
36
+ show() {
37
+ if (this.visible) {
38
+ return;
39
+ }
40
+
41
+ this.visible = true;
42
+
43
+ this.$nextTick(() => {
44
+ this.component.setActive();
45
+
46
+ setTimeout(() => {
47
+ this.el = document.getElementById(this.id);
48
+ this.$emit('shown');
49
+ }, 100);
50
+ });
51
+ },
52
+
53
+ hide() {
54
+ if (!this.visible) {
55
+ return;
56
+ }
57
+
58
+ this.visible = false;
59
+
60
+ this.$nextTick(() => {
61
+ this.component.clearActive();
62
+
63
+ setTimeout(() => {
64
+ this.el = document.getElementById(this.id);
65
+ this.$emit('hidden');
66
+ }, 100);
67
+ });
68
+ },
69
+
70
+ onDocumentClick(e) {
71
+ if (!e) {
72
+ return;
73
+ }
74
+
75
+ e.stopPropagation();
76
+
77
+ if (this.visible && !this.el.contains(e.target)) {
78
+ this.componentTrigger.cancel();
79
+ }
80
+ },
81
+
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
+ setActiveDescedant(id) {
99
+ this.activeDescedant = id;
100
+ },
101
+ },
102
+ }
@@ -0,0 +1,219 @@
1
+ export const floatingUiItemMixin = {
2
+ props: {
3
+ headless: {
4
+ type: Boolean,
5
+ default: false,
6
+ },
7
+ disabled: {
8
+ type: Boolean,
9
+ default: false,
10
+ },
11
+ },
12
+
13
+ computed: {
14
+ id() {
15
+ return `${this.componentName}-${this.apiInjected().id}-${this.index}`;
16
+ },
17
+
18
+ items() {
19
+ return this.apiInjected().items;
20
+ },
21
+
22
+ el() {
23
+ return this.$el;
24
+ },
25
+
26
+ componentContent() {
27
+ return this.apiInjected().componentContent;
28
+ },
29
+
30
+ componentTrigger() {
31
+ return this.apiInjected().componentTrigger;
32
+ },
33
+
34
+ classComputed() {
35
+ return [
36
+ // default styles
37
+ this.headless
38
+ ? `${this.componentName}`
39
+ : 'relative z-10 flex items-center gap-2 px-3 py-2 text-inherit no-underline',
40
+ // disabled state styles
41
+ this.headless
42
+ ? this.disabled
43
+ ? `${this.componentName}--disabled`
44
+ : null
45
+ : this.disabled
46
+ ? 'pointer-events-none opacity-75'
47
+ : null,
48
+ // selected state styles
49
+ this.headless
50
+ ? this.selected
51
+ ? `${this.componentName}--selected`
52
+ : null
53
+ : this.selected
54
+ ? 'bg-secondary-200/10'
55
+ : null,
56
+ ];
57
+ },
58
+ },
59
+
60
+ data() {
61
+ return {
62
+ index: null,
63
+ selected: false,
64
+ tabIndex: 0,
65
+ };
66
+ },
67
+
68
+ watch: {
69
+ selected(newValue) {
70
+ if (!newValue || !this.componentContent) return;
71
+
72
+ this.componentContent.setActiveDescedant(this.id);
73
+
74
+ const isMousemove = this.componentContent.getMousemove();
75
+
76
+ if (!isMousemove) {
77
+ this.el.scrollIntoView({ block: 'nearest' });
78
+ }
79
+ },
80
+ },
81
+
82
+ mounted() {
83
+ this.init();
84
+ this.addMouseEventListeners();
85
+ },
86
+
87
+ beforeUnmount() {
88
+ this.apiInjected().unregisterItem(this.id);
89
+ this.removeMouseEventListeners();
90
+ },
91
+
92
+ methods: {
93
+ init() {
94
+ this.index = this.items.length;
95
+
96
+ const item = {
97
+ id: this.id,
98
+ select: this.select,
99
+ unselect: this.unselect,
100
+ focus: this.focus,
101
+ onClick: this.onClick,
102
+ };
103
+
104
+ this.apiInjected().registerItem(item);
105
+ },
106
+
107
+ addMouseEventListeners() {
108
+ this.el.addEventListener('mousemove', this.onMousemove);
109
+ this.el.addEventListener('mouseleave', this.onMouseleave);
110
+ },
111
+
112
+ removeMouseEventListeners() {
113
+ this.el.removeEventListener('mousemove', this.onMousemove);
114
+ this.el.removeEventListener('mouseleave', this.onMouseleave);
115
+ },
116
+
117
+ select() {
118
+ this.selected = true;
119
+ },
120
+
121
+ unselect() {
122
+ this.selected = false;
123
+ },
124
+
125
+ focus() {
126
+ if (!this.el) return;
127
+
128
+ this.tabIndex = -1;
129
+ this.selected = true;
130
+ this.el.focus();
131
+ },
132
+
133
+ focusFirstItem() {
134
+ this.setFocusToItem(0);
135
+ },
136
+
137
+ focusLastItem() {
138
+ this.setFocusToItem(this.items.length - 1);
139
+ },
140
+
141
+ /**
142
+ * Focus the previous item in the menu.
143
+ * If is the first item, jump to the last item.
144
+ */
145
+ focusPreviousItem() {
146
+ const isLast = this.index === this.items.length - 1;
147
+ const goToIndex = isLast ? 0 : this.index + 1;
148
+
149
+ this.setFocusToItem(goToIndex);
150
+ },
151
+
152
+ /**
153
+ * Focus the next item in the menu.
154
+ * If is the last item, jump to the first item.
155
+ */
156
+ focusNextItem() {
157
+ const isFirst = this.index === 0;
158
+ const goToIndex = isFirst ? this.items.length - 1 : this.index - 1;
159
+
160
+ this.setFocusToItem(goToIndex);
161
+ },
162
+
163
+ /**
164
+ * Focus item by remove its tabindex and calling
165
+ * focus to the element.
166
+ *
167
+ * @param {Number, String} goToIndex
168
+ */
169
+ setFocusToItem(goToIndex) {
170
+ this.tabIndex = 0;
171
+ this.selected = false;
172
+
173
+ // set all selected to false
174
+ this.items.forEach((item) => item.unselect());
175
+
176
+ // focus item
177
+ this.items[goToIndex].focus();
178
+ },
179
+
180
+ leaveMenu() {
181
+ if (this.componentTrigger) {
182
+ this.componentTrigger.cancel();
183
+ this.componentTrigger.focus();
184
+ }
185
+ },
186
+
187
+ onClick() {
188
+ if (this.disabled) {
189
+ return;
190
+ }
191
+
192
+ this.value !== undefined
193
+ ? this.apiInjected().emit(this.value)
194
+ : this.$emit('click');
195
+
196
+ this.$nextTick(() => this.leaveMenu());
197
+ },
198
+
199
+ onKeyEsc() {
200
+ this.leaveMenu();
201
+ },
202
+
203
+ onMousemove() {
204
+ if (this.selected) {
205
+ return;
206
+ }
207
+
208
+ this.items.forEach((item) => item.unselect());
209
+
210
+ this.select();
211
+ this.componentContent.setMousemove();
212
+ },
213
+
214
+ onMouseleave() {
215
+ this.unselect();
216
+ this.componentContent.unsetMousemove();
217
+ },
218
+ },
219
+ };
@@ -0,0 +1,74 @@
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
+ // the min width to floating uis should be 200
55
+ // since less than that can look not as nice
56
+ const minWidthLimit = 200;
57
+ const width = rects.reference.width;
58
+ const minWidth = width < minWidthLimit ? minWidthLimit : width;
59
+
60
+ Object.assign(content.style, {
61
+ minWidth: `${minWidth}px`,
62
+ });
63
+ },
64
+ }),
65
+ ],
66
+ }).then(({ x, y }) => {
67
+ Object.assign(content.style, {
68
+ left: `${x}px`,
69
+ top: `${y}px`,
70
+ });
71
+ });
72
+ },
73
+ },
74
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.19.2-16",
3
+ "version": "0.19.2-17",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -11,9 +11,13 @@
11
11
  "access": "public"
12
12
  },
13
13
  "dependencies": {
14
+ "@floating-ui/dom": "^1.0.4",
14
15
  "@veritree/icons": "^0.36.1-0"
15
16
  },
16
17
  "devDependencies": {
17
- "@nuxt/kit": "^3.0.0"
18
+ "@nuxt/kit": "^3.0.0",
19
+ "prettier": "^2.7.1",
20
+ "prettier-plugin-tailwindcss": "^0.1.13",
21
+ "tailwindcss": "^3.2.4"
18
22
  }
19
23
  }
@@ -8,6 +8,7 @@
8
8
  'fixed inset-0 z-50 grid grid-cols-1 grid-rows-1 p-4 md:p-8': !headless,
9
9
  }"
10
10
  aria-modal="true"
11
+ v-bind="$attrs"
11
12
  @click="onClick"
12
13
  >
13
14
  <slot></slot>
@@ -21,6 +22,8 @@ import { genId } from '../../utils/ids';
21
22
  export default {
22
23
  name: 'VTDialog',
23
24
 
25
+ inheritAttrs: false,
26
+
24
27
  provide() {
25
28
  return {
26
29
  apiDialog: () => {
@@ -5,7 +5,7 @@
5
5
  :id="id"
6
6
  :class="{
7
7
  'Dialog-overlay': headless,
8
- 'fixed inset-0 bg-fd-450/80': !headless,
8
+ 'bg-primary-200/80 fixed inset-0': !headless,
9
9
  }"
10
10
  ></div>
11
11
  </FadeInOut>
@@ -5,45 +5,47 @@
5
5
  </template>
6
6
 
7
7
  <script>
8
+ import { floatingUiMixin } from '../../../mixins/floating-ui';
8
9
  import { genId } from '../../utils/ids';
9
10
 
10
11
  export default {
11
12
  name: 'VTDropdownMenu',
12
13
 
14
+ mixins: [floatingUiMixin],
15
+
13
16
  provide() {
14
17
  return {
15
18
  apiDropdownMenu: () => {
16
- const { dark: isDark, headless: isHeadless, right: isRight } = this;
17
- const { id, trigger, content, items } = this;
18
-
19
19
  const registerTrigger = (trigger) => {
20
- this.trigger = trigger;
20
+ if (!trigger) return;
21
+ this.componentTrigger = trigger;
21
22
  };
22
23
 
23
24
  const registerContent = (content) => {
24
- this.content = content;
25
+ if (!content) return;
26
+ this.componentContent = content;
25
27
  };
26
28
 
27
29
  const registerItem = (item) => {
30
+ if (!item) return;
28
31
  this.items.push(item);
29
32
  };
30
33
 
31
- const unregisterItems = () => {
32
- this.items = [];
34
+ const unregisterItem = (id) => {
35
+ const index = this.items.findIndex((item) => item.id === id);
36
+ this.items.splice(index, 1);
33
37
  };
34
38
 
35
39
  return {
36
- id,
37
- isDark,
38
- isHeadless,
39
- isRight,
40
- trigger,
41
- content,
42
- items,
40
+ id: this.componentId,
41
+ component: this.component,
42
+ componentTrigger: this.componentTrigger,
43
+ componentContent: this.componentContent,
44
+ items: this.items,
43
45
  registerTrigger,
44
46
  registerContent,
45
47
  registerItem,
46
- unregisterItems,
48
+ unregisterItem,
47
49
  };
48
50
  },
49
51
  };
@@ -54,23 +56,19 @@ export default {
54
56
  type: Boolean,
55
57
  default: false,
56
58
  },
57
- dark: {
58
- type: Boolean,
59
- default: false,
60
- },
61
- right: {
62
- type: Boolean,
63
- default: false,
64
- },
65
59
  },
66
60
 
67
61
  data() {
68
62
  return {
69
- id: `menu-${genId()}`,
70
- trigger: null,
71
- content: null,
63
+ componentId: genId(),
72
64
  items: [],
73
65
  };
74
66
  },
67
+
68
+ computed: {
69
+ id() {
70
+ return `dropdown-menu-${this.componentId}`;
71
+ },
72
+ },
75
73
  };
76
74
  </script>
@@ -1,94 +1,51 @@
1
1
  <template>
2
- <transition
3
- enter-active-class="duration-200 ease-out"
4
- enter-from-class="translate-y-[15px] opacity-0"
5
- enter-to-class="translate-y-0 opacity-100"
6
- leave-active-class="duration-200 ease-in"
7
- leave-from-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 }"
7
+ :floating-ui-class="floatingUiClass"
10
8
  >
11
- <div
12
- v-if="visible"
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>
9
+ <slot></slot>
10
+ </FloatingUi>
27
11
  </template>
28
12
 
29
13
  <script>
30
- import { genId } from '../../utils/ids';
14
+ import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
31
15
 
32
16
  export default {
33
17
  name: 'VTDropdownMenuContent',
34
18
 
35
- inject: ['apiDropdownMenu'],
19
+ mixins: [floatingUiContentMixin],
36
20
 
37
- data() {
38
- return {
39
- id: `menucontent-${genId()}`,
40
- visible: false,
41
- };
42
- },
21
+ inject: ['apiDropdownMenu'],
43
22
 
44
23
  computed: {
45
- dark() {
46
- return this.apiDropdownMenu().isDark;
24
+ id() {
25
+ return `dropdown-menu-content-${this.apiDropdownMenu().id}`;
47
26
  },
48
27
 
49
- headless() {
50
- return this.apiDropdownMenu().isHeadless;
28
+ component() {
29
+ return this.apiDropdownMenu().component;
51
30
  },
52
31
 
53
- right() {
54
- return this.apiDropdownMenu().isRight;
55
- },
56
-
57
- trigger() {
58
- return this.apiDropdownMenu().trigger;
32
+ componentTrigger() {
33
+ return this.apiDropdownMenu().componentTrigger;
59
34
  },
60
35
  },
61
36
 
62
37
  mounted() {
63
- this.apiDropdownMenu().registerContent(this);
64
-
65
- this.$nextTick(() => {
66
- if (this.trigger) this.trigger.toggleHasPopup();
67
- });
68
-
69
- // T-79 Create a directive or mixin for this
70
- document.addEventListener('click', (e) => {
71
- e.stopPropagation();
72
- if (this.visible && !this.$el.contains(e.target)) this.trigger.onClick();
73
- });
74
- },
75
-
76
- unmounted() {
77
- // TODO: Create a directive or mixin for this
78
- document.removeEventListener('click', this.trigger.onClick());
79
- },
80
-
81
- methods: {
82
- show() {
83
- this.visible = true;
84
- this.$emit('shown');
85
- },
38
+ const content = {
39
+ id: this.id,
40
+ hide: this.hide,
41
+ show: this.show,
42
+ getMousemove: this.getMousemove,
43
+ setMousemove: this.setMousemove,
44
+ unsetMousemove: this.unsetMousemove,
45
+ setActiveDescedant: this.setActiveDescedant,
46
+ };
86
47
 
87
- hide() {
88
- this.visible = false;
89
- this.$emit('hidden');
90
- this.apiDropdownMenu().unregisterItems();
91
- },
48
+ this.apiDropdownMenu().registerContent(content);
92
49
  },
93
50
  };
94
51
  </script>
@@ -3,8 +3,6 @@
3
3
  :class="{
4
4
  PopoverDivider: headless,
5
5
  '-mx-3 my-2 h-[1px]': !headless,
6
- 'bg-gray-200': !dark,
7
- 'bg-fd-500': dark,
8
6
  }"
9
7
  ></div>
10
8
  </template>
@@ -15,6 +13,11 @@ export default {
15
13
 
16
14
  inject: ['apiDropdownMenu'],
17
15
 
16
+ headless: {
17
+ type: Boolean,
18
+ default: false,
19
+ },
20
+
18
21
  computed: {
19
22
  dark() {
20
23
  return this.apiDropdownMenu().isDark;