@veritree/ui 0.19.2-2 → 0.19.2-20

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 (62) 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/mixins/form-control-icon.js +53 -0
  5. package/mixins/form-control.js +73 -0
  6. package/package.json +7 -3
  7. package/src/components/Avatar/VTAvatar.vue +32 -29
  8. package/src/components/Button/VTButton.vue +9 -5
  9. package/src/components/Dialog/VTDialog.vue +6 -11
  10. package/src/components/Dialog/VTDialogClose.vue +9 -9
  11. package/src/components/Dialog/VTDialogContent.vue +9 -9
  12. package/src/components/Dialog/VTDialogFooter.vue +5 -5
  13. package/src/components/Dialog/VTDialogHeader.vue +8 -8
  14. package/src/components/Dialog/VTDialogMain.vue +8 -8
  15. package/src/components/Dialog/VTDialogOverlay.vue +8 -8
  16. package/src/components/Dialog/VTDialogTitle.vue +4 -4
  17. package/src/components/Disclosure/VTDisclosureContent.vue +1 -1
  18. package/src/components/Disclosure/VTDisclosureDetails.vue +2 -2
  19. package/src/components/Disclosure/VTDisclosureHeader.vue +1 -1
  20. package/src/components/Disclosure/VTDisclosureIcon.vue +1 -1
  21. package/src/components/Drawer/VTDrawer.vue +14 -16
  22. package/src/components/Drawer/VTDrawerClose.vue +9 -9
  23. package/src/components/Drawer/VTDrawerContent.vue +8 -8
  24. package/src/components/Drawer/VTDrawerFooter.vue +3 -3
  25. package/src/components/Drawer/VTDrawerHeader.vue +4 -4
  26. package/src/components/Drawer/VTDrawerMain.vue +5 -5
  27. package/src/components/Drawer/VTDrawerOverlay.vue +6 -6
  28. package/src/components/Drawer/VTDrawerTitle.vue +5 -5
  29. package/src/components/DropdownMenu/VTDropdownMenu.vue +27 -29
  30. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +27 -70
  31. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +8 -5
  32. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +14 -123
  33. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +3 -3
  34. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +96 -121
  35. package/src/components/Form/VTFormFeedback.vue +33 -22
  36. package/src/components/Form/VTFormGroup.vue +5 -7
  37. package/src/components/Form/VTFormLabel.vue +22 -0
  38. package/src/components/Form/VTFormRow.vue +5 -0
  39. package/src/components/Form/VTInput.vue +40 -0
  40. package/src/components/Form/VTInputIcon.vue +35 -0
  41. package/src/components/Form/VTInputPassword.vue +55 -0
  42. package/src/components/Form/VTTextarea.vue +22 -0
  43. package/src/components/Listbox/VTListbox.vue +26 -45
  44. package/src/components/Listbox/VTListboxContent.vue +24 -116
  45. package/src/components/Listbox/VTListboxItem.vue +10 -182
  46. package/src/components/Listbox/VTListboxLabel.vue +0 -10
  47. package/src/components/Listbox/VTListboxList.vue +24 -33
  48. package/src/components/Listbox/VTListboxSearch.vue +30 -29
  49. package/src/components/Listbox/VTListboxTrigger.vue +71 -88
  50. package/src/components/Popover/VTPopover.vue +24 -30
  51. package/src/components/Popover/VTPopoverContent.vue +24 -59
  52. package/src/components/Popover/VTPopoverDivider.vue +4 -11
  53. package/src/components/Popover/VTPopoverItem.vue +21 -14
  54. package/src/components/Popover/VTPopoverTrigger.vue +126 -21
  55. package/src/components/Tabs/VTTab.vue +10 -11
  56. package/src/components/Tabs/VTTabGroup.vue +9 -7
  57. package/src/components/Tabs/VTTabPanel.vue +4 -5
  58. package/src/components/Transitions/FadeInOut.vue +2 -2
  59. package/src/components/Utils/FloatingUi.vue +58 -0
  60. package/package-lock.json +0 -13
  61. package/src/components/Modal/VTModal.vue +0 -69
  62. package/src/utils/genId.js +0 -13
@@ -1,9 +1,10 @@
1
1
  <template>
2
2
  <input
3
- v-model="search"
4
- :class="{ ListboxList: headless, 'form-control mb-1': !headless }"
3
+ :value="modelValue"
5
4
  type="text"
6
- @input="onChange"
5
+ :class="classComputed"
6
+ @input="onInput"
7
+ @click.stop
7
8
  @keydown.down.prevent="focusNextItem"
8
9
  @keydown.up.prevent="focusPreviousItem"
9
10
  @keydown.home.prevent="focusFirstItem"
@@ -15,40 +16,34 @@
15
16
  </template>
16
17
 
17
18
  <script>
19
+ import { formControlMixin } from '../../../mixins/form-control';
20
+
18
21
  export default {
19
22
  name: 'VTListboxSearch',
20
23
 
21
- inject: ['api'],
24
+ mixins: [formControlMixin],
22
25
 
23
- props: {
24
- headless: {
25
- type: Boolean,
26
- default: false,
27
- },
28
- },
26
+ inject: ['apiListbox'],
29
27
 
30
28
  data() {
31
29
  return {
32
- search: '',
30
+ name: 'listbox-search',
33
31
  index: -1,
32
+ search: '',
34
33
  };
35
34
  },
36
35
 
37
36
  computed: {
38
- content() {
39
- return this.api().content;
37
+ componentTrigger() {
38
+ return this.apiListbox().componentTrigger;
40
39
  },
41
40
 
42
- trigger() {
43
- return this.api().trigger;
44
- },
45
-
46
- list() {
47
- return this.api().list;
41
+ componentContent() {
42
+ return this.apiListbox().componentContent;
48
43
  },
49
44
 
50
45
  items() {
51
- return this.api().items;
46
+ return this.apiListbox().items;
52
47
  },
53
48
 
54
49
  item() {
@@ -61,12 +56,13 @@ export default {
61
56
  el: this.$el,
62
57
  };
63
58
 
64
- this.api().registerSearch(search);
59
+ this.apiListbox().registerSearch(search);
60
+ this.$nextTick(() => setTimeout(() => this.$el.focus(), 150));
65
61
  },
66
62
 
67
- beforeDestroy() {
63
+ beforeUnmount() {
68
64
  this.search = '';
69
- this.$emit('change', this.search.trim());
65
+ this.$emit('update:modelValue', '');
70
66
  },
71
67
 
72
68
  methods: {
@@ -81,7 +77,9 @@ export default {
81
77
  this.index = 0;
82
78
  }
83
79
 
84
- if (this.item) this.item.select();
80
+ if (this.item) {
81
+ this.item.select();
82
+ }
85
83
  },
86
84
 
87
85
  focusPreviousItem() {
@@ -109,21 +107,21 @@ export default {
109
107
  },
110
108
 
111
109
  unselectItem() {
112
- const isMousemove = this.list.getMousemove();
110
+ const isMousemove = this.componentContent.getMousemove();
113
111
 
114
112
  if (isMousemove) {
115
- this.list.unsetMousemove();
113
+ this.componentContent.unsetMousemove();
116
114
  this.items.forEach((item) => item.unselect());
117
115
  }
118
116
 
119
117
  if (this.item) this.item.unselect();
120
118
  },
121
119
 
122
- onChange() {
120
+ onInput(event) {
123
121
  this.index = 0;
124
122
  if (this.item) this.item.select();
125
123
 
126
- this.$emit('change', this.search.trim());
124
+ this.$emit('update:modelValue', event.target.value);
127
125
  },
128
126
 
129
127
  onKeyEnter() {
@@ -131,7 +129,10 @@ export default {
131
129
  },
132
130
 
133
131
  hide() {
134
- if (this.content) this.content.hide();
132
+ if (this.componentTrigger) {
133
+ this.componentTrigger.cancel();
134
+ this.componentTrigger.focus();
135
+ }
135
136
  },
136
137
  },
137
138
  };
@@ -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,7 @@
38
24
  </template>
39
25
 
40
26
  <script>
27
+ import { formControlMixin } from '../../../mixins/form-control';
41
28
  import { IconChevronDown } from '@veritree/icons';
42
29
 
43
30
  export default {
@@ -45,33 +32,26 @@ export default {
45
32
 
46
33
  components: { IconChevronDown },
47
34
 
48
- inject: ['api'],
35
+ mixins: [formControlMixin],
49
36
 
50
- props: {
51
- headless: {
52
- type: Boolean,
53
- default: false,
54
- },
55
- },
37
+ inject: ['apiListbox'],
56
38
 
57
39
  data() {
58
40
  return {
41
+ name: 'listbox-button',
59
42
  expanded: false,
60
43
  hasPopup: false,
44
+ id: null,
61
45
  };
62
46
  },
63
47
 
64
48
  computed: {
65
- dark() {
66
- return this.api().isDark;
67
- },
68
-
69
- content() {
70
- return this.api().content;
49
+ componentContent() {
50
+ return this.apiListbox().componentContent;
71
51
  },
72
52
 
73
53
  items() {
74
- return this.api().items;
54
+ return this.apiListbox().items;
75
55
  },
76
56
 
77
57
  firstMenuItem() {
@@ -84,85 +64,88 @@ export default {
84
64
  },
85
65
 
86
66
  mounted() {
67
+ this.id = `listbox-trigger-${this.apiListbox().id}`;
68
+
87
69
  const trigger = {
88
70
  el: this.$el,
71
+ cancel: this.cancel,
89
72
  focus: this.focus,
90
- onClick: this.onClick,
91
- contract: this.contract,
73
+ id: this.id,
92
74
  };
93
75
 
94
- this.api().registerTrigger(trigger);
76
+ this.apiListbox().registerTrigger(trigger);
95
77
  },
96
78
 
97
79
  methods: {
98
- /**
99
- * Shows content/menu if not already visible
100
- */
101
- showContent() {
80
+ init(e) {
81
+ if (!this.componentContent) {
82
+ return;
83
+ }
84
+
85
+ if (this.expanded) {
86
+ this.cancel();
87
+ return;
88
+ }
89
+
102
90
  this.expanded = true;
103
- this.content.show();
104
- },
105
91
 
106
- focus() {
107
- this.$el.focus();
92
+ // delay stop propagation to close other visible
93
+ // dropdowns and delay click event to control
94
+ // this dropdown visibility
95
+ setTimeout(() => e.stopImmediatePropagation(), 50);
96
+ setTimeout(() => this.showComponentContent(), 100);
108
97
  },
109
98
 
110
- contract() {
111
- if (!this.expanded) return;
99
+ cancel() {
100
+ if (!this.componentContent) {
101
+ return;
102
+ }
112
103
  this.expanded = false;
113
- },
114
104
 
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();
105
+ this.hideComponentContent();
125
106
  },
126
107
 
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;
108
+ focus() {
109
+ this.$el.focus();
110
+ },
135
111
 
136
- this.showContent();
112
+ showComponentContent() {
113
+ this.componentContent.show();
114
+ },
137
115
 
138
- this.$nextTick(() => {
139
- this.firstMenuItem.focus();
140
- });
116
+ hideComponentContent() {
117
+ this.componentContent.hide();
141
118
  },
142
119
 
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;
120
+ onClick(e) {
121
+ this.init(e);
122
+ },
151
123
 
152
- this.showContent();
124
+ onKeyDownOrUp(e) {
125
+ if (!this.expanded) {
126
+ this.$el.click(e);
127
+ }
153
128
 
129
+ const keyCode = e.code;
130
+ const listItemPosition =
131
+ keyCode === 'ArrowDown'
132
+ ? 'firstMenuItem'
133
+ : keyCode === 'ArrowUp'
134
+ ? 'lastMenuItem'
135
+ : null;
136
+
137
+ // settimeout here is delaying the focusing the element
138
+ // since it is not rendered yet. All items will only
139
+ // be available when the content is fully visible.
154
140
  this.$nextTick(() => {
155
- this.lastMenuItem.focus();
141
+ setTimeout(() => this[listItemPosition].focus(), 100);
156
142
  });
157
143
  },
158
144
 
145
+ // change it to a better name or move the methods inside to another function
159
146
  onKeyEsc() {
160
- if (!this.content) return;
161
-
162
- if (this.expanded) {
163
- this.toggleExpanded();
164
- this.content.hide();
165
- }
147
+ this.cancel();
148
+ this.focus();
166
149
  },
167
150
  },
168
151
  };
@@ -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,18 @@ 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,
60
- },
61
51
  },
62
52
 
63
53
  data() {
64
54
  return {
65
- id: `popover-${genId()}`,
66
- button: null,
67
- content: null,
55
+ componentId: genId(),
68
56
  };
69
57
  },
58
+
59
+ computed: {
60
+ id() {
61
+ return `popover-${this.componentId}`;
62
+ },
63
+ },
70
64
  };
71
65
  </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