@tak-ps/vue-tabler 4.25.0 → 4.26.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.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,18 @@
10
10
 
11
11
  ## Version History
12
12
 
13
+ ### v4.26.1
14
+
15
+ - :bug: Fix Lint Errors
16
+
17
+ ### v4.26.0
18
+
19
+ - :rocket: Smarter Dropdown Positioning
20
+
21
+ ### v4.25.1
22
+
23
+ - :rocket: Allow Dropdown strategy to be specified via prop
24
+
13
25
  ### v4.25.0
14
26
 
15
27
  - :rocket: Update Core Deps
@@ -1,36 +1,38 @@
1
1
  <template>
2
- <div :class='dropdownClasses'>
2
+ <div ref='triggerRef' class='tabler-dropdown d-inline-flex'>
3
3
  <div
4
4
  :id='id'
5
- data-bs-toggle='dropdown'
6
- :data-bs-auto-close='props.autoclose'
7
- aria-expanded='false'
8
- @click.stop.prevent=''
5
+ :aria-expanded='isOpen'
6
+ aria-haspopup='true'
7
+ @click.stop='toggle'
9
8
  >
10
9
  <slot>
11
10
  <IconSettings
12
11
  :size='32'
13
- :stoke='1'
12
+ stroke='1'
14
13
  class='cursor-pointer'
15
14
  />
16
15
  </slot>
17
16
  </div>
18
- <ul
19
- :class='menuClasses'
20
- :style='{
21
- width
22
- }'
23
- :aria-labelledby='id'
24
- >
25
- <slot name='dropdown'>
26
- Dropdown Content
27
- </slot>
28
- </ul>
17
+ <Teleport to='body'>
18
+ <ul
19
+ v-if='isOpen'
20
+ ref='menuRef'
21
+ :class='menuClasses'
22
+ :style='floatingStyle'
23
+ :aria-labelledby='id'
24
+ @click='onMenuClick'
25
+ >
26
+ <slot name='dropdown'>
27
+ Dropdown Content
28
+ </slot>
29
+ </ul>
30
+ </Teleport>
29
31
  </div>
30
32
  </template>
31
33
 
32
34
  <script setup lang="ts">
33
- import { ref, computed } from 'vue';
35
+ import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
34
36
  import {
35
37
  IconSettings
36
38
  } from '@tabler/icons-vue';
@@ -44,46 +46,142 @@ export interface DropdownProps {
44
46
  const props = withDefaults(defineProps<DropdownProps>(), {
45
47
  width: 200,
46
48
  autoclose: 'true',
47
- position: 'bottom-end'
49
+ position: 'bottom-end',
48
50
  });
49
51
 
50
52
  const id = ref('tabler-dropdown-' + Math.random().toString(36).substr(2, 9) + '-' + Date.now().toString(36));
53
+ const isOpen = ref(false);
54
+ const triggerRef = ref<HTMLElement | null>(null);
55
+ const menuRef = ref<HTMLElement | null>(null);
56
+ const floatingStyle = ref<Record<string, string>>({});
51
57
 
52
- const dropdownClasses = computed(() => {
53
- const baseClass = 'dropdown tabler-dropdown';
58
+ const menuWidth = computed(() =>
59
+ typeof props.width === 'number' ? `${props.width}px` : String(props.width)
60
+ );
54
61
 
62
+ const menuClasses = computed(() => {
63
+ const base = 'dropdown-menu dropdown-menu-card tabler-dropdown__menu show';
55
64
  switch (props.position) {
56
- case 'top':
65
+ case 'bottom-start':
57
66
  case 'top-start':
67
+ return `${base} dropdown-menu-start`;
68
+ case 'bottom-end':
58
69
  case 'top-end':
59
- return `${baseClass} dropup`;
60
- case 'left':
61
- return `${baseClass} dropstart`;
62
- case 'right':
63
- return `${baseClass} dropend`;
64
- default: // bottom, bottom-start, bottom-end
65
- return baseClass;
70
+ return `${base} dropdown-menu-end`;
71
+ default:
72
+ return base;
66
73
  }
67
74
  });
68
75
 
69
- const menuClasses = computed(() => {
70
- const baseClasses = 'dropdown-menu dropdown-menu-card tabler-dropdown__menu';
76
+ function calcPosition() {
77
+ if (!triggerRef.value || !menuRef.value) return;
78
+
79
+ const t = triggerRef.value.getBoundingClientRect();
80
+ const m = menuRef.value.getBoundingClientRect();
81
+ const vw = window.innerWidth;
82
+ const vh = window.innerHeight;
83
+ const gap = 4;
84
+
85
+ let top: number;
86
+ let left: number;
71
87
 
72
88
  switch (props.position) {
89
+ case 'bottom':
90
+ top = t.bottom + gap;
91
+ left = t.left + t.width / 2 - m.width / 2;
92
+ break;
73
93
  case 'bottom-start':
94
+ top = t.bottom + gap;
95
+ left = t.left;
96
+ break;
97
+ case 'top':
98
+ top = t.top - m.height - gap;
99
+ left = t.left + t.width / 2 - m.width / 2;
100
+ break;
74
101
  case 'top-start':
75
- return `${baseClasses} dropdown-menu-start`;
76
- case 'bottom-end':
102
+ top = t.top - m.height - gap;
103
+ left = t.left;
104
+ break;
77
105
  case 'top-end':
78
- return `${baseClasses} dropdown-menu-end dropdown-menu-arrow`;
79
- default: // bottom, top, left, right
80
- return baseClasses;
106
+ top = t.top - m.height - gap;
107
+ left = t.right - m.width;
108
+ break;
109
+ case 'left':
110
+ top = t.top;
111
+ left = t.left - m.width - gap;
112
+ break;
113
+ case 'right':
114
+ top = t.top;
115
+ left = t.right + gap;
116
+ break;
117
+ default: // bottom-end
118
+ top = t.bottom + gap;
119
+ left = t.right - m.width;
120
+ }
121
+
122
+ // Keep within viewport
123
+ left = Math.max(gap, Math.min(left, vw - m.width - gap));
124
+ top = Math.max(gap, Math.min(top, vh - gap));
125
+
126
+ // Constrain height to available space below the computed top
127
+ const maxHeight = Math.max(100, vh - top - gap);
128
+
129
+ floatingStyle.value = {
130
+ position: 'fixed',
131
+ zIndex: '9999',
132
+ top: `${top}px`,
133
+ left: `${left}px`,
134
+ minWidth: menuWidth.value,
135
+ maxHeight: `${maxHeight}px`,
136
+ overflowY: 'auto',
137
+ margin: '0',
138
+ visibility: 'visible',
139
+ };
140
+ }
141
+
142
+ async function toggle() {
143
+ if (isOpen.value) {
144
+ isOpen.value = false;
145
+ return;
146
+ }
147
+ // Render hidden first so we can measure before positioning
148
+ floatingStyle.value = { position: 'fixed', zIndex: '9999', visibility: 'hidden', minWidth: menuWidth.value };
149
+ isOpen.value = true;
150
+ await nextTick();
151
+ calcPosition();
152
+ }
153
+
154
+ function onMenuClick() {
155
+ if (props.autoclose === 'true') {
156
+ isOpen.value = false;
81
157
  }
158
+ }
159
+
160
+ function onOutsideClick(e: MouseEvent) {
161
+ if (!isOpen.value) return;
162
+ const target = e.target as Node;
163
+ if (triggerRef.value?.contains(target) || menuRef.value?.contains(target)) return;
164
+ isOpen.value = false;
165
+ }
166
+
167
+ function onEscape(e: KeyboardEvent) {
168
+ if (e.key === 'Escape') isOpen.value = false;
169
+ }
170
+
171
+ onMounted(() => {
172
+ document.addEventListener('click', onOutsideClick, true);
173
+ document.addEventListener('keydown', onEscape);
174
+ });
175
+
176
+ onBeforeUnmount(() => {
177
+ document.removeEventListener('click', onOutsideClick, true);
178
+ document.removeEventListener('keydown', onEscape);
179
+ isOpen.value = false;
82
180
  });
83
181
  </script>
84
182
 
85
- <style scoped>
86
- .tabler-dropdown__menu {
183
+ <style>
184
+ .dropdown-menu.tabler-dropdown__menu {
87
185
  --tabler-dropdown-color: rgba(255, 255, 255, 0.92);
88
186
  --tabler-dropdown-bg: rgba(20, 20, 25, 0.96);
89
187
  --tabler-dropdown-border-color: rgba(255, 255, 255, 0.25);
@@ -93,7 +191,6 @@ const menuClasses = computed(() => {
93
191
  --tabler-dropdown-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
94
192
  margin-block: 0.25rem;
95
193
  padding: 0.25rem 0;
96
- overflow: hidden;
97
194
  color: var(--tabler-dropdown-color);
98
195
  border-color: var(--tabler-dropdown-border-color);
99
196
  background: var(--tabler-dropdown-bg);
@@ -101,7 +198,7 @@ const menuClasses = computed(() => {
101
198
  box-shadow: var(--tabler-dropdown-shadow);
102
199
  }
103
200
 
104
- [data-bs-theme='light'] .tabler-dropdown__menu {
201
+ [data-bs-theme='light'] .dropdown-menu.tabler-dropdown__menu {
105
202
  --tabler-dropdown-color: var(--tblr-body-color);
106
203
  --tabler-dropdown-bg: rgba(255, 255, 255, 0.96);
107
204
  --tabler-dropdown-border-color: rgba(var(--tblr-primary-rgb), 0.15);
@@ -110,49 +207,44 @@ const menuClasses = computed(() => {
110
207
  --tabler-dropdown-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.08);
111
208
  }
112
209
 
113
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.dropdown-item),
114
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.tabler-dropdown__item),
115
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.text-white) {
210
+ [data-bs-theme='light'] .tabler-dropdown__menu .dropdown-item,
211
+ [data-bs-theme='light'] .tabler-dropdown__menu .tabler-dropdown__item,
212
+ [data-bs-theme='light'] .tabler-dropdown__menu .text-white {
116
213
  color: var(--tblr-body-color) !important;
117
214
  }
118
215
 
119
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.text-white-50) {
216
+ [data-bs-theme='light'] .tabler-dropdown__menu .text-white-50 {
120
217
  color: var(--tblr-secondary-color) !important;
121
218
  }
122
219
 
123
- .tabler-dropdown__menu.dropdown-menu-arrow::before,
124
- .tabler-dropdown__menu.dropdown-menu-arrow::after {
125
- display: none;
126
- }
127
-
128
- .tabler-dropdown__menu :deep(.dropdown-item),
129
- .tabler-dropdown__menu :deep(.tabler-dropdown__item) {
220
+ .tabler-dropdown__menu .dropdown-item,
221
+ .tabler-dropdown__menu .tabler-dropdown__item {
130
222
  cursor: pointer;
131
223
  color: inherit;
132
224
  transition: background 0.1s ease, color 0.1s ease;
133
225
  }
134
226
 
135
- .tabler-dropdown__menu :deep(.dropdown-item:hover),
136
- .tabler-dropdown__menu :deep(.tabler-dropdown__item:hover) {
227
+ .tabler-dropdown__menu .dropdown-item:hover,
228
+ .tabler-dropdown__menu .tabler-dropdown__item:hover {
137
229
  background: var(--tabler-dropdown-hover-bg);
138
230
  }
139
231
 
140
- .tabler-dropdown__menu :deep(.dropdown-item.active),
141
- .tabler-dropdown__menu :deep(.dropdown-item:active),
142
- .tabler-dropdown__menu :deep(.tabler-dropdown__item--active) {
232
+ .tabler-dropdown__menu .dropdown-item.active,
233
+ .tabler-dropdown__menu .dropdown-item:active,
234
+ .tabler-dropdown__menu .tabler-dropdown__item--active {
143
235
  background: var(--tabler-dropdown-active-bg);
144
236
  color: var(--tabler-dropdown-active-color);
145
237
  }
146
238
 
147
- .tabler-dropdown__menu :deep(.dropdown-item.active .text-white),
148
- .tabler-dropdown__menu :deep(.dropdown-item:active .text-white),
149
- .tabler-dropdown__menu :deep(.tabler-dropdown__item--active .text-white) {
239
+ .tabler-dropdown__menu .dropdown-item.active .text-white,
240
+ .tabler-dropdown__menu .dropdown-item:active .text-white,
241
+ .tabler-dropdown__menu .tabler-dropdown__item--active .text-white {
150
242
  color: var(--tabler-dropdown-active-color) !important;
151
243
  }
152
244
 
153
- .tabler-dropdown__menu :deep(.dropdown-item.active .text-white-50),
154
- .tabler-dropdown__menu :deep(.dropdown-item:active .text-white-50),
155
- .tabler-dropdown__menu :deep(.tabler-dropdown__item--active .text-white-50) {
245
+ .tabler-dropdown__menu .dropdown-item.active .text-white-50,
246
+ .tabler-dropdown__menu .dropdown-item:active .text-white-50,
247
+ .tabler-dropdown__menu .tabler-dropdown__item--active .text-white-50 {
156
248
  color: rgba(var(--tblr-primary-rgb), 0.7) !important;
157
249
  }
158
250
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tak-ps/vue-tabler",
3
3
  "type": "module",
4
- "version": "4.25.0",
4
+ "version": "4.26.1",
5
5
  "lib": "lib.ts",
6
6
  "main": "lib.ts",
7
7
  "module": "lib.ts",