@veritree/ui 0.21.1-7 → 0.21.1-9

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.
@@ -18,8 +18,9 @@ export const floatingUiContentMixin = {
18
18
 
19
19
  data() {
20
20
  return {
21
- visible: false,
22
21
  el: null,
22
+ isMousemove: false,
23
+ visible: false,
23
24
  };
24
25
  },
25
26
 
@@ -77,5 +78,25 @@ export const floatingUiContentMixin = {
77
78
  this.componentTrigger.cancel();
78
79
  }
79
80
  },
81
+
82
+ // Mousemove instead of mouseover to support keyboard navigation.
83
+ // The problem with mouseover is that when scrolling (scrollIntoView),
84
+ // mouseover event gets triggered.
85
+ setMousemove() {
86
+ this.isMousemove = true;
87
+ },
88
+
89
+ unsetMousemove() {
90
+ this.isMousemove = false;
91
+ },
92
+
93
+ getMousemove() {
94
+ return this.isMousemove;
95
+ },
96
+
97
+ //
98
+ setActiveDescedant(id) {
99
+ this.activeDescedant = id;
100
+ },
80
101
  },
81
102
  }
@@ -0,0 +1,222 @@
1
+ import FloatingUiItem from '../src/components/Utils/FloatingUiItem.vue';
2
+
3
+ export const floatingUiItemMixin = {
4
+ components: {
5
+ FloatingUiItem,
6
+ },
7
+
8
+ props: {
9
+ headless: {
10
+ type: Boolean,
11
+ default: false,
12
+ },
13
+ disabled: {
14
+ type: Boolean,
15
+ default: false,
16
+ },
17
+ },
18
+
19
+ computed: {
20
+ id() {
21
+ return `${this.componentName}-${this.apiInjected().id}-${this.index}`;
22
+ },
23
+
24
+ items() {
25
+ return this.apiInjected().items;
26
+ },
27
+
28
+ el() {
29
+ return this.$el;
30
+ },
31
+
32
+ componentContent() {
33
+ return this.apiInjected().componentContent;
34
+ },
35
+
36
+ componentTrigger() {
37
+ return this.apiInjected().componentTrigger;
38
+ },
39
+
40
+ classComputed() {
41
+ return [
42
+ // default styles
43
+ this.headless
44
+ ? `${this.componentName}`
45
+ : 'relative z-10 flex items-center gap-2 px-3 py-2 text-inherit no-underline',
46
+ // disabled state styles
47
+ this.headless
48
+ ? this.disabled
49
+ ? `${this.componentName}--disabled`
50
+ : null
51
+ : this.disabled
52
+ ? 'pointer-events-none opacity-75'
53
+ : null,
54
+ // selected state styles
55
+ this.headless
56
+ ? this.selected
57
+ ? `${this.componentName}--selected`
58
+ : null
59
+ : this.selected
60
+ ? 'bg-secondary-200/10'
61
+ : null,
62
+ ];
63
+ },
64
+ },
65
+
66
+ data() {
67
+ return {
68
+ index: null,
69
+ selected: false,
70
+ tabIndex: 0,
71
+ };
72
+ },
73
+
74
+ watch: {
75
+ selected(newValue) {
76
+ if (!newValue || !this.componentContent) return;
77
+
78
+ this.componentContent.setActiveDescedant(this.id);
79
+
80
+ const isMousemove = this.componentContent.getMousemove();
81
+
82
+ if (!isMousemove) {
83
+ this.el.scrollIntoView({ block: 'nearest' });
84
+ }
85
+ },
86
+ },
87
+
88
+ mounted() {
89
+ this.init();
90
+ this.addMouseEventListeners();
91
+ },
92
+
93
+ beforeDestroy() {
94
+ this.apiInjected().unregisterItem(this.id);
95
+ this.removeMouseEventListeners();
96
+ },
97
+
98
+ methods: {
99
+ init() {
100
+ const item = {
101
+ id: this.id,
102
+ select: this.select,
103
+ unselect: this.unselect,
104
+ focus: this.focus,
105
+ onClick: this.onClick,
106
+ };
107
+
108
+ this.apiInjected().registerItem(item);
109
+
110
+ this.index = this.items.length - 1;
111
+ },
112
+
113
+ addMouseEventListeners() {
114
+ this.el.addEventListener('mousemove', this.onMousemove);
115
+ this.el.addEventListener('mouseleave', this.onMouseleave);
116
+ },
117
+
118
+ removeMouseEventListeners() {
119
+ this.el.removeEventListener('mousemove', this.onMousemove);
120
+ this.el.removeEventListener('mouseleave', this.onMouseleave);
121
+ },
122
+
123
+ select() {
124
+ this.selected = true;
125
+ },
126
+
127
+ unselect() {
128
+ this.selected = false;
129
+ },
130
+
131
+ focus() {
132
+ if (!this.el) return;
133
+
134
+ this.tabIndex = -1;
135
+ this.selected = true;
136
+ this.el.focus();
137
+ },
138
+
139
+ focusFirstItem() {
140
+ this.setFocusToItem(0);
141
+ },
142
+
143
+ focusLastItem() {
144
+ this.setFocusToItem(this.items.length - 1);
145
+ },
146
+
147
+ /**
148
+ * Focus the previous item in the menu.
149
+ * If is the first item, jump to the last item.
150
+ */
151
+ focusPreviousItem() {
152
+ const isLast = this.index === this.items.length - 1;
153
+ const goToIndex = isLast ? 0 : this.index + 1;
154
+
155
+ this.setFocusToItem(goToIndex);
156
+ },
157
+
158
+ /**
159
+ * Focus the next item in the menu.
160
+ * If is the last item, jump to the first item.
161
+ */
162
+ focusNextItem() {
163
+ const isFirst = this.index === 0;
164
+ const goToIndex = isFirst ? this.items.length - 1 : this.index - 1;
165
+
166
+ this.setFocusToItem(goToIndex);
167
+ },
168
+
169
+ /**
170
+ * Focus item by remove its tabindex and calling
171
+ * focus to the element.
172
+ *
173
+ * @param {Number, String} goToIndex
174
+ */
175
+ setFocusToItem(goToIndex) {
176
+ this.tabIndex = 0;
177
+ this.selected = false;
178
+
179
+ // set all selected to false
180
+ this.items.forEach((item) => item.unselect());
181
+
182
+ // focus item
183
+ this.items[goToIndex].focus();
184
+ },
185
+
186
+ leaveMenu() {
187
+ if (this.componentTrigger) {
188
+ this.componentTrigger.cancel();
189
+ this.componentTrigger.focus();
190
+ }
191
+ },
192
+
193
+ onClick() {
194
+ if (this.disabled) {
195
+ return;
196
+ }
197
+
198
+ this.value ? this.apiInjected().emit(this.value) : this.$emit('click');
199
+ this.$nextTick(() => this.leaveMenu());
200
+ },
201
+
202
+ onKeyEsc() {
203
+ this.leaveMenu();
204
+ },
205
+
206
+ onMousemove() {
207
+ if (this.selected) {
208
+ return;
209
+ }
210
+
211
+ this.items.forEach((item) => item.unselect());
212
+
213
+ this.select();
214
+ this.componentContent.setMousemove();
215
+ },
216
+
217
+ onMouseleave() {
218
+ this.unselect();
219
+ this.componentContent.unsetMousemove();
220
+ },
221
+ },
222
+ };
@@ -4,8 +4,8 @@ export const floatingUiMixin = {
4
4
  props: {
5
5
  placement: {
6
6
  type: String,
7
- default: 'bottom-start'
8
- }
7
+ default: 'bottom-start',
8
+ },
9
9
  },
10
10
 
11
11
  data() {
@@ -14,7 +14,7 @@ export const floatingUiMixin = {
14
14
  componentTrigger: null,
15
15
  componentContent: null,
16
16
  active: false,
17
- }
17
+ };
18
18
  },
19
19
 
20
20
  watch: {
@@ -40,11 +40,11 @@ export const floatingUiMixin = {
40
40
  },
41
41
 
42
42
  positionContentToTrigger() {
43
+ // console.log(window.innerWidth)
44
+
43
45
  const trigger = document.getElementById(this.componentTrigger.id);
44
46
  const content = document.getElementById(this.componentContent.id);
45
47
 
46
- // console.log(this.placement);
47
-
48
48
  computePosition(trigger, content, {
49
49
  placement: this.placement,
50
50
  middleware: [
@@ -53,8 +53,14 @@ export const floatingUiMixin = {
53
53
  shift({ padding: 5 }),
54
54
  size({
55
55
  apply({ rects }) {
56
+ // the min width to floating uis should be 200
57
+ // since less than that can look not as nice
58
+ const minWidthLimit = 200;
59
+ const width = rects.reference.width;
60
+ const minWidth = width < minWidthLimit ? minWidthLimit : width;
61
+
56
62
  Object.assign(content.style, {
57
- minWidth: `${rects.reference.width}px`,
63
+ minWidth: `${minWidth}px`,
58
64
  });
59
65
  },
60
66
  }),
@@ -66,5 +72,5 @@ export const floatingUiMixin = {
66
72
  });
67
73
  });
68
74
  },
69
- }
70
- }
75
+ },
76
+ };
@@ -0,0 +1,72 @@
1
+ export const formControlMixin = {
2
+ model: {
3
+ prop: 'value',
4
+ event: 'input',
5
+ },
6
+
7
+ props: {
8
+ disabled: {
9
+ type: Boolean,
10
+ default: false,
11
+ },
12
+ headless: {
13
+ type: Boolean,
14
+ default: false,
15
+ },
16
+ value: {
17
+ type: [String, Number, Object, Array],
18
+ default: null,
19
+ },
20
+ variant: {
21
+ type: [String, Object, Function],
22
+ default: '',
23
+ },
24
+ },
25
+
26
+ computed: {
27
+ listeners() {
28
+ // `Object.assign` merges objects together to form a new object
29
+ return Object.assign(
30
+ {},
31
+ // We add all the listeners from the parent
32
+ this.$listeners,
33
+ // Then we can add custom listeners or override the
34
+ // behavior of some listeners.
35
+ {
36
+ // This ensures that the component works with v-model
37
+ input: (event) => {
38
+ // if (this.lazy) return;
39
+ this.$emit('input', event.target.value);
40
+ },
41
+ blur: (event) => {
42
+ this.$emit('blur', event);
43
+ },
44
+ }
45
+ );
46
+ },
47
+
48
+ classComputed() {
49
+ return [
50
+ this.headless
51
+ ? `${this.name}`
52
+ : 'leading-0 flex w-full max-w-full appearance-none items-center justify-between gap-3 rounded border border-solid px-3 py-2 font-inherit text-base text-inherit file:hidden focus:border-secondary-200',
53
+ // variant styles
54
+ this.headless
55
+ ? `${this.name}--${this.variant}`
56
+ : this.isError
57
+ ? 'border-error-300'
58
+ : 'border-gray-300',
59
+ // height styles
60
+ this.headless
61
+ ? null
62
+ : this.name === 'textarea'
63
+ ? 'min-h-10' // limit it because input type number height can be different from other input types
64
+ : 'h-10',
65
+ ];
66
+ },
67
+
68
+ isError() {
69
+ return this.variant === 'error';
70
+ },
71
+ },
72
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.21.1-7",
3
+ "version": "0.21.1-9",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,8 +16,6 @@ export default {
16
16
  provide() {
17
17
  return {
18
18
  apiDropdownMenu: () => {
19
- const { dark: isDark, headless: isHeadless } = this;
20
-
21
19
  const registerTrigger = (trigger) => {
22
20
  if (!trigger) return;
23
21
  this.componentTrigger = trigger;
@@ -33,8 +31,9 @@ export default {
33
31
  this.items.push(item);
34
32
  };
35
33
 
36
- const unregisterItems = () => {
37
- this.items = [];
34
+ const unregisterItem = (id) => {
35
+ const index = this.items.findIndex((item) => item.id === id);
36
+ this.items.splice(index, 1);
38
37
  };
39
38
 
40
39
  return {
@@ -46,7 +45,7 @@ export default {
46
45
  registerTrigger,
47
46
  registerContent,
48
47
  registerItem,
49
- unregisterItems,
48
+ unregisterItem,
50
49
  };
51
50
  },
52
51
  };
@@ -39,6 +39,10 @@ export default {
39
39
  id: this.id,
40
40
  hide: this.hide,
41
41
  show: this.show,
42
+ getMousemove: this.getMousemove,
43
+ setMousemove: this.setMousemove,
44
+ unsetMousemove: this.unsetMousemove,
45
+ setActiveDescedant: this.setActiveDescedant,
42
46
  };
43
47
 
44
48
  this.apiDropdownMenu().registerContent(content);
@@ -3,30 +3,10 @@
3
3
  :is="as"
4
4
  :id="id"
5
5
  :to="to"
6
- :class="[
7
- // default styles
8
- headless
9
- ? 'dropdown-menu-item'
10
- : 'relative z-10 -mx-3 flex items-center gap-2 px-3 py-2 text-inherit no-underline hover:bg-secondary-200/10',
11
- // disabled state styles
12
- headless
13
- ? disabled
14
- ? 'listbox-item--disabled'
15
- : null
16
- : disabled
17
- ? 'pointer-events-none opacity-75'
18
- : null,
19
- // selected state styles
20
- headless
21
- ? selected
22
- ? 'lisbox-item--selected'
23
- : null
24
- : selected
25
- ? 'bg-secondary-200/10'
26
- : null,
27
- ]"
28
- :tabindex="tabIndex"
6
+ :class="classComputed"
29
7
  :aria-disabled="disabled"
8
+ :tabindex="tabIndex"
9
+ class="-mx-3"
30
10
  role="menuitem"
31
11
  @click.stop.prevent="onClick"
32
12
  @keydown.down.prevent="focusPreviousItem"
@@ -42,11 +22,13 @@
42
22
  </template>
43
23
 
44
24
  <script>
45
- import { genId } from '../../utils/ids';
25
+ import { floatingUiItemMixin } from '../../../mixins/floating-ui-item';
46
26
 
47
27
  export default {
48
28
  name: 'VTDropdownMenuItem',
49
29
 
30
+ mixins: [floatingUiItemMixin],
31
+
50
32
  inject: ['apiDropdownMenu'],
51
33
 
52
34
  props: {
@@ -58,139 +40,19 @@ export default {
58
40
  type: String,
59
41
  default: null,
60
42
  },
61
- disabled: {
62
- type: Boolean,
63
- default: false,
64
- },
65
- headless: {
66
- type: Boolean,
67
- default: false,
68
- },
69
43
  },
70
44
 
71
45
  data() {
72
46
  return {
73
- index: null,
74
- selected: false,
75
- tabIndex: 0,
47
+ apiInjected: this.apiDropdownMenu,
48
+ componentName: 'dropdown-menu-item',
76
49
  };
77
50
  },
78
51
 
79
52
  computed: {
80
- id() {
81
- return `dropdown-menu-item-${this.apiDropdownMenu().id}-${genId()}`;
82
- },
83
-
84
53
  as() {
85
54
  return this.href ? 'a' : this.to ? 'NuxtLink' : 'button';
86
55
  },
87
-
88
- items() {
89
- return this.apiDropdownMenu().items;
90
- },
91
-
92
- el() {
93
- return this.$el;
94
- },
95
-
96
- componentTrigger() {
97
- return this.apiDropdownMenu().componentTrigger;
98
- },
99
- },
100
-
101
- mounted() {
102
- const item = {
103
- select: this.select,
104
- unselect: this.unselect,
105
- focus: this.focus,
106
- };
107
-
108
- this.apiDropdownMenu().registerItem(item);
109
-
110
- this.index = this.items.length - 1;
111
- },
112
-
113
- methods: {
114
- select() {
115
- this.selected = true;
116
- },
117
-
118
- unselect() {
119
- this.selected = false;
120
- },
121
-
122
- focus() {
123
- if (!this.el) return;
124
-
125
- this.tabIndex = -1;
126
- this.selected = true;
127
- this.el.focus();
128
- },
129
-
130
- focusFirstItem() {
131
- this.setFocusToItem(0);
132
- },
133
-
134
- focusLastItem() {
135
- this.setFocusToItem(this.items.length - 1);
136
- },
137
-
138
- /**
139
- * Focus the previous item in the menu.
140
- * If is the first item, jump to the last item.
141
- */
142
- focusPreviousItem() {
143
- const isLast = this.index === this.items.length - 1;
144
- const goToIndex = isLast ? 0 : this.index + 1;
145
-
146
- this.setFocusToItem(goToIndex);
147
- },
148
-
149
- /**
150
- * Focus the next item in the menu.
151
- * If is the last item, jump to the first item.
152
- */
153
- focusNextItem() {
154
- const isFirst = this.index === 0;
155
- const goToIndex = isFirst ? this.items.length - 1 : this.index - 1;
156
-
157
- this.setFocusToItem(goToIndex);
158
- },
159
-
160
- /**
161
- * Focus item by remove its tabindex and calling
162
- * focus to the element.
163
- *
164
- * @param {Number, String} goToIndex
165
- */
166
- setFocusToItem(goToIndex) {
167
- this.tabIndex = 0;
168
- this.selected = false;
169
-
170
- // set all selected to false
171
- this.items.forEach((item) => item.unselect());
172
-
173
- // focus item
174
- this.items[goToIndex].focus();
175
- },
176
-
177
- leaveMenu() {
178
- if (this.componentTrigger) {
179
- this.componentTrigger.cancel();
180
- this.componentTrigger.focus();
181
- }
182
- },
183
-
184
- onKeyEsc() {
185
- this.leaveMenu();
186
- },
187
-
188
- onClick() {
189
- if (this.disabled) return;
190
-
191
- this.$emit('click');
192
- this.$nextTick(() => this.leaveMenu());
193
- },
194
56
  },
195
57
  };
196
58
  </script>
@@ -1,52 +1,40 @@
1
1
  <template>
2
2
  <input
3
- v-bind="$attrs"
4
- :class="[
5
- headless
6
- ? 'form-control'
7
- : 'border border-solid py-2 px-3 rounded text-inherit max-w-full',
8
- headless
9
- ? `form-control--${variant}`
10
- : isError
11
- ? 'border-error-300'
12
- : 'border-gray-300',
13
- ]"
3
+ :class="classComputed"
14
4
  :value="value"
15
- @input="$emit('input', $event.target.value)"
16
- @blur="$emit('blur')"
5
+ :disabled="disabled"
6
+ v-on="listeners"
17
7
  />
18
8
  </template>
19
9
 
20
10
  <script>
21
- export default {
22
- model: {
23
- prop: 'value',
24
- event: 'input',
25
- },
11
+ import { formControlMixin } from '../../../mixins/form-control';
26
12
 
27
- props: {
28
- disabled: {
29
- type: Boolean,
30
- default: false,
31
- },
32
- value: {
33
- type: [String, Number, Object, Array],
34
- default: null,
35
- },
36
- headless: {
37
- type: Boolean,
38
- default: false,
39
- },
40
- variant: {
41
- type: [String, Object, Function],
42
- default: '',
43
- },
44
- },
13
+ export default {
14
+ mixins: [formControlMixin],
45
15
 
46
- computed: {
47
- isError() {
48
- return this.variant === 'error';
49
- },
16
+ data() {
17
+ return {
18
+ name: 'input',
19
+ };
50
20
  },
51
21
  };
52
22
  </script>
23
+
24
+ <style scoped>
25
+ input[type='date']::-webkit-inner-spin-button,
26
+ input[type='date']::-webkit-calendar-picker-indicator {
27
+ position: absolute;
28
+ opacity: 0;
29
+ }
30
+
31
+ input[type='number'] {
32
+ appearance: textfield;
33
+ }
34
+
35
+ input[type='number']::-webkit-inner-spin-button,
36
+ input[type='number']::-webkit-outer-spin-button {
37
+ appearance: none;
38
+ -webkit-appearance: none;
39
+ }
40
+ </style>