@tak-ps/vue-tabler 4.25.1 → 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,14 @@
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
+
13
21
  ### v4.25.1
14
22
 
15
23
  - :rocket: Allow Dropdown strategy to be specified via prop
@@ -1,37 +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
- :data-bs-strategy='props.strategy'
8
- aria-expanded='false'
9
- @click.stop.prevent=''
5
+ :aria-expanded='isOpen'
6
+ aria-haspopup='true'
7
+ @click.stop='toggle'
10
8
  >
11
9
  <slot>
12
10
  <IconSettings
13
11
  :size='32'
14
- :stoke='1'
12
+ stroke='1'
15
13
  class='cursor-pointer'
16
14
  />
17
15
  </slot>
18
16
  </div>
19
- <ul
20
- :class='menuClasses'
21
- :style='{
22
- width
23
- }'
24
- :aria-labelledby='id'
25
- >
26
- <slot name='dropdown'>
27
- Dropdown Content
28
- </slot>
29
- </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>
30
31
  </div>
31
32
  </template>
32
33
 
33
34
  <script setup lang="ts">
34
- import { ref, computed } from 'vue';
35
+ import { ref, computed, nextTick, onMounted, onBeforeUnmount } from 'vue';
35
36
  import {
36
37
  IconSettings
37
38
  } from '@tabler/icons-vue';
@@ -40,53 +41,147 @@ export interface DropdownProps {
40
41
  width?: number | string;
41
42
  autoclose?: string;
42
43
  position?: 'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end' | 'left' | 'right';
43
- strategy?: 'absolute' | 'fixed';
44
44
  }
45
45
 
46
46
  const props = withDefaults(defineProps<DropdownProps>(), {
47
47
  width: 200,
48
48
  autoclose: 'true',
49
49
  position: 'bottom-end',
50
- strategy: 'absolute'
51
50
  });
52
51
 
53
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>>({});
54
57
 
55
- const dropdownClasses = computed(() => {
56
- const baseClass = 'dropdown tabler-dropdown';
58
+ const menuWidth = computed(() =>
59
+ typeof props.width === 'number' ? `${props.width}px` : String(props.width)
60
+ );
57
61
 
62
+ const menuClasses = computed(() => {
63
+ const base = 'dropdown-menu dropdown-menu-card tabler-dropdown__menu show';
58
64
  switch (props.position) {
59
- case 'top':
65
+ case 'bottom-start':
60
66
  case 'top-start':
67
+ return `${base} dropdown-menu-start`;
68
+ case 'bottom-end':
61
69
  case 'top-end':
62
- return `${baseClass} dropup`;
63
- case 'left':
64
- return `${baseClass} dropstart`;
65
- case 'right':
66
- return `${baseClass} dropend`;
67
- default: // bottom, bottom-start, bottom-end
68
- return baseClass;
70
+ return `${base} dropdown-menu-end`;
71
+ default:
72
+ return base;
69
73
  }
70
74
  });
71
75
 
72
- const menuClasses = computed(() => {
73
- 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;
74
87
 
75
88
  switch (props.position) {
89
+ case 'bottom':
90
+ top = t.bottom + gap;
91
+ left = t.left + t.width / 2 - m.width / 2;
92
+ break;
76
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;
77
101
  case 'top-start':
78
- return `${baseClasses} dropdown-menu-start`;
79
- case 'bottom-end':
102
+ top = t.top - m.height - gap;
103
+ left = t.left;
104
+ break;
80
105
  case 'top-end':
81
- return `${baseClasses} dropdown-menu-end dropdown-menu-arrow`;
82
- default: // bottom, top, left, right
83
- 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;
84
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;
85
180
  });
86
181
  </script>
87
182
 
88
- <style scoped>
89
- .tabler-dropdown__menu {
183
+ <style>
184
+ .dropdown-menu.tabler-dropdown__menu {
90
185
  --tabler-dropdown-color: rgba(255, 255, 255, 0.92);
91
186
  --tabler-dropdown-bg: rgba(20, 20, 25, 0.96);
92
187
  --tabler-dropdown-border-color: rgba(255, 255, 255, 0.25);
@@ -96,7 +191,6 @@ const menuClasses = computed(() => {
96
191
  --tabler-dropdown-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.35);
97
192
  margin-block: 0.25rem;
98
193
  padding: 0.25rem 0;
99
- overflow: hidden;
100
194
  color: var(--tabler-dropdown-color);
101
195
  border-color: var(--tabler-dropdown-border-color);
102
196
  background: var(--tabler-dropdown-bg);
@@ -104,7 +198,7 @@ const menuClasses = computed(() => {
104
198
  box-shadow: var(--tabler-dropdown-shadow);
105
199
  }
106
200
 
107
- [data-bs-theme='light'] .tabler-dropdown__menu {
201
+ [data-bs-theme='light'] .dropdown-menu.tabler-dropdown__menu {
108
202
  --tabler-dropdown-color: var(--tblr-body-color);
109
203
  --tabler-dropdown-bg: rgba(255, 255, 255, 0.96);
110
204
  --tabler-dropdown-border-color: rgba(var(--tblr-primary-rgb), 0.15);
@@ -113,49 +207,44 @@ const menuClasses = computed(() => {
113
207
  --tabler-dropdown-shadow: 0 0.5rem 1rem rgba(15, 23, 42, 0.08);
114
208
  }
115
209
 
116
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.dropdown-item),
117
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.tabler-dropdown__item),
118
- [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 {
119
213
  color: var(--tblr-body-color) !important;
120
214
  }
121
215
 
122
- [data-bs-theme='light'] .tabler-dropdown__menu :deep(.text-white-50) {
216
+ [data-bs-theme='light'] .tabler-dropdown__menu .text-white-50 {
123
217
  color: var(--tblr-secondary-color) !important;
124
218
  }
125
219
 
126
- .tabler-dropdown__menu.dropdown-menu-arrow::before,
127
- .tabler-dropdown__menu.dropdown-menu-arrow::after {
128
- display: none;
129
- }
130
-
131
- .tabler-dropdown__menu :deep(.dropdown-item),
132
- .tabler-dropdown__menu :deep(.tabler-dropdown__item) {
220
+ .tabler-dropdown__menu .dropdown-item,
221
+ .tabler-dropdown__menu .tabler-dropdown__item {
133
222
  cursor: pointer;
134
223
  color: inherit;
135
224
  transition: background 0.1s ease, color 0.1s ease;
136
225
  }
137
226
 
138
- .tabler-dropdown__menu :deep(.dropdown-item:hover),
139
- .tabler-dropdown__menu :deep(.tabler-dropdown__item:hover) {
227
+ .tabler-dropdown__menu .dropdown-item:hover,
228
+ .tabler-dropdown__menu .tabler-dropdown__item:hover {
140
229
  background: var(--tabler-dropdown-hover-bg);
141
230
  }
142
231
 
143
- .tabler-dropdown__menu :deep(.dropdown-item.active),
144
- .tabler-dropdown__menu :deep(.dropdown-item:active),
145
- .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 {
146
235
  background: var(--tabler-dropdown-active-bg);
147
236
  color: var(--tabler-dropdown-active-color);
148
237
  }
149
238
 
150
- .tabler-dropdown__menu :deep(.dropdown-item.active .text-white),
151
- .tabler-dropdown__menu :deep(.dropdown-item:active .text-white),
152
- .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 {
153
242
  color: var(--tabler-dropdown-active-color) !important;
154
243
  }
155
244
 
156
- .tabler-dropdown__menu :deep(.dropdown-item.active .text-white-50),
157
- .tabler-dropdown__menu :deep(.dropdown-item:active .text-white-50),
158
- .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 {
159
248
  color: rgba(var(--tblr-primary-rgb), 0.7) !important;
160
249
  }
161
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.1",
4
+ "version": "4.26.1",
5
5
  "lib": "lib.ts",
6
6
  "main": "lib.ts",
7
7
  "module": "lib.ts",