@veritree/ui 0.19.2 → 0.20.0-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 (78) hide show
  1. package/index.js +64 -68
  2. package/mixins/floating-ui-content.js +81 -0
  3. package/mixins/floating-ui-item.js +266 -0
  4. package/mixins/floating-ui.js +67 -0
  5. package/mixins/form-control-icon.js +53 -0
  6. package/mixins/form-control.js +71 -0
  7. package/nuxt.js +30 -23
  8. package/package.json +9 -4
  9. package/src/components/Alert/VTAlert.vue +55 -14
  10. package/src/components/Avatar/VTAvatar.vue +32 -29
  11. package/src/components/Button/VTButton.vue +9 -6
  12. package/src/components/Checkbox/VTCheckbox.vue +134 -0
  13. package/src/components/Checkbox/VTCheckboxLabel.vue +3 -0
  14. package/src/components/Checkbox/VTCheckboxText.vue +20 -0
  15. package/src/components/Dialog/VTDialog.vue +22 -32
  16. package/src/components/Dialog/VTDialogClose.vue +19 -25
  17. package/src/components/Dialog/VTDialogContent.vue +24 -19
  18. package/src/components/Dialog/VTDialogFooter.vue +11 -16
  19. package/src/components/Dialog/VTDialogHeader.vue +16 -18
  20. package/src/components/Dialog/VTDialogMain.vue +11 -18
  21. package/src/components/Dialog/VTDialogOverlay.vue +14 -18
  22. package/src/components/Dialog/VTDialogTitle.vue +10 -7
  23. package/src/components/Disclosure/VTDisclosureContent.vue +1 -1
  24. package/src/components/Disclosure/VTDisclosureDetails.vue +1 -1
  25. package/src/components/Disclosure/VTDisclosureHeader.vue +2 -2
  26. package/src/components/Disclosure/VTDisclosureIcon.vue +1 -1
  27. package/src/components/Drawer/VTDrawer.vue +14 -16
  28. package/src/components/Drawer/VTDrawerClose.vue +9 -9
  29. package/src/components/Drawer/VTDrawerContent.vue +8 -8
  30. package/src/components/Drawer/VTDrawerFooter.vue +3 -3
  31. package/src/components/Drawer/VTDrawerHeader.vue +4 -4
  32. package/src/components/Drawer/VTDrawerMain.vue +5 -5
  33. package/src/components/Drawer/VTDrawerOverlay.vue +6 -6
  34. package/src/components/Drawer/VTDrawerTitle.vue +5 -5
  35. package/src/components/DropdownMenu/VTDropdownMenu.vue +45 -28
  36. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +29 -64
  37. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +8 -14
  38. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +11 -124
  39. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +3 -12
  40. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +91 -121
  41. package/src/components/Form/VTFormFeedback.vue +39 -22
  42. package/src/components/Form/VTFormGroup.vue +5 -7
  43. package/src/components/Form/VTFormLabel.vue +22 -0
  44. package/src/components/Form/VTFormRow.vue +5 -0
  45. package/src/components/Form/VTInput.vue +40 -0
  46. package/src/components/Form/VTInputIcon.vue +35 -0
  47. package/src/components/Form/VTInputPassword.vue +55 -0
  48. package/src/components/Form/VTTextarea.vue +22 -0
  49. package/src/components/Listbox/VTListbox.vue +122 -50
  50. package/src/components/Listbox/VTListboxContent.vue +20 -116
  51. package/src/components/Listbox/VTListboxItem.vue +115 -166
  52. package/src/components/Listbox/VTListboxLabel.vue +3 -14
  53. package/src/components/Listbox/VTListboxList.vue +10 -40
  54. package/src/components/Listbox/VTListboxSearch.vue +76 -68
  55. package/src/components/Listbox/VTListboxTrigger.vue +75 -86
  56. package/src/components/Popover/VTPopover.vue +42 -29
  57. package/src/components/Popover/VTPopoverContent.vue +24 -59
  58. package/src/components/Popover/VTPopoverDivider.vue +4 -11
  59. package/src/components/Popover/VTPopoverItem.vue +21 -14
  60. package/src/components/Popover/VTPopoverTrigger.vue +126 -21
  61. package/src/components/ProgressBar/VTProgressBar.vue +21 -3
  62. package/src/components/Skeleton/VTSkeleton.vue +11 -0
  63. package/src/components/Skeleton/VTSkeletonItem.vue +9 -0
  64. package/src/components/Tabs/VTTab.vue +4 -3
  65. package/src/components/Tabs/VTTabGroup.vue +9 -7
  66. package/src/components/Tabs/VTTabPanel.vue +4 -5
  67. package/src/components/Tooltip/VTTooltip.vue +65 -0
  68. package/src/components/Tooltip/VTTooltipContent.vue +59 -0
  69. package/src/components/Tooltip/VTTooltipTrigger.vue +98 -0
  70. package/src/components/Transitions/FadeInOut.vue +2 -2
  71. package/src/components/Utils/FloatingUi.vue +93 -0
  72. package/package-lock.json +0 -13
  73. package/src/components/Input/VTInput.vue +0 -82
  74. package/src/components/Input/VTInputDate.vue +0 -36
  75. package/src/components/Input/VTInputFile.vue +0 -60
  76. package/src/components/Input/VTInputUpload.vue +0 -54
  77. package/src/components/Modal/VTModal.vue +0 -69
  78. package/src/utils/genId.js +0 -13
@@ -1,34 +1,20 @@
1
1
  <template>
2
2
  <button
3
+ :id="id"
4
+ :class="classComputed"
5
+ :disabled="disabled"
3
6
  :aria-expanded="expanded"
4
7
  :aria-haspopup="hasPopup"
5
- :class="{
6
- 'Listbox-button': headless,
7
- 'flex w-full justify-between rounded-md border border-solid py-2 px-3':
8
- !headless,
9
- 'border-gray-300 text-gray-500': !dark && !headless,
10
- 'border-white/70 text-white focus-visible:ring-2 focus-visible:ring-white':
11
- dark && !headless,
12
- }"
13
8
  type="button"
14
- @click.stop.prevent="onClick"
15
- @keydown.down.prevent="onKeyArrowDown"
16
- @keydown.up.prevent="onKeyArrowUp"
9
+ @click.prevent="onClick"
10
+ @keydown.down.prevent="onKeyDownOrUp"
11
+ @keydown.up.prevent="onKeyDownOrUp"
17
12
  @keydown.esc.stop="onKeyEsc"
18
13
  >
19
- <span
20
- :class="{
21
- 'Listbox-button__text': headless,
22
- 'text-left': !headless,
23
- }"
24
- >
14
+ <span :class="[headless ? 'listbox-button__text' : 'truncate text-left']">
25
15
  <slot></slot>
26
16
  </span>
27
- <span
28
- :class="{
29
- 'Listbox-button__icon': headless,
30
- }"
31
- >
17
+ <span :class="[headless ? 'listbox-button__icon' : 'shrink-0']">
32
18
  <IconChevronDown
33
19
  class="transition-transform"
34
20
  :class="{ 'rotate-180': expanded }"
@@ -38,6 +24,10 @@
38
24
  </template>
39
25
 
40
26
  <script>
27
+ import {
28
+ formControlMixin,
29
+ formControlStyleMixin,
30
+ } from '../../../mixins/form-control';
41
31
  import { IconChevronDown } from '@veritree/icons';
42
32
 
43
33
  export default {
@@ -45,33 +35,29 @@ export default {
45
35
 
46
36
  components: { IconChevronDown },
47
37
 
48
- inject: ['api'],
38
+ mixins: [formControlMixin, formControlStyleMixin],
49
39
 
50
- props: {
51
- headless: {
52
- type: Boolean,
53
- default: false,
54
- },
55
- },
40
+ inject: ['apiListbox'],
56
41
 
57
42
  data() {
58
43
  return {
44
+ name: 'listbox-trigger',
59
45
  expanded: false,
60
46
  hasPopup: false,
61
47
  };
62
48
  },
63
49
 
64
50
  computed: {
65
- dark() {
66
- return this.api().isDark;
51
+ id() {
52
+ return `listbox-trigger-${this.apiListbox().id}`;
67
53
  },
68
54
 
69
- content() {
70
- return this.api().content;
55
+ componentContent() {
56
+ return this.apiListbox().componentContent;
71
57
  },
72
58
 
73
59
  items() {
74
- return this.api().items;
60
+ return this.apiListbox().items;
75
61
  },
76
62
 
77
63
  firstMenuItem() {
@@ -86,83 +72,86 @@ export default {
86
72
  mounted() {
87
73
  const trigger = {
88
74
  el: this.$el,
75
+ cancel: this.cancel,
89
76
  focus: this.focus,
90
- onClick: this.onClick,
91
- contract: this.contract,
77
+ id: this.id,
92
78
  };
93
79
 
94
- this.api().registerTrigger(trigger);
80
+ this.apiListbox().registerTrigger(trigger);
95
81
  },
96
82
 
97
83
  methods: {
98
- /**
99
- * Shows content/menu if not already visible
100
- */
101
- showContent() {
84
+ init(e) {
85
+ if (!this.componentContent) {
86
+ return;
87
+ }
88
+
89
+ if (this.expanded) {
90
+ this.cancel();
91
+ return;
92
+ }
93
+
102
94
  this.expanded = true;
103
- this.content.show();
104
- },
105
95
 
106
- focus() {
107
- this.$el.focus();
96
+ // delay stop propagation to close other visible
97
+ // dropdowns and delay click event to control
98
+ // this dropdown visibility
99
+ setTimeout(() => e.stopImmediatePropagation(), 50);
100
+ setTimeout(() => this.showComponentContent(), 100);
108
101
  },
109
102
 
110
- contract() {
111
- if (!this.expanded) return;
103
+ cancel() {
104
+ if (!this.componentContent) {
105
+ return;
106
+ }
107
+
112
108
  this.expanded = false;
113
- },
114
109
 
115
- /**
116
- * On click, do the following:
117
- *
118
- * 1. Toggle aria expanded attribute/state
119
- * 2. Open the menu if it's closed
120
- * 3. Close the menu if it's open
121
- */
122
- onClick() {
123
- if (!this.content) return;
124
- this.expanded ? this.content.hide() : this.showContent();
110
+ this.hideComponentContent();
125
111
  },
126
112
 
127
- /**
128
- * On key arrow down, do the following:
129
- *
130
- * 1. if the menu is not expanded, expand it and focus the first menu item
131
- * 2. if the menu is expanded, focus the first menu item
132
- */
133
- onKeyArrowDown() {
134
- if (!this.content) return;
113
+ focus() {
114
+ this.$el.focus();
115
+ },
135
116
 
136
- this.showContent();
117
+ showComponentContent() {
118
+ this.componentContent.show();
119
+ },
137
120
 
138
- this.$nextTick(() => {
139
- this.firstMenuItem.focus();
140
- });
121
+ hideComponentContent() {
122
+ this.componentContent.hide();
141
123
  },
142
124
 
143
- /**
144
- * On key arrow up, do the following:
145
- *
146
- * 1. if the menu is not expanded, expand it and focus the last menu item
147
- * 2. if the menu is expanded, focus the last menu item
148
- */
149
- onKeyArrowUp() {
150
- if (!this.content) return;
125
+ onClick(e) {
126
+ this.init(e);
127
+ this.$emit('click');
128
+ },
151
129
 
152
- this.showContent();
130
+ onKeyDownOrUp(e) {
131
+ if (!this.expanded) {
132
+ this.$el.click(e);
133
+ }
153
134
 
135
+ const keyCode = e.code;
136
+ const listItemPosition =
137
+ keyCode === 'ArrowDown'
138
+ ? 'firstMenuItem'
139
+ : keyCode === 'ArrowUp'
140
+ ? 'lastMenuItem'
141
+ : null;
142
+
143
+ // settimeout here is delaying the focusing the element
144
+ // since it is not rendered yet. All items will only
145
+ // be available when the content is fully visible.
154
146
  this.$nextTick(() => {
155
- this.lastMenuItem.focus();
147
+ setTimeout(() => this[listItemPosition].focus(), 100);
156
148
  });
157
149
  },
158
150
 
151
+ // change it to a better name or move the methods inside to another function
159
152
  onKeyEsc() {
160
- if (!this.content) return;
161
-
162
- if (this.expanded) {
163
- this.toggleExpanded();
164
- this.content.hide();
165
- }
153
+ this.cancel();
154
+ this.focus();
166
155
  },
167
156
  },
168
157
  };
@@ -2,43 +2,41 @@
2
2
  <div
3
3
  :id="id"
4
4
  class="relative"
5
- :aria-haspopup="content ? 'true' : null"
6
- :aria-controls="content ? content.id : null"
5
+ :aria-haspopup="componentContent ? 'true' : null"
6
+ :aria-controls="componentContent ? componentContent.id : null"
7
7
  >
8
8
  <slot></slot>
9
9
  </div>
10
10
  </template>
11
11
 
12
12
  <script>
13
- import { genId } from "../../utils/ids";
13
+ import { floatingUiMixin } from '../../../mixins/floating-ui';
14
+ import { genId } from '../../utils/ids';
14
15
 
15
16
  export default {
16
- name: "VTPopover",
17
+ name: 'VTPopover',
18
+
19
+ mixins: [floatingUiMixin],
17
20
 
18
21
  provide() {
19
22
  return {
20
- api: () => {
21
- const { dark: isDark, headless: isHeadless, right: isRight } = this;
22
- const { id, button, content } = this;
23
-
24
- const registerButton = (button) => {
25
- if (!button) return;
26
- this.button = button;
23
+ apiPopover: () => {
24
+ const registerTrigger = (trigger) => {
25
+ if (!trigger) return;
26
+ this.componentTrigger = trigger;
27
27
  };
28
28
 
29
29
  const registerContent = (content) => {
30
30
  if (!content) return;
31
- this.content = content;
31
+ this.componentContent = content;
32
32
  };
33
33
 
34
34
  return {
35
- id,
36
- isDark,
37
- isHeadless,
38
- isRight,
39
- button,
40
- content,
41
- registerButton,
35
+ id: this.componentId,
36
+ component: this.component,
37
+ componentTrigger: this.componentTrigger,
38
+ componentContent: this.componentContent,
39
+ registerTrigger,
42
40
  registerContent,
43
41
  };
44
42
  },
@@ -50,22 +48,37 @@ export default {
50
48
  type: Boolean,
51
49
  default: false,
52
50
  },
53
- dark: {
54
- type: Boolean,
55
- default: false,
56
- },
57
- right: {
58
- type: Boolean,
59
- default: false,
51
+ placement: {
52
+ type: String,
53
+ default: 'bottom-start',
60
54
  },
61
55
  },
62
56
 
63
57
  data() {
64
58
  return {
65
- id: `popover-${genId()}`,
66
- button: null,
67
- content: null,
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,
68
75
  };
69
76
  },
77
+
78
+ computed: {
79
+ id() {
80
+ return `popover-${this.componentId}`;
81
+ },
82
+ },
70
83
  };
71
84
  </script>
@@ -1,82 +1,47 @@
1
1
  <template>
2
- <transition
3
- enter-active-class="duration-200 ease-out"
4
- enter-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-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="{ 'popover-content': headless }"
7
+ :floating-ui-class="floatingUiClass"
10
8
  >
11
- <div
12
- v-show="visible"
13
- :id="id"
14
- :class="{
15
- PopoverPanel: headless,
16
- 'absolute top-full mt-3 rounded-md shadow-300 ': !headless,
17
- 'bg-white': !dark,
18
- 'border-gray-700 bg-forest-default shadow-gray-700': dark,
19
- 'left-0': !right,
20
- 'right-0': right,
21
- }"
22
- >
23
- <slot></slot>
24
- </div>
25
- </transition>
9
+ <slot></slot>
10
+ </FloatingUi>
26
11
  </template>
27
12
 
28
13
  <script>
29
- import { genId } from "../../utils/ids";
14
+ import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
30
15
 
31
16
  export default {
32
- name: "VTPopoverContent",
17
+ name: 'VTPopoverContent',
33
18
 
34
- inject: ["api"],
19
+ mixins: [floatingUiContentMixin],
35
20
 
36
- data() {
37
- return {
38
- id: `popover-panel-${genId()}`,
39
- visible: false,
40
- };
41
- },
21
+ inject: ['apiPopover'],
42
22
 
43
23
  computed: {
44
- dark() {
45
- return this.api().isDark;
24
+ id() {
25
+ return `popover-content-${this.apiPopover().id}`;
46
26
  },
47
27
 
48
- headless() {
49
- return this.api().isHeadless;
28
+ component() {
29
+ return this.apiPopover().component;
50
30
  },
51
31
 
52
- right() {
53
- return this.api().isRight;
32
+ componentTrigger() {
33
+ return this.apiPopover().componentTrigger;
54
34
  },
55
35
  },
56
36
 
57
37
  mounted() {
58
- this.api().registerContent(this);
59
-
60
- // TODO: Create a directive or mixin for this
61
- document.addEventListener("click", (e) => {
62
- e.stopPropagation();
63
- if (this.visible && !this.$el.contains(e.target)) this.hide();
64
- });
65
- },
66
-
67
- destroyed() {
68
- // TODO: Create a directive or mixin for this
69
- document.removeEventListener("click", this.hide());
70
- },
71
-
72
- methods: {
73
- show() {
74
- this.visible = true;
75
- },
38
+ const content = {
39
+ id: this.id,
40
+ show: this.show,
41
+ hide: this.hide,
42
+ };
76
43
 
77
- hide() {
78
- this.visible = false;
79
- },
44
+ this.apiPopover().registerContent(content);
80
45
  },
81
46
  };
82
47
  </script>
@@ -3,8 +3,6 @@
3
3
  :class="{
4
4
  PopoverDivider: headless,
5
5
  'h-[1px]': !headless,
6
- 'bg-white': !dark,
7
- 'bg-fd-500': dark,
8
6
  }"
9
7
  ></div>
10
8
  </template>
@@ -13,15 +11,10 @@
13
11
  export default {
14
12
  name: 'VTPopoverDivider',
15
13
 
16
- inject: ['api'],
17
-
18
- computed: {
19
- dark() {
20
- return this.api().isDark;
21
- },
22
-
23
- headless() {
24
- return this.api().isHeadless;
14
+ props: {
15
+ headless: {
16
+ type: Boolean,
17
+ default: false,
25
18
  },
26
19
  },
27
20
  };
@@ -1,13 +1,12 @@
1
1
  <template>
2
2
  <component
3
3
  :is="as"
4
- :class="{
5
- PopoverItem: headless,
6
- '-mx-3 flex min-w-max items-center gap-2 px-3 py-2 no-underline':
7
- !headless,
8
- 'text-fd-500': !dark,
9
- 'text-white hover:bg-fd-450': dark,
10
- }"
4
+ :class="[
5
+ // default styles
6
+ headless
7
+ ? 'popover-item'
8
+ : 'hover:bg-secondary-200/10 relative z-10 -mx-3 flex items-center gap-2 px-3 py-2 text-inherit no-underline',
9
+ ]"
11
10
  @click="onClick"
12
11
  >
13
12
  <slot></slot>
@@ -18,30 +17,38 @@
18
17
  export default {
19
18
  name: 'VTPopoverItem',
20
19
 
21
- inject: ['api'],
20
+ inject: ['apiPopover'],
22
21
 
23
22
  props: {
24
- to: {
25
- type: [String, Object],
26
- default: null,
23
+ headless: {
24
+ type: Boolean,
25
+ default: false,
27
26
  },
28
27
  href: {
29
28
  type: String,
30
29
  default: null,
31
30
  },
31
+ to: {
32
+ type: [String, Object],
33
+ default: null,
34
+ },
32
35
  },
33
36
 
34
37
  computed: {
35
38
  dark() {
36
- return this.api().isDark;
39
+ return this.apiPopover().isDark;
37
40
  },
38
41
 
39
42
  headless() {
40
- return this.api().isHeadless;
43
+ return this.apiPopover().isHeadless;
41
44
  },
42
45
 
43
46
  as() {
44
- return this.href ? 'a' : this.to ? 'NuxtLink' : 'button';
47
+ return this.href
48
+ ? 'a'
49
+ : this.to
50
+ ? resolveComponent('NuxtLink')
51
+ : 'button';
45
52
  },
46
53
  },
47
54