@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.
Files changed (40) hide show
  1. package/mixins/floating-ui-content.js +1 -22
  2. package/mixins/floating-ui-item.js +93 -46
  3. package/mixins/floating-ui.js +5 -12
  4. package/package.json +1 -1
  5. package/src/components/Alert/VTAlert.vue +55 -14
  6. package/src/components/Dialog/VTDialog.vue +13 -11
  7. package/src/components/Dialog/VTDialogClose.vue +13 -19
  8. package/src/components/Dialog/VTDialogContent.vue +19 -14
  9. package/src/components/Dialog/VTDialogFooter.vue +9 -14
  10. package/src/components/Dialog/VTDialogHeader.vue +11 -13
  11. package/src/components/Dialog/VTDialogMain.vue +6 -13
  12. package/src/components/Dialog/VTDialogOverlay.vue +9 -13
  13. package/src/components/Dialog/VTDialogTitle.vue +8 -5
  14. package/src/components/Disclosure/VTDisclosureHeader.vue +1 -1
  15. package/src/components/DropdownMenu/VTDropdownMenu.vue +19 -0
  16. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +14 -6
  17. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +7 -16
  18. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +3 -7
  19. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +1 -10
  20. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +4 -9
  21. package/src/components/Listbox/VTListbox.vue +105 -14
  22. package/src/components/Listbox/VTListboxContent.vue +3 -7
  23. package/src/components/Listbox/VTListboxItem.vue +127 -6
  24. package/src/components/Listbox/VTListboxLabel.vue +3 -4
  25. package/src/components/Listbox/VTListboxList.vue +3 -24
  26. package/src/components/Listbox/VTListboxSearch.vue +60 -53
  27. package/src/components/Listbox/VTListboxTrigger.vue +12 -6
  28. package/src/components/Popover/VTPopover.vue +19 -0
  29. package/src/components/ProgressBar/VTProgressBar.vue +21 -3
  30. package/src/components/Skeleton/VTSkeleton.vue +11 -0
  31. package/src/components/Skeleton/VTSkeletonItem.vue +9 -0
  32. package/src/components/Tabs/VTTab.vue +13 -11
  33. package/src/components/Tooltip/VTTooltip.vue +65 -0
  34. package/src/components/Tooltip/VTTooltipContent.vue +59 -0
  35. package/src/components/Tooltip/VTTooltipTrigger.vue +98 -0
  36. package/src/components/Utils/FloatingUi.vue +52 -17
  37. package/src/components/Input/VTInput.vue +0 -82
  38. package/src/components/Input/VTInputDate.vue +0 -36
  39. package/src/components/Input/VTInputFile.vue +0 -60
  40. 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.down.prevent="focusPreviousItem"
11
- @keydown.up.prevent="focusNextItem"
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.apiInjected().search;
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
- ListboxLabel: headless,
6
- 'mb-2 block text-xs font-normal uppercase': !headless,
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 ? 'listbox-list' : 'max-h-[160px] w-auto overflow-y-auto -mx-3',
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
- <input
3
- :value="modelValue"
4
- type="text"
5
- :class="classComputed"
6
- @input="onInput"
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
+ :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: -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
  beforeUnmount() {
@@ -67,56 +69,61 @@ 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) {
81
- this.item.select();
82
- }
78
+ this.selectItem(selectedIndex, newSelectedIndex);
83
79
  },
84
80
 
85
81
  focusPreviousItem() {
86
- this.unselectItem();
87
-
88
- this.index--;
89
-
90
- if (this.index < 0) {
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.item.select();
88
+ this.selectItem(selectedIndex, newSelectedIndex);
95
89
  },
96
90
 
97
91
  focusFirstItem() {
98
- this.unselectItem();
99
- this.index = 0;
100
- this.item.select();
92
+ const selectedIndex = this.getItemSelectedIndex();
93
+ const newSelectedIndex = 0;
94
+
95
+ this.selectItem(selectedIndex, newSelectedIndex);
101
96
  },
102
97
 
103
98
  focusLastItem() {
104
- this.unselectItem();
105
- this.index = this.items.length - 1;
106
- this.item.select();
99
+ const selectedIndex = this.getItemSelectedIndex();
100
+ const newSelectedIndex = this.items.length - 1;
101
+
102
+ this.selectItem(selectedIndex, newSelectedIndex);
107
103
  },
108
104
 
109
- unselectItem() {
110
- const isMousemove = this.componentContent.getMousemove();
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
- if (isMousemove) {
113
- this.componentContent.unsetMousemove();
114
- this.items.forEach((item) => item.unselect());
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 { 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,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-button',
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="100"
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 bg-secondary-300 transition-all',
18
+ : 'bg-secondary-200 absolute left-0 h-full transition-all',
19
19
  ]"
20
- :style="{ width: `${value}%` }"
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>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <div class="animate-pulse">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'VTSkeleton',
10
+ };
11
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <div class="bg-gray-300" />
3
+ </template>
4
+
5
+ <script>
6
+ export default {
7
+ name: 'VTSkeletonItem',
8
+ };
9
+ </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: ['apiTabs'],
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.apiTabs().registerTab(this);
60
+ this.api.registerTab(this);
60
61
  },
61
62
 
62
63
  beforeUnmount() {
63
- this.apiTabs().unregisterTab(this.index);
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.apiTabs().focusTab(0);
81
+ this.api.focusTab(0);
81
82
  },
82
83
 
83
84
  focusLastTab(lastTabIndex) {
84
- this.apiTabs().focusTab(lastTabIndex);
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.apiTabs().focusTab(this.indexFocus - 1);
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.apiTabs().focusTab(this.indexFocus + 1);
103
+ this.api.focusTab(this.indexFocus + 1);
103
104
  },
104
105
 
105
106
  onClick() {
106
- this.apiTabs().selectTab(this.index);
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.apiTabs().getTabsLength() - 1;
114
+ const lastTabIndex = this.api.getTabsLength() - 1;
113
115
 
114
116
  if (this.indexFocus === null) {
115
117
  this.indexFocus = this.index;