@veritree/ui 0.21.0 → 0.21.1-1

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.
@@ -0,0 +1,68 @@
1
+ import { computePosition, flip, shift, offset, size } from '@floating-ui/dom';
2
+
3
+ export const floatingUiMixin = {
4
+ props: {
5
+ placement: {
6
+ type: String,
7
+ default: 'bottom-start'
8
+ }
9
+ },
10
+
11
+ data() {
12
+ return {
13
+ component: null,
14
+ componentTrigger: null,
15
+ componentContent: null,
16
+ active: false,
17
+ }
18
+ },
19
+
20
+ watch: {
21
+ active(newVal) {
22
+ if (newVal) this.$nextTick(() => this.positionContentToTrigger());
23
+ },
24
+ },
25
+
26
+ mounted() {
27
+ this.component = {
28
+ setActive: this.setActive,
29
+ clearActive: this.clearActive,
30
+ };
31
+ },
32
+
33
+ methods: {
34
+ setActive() {
35
+ this.active = true;
36
+ },
37
+
38
+ clearActive() {
39
+ this.active = null;
40
+ },
41
+
42
+ positionContentToTrigger() {
43
+ const trigger = document.getElementById(this.componentTrigger.id);
44
+ const content = document.getElementById(this.componentContent.id);
45
+
46
+ computePosition(trigger, content, {
47
+ placement: this.placement,
48
+ middleware: [
49
+ offset(6),
50
+ flip(),
51
+ shift({ padding: 5 }),
52
+ size({
53
+ apply({ rects }) {
54
+ Object.assign(content.style, {
55
+ width: `${rects.reference.width}px`,
56
+ });
57
+ },
58
+ }),
59
+ ],
60
+ }).then(({ x, y }) => {
61
+ Object.assign(content.style, {
62
+ left: `${x}px`,
63
+ top: `${y}px`,
64
+ });
65
+ });
66
+ },
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.21.0",
3
+ "version": "0.21.1-1",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -11,6 +11,7 @@
11
11
  "access": "public"
12
12
  },
13
13
  "dependencies": {
14
+ "@floating-ui/dom": "^1.0.4",
14
15
  "@linusborg/vue-simple-portal": "^0.1.5",
15
16
  "@veritree/icons": "^0.19.0"
16
17
  },
@@ -1,14 +1,21 @@
1
1
  <template>
2
2
  <div
3
- :class="{
4
- Avatar: headless,
5
- 'flex items-center justify-center overflow-hidden rounded-full':
6
- !headless,
7
- 'h-10 w-10': !small,
8
- 'h-8 w-8': small,
9
- 'border border-solid': !dark,
10
- 'bg-white text-fd-500': dark,
11
- }"
3
+ :class="[
4
+ // default styles
5
+ headless
6
+ ? 'avatar'
7
+ : 'flex items-center justify-center overflow-hidden rounded-full bg-white border border-solid',
8
+ // variant styles
9
+ headless ? `avatar--${variant}` : null,
10
+ // sizes styles
11
+ headless
12
+ ? `avatar--${size}`
13
+ : isSmall
14
+ ? 'h-8 w-8'
15
+ : isLarge
16
+ ? 'h-10 w-10'
17
+ : null,
18
+ ]"
12
19
  >
13
20
  <slot></slot>
14
21
  </div>
@@ -18,32 +25,28 @@
18
25
  export default {
19
26
  name: 'VTAvatar',
20
27
 
21
- provide() {
22
- return {
23
- api: () => {
24
- const { dark: isDark, headless: isHeadless, light: isLight } = this;
25
-
26
- return {
27
- isDark,
28
- isHeadless,
29
- isLight,
30
- };
31
- },
32
- };
33
- },
34
-
35
28
  props: {
36
29
  headless: {
37
30
  type: Boolean,
38
31
  default: false,
39
32
  },
40
- dark: {
41
- type: Boolean,
42
- default: false,
33
+ variant: {
34
+ type: String,
35
+ default: 'primary',
43
36
  },
44
- small: {
45
- type: Boolean,
46
- default: false,
37
+ size: {
38
+ type: String,
39
+ default: 'large',
40
+ },
41
+ },
42
+
43
+ computed: {
44
+ isLarge() {
45
+ return this.size === 'large';
46
+ },
47
+
48
+ isSmall() {
49
+ return this.size === 'small';
47
50
  },
48
51
  },
49
52
  };
@@ -11,7 +11,7 @@
11
11
  ? 'button'
12
12
  : isIcon
13
13
  ? 'inline-flex items-center justify-center rounded-full [&_svg]:max-h-full [&_svg]:max-w-full'
14
- : 'relative inline-flex rounded border border-solid px-4 text-sm font-semibold leading-none no-underline transition-all',
14
+ : 'inline-flex rounded border border-solid px-4 text-sm font-semibold leading-none no-underline transition-all',
15
15
  // variant styles
16
16
  headless
17
17
  ? `button--${variant}`
@@ -5,26 +5,31 @@
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: "VTDropdownMenu",
12
+ name: 'VTDropdownMenu',
13
+
14
+ mixins: [floatingUiMixin],
12
15
 
13
16
  provide() {
14
17
  return {
15
18
  api: () => {
16
- const { dark: isDark, headless: isHeadless, right: isRight } = this;
17
- const { id, trigger, content, items } = this;
19
+ const { dark: isDark, headless: isHeadless } = this;
18
20
 
19
21
  const registerTrigger = (trigger) => {
20
- this.trigger = trigger;
22
+ if (!trigger) return;
23
+ this.componentTrigger = trigger;
21
24
  };
22
25
 
23
26
  const registerContent = (content) => {
24
- this.content = content;
27
+ if (!content) return;
28
+ this.componentContent = content;
25
29
  };
26
30
 
27
31
  const registerItem = (item) => {
32
+ if (!item) return;
28
33
  this.items.push(item);
29
34
  };
30
35
 
@@ -33,13 +38,13 @@ export default {
33
38
  };
34
39
 
35
40
  return {
36
- id,
41
+ id: this.componentId,
37
42
  isDark,
38
43
  isHeadless,
39
- isRight,
40
- trigger,
41
- content,
42
- items,
44
+ component: this.component,
45
+ componentTrigger: this.componentTrigger,
46
+ componentContent: this.componentContent,
47
+ items: this.items,
43
48
  registerTrigger,
44
49
  registerContent,
45
50
  registerItem,
@@ -66,11 +71,15 @@ export default {
66
71
 
67
72
  data() {
68
73
  return {
69
- id: `menu-${genId()}`,
70
- trigger: null,
71
- content: null,
74
+ componentId: genId(),
72
75
  items: [],
73
76
  };
74
77
  },
78
+
79
+ computed: {
80
+ id() {
81
+ return `dropdown-menu-${this.componentId}`;
82
+ },
83
+ },
75
84
  };
76
85
  </script>
@@ -1,47 +1,37 @@
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
+ :headless="headless"
6
+ :class="{ 'dropdown-menu-content': headless }"
10
7
  >
11
- <div
12
- v-if="visible"
13
- :id="id"
14
- :class="{
15
- MenuList: headless,
16
- 'absolute top-full mt-3 grid min-w-[222px] overflow-hidden rounded border border-solid py-2 px-3 transition-all':
17
- !headless,
18
- 'border-gray-100 bg-white shadow-300': !dark,
19
- 'bg-forest-default border-gray-700 shadow-gray-700': dark,
20
- 'left-0': !right,
21
- 'right-0': right,
22
- }"
23
- >
24
- <slot></slot>
25
- </div>
26
- </transition>
8
+ <slot></slot>
9
+ </FloatingUi>
27
10
  </template>
28
11
 
29
12
  <script>
30
- import { genId } from '../../utils/ids';
13
+ import FloatingUi from '../Utils/FloatingUi.vue';
31
14
 
32
15
  export default {
33
16
  name: 'VTDropdownMenuContent',
34
17
 
18
+ components: {
19
+ FloatingUi,
20
+ },
21
+
35
22
  inject: ['api'],
36
23
 
37
24
  data() {
38
25
  return {
39
- id: `menucontent-${genId()}`,
40
26
  visible: false,
41
27
  };
42
28
  },
43
29
 
44
30
  computed: {
31
+ id() {
32
+ return `dropdown-menu-content-${this.api().id}`;
33
+ },
34
+
45
35
  dark() {
46
36
  return this.api().isDark;
47
37
  },
@@ -50,44 +40,59 @@ export default {
50
40
  return this.api().isHeadless;
51
41
  },
52
42
 
53
- right() {
54
- return this.api().isRight;
43
+ component() {
44
+ return this.api().component;
55
45
  },
56
46
 
57
- trigger() {
58
- return this.api().trigger;
47
+ componentTrigger() {
48
+ return this.api().componentTrigger;
59
49
  },
60
50
  },
61
51
 
62
52
  mounted() {
63
53
  this.api().registerContent(this);
64
54
 
65
- this.$nextTick(() => {
66
- if (this.trigger) this.trigger.toggleHasPopup();
67
- });
68
-
69
- // TODO: Create a directive or mixin for this
55
+ // T-218 Create a directive or mixin for this
70
56
  document.addEventListener('click', (e) => {
57
+ if (!e) {
58
+ return;
59
+ }
60
+
71
61
  e.stopPropagation();
72
- if (this.visible && !this.$el.contains(e.target)) this.trigger.onClick();
62
+
63
+ if (this.visible && !this.$el.contains(e.target)) {
64
+ this.componentTrigger.onClick();
65
+ }
73
66
  });
74
67
  },
75
68
 
76
69
  destroyed() {
77
- // TODO: Create a directive or mixin for this
78
- document.removeEventListener('click', this.trigger.onClick());
70
+ document.removeEventListener('click', this.componentTrigger.onClick);
79
71
  },
80
72
 
81
73
  methods: {
82
74
  show() {
75
+ if (this.visible) return;
76
+
83
77
  this.visible = true;
84
- this.$emit('shown');
78
+
79
+ this.$nextTick(() => {
80
+ this.component.setActive();
81
+ this.$emit('shown');
82
+ });
85
83
  },
86
84
 
87
85
  hide() {
86
+ if (!this.visible) return;
87
+
88
88
  this.visible = false;
89
- this.$emit('hidden');
90
- this.api().unregisterItems();
89
+
90
+ this.$nextTick(() => {
91
+ this.componentTrigger.focus();
92
+ this.componentTrigger.hideExpanded();
93
+ this.component.clearActive();
94
+ this.$emit('hidden');
95
+ });
91
96
  },
92
97
  },
93
98
  };
@@ -5,11 +5,12 @@
5
5
  :to="to"
6
6
  :class="{
7
7
  MenuItem: headless,
8
- '-mx-3 flex min-w-max items-center gap-3 px-3 py-2 text-inherit no-underline':
8
+ '-mx-3 flex min-w-max items-center gap-2 px-3 py-2 text-inherit no-underline':
9
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,
10
+ 'hover:bg-secondary-200/10': !dark && !headless,
11
+ 'text-white': dark && !headless,
12
+ 'pointer-events-none opacity-75': disabled && !headless,
13
+ 'bg-secondary-200/10': selected && !headless,
13
14
  }"
14
15
  :tabindex="tabIndex"
15
16
  :aria-disabled="disabled"
@@ -52,13 +53,17 @@ export default {
52
53
 
53
54
  data() {
54
55
  return {
55
- id: `menuitem-${genId()}`,
56
56
  index: null,
57
+ selected: false,
57
58
  tabIndex: 0,
58
59
  };
59
60
  },
60
61
 
61
62
  computed: {
63
+ id() {
64
+ return `dropdown-menu-item-${this.api().id}-${genId()}`;
65
+ },
66
+
62
67
  dark() {
63
68
  return this.api().isDark;
64
69
  },
@@ -79,19 +84,20 @@ export default {
79
84
  return this.$el;
80
85
  },
81
86
 
82
- trigger() {
83
- return this.api().trigger;
87
+ componentTrigger() {
88
+ return this.api().componentTrigger;
84
89
  },
85
90
 
86
- content() {
87
- return this.api().content;
91
+ componentContent() {
92
+ return this.api().componentContent;
88
93
  },
89
94
  },
90
95
 
91
96
  mounted() {
92
97
  const item = {
98
+ select: this.select,
99
+ unselect: this.unselect,
93
100
  focus: this.focus,
94
- el: this.el,
95
101
  };
96
102
 
97
103
  this.api().registerItem(item);
@@ -100,10 +106,19 @@ export default {
100
106
  },
101
107
 
102
108
  methods: {
109
+ select() {
110
+ this.selected = true;
111
+ },
112
+
113
+ unselect() {
114
+ this.selected = false;
115
+ },
116
+
103
117
  focus() {
104
118
  if (!this.el) return;
105
119
 
106
120
  this.tabIndex = -1;
121
+ this.selected = true;
107
122
  this.el.focus();
108
123
  },
109
124
 
@@ -145,6 +160,12 @@ export default {
145
160
  */
146
161
  setFocusToItem(goToIndex) {
147
162
  this.tabIndex = 0;
163
+ this.selected = false;
164
+
165
+ // set all selected to false
166
+ this.items.forEach((item) => item.unselect());
167
+
168
+ // focus item
148
169
  this.items[goToIndex].focus();
149
170
  },
150
171
 
@@ -152,8 +173,8 @@ export default {
152
173
  * Hides content/menu and focus on trigger
153
174
  */
154
175
  leaveMenu() {
155
- if (this.content) this.content.hide();
156
- if (this.trigger) this.trigger.focus();
176
+ if (this.componentContent) this.componentContent.hide();
177
+ if (this.componentTrigger) this.componentTrigger.focus();
157
178
  },
158
179
 
159
180
  onKeyEsc() {