@veritree/ui 0.20.1 → 0.21.1-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.
@@ -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' : 'flex gap-2 mt-1 items-baseline',
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 shrink-0 h-4 w-4',
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-gray-500 text-sm']">
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() {
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <input
3
+ v-bind="$attrs"
4
+ :class="[
5
+ headless
6
+ ? 'form-control'
7
+ : 'border border-solid py-2 px-3 rounded text-inherit',
8
+ headless
9
+ ? `form-control--${variant}`
10
+ : isError
11
+ ? 'border-error-300'
12
+ : 'border-gray-300',
13
+ ]"
14
+ :value="value"
15
+ @input="$emit('input', $event.target.value)"
16
+ @blur="$emit('blur')"
17
+ />
18
+ </template>
19
+
20
+ <script>
21
+ export default {
22
+ model: {
23
+ prop: 'value',
24
+ event: 'input',
25
+ },
26
+
27
+ props: {
28
+ disabled: {
29
+ type: Boolean,
30
+ default: false,
31
+ },
32
+ value: {
33
+ type: [String, Number, Object, Array],
34
+ default: null,
35
+ },
36
+ variant: {
37
+ type: [String, Object, Function],
38
+ default: '',
39
+ },
40
+ headless: {
41
+ type: Boolean,
42
+ default: false,
43
+ },
44
+ },
45
+
46
+ computed: {
47
+ isError() {
48
+ return this.variant === 'error';
49
+ },
50
+ },
51
+ };
52
+ </script>
@@ -1,29 +1,31 @@
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 { genId } from "../../utils/ids";
8
+ import { floatingUiMixin } from '../../../mixins/floating-ui';
9
+ import { genId } from '../../utils/ids';
9
10
 
10
11
  export default {
11
- name: "VTListbox",
12
+ name: 'VTListbox',
13
+
14
+ mixins: [floatingUiMixin],
12
15
 
13
16
  provide() {
14
17
  return {
15
18
  api: () => {
16
- const { dark: isDark, right: isRight } = this;
17
- const { id, listbox, trigger, content, search, list, items } = this;
19
+ const { dark: isDark } = this;
18
20
 
19
21
  const registerTrigger = (trigger) => {
20
22
  if (!trigger) return;
21
- this.trigger = trigger;
23
+ this.componentTrigger = trigger;
22
24
  };
23
25
 
24
26
  const registerContent = (content) => {
25
27
  if (!content) return;
26
- this.content = content;
28
+ this.componentContent = content;
27
29
  };
28
30
 
29
31
  const registerSearch = (search) => {
@@ -47,20 +49,19 @@ export default {
47
49
  };
48
50
 
49
51
  const emit = (value) => {
50
- this.$emit("input", value);
51
- this.$emit("change", value);
52
+ this.$emit('input', value);
53
+ this.$emit('change', value);
52
54
  };
53
55
 
54
56
  return {
55
- id,
57
+ id: this.componentId,
56
58
  isDark,
57
- isRight,
58
- listbox,
59
- trigger,
60
- search,
61
- content,
62
- list,
63
- items,
59
+ component: this.component,
60
+ componentTrigger: this.componentTrigger,
61
+ componentContent: this.componentContent,
62
+ list: this.list,
63
+ items: this.items,
64
+ search: this.search,
64
65
  registerTrigger,
65
66
  registerContent,
66
67
  registerSearch,
@@ -94,31 +95,16 @@ export default {
94
95
 
95
96
  data() {
96
97
  return {
97
- id: `listbox-${genId()}`,
98
- listbox: null,
99
- trigger: null,
100
- content: null,
98
+ componentId: genId(),
101
99
  search: null,
102
100
  list: null,
103
101
  items: [],
104
- active: false,
105
- };
106
- },
107
-
108
- mounted() {
109
- this.listbox = {
110
- setActive: this.setActive,
111
- clearActive: this.clearActive,
112
102
  };
113
103
  },
114
104
 
115
- methods: {
116
- setActive() {
117
- this.active = true;
118
- },
119
-
120
- clearActive() {
121
- this.active = null;
105
+ computed: {
106
+ id() {
107
+ return `listbox-${this.componentId}`;
122
108
  },
123
109
  },
124
110
  };
@@ -1,43 +1,27 @@
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
+ :aria-activedescendant="activeDescedant"
6
+ :headless="headless"
7
+ :class="{ 'listbox-content': headless }"
8
+ role="listbox"
10
9
  >
11
- <div
12
- v-if="visible"
13
- :id="id"
14
- :aria-activedescendant="activeDescedant"
15
- :class="{
16
- MenuList: headless,
17
- 'absolute z-10 grid w-full min-w-[222px] overflow-hidden rounded-md py-2 px-3':
18
- !headless,
19
- 'border-gray-100 bg-white shadow-300': !dark && !headless,
20
- 'bg-forest-default border border-solid border-gray-700 shadow-gray-700':
21
- dark && !headless,
22
- 'left-0': !right && !headless,
23
- 'right-0': right && !headless,
24
- 'top-full mt-3': isTop && !headless,
25
- 'bottom-full mb-3': isBottom && !headless,
26
- }"
27
- role="listbox"
28
- >
29
- <slot></slot>
30
- </div>
31
- </transition>
10
+ <slot></slot>
11
+ </FloatingUi>
32
12
  </template>
33
13
 
34
14
  <script>
35
- import { genId } from "../../utils/ids";
15
+ import FloatingUi from '../Utils/FloatingUi.vue';
36
16
 
37
17
  export default {
38
- name: "VTListboxContent",
18
+ name: 'VTListboxContent',
39
19
 
40
- inject: ["api"],
20
+ components: {
21
+ FloatingUi,
22
+ },
23
+
24
+ inject: ['api'],
41
25
 
42
26
  props: {
43
27
  headless: {
@@ -56,41 +40,27 @@ export default {
56
40
 
57
41
  data() {
58
42
  return {
59
- id: `listboxcontent-${genId()}`,
60
43
  activeDescedant: null,
61
44
  visible: false,
62
45
  };
63
46
  },
64
47
 
65
48
  computed: {
66
- dark() {
67
- return this.api().isDark;
68
- },
69
-
70
- right() {
71
- return this.api().isRight;
49
+ id() {
50
+ return `listbox-content-${this.api().id}`;
72
51
  },
73
52
 
74
- listbox() {
75
- return this.api().listbox;
53
+ component() {
54
+ return this.api().component;
76
55
  },
77
56
 
78
- trigger() {
79
- return this.api().trigger;
57
+ componentTrigger() {
58
+ return this.api().componentTrigger;
80
59
  },
81
60
 
82
61
  search() {
83
62
  return this.api().search;
84
63
  },
85
-
86
- // directions
87
- isTop() {
88
- return this.top && !this.bottom;
89
- },
90
-
91
- isBottom() {
92
- return this.bottom;
93
- },
94
64
  },
95
65
 
96
66
  mounted() {
@@ -103,16 +73,23 @@ export default {
103
73
 
104
74
  this.api().registerContent(content);
105
75
 
106
- // TODO: Create a directive or mixin for this
107
- document.addEventListener("click", (e) => {
76
+ // T-107 Create a directive or mixin for this
77
+ document.addEventListener('click', (e) => {
78
+ if (!e) {
79
+ return;
80
+ }
81
+
108
82
  e.stopPropagation();
109
- if (this.visible && !this.$el.contains(e.target)) this.trigger.onClick();
83
+
84
+ if (this.visible && !this.$el.contains(e.target)) {
85
+ this.componentTrigger.onClick();
86
+ }
110
87
  });
111
88
  },
112
89
 
113
90
  destroyed() {
114
- // TODO: Create a directive or mixin for this
115
- document.removeEventListener("click", this.trigger.onClick());
91
+ // T-162 Create a directive or mixin for this
92
+ document.removeEventListener('click', this.componentTrigger.onClick);
116
93
  },
117
94
 
118
95
  methods: {
@@ -122,8 +99,7 @@ export default {
122
99
  this.visible = true;
123
100
 
124
101
  this.$nextTick(() => {
125
- this.listbox.setActive();
126
-
102
+ this.component.setActive();
127
103
  if (this.search) this.search.el.focus();
128
104
  });
129
105
  },
@@ -134,12 +110,9 @@ export default {
134
110
  this.visible = false;
135
111
 
136
112
  this.$nextTick(() => {
137
- this.trigger.focus();
138
-
139
- setTimeout(() => {
140
- this.listbox.clearActive();
141
- this.trigger.contract();
142
- }, 100);
113
+ this.componentTrigger.focus();
114
+ this.componentTrigger.toggleExpanded();
115
+ this.component.clearActive();
143
116
  });
144
117
  },
145
118
 
@@ -29,13 +29,13 @@
29
29
  </template>
30
30
 
31
31
  <script>
32
- import { scrollElementIntoView } from "../../utils/components";
33
- import { genId } from "../../utils/ids";
32
+ import { scrollElementIntoView } from '../../utils/components';
33
+ import { genId } from '../../utils/ids';
34
34
 
35
35
  export default {
36
- name: "VTListboxItem",
36
+ name: 'VTListboxItem',
37
37
 
38
- inject: ["api"],
38
+ inject: ['api'],
39
39
 
40
40
  props: {
41
41
  headless: {
@@ -74,12 +74,12 @@ export default {
74
74
  return this.$el;
75
75
  },
76
76
 
77
- trigger() {
78
- return this.api().trigger;
77
+ componentTrigger() {
78
+ return this.api().componentTrigger;
79
79
  },
80
80
 
81
- content() {
82
- return this.api().content;
81
+ componentContent() {
82
+ return this.api().componentContent;
83
83
  },
84
84
 
85
85
  list() {
@@ -95,8 +95,8 @@ export default {
95
95
  selected(newValue) {
96
96
  if (!newValue || !this.list) return;
97
97
 
98
- if (this.content) {
99
- this.content.setActiveDescedant(this.id);
98
+ if (this.componentContent) {
99
+ this.componentContent.setActiveDescedant(this.id);
100
100
  }
101
101
 
102
102
  const isMousemove = this.list.getMousemove();
@@ -193,10 +193,10 @@ export default {
193
193
  },
194
194
 
195
195
  /**
196
- * Hides content/menu and focus on trigger
196
+ * Hides componentContent/menu and focus on componentTrigger
197
197
  */
198
198
  leaveMenu() {
199
- if (this.content) this.content.hide();
199
+ if (this.componentContent) this.componentContent.hide();
200
200
  },
201
201
 
202
202
  onKeyEsc() {
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <input
3
3
  v-model="search"
4
- :class="{ ListboxList: headless, 'form-control mb-1': !headless }"
4
+ :class="{ 'listbox-search': headless, 'form-control mb-1': !headless }"
5
5
  type="text"
6
6
  @input="onChange"
7
7
  @keydown.down.prevent="focusNextItem"
@@ -35,12 +35,8 @@ export default {
35
35
  },
36
36
 
37
37
  computed: {
38
- content() {
39
- return this.api().content;
40
- },
41
-
42
- trigger() {
43
- return this.api().trigger;
38
+ componentContent() {
39
+ return this.api().componentContent;
44
40
  },
45
41
 
46
42
  list() {
@@ -131,7 +127,7 @@ export default {
131
127
  },
132
128
 
133
129
  hide() {
134
- if (this.content) this.content.hide();
130
+ if (this.componentContent) this.componentContent.hide();
135
131
  },
136
132
  },
137
133
  };
@@ -1,9 +1,10 @@
1
1
  <template>
2
2
  <button
3
+ :id="id"
3
4
  :aria-expanded="expanded"
4
5
  :aria-haspopup="hasPopup"
5
6
  :class="{
6
- 'Listbox-button': headless,
7
+ 'listbox-button': headless,
7
8
  'flex w-full justify-between rounded-md border border-solid py-2 px-3':
8
9
  !headless,
9
10
  'border-gray-300 text-gray-500': !dark && !headless,
@@ -11,14 +12,14 @@
11
12
  dark && !headless,
12
13
  }"
13
14
  type="button"
14
- @click.stop.prevent="onClick"
15
+ @click.prevent="onClick"
15
16
  @keydown.down.prevent="onKeyArrowDown"
16
17
  @keydown.up.prevent="onKeyArrowUp"
17
18
  @keydown.esc.stop="onKeyEsc"
18
19
  >
19
20
  <span
20
21
  :class="{
21
- 'Listbox-button__text': headless,
22
+ 'listbox-button__text': headless,
22
23
  'text-left': !headless,
23
24
  }"
24
25
  >
@@ -26,7 +27,7 @@
26
27
  </span>
27
28
  <span
28
29
  :class="{
29
- 'Listbox-button__icon': headless,
30
+ 'listbox-button__icon': headless,
30
31
  }"
31
32
  >
32
33
  <IconChevronDown
@@ -62,12 +63,16 @@ export default {
62
63
  },
63
64
 
64
65
  computed: {
66
+ id() {
67
+ return `listbox-trigger-${this.api().id}`;
68
+ },
69
+
65
70
  dark() {
66
71
  return this.api().isDark;
67
72
  },
68
73
 
69
- content() {
70
- return this.api().content;
74
+ componentContent() {
75
+ return this.api().componentContent;
71
76
  },
72
77
 
73
78
  items() {
@@ -85,10 +90,11 @@ export default {
85
90
 
86
91
  mounted() {
87
92
  const trigger = {
93
+ toggleExpanded: this.toggleExpanded,
88
94
  el: this.$el,
89
95
  focus: this.focus,
96
+ id: this.id,
90
97
  onClick: this.onClick,
91
- contract: this.contract,
92
98
  };
93
99
 
94
100
  this.api().registerTrigger(trigger);
@@ -100,14 +106,14 @@ export default {
100
106
  */
101
107
  showContent() {
102
108
  this.expanded = true;
103
- this.content.show();
109
+ this.componentContent.show();
104
110
  },
105
111
 
106
112
  focus() {
107
113
  this.$el.focus();
108
114
  },
109
115
 
110
- contract() {
116
+ toggleExpanded() {
111
117
  if (!this.expanded) return;
112
118
  this.expanded = false;
113
119
  },
@@ -119,9 +125,19 @@ export default {
119
125
  * 2. Open the menu if it's closed
120
126
  * 3. Close the menu if it's open
121
127
  */
122
- onClick() {
123
- if (!this.content) return;
124
- this.expanded ? this.content.hide() : this.showContent();
128
+ onClick(e) {
129
+ if (!this.componentContent) return;
130
+
131
+ if (this.expanded) {
132
+ this.componentContent.hide();
133
+ return;
134
+ }
135
+
136
+ // delay stop propagation to close other visible
137
+ // dropdowns and delay click event to control
138
+ // this dropdown visibility
139
+ setTimeout(() => e.stopImmediatePropagation(), 50);
140
+ setTimeout(() => this.showContent(), 100);
125
141
  },
126
142
 
127
143
  /**
@@ -131,12 +147,15 @@ export default {
131
147
  * 2. if the menu is expanded, focus the first menu item
132
148
  */
133
149
  onKeyArrowDown() {
134
- if (!this.content) return;
150
+ if (!this.componentContent) return;
135
151
 
136
152
  this.showContent();
137
153
 
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.
138
157
  this.$nextTick(() => {
139
- this.firstMenuItem.focus();
158
+ setTimeout(() => this.firstMenuItem.focus(), 150);
140
159
  });
141
160
  },
142
161
 
@@ -147,21 +166,24 @@ export default {
147
166
  * 2. if the menu is expanded, focus the last menu item
148
167
  */
149
168
  onKeyArrowUp() {
150
- if (!this.content) return;
169
+ if (!this.componentContent) return;
151
170
 
152
171
  this.showContent();
153
172
 
173
+ // settimeout here is delaying the focusing the element
174
+ // since it is not rendered yet. All items will only
175
+ // be available when the content is fully visible.
154
176
  this.$nextTick(() => {
155
- this.lastMenuItem.focus();
177
+ setTimeout(() => this.lastMenuItem.focus(), 150);
156
178
  });
157
179
  },
158
180
 
159
181
  onKeyEsc() {
160
- if (!this.content) return;
182
+ if (!this.componentContent) return;
161
183
 
162
184
  if (this.expanded) {
163
185
  this.toggleExpanded();
164
- this.content.hide();
186
+ this.componentContent.hide();
165
187
  }
166
188
  },
167
189
  },