@veritree/ui 0.22.2 → 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.
Files changed (37) hide show
  1. package/mixins/floating-ui-content.js +0 -21
  2. package/mixins/floating-ui-item.js +93 -47
  3. package/mixins/floating-ui.js +4 -13
  4. package/mixins/form-control-icon.js +2 -2
  5. package/mixins/form-control.js +9 -5
  6. package/nuxt.js +1 -0
  7. package/package.json +1 -1
  8. package/src/components/Button/VTButton.vue +5 -5
  9. package/src/components/Dialog/VTDialog.vue +6 -15
  10. package/src/components/Dialog/VTDialogClose.vue +19 -25
  11. package/src/components/Dialog/VTDialogContent.vue +18 -21
  12. package/src/components/Dialog/VTDialogFooter.vue +7 -18
  13. package/src/components/Dialog/VTDialogHeader.vue +15 -18
  14. package/src/components/Dialog/VTDialogMain.vue +11 -18
  15. package/src/components/Dialog/VTDialogOverlay.vue +14 -18
  16. package/src/components/Dialog/VTDialogTitle.vue +10 -7
  17. package/src/components/Drawer/VTDrawerContent.vue +1 -1
  18. package/src/components/Drawer/VTDrawerFooter.vue +1 -1
  19. package/src/components/DropdownMenu/VTDropdownMenu.vue +19 -0
  20. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +5 -6
  21. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +2 -2
  22. package/src/components/Form/VTFormFeedback.vue +5 -5
  23. package/src/components/Form/VTInput.vue +5 -2
  24. package/src/components/Form/VTTextarea.vue +5 -2
  25. package/src/components/Listbox/VTListbox.vue +35 -11
  26. package/src/components/Listbox/VTListboxContent.vue +4 -7
  27. package/src/components/Listbox/VTListboxItem.vue +117 -6
  28. package/src/components/Listbox/VTListboxList.vue +1 -24
  29. package/src/components/Listbox/VTListboxSearch.vue +58 -52
  30. package/src/components/Listbox/VTListboxTrigger.vue +7 -4
  31. package/src/components/Modal/VTModal.vue +1 -1
  32. package/src/components/Popover/VTPopover.vue +19 -0
  33. package/src/components/Popover/VTPopoverContent.vue +3 -3
  34. package/src/components/Tooltip/VTTooltip.vue +65 -0
  35. package/src/components/Tooltip/VTTooltipContent.vue +59 -0
  36. package/src/components/Tooltip/VTTooltipTrigger.vue +100 -0
  37. package/src/components/Utils/FloatingUi.vue +27 -13
@@ -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 -mx-3',
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
- <input
3
- v-model="search"
4
- type="text"
5
- :class="classComputed"
6
- @input="onChange"
7
- @click.stop
8
- @keydown.down.prevent="focusNextItem"
9
- @keydown.up.prevent="focusPreviousItem"
10
- @keydown.home.prevent="focusFirstItem"
11
- @keydown.end.prevent="focusLastItem"
12
- @keydown.enter.prevent="onKeyEnter"
13
- @keydown.esc.stop="hide"
14
- @keydown.tab.prevent="hide"
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: -1,
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 search = {
57
+ const componentSearch = {
56
58
  el: this.$el,
57
59
  };
58
60
 
59
- this.apiListbox().registerSearch(search);
60
- this.$nextTick(() => setTimeout(() => this.$el.focus(), 150));
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
- if (this.index !== -1) {
71
- this.unselectItem();
72
- }
73
-
74
- this.index++;
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
- if (this.index > this.items.length - 1) {
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.unselectItem();
85
-
86
- this.index--;
87
-
88
- if (this.index < 0) {
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.item.select();
88
+ this.selectItem(selectedIndex, newSelectedIndex);
93
89
  },
94
90
 
95
91
  focusFirstItem() {
96
- this.unselectItem();
97
- this.index = 0;
98
- this.item.select();
92
+ const selectedIndex = this.getItemSelectedIndex();
93
+ const newSelectedIndex = 0;
94
+
95
+ this.selectItem(selectedIndex, newSelectedIndex);
99
96
  },
100
97
 
101
98
  focusLastItem() {
102
- this.unselectItem();
103
- this.index = this.items.length - 1;
104
- this.item.select();
105
- },
99
+ const selectedIndex = this.getItemSelectedIndex();
100
+ const newSelectedIndex = this.items.length - 1;
106
101
 
107
- unselectItem() {
108
- const isMousemove = this.componentContent.getMousemove();
102
+ this.selectItem(selectedIndex, newSelectedIndex);
103
+ },
109
104
 
110
- if (isMousemove) {
111
- this.componentContent.unsetMousemove();
112
- this.items.forEach((item) => item.unselect());
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 truncate']">
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 { formControlMixin } from '../../../mixins/form-control';
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();
@@ -11,7 +11,7 @@
11
11
  >
12
12
  <div
13
13
  v-if="visible"
14
- class="fixed inset-0 z-50 flex flex-col justify-center bg-fd-700/75"
14
+ class="bg-fd-700/75 fixed inset-0 z-50 flex flex-col justify-center"
15
15
  tabindex="-1"
16
16
  @keyup.esc="close"
17
17
  @click="close"
@@ -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="{ 'popover-content': headless }"
7
- :floating-ui-class="floatingUiClass"
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>
@@ -7,19 +7,19 @@
7
7
  leave-active-class="duration-200 ease-in"
8
8
  leave-class="translate-y-0 opacity-100"
9
9
  leave-to-class="translate-y-[15px] opacity-0"
10
- @after-leave="hide"
10
+ @after-leave="hidden"
11
+ @after-enter="shown"
11
12
  >
12
13
  <div
13
14
  v-if="visible"
14
- :id="id"
15
15
  :class="[
16
16
  headless
17
- ? null
18
- : 'absolute z-50 grid overflow-x-hidden rounded-md py-2 px-3 border-gray-100 bg-white shadow-300',
19
- floatingUiClass ? floatingUiClass : null,
17
+ ? `${this.component}-content`
18
+ : `shadow-300 absolute z-50 grid overflow-x-hidden rounded-md py-2 px-3 ${this.classes} ${this.portalClass}`,
20
19
  ]"
20
+ v-bind="$attrs"
21
21
  >
22
- <slot></slot>
22
+ <slot />
23
23
  </div>
24
24
  </transition>
25
25
  </Portal>
@@ -34,27 +34,41 @@ export default {
34
34
  },
35
35
 
36
36
  props: {
37
+ component: {
38
+ type: String,
39
+ default: null,
40
+ },
37
41
  headless: {
38
42
  type: Boolean,
39
43
  default: false,
40
44
  },
41
- id: {
42
- type: [String, Number],
43
- default: null,
44
- },
45
45
  visible: {
46
46
  type: Boolean,
47
47
  default: false,
48
48
  },
49
- floatingUiClass: {
49
+ portalClass: {
50
50
  type: [String, Function],
51
51
  default: null,
52
52
  },
53
53
  },
54
54
 
55
+ computed: {
56
+ isTooltip() {
57
+ return this.component === 'tooltip';
58
+ },
59
+
60
+ classes() {
61
+ return this.isTooltip ? 'bg-gray-800 text-sm text-white' : 'bg-white';
62
+ },
63
+ },
64
+
55
65
  methods: {
56
- hide() {
57
- this.$emit('hide');
66
+ shown() {
67
+ this.$emit('shown');
68
+ },
69
+
70
+ hidden() {
71
+ this.$emit('hidden');
58
72
  },
59
73
  },
60
74
  };