@farm-investimentos/front-mfe-components-vue3 1.3.0 → 1.4.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.
@@ -1,134 +1,414 @@
1
1
  <template>
2
- <span
3
- :class="{ 'farm-tooltip': true }"
4
- ref="parent"
2
+ <div
3
+ class="tooltip-container"
4
+ ref="containerRef"
5
5
  >
6
- <span
7
- class="farm-tooltip__activator"
8
- ref="activator"
9
- @mouseover="onOver"
10
- @mouseout="onOut"
6
+ <div
7
+ ref="activatorRef"
8
+ class="tooltip-activator"
9
+ @mouseover="show"
10
+ @mouseout="hide"
11
+ @mouseleave="hide"
11
12
  >
12
13
  <slot name="activator" />
13
- </span>
14
-
15
- <span
16
- ref="popup"
17
- :class="{
18
- 'farm-tooltip__popup': true,
19
- ['farm-tooltip--' + color]: true,
20
- 'farm-tooltip__popup--visible':
21
- (!externalControl && showOver) || (externalControl && toggleComponent),
22
- }"
23
- :style="styles"
24
- @mouseout="onOut"
14
+ </div>
15
+
16
+ <div
17
+ v-if="isVisible"
18
+ ref="tooltipRef"
19
+ :class="tooltipClasses"
20
+ :style="tooltipStyles"
25
21
  >
26
- <slot />
27
- </span>
28
- </span>
22
+ <div
23
+ v-if="hasTitle || showCloseButton"
24
+ class="tooltip-header"
25
+ >
26
+ <div
27
+ v-if="hasTitle"
28
+ class="tooltip-title"
29
+ >
30
+ <slot name="title" />
31
+ </div>
32
+ <span
33
+ v-if="showCloseButton"
34
+ class="tooltip-close"
35
+ @click="close"
36
+ >×</span
37
+ >
38
+ </div>
39
+
40
+ <div class="tooltip-content">
41
+ <slot />
42
+ </div>
43
+
44
+ <div
45
+ class="tooltip-arrow"
46
+ :style="arrowStyles"
47
+ ></div>
48
+ </div>
49
+ </div>
29
50
  </template>
30
- <script lang="ts">
31
- import { PropType, ref, computed, reactive, onBeforeUnmount } from 'vue';
32
51
 
33
- import { calculateMainZindex } from '../../helpers';
52
+ <script setup lang="ts">
53
+ import { ref, computed, onBeforeUnmount, nextTick, watch, useSlots } from 'vue';
34
54
 
35
- export default {
55
+ defineOptions({
36
56
  name: 'farm-tooltip',
37
- props: {
38
- /*
39
- * Tooltip color
40
- */
41
- color: {
42
- type: String as PropType<
43
- | 'primary'
44
- | 'secondary'
45
- | 'secondary-green'
46
- | 'secondary-golden'
47
- | 'neutral'
48
- | 'info'
49
- | 'success'
50
- | 'error'
51
- | 'warning'
52
- | 'extra-1'
53
- | 'extra-2'
54
- | 'gray'
55
- >,
56
- default: 'gray',
57
- },
58
- /**
59
- * Control visibility
60
- * v-model bind
61
- */
62
- modelValue: {
63
- type: Boolean,
64
- default: undefined,
65
- },
66
- },
67
- setup(props) {
68
- const parent = ref(null);
69
- const popup = ref(null);
70
- const activator = ref(null);
71
- const showOver = ref(false);
72
- const styles = reactive({
73
- left: '0',
74
- top: '0',
75
- zIndex: 1,
76
- });
77
-
78
- const toggleComponent = computed(() => props.modelValue);
79
- const externalControl = computed(() => props.modelValue !== undefined);
80
-
81
- let hasBeenBoostrapped = false;
82
-
83
- const onOver = () => {
84
- showOver.value = true;
85
-
86
- if (!hasBeenBoostrapped) {
87
- document.querySelector('body').appendChild(popup.value);
88
- const parentBoundingClientRect = parent.value.getBoundingClientRect();
89
- const activatorBoundingClientRect = activator.value.getBoundingClientRect();
90
- const popupBoundingClientRect = popup.value.getBoundingClientRect();
91
-
92
- styles.left =
93
- parentBoundingClientRect.left +
94
- window.scrollX -
95
- (80 - activatorBoundingClientRect.width / 2) +
96
- 'px';
97
- styles.top =
98
- parentBoundingClientRect.top +
99
- window.scrollY -
100
- (popupBoundingClientRect.height + 8) +
101
- 'px';
102
- styles.zIndex = calculateMainZindex();
103
-
104
- hasBeenBoostrapped = true;
105
- }
106
- };
107
- const onOut = (event: MouseEvent) => {
108
- showOver.value = parent.value.contains(event.relatedTarget);
109
- };
110
-
111
- onBeforeUnmount(() => {
112
- if (hasBeenBoostrapped) {
113
- document.querySelector('body').removeChild(popup.value);
57
+ });
58
+
59
+ export interface TooltipProps {
60
+ modelValue?: boolean;
61
+ trigger?: 'hover' | 'click' | 'manual';
62
+ placement?:
63
+ | 'top-left'
64
+ | 'top-center'
65
+ | 'top-right'
66
+ | 'bottom-left'
67
+ | 'bottom-center'
68
+ | 'bottom-right';
69
+ offset?: number;
70
+ variant?: 'dark' | 'light';
71
+ size?: 'sm' | 'md' | 'lg';
72
+ maxWidth?: string | number;
73
+ delay?: number | [number, number];
74
+ disabled?: boolean;
75
+ fluid?: boolean;
76
+ }
77
+
78
+ const props = withDefaults(defineProps<TooltipProps>(), {
79
+ modelValue: undefined,
80
+ trigger: 'hover',
81
+ placement: 'top-center',
82
+ offset: 8,
83
+ variant: 'dark',
84
+ size: 'md',
85
+ maxWidth: undefined,
86
+ delay: () => [100, 50],
87
+ disabled: false,
88
+ fluid: false,
89
+ });
90
+
91
+ const emit = defineEmits<{
92
+ 'update:modelValue': [value: boolean];
93
+ show: [];
94
+ hide: [];
95
+ }>();
96
+
97
+ const slots = useSlots();
98
+
99
+ const containerRef = ref<HTMLElement | null>(null);
100
+ const activatorRef = ref<HTMLElement | null>(null);
101
+ const tooltipRef = ref<HTMLElement | null>(null);
102
+ const scrollableElementsRef = ref<Element[] | null>(null);
103
+
104
+ const ARROW_OFFSET = 18;
105
+ const Z_INDEX_OFFSET = 1000;
106
+ const DEFAULT_Z_INDEX = 10001;
107
+
108
+ let modalCache: { modals: Element[]; timestamp: number } | null = null;
109
+
110
+ const isVisible = ref(false);
111
+
112
+ const isControlled = computed(() => props.modelValue !== undefined);
113
+ const hasTitle = computed(() => !!slots.title);
114
+ const showCloseButton = computed(() => isControlled.value && hasTitle.value);
115
+
116
+ const normalizedMaxWidth = computed(() => {
117
+ if (props.fluid) {
118
+ return '300px';
119
+ }
120
+ return props.maxWidth;
121
+ });
122
+
123
+ const tooltipClasses = computed(() => ({
124
+ 'tooltip-popup': true,
125
+ 'tooltip-popup--visible': isVisible.value,
126
+ [`tooltip-popup--${props.variant}`]: true,
127
+ [`tooltip-popup--${props.size}`]: true,
128
+ 'tooltip-popup--has-title': hasTitle.value,
129
+ [`tooltip-popup--${props.placement}`]: true,
130
+ }));
131
+
132
+ const tooltipStyles = computed(() => {
133
+ const styles: Record<string, string> = {
134
+ position: 'fixed',
135
+ zIndex: String(getTooltipZIndex()),
136
+ };
137
+
138
+ if (normalizedMaxWidth.value) {
139
+ styles.maxWidth =
140
+ typeof normalizedMaxWidth.value === 'number'
141
+ ? `${normalizedMaxWidth.value}px`
142
+ : normalizedMaxWidth.value;
143
+ styles.width = 'auto';
144
+ }
145
+
146
+ return styles;
147
+ });
148
+
149
+ const arrowStyles = computed(() => {
150
+ const [verticalPos] = props.placement.split('-');
151
+ const arrowColor = props.variant === 'light' ? '#ffffff' : '#333333';
152
+
153
+ const styles: Record<string, string> = {
154
+ position: 'absolute',
155
+ width: '0',
156
+ height: '0',
157
+ borderStyle: 'solid',
158
+ zIndex: 'inherit',
159
+ };
160
+
161
+ if (verticalPos === 'top') {
162
+ styles.bottom = '-6px';
163
+ styles.borderWidth = '6px 6px 0 6px';
164
+ styles.borderColor = `${arrowColor} transparent transparent transparent`;
165
+ } else {
166
+ styles.top = '-6px';
167
+ styles.borderWidth = '0 6px 6px 6px';
168
+ styles.borderColor = `transparent transparent ${arrowColor} transparent`;
169
+ }
170
+ styles.left = '50%';
171
+ styles.transform = 'translateX(-50%)';
172
+
173
+ return styles;
174
+ });
175
+
176
+ const getTooltipZIndex = () => {
177
+ const now = Date.now();
178
+ let modals: Element[];
179
+
180
+ if (modalCache && now - modalCache.timestamp < 500) {
181
+ modals = modalCache.modals;
182
+ } else {
183
+ modals = Array.from(document.querySelectorAll('.farm-modal'));
184
+ modalCache = { modals, timestamp: now };
185
+ }
186
+
187
+ let maxModalZIndex = 0;
188
+
189
+ modals.forEach(modal => {
190
+ const htmlModal = modal as HTMLElement;
191
+
192
+ let zIndex = parseInt(htmlModal.style.zIndex, 10);
193
+
194
+ if (Number.isNaN(zIndex)) {
195
+ const computedZIndex = window.getComputedStyle(htmlModal).zIndex;
196
+ if (computedZIndex === 'auto') {
197
+ zIndex = 0;
198
+ } else {
199
+ zIndex = parseInt(computedZIndex, 10) || 0;
114
200
  }
115
- });
116
-
117
- return {
118
- parent,
119
- popup,
120
- activator,
121
- showOver,
122
- toggleComponent,
123
- externalControl,
124
- styles,
125
- onOver,
126
- onOut,
127
- };
128
- },
201
+ }
202
+
203
+ if (zIndex > maxModalZIndex) {
204
+ maxModalZIndex = zIndex;
205
+ }
206
+ });
207
+
208
+ return maxModalZIndex > 0 ? maxModalZIndex + Z_INDEX_OFFSET : DEFAULT_Z_INDEX;
209
+ };
210
+
211
+ const calculateTooltipPosition = (
212
+ activatorRect: DOMRect,
213
+ tooltipRect: DOMRect,
214
+ placement: string,
215
+ offset: number = 8
216
+ ) => {
217
+ const [verticalPos, horizontalAlign] = placement.split('-');
218
+
219
+ let left = 0;
220
+ let top = 0;
221
+
222
+ if (verticalPos === 'top') {
223
+ top = activatorRect.top - tooltipRect.height - offset;
224
+ } else {
225
+ top = activatorRect.bottom + offset;
226
+ }
227
+
228
+ switch (horizontalAlign) {
229
+ case 'left':
230
+ left = activatorRect.left + activatorRect.width / 2 - ARROW_OFFSET;
231
+ break;
232
+ case 'right':
233
+ left =
234
+ activatorRect.left + activatorRect.width / 2 - (tooltipRect.width - ARROW_OFFSET);
235
+ break;
236
+ case 'center':
237
+ default:
238
+ left = activatorRect.left + activatorRect.width / 2 - tooltipRect.width / 2;
239
+ break;
240
+ }
241
+
242
+ if (left < offset) left = offset;
243
+ if (left + tooltipRect.width > window.innerWidth - offset) {
244
+ left = window.innerWidth - tooltipRect.width - offset;
245
+ }
246
+
247
+ return { left, top };
248
+ };
249
+
250
+ const moveToBody = (element: HTMLElement): void => {
251
+ if (element.parentNode !== document.body) {
252
+ document.body.appendChild(element);
253
+ }
129
254
  };
255
+
256
+ const moveToContainer = (element: HTMLElement, container: HTMLElement): void => {
257
+ if (element.parentNode === document.body) {
258
+ container.appendChild(element);
259
+ }
260
+ };
261
+
262
+ const show = () => {
263
+ if (props.disabled || isControlled.value) return;
264
+
265
+ isVisible.value = true;
266
+ emit('show');
267
+
268
+ nextTick(() => {
269
+ if (tooltipRef.value && activatorRef.value) {
270
+ moveToBody(tooltipRef.value);
271
+ updatePosition();
272
+ addScrollListener();
273
+ }
274
+ });
275
+ };
276
+
277
+ const hide = () => {
278
+ if (props.disabled || isControlled.value) return;
279
+
280
+ isVisible.value = false;
281
+ emit('hide');
282
+ removeScrollListener();
283
+ };
284
+
285
+ const close = () => {
286
+ if (isControlled.value) {
287
+ emit('update:modelValue', false);
288
+ } else {
289
+ hide();
290
+ }
291
+ };
292
+
293
+ const updatePosition = () => {
294
+ if (!activatorRef.value || !tooltipRef.value) return;
295
+
296
+ const activatorRect = activatorRef.value.getBoundingClientRect();
297
+ const tooltipRect = tooltipRef.value.getBoundingClientRect();
298
+
299
+ const isActivatorVisible =
300
+ activatorRect.top < window.innerHeight &&
301
+ activatorRect.bottom > 0 &&
302
+ activatorRect.left < window.innerWidth &&
303
+ activatorRect.right > 0;
304
+
305
+ if (!isActivatorVisible && isVisible.value && !isControlled.value) {
306
+ hide();
307
+ return;
308
+ }
309
+
310
+ const position = calculateTooltipPosition(
311
+ activatorRect,
312
+ tooltipRect,
313
+ props.placement,
314
+ props.offset
315
+ );
316
+
317
+ tooltipRef.value.style.left = `${position.left}px`;
318
+ tooltipRef.value.style.top = `${position.top}px`;
319
+ updateArrowPosition(activatorRect, position);
320
+ };
321
+
322
+ const updateArrowPosition = (
323
+ activatorRect: DOMRect,
324
+ tooltipPosition: { left: number; top: number }
325
+ ) => {
326
+ if (!tooltipRef.value) return;
327
+
328
+ const arrow = tooltipRef.value.querySelector('.tooltip-arrow') as HTMLElement;
329
+ if (!arrow) return;
330
+
331
+ const activatorCenterX = activatorRect.left + activatorRect.width / 2;
332
+ const arrowX = activatorCenterX - tooltipPosition.left;
333
+ const tooltipWidth = tooltipRef.value.offsetWidth;
334
+ const minArrowX = 12;
335
+ const maxArrowX = tooltipWidth - 12;
336
+
337
+ const clampedArrowX = Math.max(minArrowX, Math.min(maxArrowX, arrowX));
338
+ arrow.style.left = `${clampedArrowX}px`;
339
+ arrow.style.transform = 'translateX(-50%)';
340
+ };
341
+
342
+ const getScrollableElements = () => {
343
+ if (!scrollableElementsRef.value) {
344
+ const nodeList = document.querySelectorAll(
345
+ '.farm-modal, .modal-content, [style*="overflow-y: auto"], [style*="overflow-y: scroll"]'
346
+ );
347
+ scrollableElementsRef.value = Array.from(nodeList);
348
+ }
349
+ return scrollableElementsRef.value;
350
+ };
351
+
352
+ const addScrollListener = () => {
353
+ window.addEventListener('scroll', updatePosition, { passive: true });
354
+
355
+ const scrollableElements = getScrollableElements();
356
+ scrollableElements.forEach(element => {
357
+ element.addEventListener('scroll', updatePosition, { passive: true });
358
+ });
359
+ };
360
+
361
+ const removeScrollListener = () => {
362
+ window.removeEventListener('scroll', updatePosition);
363
+
364
+ const scrollableElements = getScrollableElements();
365
+ scrollableElements.forEach(element => {
366
+ element.removeEventListener('scroll', updatePosition);
367
+ });
368
+ };
369
+
370
+ if (isControlled.value) {
371
+ isVisible.value = props.modelValue || false;
372
+ }
373
+
374
+ watch(
375
+ () => props.modelValue,
376
+ newValue => {
377
+ if (isControlled.value) {
378
+ isVisible.value = newValue || false;
379
+
380
+ if (isVisible.value) {
381
+ nextTick(() => {
382
+ if (tooltipRef.value) {
383
+ moveToBody(tooltipRef.value);
384
+ updatePosition();
385
+ addScrollListener();
386
+ }
387
+ });
388
+ } else {
389
+ removeScrollListener();
390
+ }
391
+ }
392
+ }
393
+ );
394
+
395
+ onBeforeUnmount(() => {
396
+ if (tooltipRef.value && containerRef.value) {
397
+ moveToContainer(tooltipRef.value, containerRef.value);
398
+ }
399
+ removeScrollListener();
400
+ });
130
401
  </script>
131
402
 
132
403
  <style lang="scss" scoped>
133
- @import './Tooltip';
404
+ @import './Tooltip.scss';
405
+
406
+ .tooltip-container {
407
+ display: inline-block;
408
+ position: relative;
409
+ }
410
+
411
+ .tooltip-activator {
412
+ display: inline-block;
413
+ }
134
414
  </style>