@veritree/ui 0.21.1-0 → 0.21.1-10

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 (29) hide show
  1. package/mixins/floating-ui-content.js +102 -0
  2. package/mixins/floating-ui-item.js +216 -0
  3. package/mixins/floating-ui.js +14 -6
  4. package/mixins/form-control.js +72 -0
  5. package/package.json +2 -2
  6. package/src/components/Avatar/VTAvatar.vue +32 -29
  7. package/src/components/DropdownMenu/VTDropdownMenu.vue +5 -16
  8. package/src/components/DropdownMenu/VTDropdownMenuContent.vue +17 -65
  9. package/src/components/DropdownMenu/VTDropdownMenuDivider.vue +8 -5
  10. package/src/components/DropdownMenu/VTDropdownMenuItem.vue +9 -143
  11. package/src/components/DropdownMenu/VTDropdownMenuLabel.vue +3 -3
  12. package/src/components/DropdownMenu/VTDropdownMenuTrigger.vue +67 -103
  13. package/src/components/Form/VTInput.vue +28 -40
  14. package/src/components/Form/VTInputQty.vue +165 -0
  15. package/src/components/Form/VTTextarea.vue +22 -0
  16. package/src/components/Listbox/VTListbox.vue +6 -11
  17. package/src/components/Listbox/VTListboxContent.vue +11 -76
  18. package/src/components/Listbox/VTListboxItem.vue +9 -181
  19. package/src/components/Listbox/VTListboxLabel.vue +0 -10
  20. package/src/components/Listbox/VTListboxList.vue +24 -33
  21. package/src/components/Listbox/VTListboxSearch.vue +21 -18
  22. package/src/components/Listbox/VTListboxTrigger.vue +58 -97
  23. package/src/components/Popover/VTPopover.vue +1 -14
  24. package/src/components/Popover/VTPopoverContent.vue +14 -65
  25. package/src/components/Popover/VTPopoverDivider.vue +4 -11
  26. package/src/components/Popover/VTPopoverItem.vue +16 -13
  27. package/src/components/Popover/VTPopoverTrigger.vue +118 -21
  28. package/src/components/Utils/FloatingUi.vue +6 -1
  29. package/src/utils/components.js +0 -18
@@ -1,9 +1,10 @@
1
1
  <template>
2
2
  <input
3
3
  v-model="search"
4
- :class="{ 'listbox-search': headless, 'form-control mb-1': !headless }"
5
4
  type="text"
5
+ :class="classComputed"
6
6
  @input="onChange"
7
+ @click.stop
7
8
  @keydown.down.prevent="focusNextItem"
8
9
  @keydown.up.prevent="focusPreviousItem"
9
10
  @keydown.home.prevent="focusFirstItem"
@@ -15,36 +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
- componentContent() {
39
- return this.api().componentContent;
37
+ componentTrigger() {
38
+ return this.apiListbox().componentTrigger;
40
39
  },
41
40
 
42
- list() {
43
- return this.api().list;
41
+ componentContent() {
42
+ return this.apiListbox().componentContent;
44
43
  },
45
44
 
46
45
  items() {
47
- return this.api().items;
46
+ return this.apiListbox().items;
48
47
  },
49
48
 
50
49
  item() {
@@ -57,7 +56,8 @@ export default {
57
56
  el: this.$el,
58
57
  };
59
58
 
60
- this.api().registerSearch(search);
59
+ this.apiListbox().registerSearch(search);
60
+ this.$nextTick(() => setTimeout(() => this.$el.focus(), 150));
61
61
  },
62
62
 
63
63
  beforeDestroy() {
@@ -105,10 +105,10 @@ export default {
105
105
  },
106
106
 
107
107
  unselectItem() {
108
- const isMousemove = this.list.getMousemove();
108
+ const isMousemove = this.componentContent.getMousemove();
109
109
 
110
110
  if (isMousemove) {
111
- this.list.unsetMousemove();
111
+ this.componentContent.unsetMousemove();
112
112
  this.items.forEach((item) => item.unselect());
113
113
  }
114
114
 
@@ -127,7 +127,10 @@ export default {
127
127
  },
128
128
 
129
129
  hide() {
130
- if (this.componentContent) this.componentContent.hide();
130
+ if (this.componentTrigger) {
131
+ this.componentTrigger.cancel();
132
+ this.componentTrigger.focus();
133
+ }
131
134
  },
132
135
  },
133
136
  };
@@ -1,35 +1,19 @@
1
1
  <template>
2
2
  <button
3
3
  :id="id"
4
+ :class="classComputed"
4
5
  :aria-expanded="expanded"
5
6
  :aria-haspopup="hasPopup"
6
- :class="{
7
- 'listbox-button': headless,
8
- 'flex w-full justify-between rounded-md border border-solid py-2 px-3':
9
- !headless,
10
- 'border-gray-300 text-gray-500': !dark && !headless,
11
- 'border-white/70 text-white focus-visible:ring-2 focus-visible:ring-white':
12
- dark && !headless,
13
- }"
14
7
  type="button"
15
8
  @click.prevent="onClick"
16
- @keydown.down.prevent="onKeyArrowDown"
17
- @keydown.up.prevent="onKeyArrowUp"
9
+ @keydown.down.prevent="onKeyDownOrUp"
10
+ @keydown.up.prevent="onKeyDownOrUp"
18
11
  @keydown.esc.stop="onKeyEsc"
19
12
  >
20
- <span
21
- :class="{
22
- 'listbox-button__text': headless,
23
- 'text-left': !headless,
24
- }"
25
- >
13
+ <span :class="[headless ? 'listbox-button__text' : 'text-left truncate']">
26
14
  <slot></slot>
27
15
  </span>
28
- <span
29
- :class="{
30
- 'listbox-button__icon': headless,
31
- }"
32
- >
16
+ <span :class="[headless ? 'listbox-button__icon' : 'shrink-0']">
33
17
  <IconChevronDown
34
18
  class="transition-transform"
35
19
  :class="{ 'rotate-180': expanded }"
@@ -39,6 +23,7 @@
39
23
  </template>
40
24
 
41
25
  <script>
26
+ import { formControlMixin } from '../../../mixins/form-control';
42
27
  import { IconChevronDown } from '@veritree/icons';
43
28
 
44
29
  export default {
@@ -46,17 +31,13 @@ export default {
46
31
 
47
32
  components: { IconChevronDown },
48
33
 
49
- inject: ['api'],
34
+ mixins: [formControlMixin],
50
35
 
51
- props: {
52
- headless: {
53
- type: Boolean,
54
- default: false,
55
- },
56
- },
36
+ inject: ['apiListbox'],
57
37
 
58
38
  data() {
59
39
  return {
40
+ name: 'listbox-button',
60
41
  expanded: false,
61
42
  hasPopup: false,
62
43
  };
@@ -64,19 +45,15 @@ export default {
64
45
 
65
46
  computed: {
66
47
  id() {
67
- return `listbox-trigger-${this.api().id}`;
68
- },
69
-
70
- dark() {
71
- return this.api().isDark;
48
+ return `listbox-trigger-${this.apiListbox().id}`;
72
49
  },
73
50
 
74
51
  componentContent() {
75
- return this.api().componentContent;
52
+ return this.apiListbox().componentContent;
76
53
  },
77
54
 
78
55
  items() {
79
- return this.api().items;
56
+ return this.apiListbox().items;
80
57
  },
81
58
 
82
59
  firstMenuItem() {
@@ -90,101 +67,85 @@ export default {
90
67
 
91
68
  mounted() {
92
69
  const trigger = {
93
- toggleExpanded: this.toggleExpanded,
94
70
  el: this.$el,
71
+ cancel: this.cancel,
95
72
  focus: this.focus,
96
73
  id: this.id,
97
- onClick: this.onClick,
98
74
  };
99
75
 
100
- this.api().registerTrigger(trigger);
76
+ this.apiListbox().registerTrigger(trigger);
101
77
  },
102
78
 
103
79
  methods: {
104
- /**
105
- * Shows content/menu if not already visible
106
- */
107
- showContent() {
108
- this.expanded = true;
109
- this.componentContent.show();
110
- },
111
-
112
- focus() {
113
- this.$el.focus();
114
- },
115
-
116
- toggleExpanded() {
117
- if (!this.expanded) return;
118
- this.expanded = false;
119
- },
120
-
121
- /**
122
- * On click, do the following:
123
- *
124
- * 1. Toggle aria expanded attribute/state
125
- * 2. Open the menu if it's closed
126
- * 3. Close the menu if it's open
127
- */
128
- onClick(e) {
129
- if (!this.componentContent) return;
80
+ init(e) {
81
+ if (!this.componentContent) {
82
+ return;
83
+ }
130
84
 
131
85
  if (this.expanded) {
132
- this.componentContent.hide();
86
+ this.cancel();
133
87
  return;
134
88
  }
135
89
 
90
+ this.expanded = true;
91
+
136
92
  // delay stop propagation to close other visible
137
93
  // dropdowns and delay click event to control
138
94
  // this dropdown visibility
139
95
  setTimeout(() => e.stopImmediatePropagation(), 50);
140
- setTimeout(() => this.showContent(), 100);
96
+ setTimeout(() => this.showComponentContent(), 100);
141
97
  },
142
98
 
143
- /**
144
- * On key arrow down, do the following:
145
- *
146
- * 1. if the menu is not expanded, expand it and focus the first menu item
147
- * 2. if the menu is expanded, focus the first menu item
148
- */
149
- onKeyArrowDown() {
150
- if (!this.componentContent) return;
99
+ cancel() {
100
+ if (!this.componentContent) {
101
+ return;
102
+ }
103
+ this.expanded = false;
151
104
 
152
- this.showContent();
105
+ this.hideComponentContent();
106
+ },
153
107
 
154
- // settimeout here is delaying the focusing the element
155
- // since it is not rendered yet. All items will only
156
- // be available when the content is fully visible.
157
- this.$nextTick(() => {
158
- setTimeout(() => this.firstMenuItem.focus(), 150);
159
- });
108
+ focus() {
109
+ this.$el.focus();
160
110
  },
161
111
 
162
- /**
163
- * On key arrow up, do the following:
164
- *
165
- * 1. if the menu is not expanded, expand it and focus the last menu item
166
- * 2. if the menu is expanded, focus the last menu item
167
- */
168
- onKeyArrowUp() {
169
- if (!this.componentContent) return;
112
+ showComponentContent() {
113
+ this.componentContent.show();
114
+ },
170
115
 
171
- this.showContent();
116
+ hideComponentContent() {
117
+ this.componentContent.hide();
118
+ },
119
+
120
+ onClick(e) {
121
+ this.init(e);
122
+ },
123
+
124
+ onKeyDownOrUp(e) {
125
+ if (!this.expanded) {
126
+ this.$el.click(e);
127
+ }
128
+
129
+ const keyCode = e.code;
130
+ const listItemPosition =
131
+ keyCode === 'ArrowDown'
132
+ ? 'firstMenuItem'
133
+ : keyCode === 'ArrowUp'
134
+ ? 'lastMenuItem'
135
+ : null;
172
136
 
173
137
  // settimeout here is delaying the focusing the element
174
138
  // since it is not rendered yet. All items will only
175
139
  // be available when the content is fully visible.
176
140
  this.$nextTick(() => {
177
- setTimeout(() => this.lastMenuItem.focus(), 150);
141
+ setTimeout(() => this[listItemPosition].focus(), 100);
178
142
  });
179
143
  },
180
144
 
145
+ // change it to a better name or move the methods inside to another function
181
146
  onKeyEsc() {
182
- if (!this.componentContent) return;
183
-
184
- if (this.expanded) {
185
- this.toggleExpanded();
186
- this.componentContent.hide();
187
- }
147
+ this.cancel();
148
+ this.focus();
188
149
  },
189
150
  },
190
151
  };
@@ -20,9 +20,7 @@ export default {
20
20
 
21
21
  provide() {
22
22
  return {
23
- api: () => {
24
- const { dark: isDark, headless: isHeadless, right: isRight } = this;
25
-
23
+ apiPopover: () => {
26
24
  const registerTrigger = (trigger) => {
27
25
  if (!trigger) return;
28
26
  this.componentTrigger = trigger;
@@ -35,9 +33,6 @@ export default {
35
33
 
36
34
  return {
37
35
  id: this.componentId,
38
- isDark,
39
- isHeadless,
40
- isRight,
41
36
  component: this.component,
42
37
  componentTrigger: this.componentTrigger,
43
38
  componentContent: this.componentContent,
@@ -53,14 +48,6 @@ export default {
53
48
  type: Boolean,
54
49
  default: false,
55
50
  },
56
- dark: {
57
- type: Boolean,
58
- default: false,
59
- },
60
- right: {
61
- type: Boolean,
62
- default: false,
63
- },
64
51
  },
65
52
 
66
53
  data() {
@@ -3,96 +3,45 @@
3
3
  :visible="visible"
4
4
  :id="id"
5
5
  :headless="headless"
6
- :class="{
7
- 'popover-content': headless,
8
- }"
6
+ :class="{ 'popover-content': headless }"
7
+ :floating-ui-class="floatingUiClass"
9
8
  >
10
9
  <slot></slot>
11
10
  </FloatingUi>
12
11
  </template>
13
12
 
14
13
  <script>
15
- import FloatingUi from '../Utils/FloatingUi.vue';
14
+ import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
16
15
 
17
16
  export default {
18
17
  name: 'VTPopoverContent',
19
18
 
20
- components: {
21
- FloatingUi,
22
- },
23
-
24
- inject: ['api'],
19
+ mixins: [floatingUiContentMixin],
25
20
 
26
- data() {
27
- return {
28
- visible: false,
29
- };
30
- },
21
+ inject: ['apiPopover'],
31
22
 
32
23
  computed: {
33
24
  id() {
34
- return `popover-content-${this.api().id}`;
35
- },
36
-
37
- headless() {
38
- return this.api().isHeadless;
25
+ return `popover-content-${this.apiPopover().id}`;
39
26
  },
40
27
 
41
28
  component() {
42
- return this.api().component;
29
+ return this.apiPopover().component;
43
30
  },
44
31
 
45
32
  componentTrigger() {
46
- return this.api().componentTrigger;
33
+ return this.apiPopover().componentTrigger;
47
34
  },
48
35
  },
49
36
 
50
37
  mounted() {
51
- this.api().registerContent(this);
52
-
53
- // T-307 Create a directive or mixin for this
54
- document.addEventListener('click', (e) => {
55
- if (!e) {
56
- return;
57
- }
58
-
59
- e.stopPropagation();
60
-
61
- if (this.visible && !this.$el.contains(e.target)) {
62
- this.hide();
63
- }
64
- });
65
- },
66
-
67
- destroyed() {
68
- // T-325 Create a directive or mixin for this
69
- document.removeEventListener('click', this.componentTrigger.onClick);
70
- },
71
-
72
- methods: {
73
- show() {
74
- if (this.visible) return;
75
-
76
- this.visible = true;
77
-
78
- this.$nextTick(() => {
79
- this.component.setActive();
80
- });
81
- },
82
-
83
- hide() {
84
- if (!this.visible) return;
85
-
86
- this.visible = false;
87
-
88
- this.$nextTick(() => {
89
- this.componentTrigger.focus();
38
+ const content = {
39
+ id: this.id,
40
+ show: this.show,
41
+ hide: this.hide,
42
+ };
90
43
 
91
- setTimeout(() => {
92
- this.component.clearActive();
93
- }, 100);
94
- });
95
- },
44
+ this.apiPopover().registerContent(content);
96
45
  },
97
46
  };
98
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
+ : 'relative z-10 -mx-3 flex items-center gap-2 px-3 py-2 text-inherit no-underline hover:bg-secondary-200/10',
9
+ ]"
11
10
  @click="onClick"
12
11
  >
13
12
  <slot></slot>
@@ -18,26 +17,30 @@
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() {