@veritree/ui 0.21.1-1 → 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/Button/VTButton.vue +1 -1
  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,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() {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :id="id" @keydown.esc.prevent="onEsc">
2
+ <div :id="id" @keydown.esc.prevent="onKeyEsc">
3
3
  <slot></slot>
4
4
  </div>
5
5
  </template>
@@ -8,52 +8,149 @@
8
8
  export default {
9
9
  name: 'VTPopoverTrigger',
10
10
 
11
- inject: ['api'],
11
+ inject: ['apiPopover'],
12
+
13
+ data() {
14
+ return {
15
+ expanded: false,
16
+ hasPopup: false,
17
+ controls: null,
18
+ trigger: null,
19
+ };
20
+ },
12
21
 
13
22
  computed: {
14
23
  id() {
15
- return `popover-trigger-${this.api().id}`;
24
+ return `popover-trigger-${this.apiPopover().id}`;
16
25
  },
17
26
 
18
27
  componentContent() {
19
- return this.api().componentContent;
28
+ return this.apiPopover().componentContent;
20
29
  },
21
30
  },
22
31
 
23
32
  mounted() {
24
- this.api().registerTrigger(this);
25
- this.handleSlotTrigger();
33
+ const trigger = {
34
+ id: this.id,
35
+ el: this.$el,
36
+ cancel: this.cancel,
37
+ focus: this.focus,
38
+ };
39
+
40
+ this.apiPopover().registerTrigger(trigger);
41
+
42
+ this.setTrigger();
43
+ this.addTriggerEvents();
44
+ },
45
+
46
+ destroyed() {
47
+ this.trigger.removeEventListener('click', this.onClick);
26
48
  },
27
49
 
28
50
  methods: {
29
- handleSlotTrigger() {
30
- const slot = this.$slots.default;
31
- const trigger = slot[0].elm;
51
+ setTrigger() {
52
+ this.trigger = this.$el.querySelector(':first-child');
53
+ },
54
+
55
+ /**
56
+ * Add event listener to slot element
57
+ *
58
+ * The click event has to be added to the slot child element
59
+ * since we are not setting the onclick on the component
60
+ * itself.
61
+ *
62
+ * Slot must have only one child element. It avoids
63
+ * errors related to adding the event listener.
64
+ */
65
+ addTriggerEvents() {
66
+ this.trigger.addEventListener('click', (e) => {
67
+ this.onClick(e);
68
+ });
69
+ },
32
70
 
33
- if (slot.length > 1) {
34
- console.error('VTPopoverButton only accepts one item in its slot');
71
+ init(e) {
72
+ if (!this.componentContent) {
35
73
  return;
36
74
  }
37
75
 
38
- trigger.addEventListener('click', (e) => {
39
- this.onClick();
40
- e.stopPropagation();
41
- });
76
+ if (this.expanded) {
77
+ this.cancel();
78
+ return;
79
+ }
80
+
81
+ this.expanded = true;
82
+
83
+ // delay stop propagation to close other visible
84
+ // dropdowns and delay click event to control
85
+ // this dropdown visibility
86
+ setTimeout(() => e.stopImmediatePropagation(), 50);
87
+ setTimeout(() => this.showComponentContent(), 100);
88
+ },
89
+
90
+ cancel() {
91
+ if (!this.componentContent) {
92
+ return;
93
+ }
94
+
95
+ this.expanded = false;
96
+
97
+ this.hideComponentContent();
42
98
  },
43
99
 
44
100
  focus() {
45
- this.$el.focus();
101
+ if (this.trigger) this.trigger.focus();
46
102
  },
47
103
 
48
- onClick() {
49
- if (this.componentContent.visible) this.componentContent.hide();
50
- else this.componentContent.show();
104
+ showComponentContent() {
105
+ this.componentContent.show();
51
106
  },
52
107
 
53
- onEsc() {
54
- if (!this.componentContent) return;
108
+ hideComponentContent() {
55
109
  this.componentContent.hide();
56
110
  },
111
+
112
+ toggleAriaHasPopup() {
113
+ if (this.expanded) {
114
+ this.hasPopup = this.componentContent !== null;
115
+ this.controls = this.hasPopup ? this.componentContent.id : null;
116
+
117
+ return;
118
+ }
119
+
120
+ this.hasPopup = null;
121
+ this.controls = null;
122
+ },
123
+
124
+ onClick(e) {
125
+ this.init(e);
126
+ },
127
+
128
+ onKeyDownOrUp(e) {
129
+ if (!this.expanded) {
130
+ this.$el.click(e);
131
+ }
132
+
133
+ const keyCode = e.code;
134
+ const listItemPosition =
135
+ keyCode === 'ArrowDown'
136
+ ? 'firstMenuItem'
137
+ : keyCode === 'ArrowUp'
138
+ ? 'lastMenuItem'
139
+ : null;
140
+
141
+ // settimeout here is delaying the focusing the element
142
+ // since it is not rendered yet. All items will only
143
+ // be available when the content is fully visible.
144
+ this.$nextTick(() => {
145
+ setTimeout(() => this[listItemPosition].focus(), 100);
146
+ });
147
+ },
148
+
149
+ // change it to a better name or move the methods inside to another function
150
+ onKeyEsc() {
151
+ this.cancel();
152
+ this.focus();
153
+ },
57
154
  },
58
155
  };
59
156
  </script>
@@ -15,7 +15,8 @@
15
15
  :class="[
16
16
  headless
17
17
  ? null
18
- : 'absolute z-50 grid min-w-[222px] overflow-hidden rounded-md py-2 px-3 border-gray-100 bg-white shadow-300',
18
+ : 'absolute z-50 grid overflow-x-hidden rounded-md py-2 px-3 border-gray-100 bg-white shadow-300',
19
+ floatingUiClass ? floatingUiClass : null,
19
20
  ]"
20
21
  >
21
22
  <slot></slot>
@@ -45,6 +46,10 @@ export default {
45
46
  type: Boolean,
46
47
  default: false,
47
48
  },
49
+ floatingUiClass: {
50
+ type: [String, Function],
51
+ default: null,
52
+ },
48
53
  },
49
54
 
50
55
  methods: {
@@ -1,18 +0,0 @@
1
- /**
2
- *
3
- * @param {HTMLElement} el
4
- * @param {HTMLElement} parent
5
- */
6
- export const scrollElementIntoView = (el, parent) => {
7
- // this works better than scrollIntoView
8
- if (parent.scrollHeight <= parent.clientHeight) return;
9
-
10
- const scrollBottom = parent.clientHeight + parent.scrollTop;
11
- const elBottom = el.offsetTop + el.offsetHeight;
12
-
13
- if (elBottom > scrollBottom) {
14
- parent.scrollTop = elBottom - parent.clientHeight;
15
- } else if (el.offsetTop < parent.scrollTop) {
16
- parent.scrollTop = el.offsetTop;
17
- }
18
- };