@veritree/ui 0.21.1-6 → 0.21.1-8

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,81 @@
1
+ import FloatingUi from '../src/components/Utils/FloatingUi.vue';
2
+
3
+ export const floatingUiContentMixin = {
4
+ components: {
5
+ FloatingUi,
6
+ },
7
+
8
+ props: {
9
+ headless: {
10
+ type: Boolean,
11
+ default: false,
12
+ },
13
+ floatingUiClass: {
14
+ type: [String, Function],
15
+ default: null,
16
+ },
17
+ },
18
+
19
+ data() {
20
+ return {
21
+ visible: false,
22
+ el: null,
23
+ };
24
+ },
25
+
26
+ mounted() {
27
+ document.addEventListener('click', (e) => this.onDocumentClick(e));
28
+ },
29
+
30
+ destroyed() {
31
+ document.removeEventListener('click', this.onDocumentClick);
32
+ },
33
+
34
+ methods: {
35
+ show() {
36
+ if (this.visible) {
37
+ return;
38
+ }
39
+
40
+ this.visible = true;
41
+
42
+ this.$nextTick(() => {
43
+ this.component.setActive();
44
+
45
+ setTimeout(() => {
46
+ this.el = document.getElementById(this.id);
47
+ this.$emit('shown');
48
+ }, 100);
49
+ });
50
+ },
51
+
52
+ hide() {
53
+ if (!this.visible) {
54
+ return;
55
+ }
56
+
57
+ this.visible = false;
58
+
59
+ this.$nextTick(() => {
60
+ this.component.clearActive();
61
+
62
+ setTimeout(() => {
63
+ this.el = document.getElementById(this.id);
64
+ this.$emit('hidden');
65
+ }, 100);
66
+ });
67
+ },
68
+
69
+ onDocumentClick(e) {
70
+ if (!e) {
71
+ return;
72
+ }
73
+
74
+ e.stopPropagation();
75
+
76
+ if (this.visible && !this.el.contains(e.target)) {
77
+ this.componentTrigger.cancel();
78
+ }
79
+ },
80
+ },
81
+ }
@@ -43,6 +43,8 @@ export const floatingUiMixin = {
43
43
  const trigger = document.getElementById(this.componentTrigger.id);
44
44
  const content = document.getElementById(this.componentContent.id);
45
45
 
46
+ // console.log(this.placement);
47
+
46
48
  computePosition(trigger, content, {
47
49
  placement: this.placement,
48
50
  middleware: [
@@ -52,7 +54,7 @@ export const floatingUiMixin = {
52
54
  size({
53
55
  apply({ rects }) {
54
56
  Object.assign(content.style, {
55
- width: `${rects.reference.width}px`,
57
+ minWidth: `${rects.reference.width}px`,
56
58
  });
57
59
  },
58
60
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veritree/ui",
3
- "version": "0.21.1-6",
3
+ "version": "0.21.1-8",
4
4
  "description": "veritree ui library",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -15,7 +15,7 @@ export default {
15
15
 
16
16
  provide() {
17
17
  return {
18
- api: () => {
18
+ apiDropdownMenu: () => {
19
19
  const { dark: isDark, headless: isHeadless } = this;
20
20
 
21
21
  const registerTrigger = (trigger) => {
@@ -39,8 +39,6 @@ export default {
39
39
 
40
40
  return {
41
41
  id: this.componentId,
42
- isDark,
43
- isHeadless,
44
42
  component: this.component,
45
43
  componentTrigger: this.componentTrigger,
46
44
  componentContent: this.componentContent,
@@ -59,14 +57,6 @@ export default {
59
57
  type: Boolean,
60
58
  default: false,
61
59
  },
62
- dark: {
63
- type: Boolean,
64
- default: false,
65
- },
66
- right: {
67
- type: Boolean,
68
- default: false,
69
- },
70
60
  },
71
61
 
72
62
  data() {
@@ -4,58 +4,36 @@
4
4
  :id="id"
5
5
  :headless="headless"
6
6
  :class="{ 'dropdown-menu-content': headless }"
7
+ :floating-ui-class="floatingUiClass"
7
8
  >
8
9
  <slot></slot>
9
10
  </FloatingUi>
10
11
  </template>
11
12
 
12
13
  <script>
13
- import FloatingUi from '../Utils/FloatingUi.vue';
14
+ import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
14
15
 
15
16
  export default {
16
17
  name: 'VTDropdownMenuContent',
17
18
 
18
- components: {
19
- FloatingUi,
20
- },
21
-
22
- inject: ['api'],
19
+ mixins: [floatingUiContentMixin],
23
20
 
24
- props: {
25
- headless: {
26
- type: Boolean,
27
- default: false,
28
- },
29
- },
30
-
31
- data() {
32
- return {
33
- visible: false,
34
- };
35
- },
21
+ inject: ['apiDropdownMenu'],
36
22
 
37
23
  computed: {
38
24
  id() {
39
- return `dropdown-menu-content-${this.api().id}`;
40
- },
41
-
42
- el() {
43
- return document.getElementById(this.id);
25
+ return `dropdown-menu-content-${this.apiDropdownMenu().id}`;
44
26
  },
45
27
 
46
28
  component() {
47
- return this.api().component;
29
+ return this.apiDropdownMenu().component;
48
30
  },
49
31
 
50
32
  componentTrigger() {
51
- return this.api().componentTrigger;
33
+ return this.apiDropdownMenu().componentTrigger;
52
34
  },
53
35
  },
54
36
 
55
- created() {
56
- console.log(this.$attrs);
57
- },
58
-
59
37
  mounted() {
60
38
  const content = {
61
39
  id: this.id,
@@ -63,47 +41,7 @@ export default {
63
41
  show: this.show,
64
42
  };
65
43
 
66
- this.api().registerContent(content);
67
-
68
- document.addEventListener('click', (e) => {
69
- if (!e) {
70
- return;
71
- }
72
-
73
- e.stopPropagation();
74
-
75
- if (this.visible && !this.el.contains(e.target)) {
76
- this.componentTrigger.cancel();
77
- }
78
- });
79
- },
80
-
81
- destroyed() {
82
- document.removeEventListener('click', this.componentTrigger.cancel);
83
- },
84
-
85
- methods: {
86
- show() {
87
- if (this.visible) return;
88
-
89
- this.visible = true;
90
-
91
- this.$nextTick(() => {
92
- this.component.setActive();
93
- this.$emit('shown');
94
- });
95
- },
96
-
97
- hide() {
98
- if (!this.visible) return;
99
-
100
- this.visible = false;
101
-
102
- this.$nextTick(() => {
103
- this.component.clearActive();
104
- this.$emit('hidden');
105
- });
106
- },
44
+ this.apiDropdownMenu().registerContent(content);
107
45
  },
108
46
  };
109
47
  </script>
@@ -3,8 +3,6 @@
3
3
  :class="{
4
4
  PopoverDivider: headless,
5
5
  '-mx-3 my-2 h-[1px]': !headless,
6
- 'bg-gray-200': !dark,
7
- 'bg-fd-500': dark,
8
6
  }"
9
7
  ></div>
10
8
  </template>
@@ -13,15 +11,20 @@
13
11
  export default {
14
12
  name: 'VTDropdownMenuDivider',
15
13
 
16
- inject: ['api'],
14
+ inject: ['apiDropdownMenu'],
15
+
16
+ headless: {
17
+ type: Boolean,
18
+ default: false,
19
+ },
17
20
 
18
21
  computed: {
19
22
  dark() {
20
- return this.api().isDark;
23
+ return this.apiDropdownMenu().isDark;
21
24
  },
22
25
 
23
26
  headless() {
24
- return this.api().isHeadless;
27
+ return this.apiDropdownMenu().isHeadless;
25
28
  },
26
29
  },
27
30
  };
@@ -3,15 +3,28 @@
3
3
  :is="as"
4
4
  :id="id"
5
5
  :to="to"
6
- :class="{
7
- MenuItem: headless,
8
- '-mx-3 flex min-w-max items-center gap-2 px-3 py-2 text-inherit no-underline':
9
- !headless,
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,
14
- }"
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
+ ]"
15
28
  :tabindex="tabIndex"
16
29
  :aria-disabled="disabled"
17
30
  role="menuitem"
@@ -34,7 +47,7 @@ import { genId } from '../../utils/ids';
34
47
  export default {
35
48
  name: 'VTDropdownMenuItem',
36
49
 
37
- inject: ['api'],
50
+ inject: ['apiDropdownMenu'],
38
51
 
39
52
  props: {
40
53
  to: {
@@ -49,6 +62,10 @@ export default {
49
62
  type: Boolean,
50
63
  default: false,
51
64
  },
65
+ headless: {
66
+ type: Boolean,
67
+ default: false,
68
+ },
52
69
  },
53
70
 
54
71
  data() {
@@ -61,15 +78,7 @@ export default {
61
78
 
62
79
  computed: {
63
80
  id() {
64
- return `dropdown-menu-item-${this.api().id}-${genId()}`;
65
- },
66
-
67
- dark() {
68
- return this.api().isDark;
69
- },
70
-
71
- headless() {
72
- return this.api().isHeadless;
81
+ return `dropdown-menu-item-${this.apiDropdownMenu().id}-${genId()}`;
73
82
  },
74
83
 
75
84
  as() {
@@ -77,7 +86,7 @@ export default {
77
86
  },
78
87
 
79
88
  items() {
80
- return this.api().items;
89
+ return this.apiDropdownMenu().items;
81
90
  },
82
91
 
83
92
  el() {
@@ -85,7 +94,7 @@ export default {
85
94
  },
86
95
 
87
96
  componentTrigger() {
88
- return this.api().componentTrigger;
97
+ return this.apiDropdownMenu().componentTrigger;
89
98
  },
90
99
  },
91
100
 
@@ -96,7 +105,7 @@ export default {
96
105
  focus: this.focus,
97
106
  };
98
107
 
99
- this.api().registerItem(item);
108
+ this.apiDropdownMenu().registerItem(item);
100
109
 
101
110
  this.index = this.items.length - 1;
102
111
  },
@@ -15,15 +15,15 @@
15
15
  export default {
16
16
  name: 'VTDropdownMenuLabel',
17
17
 
18
- inject: ['api'],
18
+ inject: ['apiDropdownMenu'],
19
19
 
20
20
  computed: {
21
21
  dark() {
22
- return this.api().isDark;
22
+ return this.apiDropdownMenu().isDark;
23
23
  },
24
24
 
25
25
  headless() {
26
- return this.api().isHeadless;
26
+ return this.apiDropdownMenu().isHeadless;
27
27
  },
28
28
  },
29
29
  };
@@ -6,7 +6,7 @@
6
6
  :aria-controls="controls"
7
7
  @keydown.down.prevent="onKeyDownOrUp"
8
8
  @keydown.up.prevent="onKeyDownOrUp"
9
- @keydown.esc.stop="onKeyesc"
9
+ @keydown.esc.stop="onKeyEsc"
10
10
  >
11
11
  <slot></slot>
12
12
  </div>
@@ -16,7 +16,7 @@
16
16
  export default {
17
17
  name: 'VTDropdownMenuTrigger',
18
18
 
19
- inject: ['api'],
19
+ inject: ['apiDropdownMenu'],
20
20
 
21
21
  data() {
22
22
  return {
@@ -29,15 +29,15 @@ export default {
29
29
 
30
30
  computed: {
31
31
  id() {
32
- return `dropdown-menu-trigger-${this.api().id}`;
32
+ return `dropdown-menu-trigger-${this.apiDropdownMenu().id}`;
33
33
  },
34
34
 
35
35
  componentContent() {
36
- return this.api().componentContent;
36
+ return this.apiDropdownMenu().componentContent;
37
37
  },
38
38
 
39
39
  items() {
40
- return this.api().items;
40
+ return this.apiDropdownMenu().items;
41
41
  },
42
42
 
43
43
  firstMenuItem() {
@@ -52,8 +52,6 @@ export default {
52
52
  watch: {
53
53
  expanded() {
54
54
  this.toggleAriaHasPopup();
55
- if (this.expanded) {
56
- }
57
55
  },
58
56
  },
59
57
 
@@ -65,7 +63,7 @@ export default {
65
63
  focus: this.focus,
66
64
  };
67
65
 
68
- this.api().registerTrigger(trigger);
66
+ this.apiDropdownMenu().registerTrigger(trigger);
69
67
 
70
68
  this.setTrigger();
71
69
  this.addTriggerEvents();
@@ -15,9 +15,7 @@ export default {
15
15
 
16
16
  provide() {
17
17
  return {
18
- api: () => {
19
- const { dark: isDark } = this;
20
-
18
+ apiListbox: () => {
21
19
  const registerTrigger = (trigger) => {
22
20
  if (!trigger) return;
23
21
  this.componentTrigger = trigger;
@@ -55,7 +53,6 @@ export default {
55
53
 
56
54
  return {
57
55
  id: this.componentId,
58
- isDark,
59
56
  component: this.component,
60
57
  componentTrigger: this.componentTrigger,
61
58
  componentContent: this.componentContent,
@@ -5,6 +5,7 @@
5
5
  :aria-activedescendant="activeDescedant"
6
6
  :headless="headless"
7
7
  :class="{ 'listbox-content': headless }"
8
+ :floating-ui-class="floatingUiClass"
8
9
  role="listbox"
9
10
  >
10
11
  <slot></slot>
@@ -12,54 +13,32 @@
12
13
  </template>
13
14
 
14
15
  <script>
15
- import FloatingUi from '../Utils/FloatingUi.vue';
16
+ import { floatingUiContentMixin } from '../../../mixins/floating-ui-content';
16
17
 
17
18
  export default {
18
19
  name: 'VTListboxContent',
19
20
 
20
- components: {
21
- FloatingUi,
22
- },
23
-
24
- inject: ['api'],
21
+ mixins: [floatingUiContentMixin],
25
22
 
26
- props: {
27
- headless: {
28
- type: Boolean,
29
- default: false,
30
- },
31
- bottom: {
32
- type: Boolean,
33
- default: false,
34
- },
35
- top: {
36
- type: Boolean,
37
- default: true,
38
- },
39
- },
23
+ inject: ['apiListbox'],
40
24
 
41
25
  data() {
42
26
  return {
43
27
  activeDescedant: null,
44
- visible: false,
45
28
  };
46
29
  },
47
30
 
48
31
  computed: {
49
32
  id() {
50
- return `listbox-content-${this.api().id}`;
51
- },
52
-
53
- el() {
54
- return document.getElementById(this.id);
33
+ return `listbox-content-${this.apiListbox().id}`;
55
34
  },
56
35
 
57
36
  component() {
58
- return this.api().component;
37
+ return this.apiListbox().component;
59
38
  },
60
39
 
61
40
  componentTrigger() {
62
- return this.api().componentTrigger;
41
+ return this.apiListbox().componentTrigger;
63
42
  },
64
43
  },
65
44
 
@@ -71,52 +50,10 @@ export default {
71
50
  setActiveDescedant: this.setActiveDescedant,
72
51
  };
73
52
 
74
- this.api().registerContent(content);
75
-
76
- // T-107 Create a directive or mixin for this
77
- document.addEventListener('click', (e) => {
78
- if (!e) {
79
- return;
80
- }
81
-
82
- e.stopPropagation();
83
-
84
- if (this.visible && !this.el.contains(e.target)) {
85
- this.componentTrigger.cancel();
86
- }
87
- });
88
- },
89
-
90
- destroyed() {
91
- // T-162 Create a directive or mixin for this
92
- document.removeEventListener('click', this.componentTrigger.cancel);
53
+ this.apiListbox().registerContent(content);
93
54
  },
94
55
 
95
56
  methods: {
96
- show() {
97
- if (this.visible) {
98
- return;
99
- }
100
-
101
- this.visible = true;
102
-
103
- this.$nextTick(() => {
104
- this.component.setActive();
105
- });
106
- },
107
-
108
- hide() {
109
- if (!this.visible) {
110
- return;
111
- }
112
-
113
- this.visible = false;
114
-
115
- this.$nextTick(() => {
116
- this.component.clearActive();
117
- });
118
- },
119
-
120
57
  setActiveDescedant(id) {
121
58
  this.activeDescedant = id;
122
59
  },
@@ -1,14 +1,28 @@
1
1
  <template>
2
2
  <li
3
3
  :id="id"
4
- :class="{
5
- ListboxItem: headless,
6
- 'relative z-10 flex items-center gap-2 px-3 py-2 no-underline': !headless,
7
- 'hover:bg-secondary-200/10': !dark && !headless,
8
- 'text-white': dark && !headless,
9
- 'pointer-events-none opacity-75': disabled && !headless,
10
- 'bg-secondary-200/10': selected && !headless,
11
- }"
4
+ :class="[
5
+ // default styles
6
+ headless
7
+ ? 'listbox-item'
8
+ : 'relative z-10 flex items-center gap-2 px-3 py-2 text-inherit no-underline hover:bg-secondary-200/10',
9
+ // disabled state styles
10
+ headless
11
+ ? disabled
12
+ ? 'listbox-item--disabled'
13
+ : null
14
+ : disabled
15
+ ? 'pointer-events-none opacity-75'
16
+ : null,
17
+ // selected state styles
18
+ headless
19
+ ? selected
20
+ ? 'lisbox-item--selected'
21
+ : null
22
+ : selected
23
+ ? 'bg-secondary-200/10'
24
+ : null,
25
+ ]"
12
26
  :aria-disabled="disabled"
13
27
  :aria-selected="String(selected)"
14
28
  :tabindex="tabIndex"
@@ -35,7 +49,7 @@ import { genId } from '../../utils/ids';
35
49
  export default {
36
50
  name: 'VTListboxItem',
37
51
 
38
- inject: ['api'],
52
+ inject: ['apiListbox'],
39
53
 
40
54
  props: {
41
55
  headless: {
@@ -62,12 +76,8 @@ export default {
62
76
  },
63
77
 
64
78
  computed: {
65
- dark() {
66
- return this.api().isDark;
67
- },
68
-
69
79
  items() {
70
- return this.api().items;
80
+ return this.apiListbox().items;
71
81
  },
72
82
 
73
83
  el() {
@@ -75,19 +85,19 @@ export default {
75
85
  },
76
86
 
77
87
  componentTrigger() {
78
- return this.api().componentTrigger;
88
+ return this.apiListbox().componentTrigger;
79
89
  },
80
90
 
81
91
  componentContent() {
82
- return this.api().componentContent;
92
+ return this.apiListbox().componentContent;
83
93
  },
84
94
 
85
95
  list() {
86
- return this.api().list;
96
+ return this.apiListbox().list;
87
97
  },
88
98
 
89
99
  search() {
90
- return this.api().search;
100
+ return this.apiListbox().search;
91
101
  },
92
102
  },
93
103
 
@@ -116,13 +126,13 @@ export default {
116
126
  onClick: this.onClick,
117
127
  };
118
128
 
119
- this.api().registerItem(item);
129
+ this.apiListbox().registerItem(item);
120
130
 
121
131
  this.index = this.items.length - 1;
122
132
  },
123
133
 
124
134
  beforeDestroy() {
125
- this.api().unregisterItem(this.id);
135
+ this.apiListbox().unregisterItem(this.id);
126
136
  },
127
137
 
128
138
  methods: {
@@ -209,7 +219,7 @@ export default {
209
219
  onClick() {
210
220
  if (this.disabled) return;
211
221
 
212
- this.api().emit(this.value);
222
+ this.apiListbox().emit(this.value);
213
223
  this.$nextTick(() => this.leaveMenu());
214
224
  },
215
225
 
@@ -4,8 +4,6 @@
4
4
  :class="{
5
5
  ListboxLabel: headless,
6
6
  'mb-2 block text-xs font-normal uppercase': !headless,
7
- 'text-inherit': !dark && !headless,
8
- 'text-white': dark && !headless,
9
7
  }"
10
8
  >
11
9
  <slot></slot>
@@ -16,8 +14,6 @@
16
14
  export default {
17
15
  name: 'VTListboxLabel',
18
16
 
19
- inject: ['api'],
20
-
21
17
  props: {
22
18
  as: {
23
19
  type: String,
@@ -28,11 +24,5 @@ export default {
28
24
  default: false,
29
25
  },
30
26
  },
31
-
32
- computed: {
33
- dark() {
34
- return this.api().isDark;
35
- },
36
- },
37
27
  };
38
28
  </script>
@@ -1,22 +1,19 @@
1
1
  <template>
2
2
  <ul
3
3
  :id="id"
4
- :class="{
5
- ListboxList: headless,
6
- '-mx-3 max-h-[160px] w-auto overflow-y-auto': !headless,
7
- }"
4
+ :class="[
5
+ headless ? 'listbox-list' : 'max-h-[160px] w-auto overflow-y-auto -mx-3',
6
+ ]"
8
7
  >
9
8
  <slot></slot>
10
9
  </ul>
11
10
  </template>
12
11
 
13
12
  <script>
14
- import { genId } from "../../utils/ids";
15
-
16
13
  export default {
17
- name: "VTListboxList",
14
+ name: 'VTListboxList',
18
15
 
19
- inject: ["api"],
16
+ inject: ['apiListbox'],
20
17
 
21
18
  props: {
22
19
  headless: {
@@ -27,11 +24,16 @@ export default {
27
24
 
28
25
  data() {
29
26
  return {
30
- id: `listboxlist-${genId()}`,
31
27
  isMousemove: false,
32
28
  };
33
29
  },
34
30
 
31
+ computed: {
32
+ id() {
33
+ return `listbox-list-${this.apiListbox().id}`;
34
+ },
35
+ },
36
+
35
37
  mounted() {
36
38
  const list = {
37
39
  el: this.$el,
@@ -40,7 +42,7 @@ export default {
40
42
  unsetMousemove: this.unsetMousemove,
41
43
  };
42
44
 
43
- this.api().registerList(list);
45
+ this.apiListbox().registerList(list);
44
46
  },
45
47
 
46
48
  methods: {
@@ -19,7 +19,7 @@
19
19
  export default {
20
20
  name: 'VTListboxSearch',
21
21
 
22
- inject: ['api'],
22
+ inject: ['apiListbox'],
23
23
 
24
24
  props: {
25
25
  headless: {
@@ -37,15 +37,15 @@ export default {
37
37
 
38
38
  computed: {
39
39
  componentTrigger() {
40
- return this.api().componentTrigger;
40
+ return this.apiListbox().componentTrigger;
41
41
  },
42
42
 
43
43
  list() {
44
- return this.api().list;
44
+ return this.apiListbox().list;
45
45
  },
46
46
 
47
47
  items() {
48
- return this.api().items;
48
+ return this.apiListbox().items;
49
49
  },
50
50
 
51
51
  item() {
@@ -58,7 +58,7 @@ export default {
58
58
  el: this.$el,
59
59
  };
60
60
 
61
- this.api().registerSearch(search);
61
+ this.apiListbox().registerSearch(search);
62
62
  this.$nextTick(() => setTimeout(() => this.$el.focus(), 150));
63
63
  },
64
64
 
@@ -6,7 +6,7 @@
6
6
  :class="[
7
7
  headless
8
8
  ? 'listbox-button'
9
- : 'flex w-full justify-between border border-solid py-2 px-3 rounded text-inherit max-w-full',
9
+ : 'flex w-full justify-between border border-solid py-2 px-3 gap-3 rounded text-inherit max-w-full',
10
10
  headless
11
11
  ? `listbox-button--${variant}`
12
12
  : isError
@@ -48,7 +48,7 @@ export default {
48
48
 
49
49
  components: { IconChevronDown },
50
50
 
51
- inject: ['api'],
51
+ inject: ['apiListbox'],
52
52
 
53
53
  props: {
54
54
  disabled: {
@@ -74,7 +74,7 @@ export default {
74
74
 
75
75
  computed: {
76
76
  id() {
77
- return `listbox-trigger-${this.api().id}`;
77
+ return `listbox-trigger-${this.apiListbox().id}`;
78
78
  },
79
79
 
80
80
  isError() {
@@ -82,11 +82,11 @@ export default {
82
82
  },
83
83
 
84
84
  componentContent() {
85
- return this.api().componentContent;
85
+ return this.apiListbox().componentContent;
86
86
  },
87
87
 
88
88
  items() {
89
- return this.api().items;
89
+ return this.apiListbox().items;
90
90
  },
91
91
 
92
92
  firstMenuItem() {
@@ -106,7 +106,7 @@ export default {
106
106
  id: this.id,
107
107
  };
108
108
 
109
- this.api().registerTrigger(trigger);
109
+ this.apiListbox().registerTrigger(trigger);
110
110
  },
111
111
 
112
112
  methods: {
@@ -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>
@@ -16,6 +16,7 @@
16
16
  headless
17
17
  ? null
18
18
  : 'absolute z-50 grid min-w-min overflow-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: {