@veritree/ui 0.19.2-16 → 0.19.2-18

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 (28) 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/VTFormFeedback.vue +33 -22
  14. package/src/components/Form/VTFormLabel.vue +3 -1
  15. package/src/components/Form/VTFormRow.vue +5 -0
  16. package/src/components/Listbox/VTListbox.vue +22 -41
  17. package/src/components/Listbox/VTListboxContent.vue +22 -114
  18. package/src/components/Listbox/VTListboxItem.vue +10 -183
  19. package/src/components/Listbox/VTListboxLabel.vue +0 -10
  20. package/src/components/Listbox/VTListboxList.vue +22 -31
  21. package/src/components/Listbox/VTListboxSearch.vue +28 -29
  22. package/src/components/Listbox/VTListboxTrigger.vue +69 -86
  23. package/src/components/Popover/VTPopover.vue +21 -27
  24. package/src/components/Popover/VTPopoverContent.vue +23 -58
  25. package/src/components/Popover/VTPopoverDivider.vue +4 -11
  26. package/src/components/Popover/VTPopoverItem.vue +13 -10
  27. package/src/components/Popover/VTPopoverTrigger.vue +120 -20
  28. package/src/components/Utils/FloatingUi.vue +58 -0
@@ -3,16 +3,10 @@
3
3
  :is="as"
4
4
  :id="id"
5
5
  :to="to"
6
- :class="{
7
- MenuItem: headless,
8
- '-mx-3 flex min-w-max items-center gap-3 px-3 py-2 text-inherit no-underline':
9
- !headless,
10
- 'hover:bg-secondary-200/10': !dark,
11
- 'text-white hover:bg-fd-450 focus:bg-fd-450': dark,
12
- 'pointer-events-none opacity-75': disabled,
13
- }"
14
- :tabindex="tabIndex"
6
+ :class="classComputed"
15
7
  :aria-disabled="disabled"
8
+ :tabindex="tabIndex"
9
+ class="-mx-3"
16
10
  role="menuitem"
17
11
  @click.stop.prevent="onClick"
18
12
  @keydown.down.prevent="focusPreviousItem"
@@ -28,11 +22,13 @@
28
22
  </template>
29
23
 
30
24
  <script>
31
- import { genId } from '../../utils/ids';
25
+ import { floatingUiItemMixin } from '../../../mixins/floating-ui-item';
32
26
 
33
27
  export default {
34
28
  name: 'VTDropdownMenuItem',
35
29
 
30
+ mixins: [floatingUiItemMixin],
31
+
36
32
  inject: ['apiDropdownMenu'],
37
33
 
38
34
  props: {
@@ -44,29 +40,16 @@ export default {
44
40
  type: String,
45
41
  default: null,
46
42
  },
47
- disabled: {
48
- type: Boolean,
49
- default: false,
50
- },
51
43
  },
52
44
 
53
45
  data() {
54
46
  return {
55
- id: `menuitem-${genId()}`,
56
- index: null,
57
- tabIndex: 0,
47
+ apiInjected: this.apiDropdownMenu,
48
+ componentName: 'dropdown-menu-item',
58
49
  };
59
50
  },
60
51
 
61
52
  computed: {
62
- dark() {
63
- return this.apiDropdownMenu().isDark;
64
- },
65
-
66
- headless() {
67
- return this.apiDropdownMenu().isHeadless;
68
- },
69
-
70
53
  as() {
71
54
  return this.href
72
55
  ? 'a'
@@ -74,109 +57,6 @@ export default {
74
57
  ? resolveComponent('NuxtLink')
75
58
  : 'button';
76
59
  },
77
-
78
- items() {
79
- return this.apiDropdownMenu().items;
80
- },
81
-
82
- el() {
83
- return this.$el;
84
- },
85
-
86
- trigger() {
87
- return this.apiDropdownMenu().trigger;
88
- },
89
-
90
- content() {
91
- return this.apiDropdownMenu().content;
92
- },
93
- },
94
-
95
- mounted() {
96
- const item = {
97
- focus: this.focus,
98
- el: this.el,
99
- };
100
-
101
- this.apiDropdownMenu().registerItem(item);
102
-
103
- this.index = this.items.length - 1;
104
- },
105
-
106
- methods: {
107
- focus() {
108
- if (!this.el) return;
109
-
110
- this.tabIndex = -1;
111
- this.el.focus();
112
- },
113
-
114
- focusFirstItem() {
115
- this.setFocusToItem(0);
116
- },
117
-
118
- focusLastItem() {
119
- this.setFocusToItem(this.items.length - 1);
120
- },
121
-
122
- /**
123
- * Focus the previous item in the menu.
124
- * If is the first item, jump to the last item.
125
- */
126
- focusPreviousItem() {
127
- const isLast = this.index === this.items.length - 1;
128
- const goToIndex = isLast ? 0 : this.index + 1;
129
-
130
- this.setFocusToItem(goToIndex);
131
- },
132
-
133
- /**
134
- * Focus the next item in the menu.
135
- * If is the last item, jump to the first item.
136
- */
137
- focusNextItem() {
138
- const isFirst = this.index === 0;
139
- const goToIndex = isFirst ? this.items.length - 1 : this.index - 1;
140
-
141
- this.setFocusToItem(goToIndex);
142
- },
143
-
144
- /**
145
- * Focus item by remove its tabindex and calling
146
- * focus to the element.
147
- *
148
- * @param {Number, String} goToIndex
149
- */
150
- setFocusToItem(goToIndex) {
151
- this.tabIndex = 0;
152
- this.items[goToIndex].focus();
153
- },
154
-
155
- /**
156
- * Hides content/menu and focus on trigger
157
- */
158
- leaveMenu() {
159
- if (this.content) this.content.hide();
160
- if (this.trigger) this.trigger.focus();
161
- },
162
-
163
- onKeyEsc() {
164
- this.leaveMenu();
165
- },
166
-
167
- onClick(ev) {
168
- if (this.disabled) return;
169
-
170
- // Nuxtlink doesn't understand enter as a click
171
- // so, we need to force it here
172
- if (ev.key === 'Enter') {
173
- ev.target.click();
174
- return;
175
- }
176
-
177
- this.$emit('click');
178
- this.$nextTick(() => this.leaveMenu());
179
- },
180
60
  },
181
61
  };
182
62
  </script>
@@ -15,15 +15,15 @@
15
15
  export default {
16
16
  name: 'VTDropdownMenuLabel',
17
17
 
18
- inject: ['api'],
18
+ inject: ['apiDropdownMenu'],
19
19
 
20
20
  computed: {
21
21
  dark() {
22
- return this.api().isDark;
22
+ return this.apiDropdownMenu().isDark;
23
23
  },
24
24
 
25
25
  headless() {
26
- return this.api().isHeadless;
26
+ return this.apiDropdownMenu().isHeadless;
27
27
  },
28
28
  },
29
29
  };
@@ -1,11 +1,12 @@
1
1
  <template>
2
2
  <div
3
+ :id="id"
3
4
  :aria-haspopup="hasPopup"
4
5
  :aria-expanded="expanded"
5
6
  :aria-controls="controls"
6
- @keydown.down.prevent="onKeyArrowDown"
7
- @keydown.up.prevent="onKeyArrowUp"
8
- @keydown.esc.prevent="onKeyesc"
7
+ @keydown.down.prevent="onKeyDownOrUp"
8
+ @keydown.up.prevent="onKeyDownOrUp"
9
+ @keydown.esc.stop="onKeyEsc"
9
10
  >
10
11
  <slot></slot>
11
12
  </div>
@@ -19,17 +20,21 @@ export default {
19
20
 
20
21
  data() {
21
22
  return {
22
- trigger: null,
23
23
  expanded: false,
24
- controls: null,
25
24
  hasPopup: false,
25
+ controls: null,
26
+ trigger: null,
27
+ id: null,
26
28
  };
27
29
  },
28
30
 
29
31
  computed: {
30
- // gets slot element
31
- content() {
32
- return this.apiDropdownMenu().content;
32
+ // id() {
33
+ // return `dropdown-menu-trigger-${this.apiDropdownMenu().id}`;
34
+ // },
35
+
36
+ componentContent() {
37
+ return this.apiDropdownMenu().componentContent;
33
38
  },
34
39
 
35
40
  items() {
@@ -45,14 +50,30 @@ export default {
45
50
  },
46
51
  },
47
52
 
53
+ watch: {
54
+ expanded() {
55
+ this.toggleAriaHasPopup();
56
+ },
57
+ },
58
+
48
59
  mounted() {
49
- this.apiDropdownMenu().registerTrigger(this);
60
+ this.id = `dropdown-menu-trigger-${this.apiDropdownMenu().id}`;
61
+
62
+ const trigger = {
63
+ id: this.id,
64
+ el: this.$el,
65
+ cancel: this.cancel,
66
+ focus: this.focus,
67
+ };
68
+
69
+ this.apiDropdownMenu().registerTrigger(trigger);
70
+
50
71
  this.setTrigger();
51
72
  this.addTriggerEvents();
52
73
  },
53
74
 
54
- unmounted() {
55
- this.trigger.removeEventListener('click', this.onClick());
75
+ destroyed() {
76
+ this.trigger.removeEventListener('click', this.onClick);
56
77
  },
57
78
 
58
79
  methods: {
@@ -60,140 +81,104 @@ export default {
60
81
  this.trigger = this.$el.querySelector(':first-child');
61
82
  },
62
83
 
84
+ /**
85
+ * Add event listener to slot element
86
+ *
87
+ * The click event has to be added to the slot child element
88
+ * since we are not setting the onclick on the component
89
+ * itself.
90
+ *
91
+ * Slot must have only one child element. It avoids
92
+ * errors related to adding the event listener.
93
+ */
63
94
  addTriggerEvents() {
64
95
  this.trigger.addEventListener('click', (e) => {
65
- if (this.expanded) {
66
- this.onClick();
67
- return;
68
- }
69
-
70
- // delay stop propagation to close other visible
71
- // dropdowns and delay click event to control
72
- // this dropdown visibility
73
- setTimeout(() => e.stopImmediatePropagation(), 50);
74
- setTimeout(() => this.onClick(), 100);
96
+ this.onClick(e);
75
97
  });
76
98
  },
77
99
 
78
- /**
79
- * Shows content/menu if not already visible
80
- */
81
- showContent() {
82
- if (!this.expanded) {
83
- this.toggleExpanded();
84
- this.content.show();
100
+ init(e) {
101
+ if (!this.componentContent) {
102
+ return;
85
103
  }
86
- },
87
104
 
88
- /**
89
- * Focus slot element if it exists and toggle expanded
90
- */
91
- focus() {
92
- if (!this.trigger) return;
93
-
94
- this.trigger.focus();
95
- this.toggleExpanded();
96
- },
105
+ if (this.expanded) {
106
+ this.cancel();
107
+ return;
108
+ }
97
109
 
98
- /**
99
- * Toggles aria expanded attribute/state
100
- */
101
- toggleExpanded() {
102
- this.expanded = !this.expanded;
103
- },
110
+ this.expanded = true;
104
111
 
105
- /**
106
- * Sets aria expanded attribute/state to false
107
- */
108
- hideExpanded() {
109
- this.expanded = false;
112
+ // delay stop propagation to close other visible
113
+ // dropdowns and delay click event to control
114
+ // this dropdown visibility
115
+ setTimeout(() => e.stopImmediatePropagation(), 50);
116
+ setTimeout(() => this.showComponentContent(), 100);
110
117
  },
111
118
 
112
- /**
113
- * Toggles aria popup/controls attribute/state
114
- */
115
- toggleHasPopup() {
116
- if (!this.content) return;
117
-
118
- this.hasPopup = !this.hasPopup;
119
-
120
- if (!this.hasPopup) {
121
- this.controls = null;
119
+ cancel() {
120
+ if (!this.componentContent) {
122
121
  return;
123
122
  }
124
123
 
125
- this.controls = this.content.id;
126
- },
127
-
128
- /**
129
- * On click, do the following:
130
- *
131
- * 1. Toggle aria expanded attribute/state
132
- * 2. Open the menu if it's closed
133
- * 3. Close the menu if it's open
134
- */
135
- onClick() {
136
- if (!this.content) return;
124
+ this.expanded = false;
137
125
 
138
- this.toggleExpanded();
126
+ this.hideComponentContent();
127
+ },
139
128
 
140
- this.$nextTick(() => {
141
- if (this.expanded) this.content.show();
142
- else this.content.hide();
143
- });
129
+ focus() {
130
+ if (this.trigger) this.trigger.focus();
144
131
  },
145
132
 
146
- /**
147
- * 1. Set aria expanded attribute/state to false
148
- * 2. Close the menu
149
- */
150
- hide() {
151
- this.hideExpanded();
133
+ showComponentContent() {
134
+ this.componentContent.show();
135
+ },
152
136
 
153
- this.$nextTick(() => {
154
- this.content.hide();
155
- });
137
+ hideComponentContent() {
138
+ this.componentContent.hide();
156
139
  },
157
140
 
158
- /**
159
- * On key arrow down, do the following:
160
- *
161
- * 1. if the menu is not expanded, expand it and focus the first menu item
162
- * 2. if the menu is expanded, focus the first menu item
163
- */
164
- onKeyArrowDown() {
165
- if (!this.content) return;
141
+ toggleAriaHasPopup() {
142
+ if (this.expanded) {
143
+ this.hasPopup = this.componentContent !== null;
144
+ this.controls = this.hasPopup ? this.componentContent.id : null;
166
145
 
167
- this.showContent();
146
+ return;
147
+ }
168
148
 
169
- this.$nextTick(() => {
170
- this.firstMenuItem.focus();
171
- });
149
+ this.hasPopup = null;
150
+ this.controls = null;
172
151
  },
173
152
 
174
- /**
175
- * On key arrow up, do the following:
176
- *
177
- * 1. if the menu is not expanded, expand it and focus the last menu item
178
- * 2. if the menu is expanded, focus the last menu item
179
- */
180
- onKeyArrowUp() {
181
- if (!this.content) return;
153
+ onClick(e) {
154
+ this.init(e);
155
+ },
182
156
 
183
- this.showContent();
157
+ onKeyDownOrUp(e) {
158
+ if (!this.expanded) {
159
+ this.$el.click(e);
160
+ }
184
161
 
162
+ const keyCode = e.code;
163
+ const listItemPosition =
164
+ keyCode === 'ArrowDown'
165
+ ? 'firstMenuItem'
166
+ : keyCode === 'ArrowUp'
167
+ ? 'lastMenuItem'
168
+ : null;
169
+
170
+ // settimeout here is delaying the focusing the element
171
+ // since it is not rendered yet. All items will only
172
+ // be available when the content is fully visible.
185
173
  this.$nextTick(() => {
186
- this.lastMenuItem.focus();
174
+ setTimeout(() => this[listItemPosition].focus(), 100);
187
175
  });
188
176
  },
189
177
 
190
- onKeyesc() {
191
- if (!this.content) return;
192
-
193
- if (this.expanded) {
194
- this.toggleExpanded();
195
- this.content.hide();
196
- }
178
+ // change it to a better name or move the methods inside to another function
179
+ onKeyEsc() {
180
+ this.cancel();
181
+ this.focus();
197
182
  },
198
183
  },
199
184
  };
@@ -1,7 +1,27 @@
1
1
  <template>
2
- <div class="form-feedback" :class="classes">
3
- <component :is="icon" v-if="showIcon" />
4
- <span class="text-gray-500"><slot></slot></span>
2
+ <div
3
+ :class="[
4
+ headless ? 'form-feedback' : 'mt-1 flex items-baseline gap-2',
5
+ // variant styles
6
+ headless ? `form-feedback--${variant}` : null,
7
+ ]"
8
+ >
9
+ <component
10
+ v-if="showIcon"
11
+ :is="icon"
12
+ :class="[
13
+ headless ? 'form-feedback__icon' : 'relative top-1 h-4 w-4 shrink-0',
14
+ // variant styles
15
+ headless
16
+ ? `form-feedback__icon--${variant}`
17
+ : isError
18
+ ? 'text-error-500'
19
+ : null,
20
+ ]"
21
+ />
22
+ <span :class="[headless ? 'form-feedback--text' : 'text-sm text-gray-500']">
23
+ <slot />
24
+ </span>
5
25
  </div>
6
26
  </template>
7
27
 
@@ -17,32 +37,23 @@ export default {
17
37
  },
18
38
 
19
39
  props: {
20
- variant: {
21
- type: [String, Object],
22
- default: '',
23
- validator(value) {
24
- if (value === '' || typeof value === 'object') {
25
- return true;
26
- }
27
-
28
- return ['success', 'warning', 'error'].includes(value);
29
- },
30
- },
31
40
  hideIcon: {
32
41
  type: Boolean,
33
42
  default: false,
34
43
  },
44
+ headless: {
45
+ type: Boolean,
46
+ default: false,
47
+ },
48
+ variant: {
49
+ type: [String, Object],
50
+ default: '',
51
+ },
35
52
  },
36
53
 
37
54
  computed: {
38
- classes() {
39
- const classes = {};
40
-
41
- if (this.variant) {
42
- classes[`form-feedback--${this.variant}`] = true;
43
- }
44
-
45
- return classes;
55
+ isError() {
56
+ return this.variant === 'error';
46
57
  },
47
58
 
48
59
  icon() {
@@ -1,7 +1,9 @@
1
1
  <template>
2
2
  <label
3
3
  :class="[
4
- headless ? 'form-label' : 'font-semibold mb-1 flex justify-between gap-3',
4
+ headless
5
+ ? 'form-label'
6
+ : 'mb-1 flex justify-between gap-3 text-sm font-semibold',
5
7
  ]"
6
8
  >
7
9
  <slot />
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <div class="grid grid-cols-1 md:grid-cols-2 md:gap-4">
3
+ <slot />
4
+ </div>
5
+ </template>
@@ -1,29 +1,29 @@
1
1
  <template>
2
- <div :class="{ Listbox: headless, relative: !headless, 'z-20': active }">
2
+ <div :class="{ listbox: headless }">
3
3
  <slot></slot>
4
4
  </div>
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: 'VTListbox',
12
13
 
14
+ mixins: [floatingUiMixin],
15
+
13
16
  provide() {
14
17
  return {
15
18
  apiListbox: () => {
16
- const { dark: isDark, right: isRight } = this;
17
- const { id, listbox, trigger, content, search, list, items } = this;
18
-
19
19
  const registerTrigger = (trigger) => {
20
20
  if (!trigger) return;
21
- this.trigger = trigger;
21
+ this.componentTrigger = trigger;
22
22
  };
23
23
 
24
24
  const registerContent = (content) => {
25
25
  if (!content) return;
26
- this.content = content;
26
+ this.componentContent = content;
27
27
  };
28
28
 
29
29
  const registerSearch = (search) => {
@@ -31,10 +31,10 @@ export default {
31
31
  this.search = search;
32
32
  };
33
33
 
34
- const registerList = (list) => {
35
- if (!list) return;
36
- this.list = list;
37
- };
34
+ // const registerList = (list) => {
35
+ // if (!list) return;
36
+ // this.list = list;
37
+ // };
38
38
 
39
39
  const registerItem = (item) => {
40
40
  if (!item) return;
@@ -52,19 +52,16 @@ export default {
52
52
  };
53
53
 
54
54
  return {
55
- id,
56
- isDark,
57
- isRight,
58
- listbox,
59
- trigger,
60
- search,
61
- content,
62
- list,
63
- items,
55
+ id: this.componentId,
56
+ component: this.component,
57
+ componentTrigger: this.componentTrigger,
58
+ componentContent: this.componentContent,
59
+ items: this.items,
60
+ search: this.search,
64
61
  registerTrigger,
65
62
  registerContent,
66
63
  registerSearch,
67
- registerList,
64
+ // registerList,
68
65
  registerItem,
69
66
  unregisterItem,
70
67
  emit,
@@ -74,7 +71,7 @@ export default {
74
71
  },
75
72
 
76
73
  props: {
77
- modelValue: {
74
+ value: {
78
75
  type: [String, Number, Object],
79
76
  default: null,
80
77
  },
@@ -94,31 +91,15 @@ export default {
94
91
 
95
92
  data() {
96
93
  return {
97
- id: `listbox-${genId()}`,
98
- listbox: null,
99
- trigger: null,
100
- content: null,
94
+ componentId: genId(),
101
95
  search: null,
102
- list: null,
103
96
  items: [],
104
- active: false,
105
- };
106
- },
107
-
108
- mounted() {
109
- this.listbox = {
110
- setActive: this.setActive,
111
- clearActive: this.clearActive,
112
97
  };
113
98
  },
114
99
 
115
- methods: {
116
- setActive() {
117
- this.active = true;
118
- },
119
-
120
- clearActive() {
121
- this.active = null;
100
+ computed: {
101
+ id() {
102
+ return `listbox-${this.componentId}`;
122
103
  },
123
104
  },
124
105
  };