@farm-investimentos/front-mfe-components 15.14.4 → 15.14.6

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,79 +1,58 @@
1
1
  <template>
2
- <span :class="{ 'farm-tooltip': true }" ref="parent">
3
- <span
4
- class="farm-tooltip__activator"
5
- ref="activator"
6
- @mouseover="onOver"
7
- @mouseout="onOut"
8
- @mouseleave="onOut"
2
+ <div class="tooltip-container" ref="containerRef">
3
+ <div
4
+ ref="activatorRef"
5
+ class="tooltip-activator"
6
+ @mouseover="show"
7
+ @mouseout="hide"
8
+ @mouseleave="hide"
9
9
  >
10
10
  <slot name="activator" />
11
- </span>
12
-
13
- <span
14
- ref="popup"
15
- :class="{
16
- 'farm-tooltip__popup': true,
17
- 'farm-tooltip__popup--visible':
18
- (!externalControl && showOver) || (externalControl && toggleComponent),
19
- 'farm-tooltip__popup--fluid': fluid,
20
- [`farm-tooltip__popup--${position}`]: position,
21
- 'farm-tooltip__popup--has-title': hasTitle,
22
- }"
23
- :style="styles"
24
- @mouseout="onOut"
25
- @mouseleave="onOut"
26
- >
27
- <div v-if="hasTitle" class="farm-tooltip__header">
28
- <div class="farm-tooltip__title">
29
- <slot name="title"></slot>
11
+ </div>
12
+
13
+ <div v-if="isVisible" ref="tooltipRef" :class="tooltipClasses" :style="tooltipStyles">
14
+ <div v-if="hasTitle || showCloseButton" class="tooltip-header">
15
+ <div v-if="hasTitle" class="tooltip-title">
16
+ <slot name="title" />
30
17
  </div>
31
- <span v-if="externalControl" class="farm-tooltip__close" @click="onClose"
32
-
33
- </span>
18
+ <span v-if="showCloseButton" class="tooltip-close" @click="close">×</span>
34
19
  </div>
35
- <div class="farm-tooltip__content">
20
+
21
+ <div class="tooltip-content">
36
22
  <slot />
37
23
  </div>
38
- <span v-if="hasPosition" class="farm-tooltip__arrow"></span>
39
- </span>
40
- </span>
24
+
25
+ <div class="tooltip-arrow" :style="arrowStyles"></div>
26
+ </div>
27
+ </div>
41
28
  </template>
29
+
42
30
  <script lang="ts">
43
- import { PropType, ref, computed, reactive, onBeforeUnmount, defineComponent, useSlots } from 'vue';
44
- import { calculateMainZindex } from '../../helpers';
31
+ import { defineComponent, ref, computed, PropType, onBeforeUnmount, nextTick, watch } from 'vue';
32
+ import {
33
+ TooltipPlacement,
34
+ TooltipTrigger,
35
+ TooltipVariant,
36
+ TooltipSize,
37
+ } from './types/tooltip.types';
38
+ import { calculateTooltipPosition, moveToBody, moveToContainer } from './utils/tooltip.utils';
45
39
 
46
- export type TooltipPosition =
47
- | 'top-left'
48
- | 'top-center'
49
- | 'top-right'
50
- | 'bottom-left'
51
- | 'bottom-center'
52
- | 'bottom-right';
40
+ const ARROW_OFFSET = 18;
53
41
 
54
42
  export default defineComponent({
55
43
  name: 'farm-tooltip',
56
44
  props: {
57
- /**
58
- * Control visibility with v-model
59
- */
60
45
  value: {
61
46
  type: Boolean,
62
47
  default: undefined,
63
48
  },
64
- /**
65
- * Fluid width (grows based on content)
66
- */
67
- fluid: {
68
- type: Boolean,
69
- default: false,
49
+ trigger: {
50
+ type: String as PropType<TooltipTrigger>,
51
+ default: 'hover',
70
52
  },
71
- /**
72
- * Position of the tooltip relative to the activator
73
- */
74
- position: {
75
- type: String as PropType<TooltipPosition>,
76
- default: undefined,
53
+ placement: {
54
+ type: String as PropType<TooltipPlacement>,
55
+ default: 'top-center',
77
56
  validator: (value: string) => {
78
57
  return [
79
58
  'top-left',
@@ -85,288 +64,272 @@ export default defineComponent({
85
64
  ].includes(value);
86
65
  },
87
66
  },
67
+ offset: {
68
+ type: Number,
69
+ default: 8,
70
+ },
71
+ variant: {
72
+ type: String as PropType<TooltipVariant>,
73
+ default: 'dark',
74
+ },
75
+ size: {
76
+ type: String as PropType<TooltipSize>,
77
+ default: 'md',
78
+ },
79
+ maxWidth: {
80
+ type: [String, Number],
81
+ default: undefined,
82
+ },
83
+ delay: {
84
+ type: [Number, Array],
85
+ default: () => [100, 50],
86
+ },
87
+ disabled: {
88
+ type: Boolean,
89
+ default: false,
90
+ },
91
+ fluid: {
92
+ type: Boolean,
93
+ default: false,
94
+ },
95
+ position: {
96
+ type: String,
97
+ default: undefined,
98
+ },
88
99
  },
89
- setup(props, { emit }) {
90
- const parent = ref(null);
91
- const popup = ref(null);
92
- const activator = ref(null);
93
- const showOver = ref(false);
94
- const styles = reactive({
95
- left: '0',
96
- top: '0',
97
- zIndex: 1,
98
- });
99
- const slots = useSlots();
100
- let hideTimeout: number | null = null;
100
+ emits: ['input', 'show', 'hide'],
101
+ setup(props, { emit, slots }) {
102
+ const containerRef = ref<HTMLElement | null>(null);
103
+ const activatorRef = ref<HTMLElement | null>(null);
104
+ const tooltipRef = ref<HTMLElement | null>(null);
105
+ const scrollableElementsRef = ref<Element[] | null>(null);
106
+
107
+ const isVisible = ref(false);
101
108
 
102
- const toggleComponent = computed(() => props.value);
103
- const externalControl = computed(() => props.value !== undefined);
104
- const hasPosition = computed(() => !!props.position);
109
+ const isControlled = computed(() => props.value !== undefined);
105
110
  const hasTitle = computed(() => !!slots.title);
111
+ const showCloseButton = computed(() => isControlled.value && hasTitle.value);
106
112
 
107
- let hasBeenBoostrapped = false;
108
- let scrollListener = null;
109
- let isInsideModal = false;
110
- let modalScrollElements = [];
111
-
112
- const calculatePosition = () => {
113
- const parentBoundingClientRect = parent.value.getBoundingClientRect();
114
- const activatorBoundingClientRect = activator.value.getBoundingClientRect();
115
- const popupBoundingClientRect = popup.value.getBoundingClientRect();
116
-
117
- const activatorWidth = activatorBoundingClientRect.width;
118
- const activatorHeight = activatorBoundingClientRect.height;
119
- const popupWidth = popupBoundingClientRect.width;
120
- const popupHeight = popupBoundingClientRect.height;
121
-
122
- let left = 0;
123
- let top = 0;
124
-
125
- // Se estiver dentro de um modal, usar coordenadas da viewport (position fixed)
126
- if (isInsideModal) {
127
- if (!props.position) {
128
- left = activatorBoundingClientRect.left + activatorWidth / 2 - popupWidth / 2;
129
- top = activatorBoundingClientRect.top - popupHeight - 8;
130
- } else {
131
- const [verticalPosition, horizontalAlignment] = props.position.split('-');
132
-
133
- switch (horizontalAlignment) {
134
- case 'left':
135
- left = activatorBoundingClientRect.left - 8;
136
- break;
137
- case 'right':
138
- left = activatorBoundingClientRect.left + activatorWidth - popupWidth + 8;
139
- break;
140
- case 'center':
141
- default:
142
- left = activatorBoundingClientRect.left + activatorWidth / 2 - popupWidth / 2;
143
- break;
144
- }
113
+ const normalizedPlacement = computed(() => {
114
+ if (props.position) {
115
+ return props.position as TooltipPlacement;
116
+ }
117
+ return props.placement;
118
+ });
145
119
 
146
- if (verticalPosition === 'top') {
147
- top = activatorBoundingClientRect.top - popupHeight - 8;
148
- } else {
149
- top = activatorBoundingClientRect.top + activatorHeight + 8;
150
- }
151
- }
120
+ const normalizedMaxWidth = computed(() => {
121
+ if (props.fluid) {
122
+ return '300px';
123
+ }
124
+ return props.maxWidth;
125
+ });
152
126
 
153
- // Ajustar para não sair da viewport
154
- if (left < 5) {
155
- left = 5;
156
- } else if (left + popupWidth > window.innerWidth - 5) {
157
- left = window.innerWidth - popupWidth - 5;
158
- }
159
- } else {
160
- // Comportamento original para tooltips fora de modais
161
- if (!props.position) {
162
- left =
163
- parentBoundingClientRect.left +
164
- window.scrollX +
165
- activatorWidth / 2 -
166
- popupWidth / 2;
167
-
168
- top = parentBoundingClientRect.top + window.scrollY - popupHeight - 8;
169
- } else {
170
- const [verticalPosition, horizontalAlignment] = props.position.split('-');
171
-
172
- switch (horizontalAlignment) {
173
- case 'left':
174
- left = parentBoundingClientRect.left + window.scrollX - 8;
175
- break;
176
- case 'right':
177
- left =
178
- parentBoundingClientRect.left +
179
- window.scrollX +
180
- activatorWidth -
181
- popupWidth +
182
- 8;
183
- break;
184
- case 'center':
185
- default:
186
- left =
187
- parentBoundingClientRect.left +
188
- window.scrollX +
189
- activatorWidth / 2 -
190
- popupWidth / 2;
191
- break;
192
- }
127
+ const tooltipClasses = computed(() => ({
128
+ 'tooltip-popup': true,
129
+ 'tooltip-popup--visible': isVisible.value,
130
+ [`tooltip-popup--${props.variant}`]: true,
131
+ [`tooltip-popup--${props.size}`]: true,
132
+ 'tooltip-popup--has-title': hasTitle.value,
133
+ [`tooltip-popup--${normalizedPlacement.value}`]: true,
134
+ }));
193
135
 
194
- if (verticalPosition === 'top') {
195
- top = parentBoundingClientRect.top + window.scrollY - popupHeight - 8;
196
- } else {
197
- top = parentBoundingClientRect.top + window.scrollY + activatorHeight + 8;
198
- }
199
- }
136
+ const tooltipStyles = computed(() => {
137
+ const styles: Record<string, string> = {
138
+ position: 'fixed',
139
+ zIndex: '9999',
140
+ };
200
141
 
201
- if (left < window.scrollX) {
202
- left = window.scrollX + 5;
203
- } else if (left + popupWidth > window.innerWidth + window.scrollX) {
204
- left = window.innerWidth + window.scrollX - popupWidth - 5;
205
- }
142
+ if (normalizedMaxWidth.value) {
143
+ styles.maxWidth =
144
+ typeof normalizedMaxWidth.value === 'number'
145
+ ? `${normalizedMaxWidth.value}px`
146
+ : normalizedMaxWidth.value;
147
+ styles.width = 'auto';
206
148
  }
207
149
 
208
- return { left, top };
209
- };
150
+ return styles;
151
+ });
152
+
153
+ const arrowStyles = computed(() => {
154
+ const [verticalPos, horizontalAlign] = normalizedPlacement.value.split('-');
155
+
156
+ const styles: Record<string, string> = {
157
+ position: 'absolute',
158
+ width: '0',
159
+ height: '0',
160
+ borderStyle: 'solid',
161
+ zIndex: '10000',
162
+ };
210
163
 
211
- const onOver = () => {
212
- // Limpa qualquer timeout de hide
213
- if (hideTimeout) {
214
- clearTimeout(hideTimeout);
215
- hideTimeout = null;
164
+ if (verticalPos === 'top') {
165
+ styles.bottom = '-6px';
166
+ styles.borderWidth = '6px 6px 0 6px';
167
+ styles.borderColor = '#333333 transparent transparent transparent';
168
+ } else {
169
+ styles.top = '-6px';
170
+ styles.borderWidth = '0 6px 6px 6px';
171
+ styles.borderColor = 'transparent transparent #333333 transparent';
216
172
  }
217
173
 
218
- showOver.value = true;
219
-
220
- if (!hasBeenBoostrapped) {
221
- document.querySelector('body').appendChild(popup.value);
222
-
223
- // Detectar se está dentro de um modal
224
- isInsideModal = !!parent.value.closest('.farm-modal');
225
-
226
- if (isInsideModal) {
227
- // Usar position fixed para tooltips dentro de modais
228
- popup.value.style.position = 'fixed';
229
- // Adicionar listener de scroll para recalcular posição
230
- scrollListener = () => {
231
- const { left, top } = calculatePosition();
232
- styles.left = `${left}px`;
233
- styles.top = `${top}px`;
234
- };
235
-
236
- // Escutar scroll da window E do modal
237
- window.addEventListener('scroll', scrollListener, true);
238
-
239
- // Encontrar o modal e escutar seu scroll interno também
240
- const modalElement = parent.value.closest('.farm-modal');
241
- if (modalElement) {
242
- // Função para detectar elementos scrolláveis
243
- const isScrollable = (element) => {
244
- const style = window.getComputedStyle(element);
245
- return (
246
- style.overflow === 'auto' ||
247
- style.overflow === 'scroll' ||
248
- style.overflowY === 'auto' ||
249
- style.overflowY === 'scroll' ||
250
- style.overflowX === 'auto' ||
251
- style.overflowX === 'scroll'
252
- );
253
- };
254
-
255
- // Buscar todos os elementos dentro do modal
256
- const allElements = modalElement.querySelectorAll('*');
257
- const scrollableElements = Array.from(allElements).filter(isScrollable);
258
-
259
- // Adicionar elementos específicos do modal que podem ter scroll
260
- const modalSpecificElements = modalElement.querySelectorAll(
261
- '.farm-modal--content, .farm-modal--content > div, [data-simplebar], .simplebar-content-wrapper'
262
- );
263
-
264
- // Combinar e remover duplicatas
265
- const elementsToWatch = [...new Set([...scrollableElements, ...modalSpecificElements, modalElement])];
266
-
267
- elementsToWatch.forEach(element => {
268
- element.addEventListener('scroll', scrollListener, true);
269
- modalScrollElements.push(element);
270
- });
271
- }
272
- } else {
273
- // Comportamento original para tooltips fora de modais
274
- popup.value.style.position = 'absolute';
174
+ if (horizontalAlign === 'left') {
175
+ styles.left = `${ARROW_OFFSET}px`;
176
+ } else if (horizontalAlign === 'right') {
177
+ styles.right = `${ARROW_OFFSET}px`;
178
+ } else {
179
+ styles.left = '50%';
180
+ styles.transform = 'translateX(-50%)';
181
+ }
182
+
183
+ return styles;
184
+ });
185
+
186
+ const show = () => {
187
+ if (props.disabled || isControlled.value) return;
188
+
189
+ isVisible.value = true;
190
+ emit('show');
191
+
192
+ nextTick(() => {
193
+ if (tooltipRef.value && activatorRef.value) {
194
+ moveToBody(tooltipRef.value);
195
+ updatePosition();
196
+ addScrollListener();
275
197
  }
276
-
277
- const { left, top } = calculatePosition();
278
- styles.left = `${left}px`;
279
- styles.top = `${top}px`;
280
- styles.zIndex = calculateMainZindex();
198
+ });
199
+ };
281
200
 
282
- hasBeenBoostrapped = true;
283
- }
201
+ const hide = () => {
202
+ if (props.disabled || isControlled.value) return;
203
+
204
+ isVisible.value = false;
205
+ emit('hide');
206
+ removeScrollListener();
284
207
  };
285
208
 
286
- const onOut = (event: MouseEvent) => {
287
- // Limpa qualquer timeout anterior
288
- if (hideTimeout) {
289
- clearTimeout(hideTimeout);
290
- hideTimeout = null;
209
+ const close = () => {
210
+ if (isControlled.value) {
211
+ emit('input', false);
212
+ } else {
213
+ hide();
291
214
  }
215
+ };
292
216
 
293
- // Verifica se o relatedTarget está contido no parent
294
- const isRelatedTargetInParent =
295
- event.relatedTarget && parent.value.contains(event.relatedTarget);
296
-
297
- if (!isRelatedTargetInParent) {
298
- // Se não está no parent, agenda o hide com um pequeno delay para evitar flickering
299
- hideTimeout = window.setTimeout(() => {
300
- showOver.value = false;
301
-
302
- // Remover listeners de scroll quando tooltip for escondido
303
- if (scrollListener) {
304
- window.removeEventListener('scroll', scrollListener, true);
305
-
306
- // Remover listeners dos elementos de scroll do modal
307
- modalScrollElements.forEach(element => {
308
- element.removeEventListener('scroll', scrollListener, true);
309
- });
310
- modalScrollElements = [];
311
-
312
- scrollListener = null;
313
- }
314
-
315
- hideTimeout = null;
316
- }, 50);
217
+ const updatePosition = () => {
218
+ if (!activatorRef.value || !tooltipRef.value) return;
219
+
220
+ const activatorRect = activatorRef.value.getBoundingClientRect();
221
+ const tooltipRect = tooltipRef.value.getBoundingClientRect();
222
+
223
+ const isActivatorVisible =
224
+ activatorRect.top < window.innerHeight &&
225
+ activatorRect.bottom > 0 &&
226
+ activatorRect.left < window.innerWidth &&
227
+ activatorRect.right > 0;
228
+
229
+ if (!isActivatorVisible && isVisible.value && !isControlled.value) {
230
+ hide();
231
+ return;
317
232
  }
233
+
234
+ const position = calculateTooltipPosition(
235
+ activatorRect,
236
+ tooltipRect,
237
+ normalizedPlacement.value,
238
+ props.offset
239
+ );
240
+
241
+ tooltipRef.value.style.left = `${position.left}px`;
242
+ tooltipRef.value.style.top = `${position.top}px`;
318
243
  };
319
244
 
320
- const onClose = () => {
321
- showOver.value = false;
322
- if (externalControl.value) {
323
- emit('input', false);
245
+ const getScrollableElements = () => {
246
+ if (!scrollableElementsRef.value) {
247
+ const nodeList = document.querySelectorAll(
248
+ '.farm-modal, .modal-content, [style*="overflow-y: auto"], [style*="overflow-y: scroll"]'
249
+ );
250
+ scrollableElementsRef.value = Array.from(nodeList);
324
251
  }
252
+ return scrollableElementsRef.value;
325
253
  };
326
254
 
327
- onBeforeUnmount(() => {
328
- // Limpa o timeout se existir
329
- if (hideTimeout) {
330
- clearTimeout(hideTimeout);
331
- hideTimeout = null;
332
- }
255
+ const addScrollListener = () => {
256
+ window.addEventListener('scroll', updatePosition, { passive: true });
257
+
258
+ const scrollableElements = getScrollableElements();
259
+ scrollableElements.forEach(element => {
260
+ element.addEventListener('scroll', updatePosition, { passive: true });
261
+ });
262
+ };
333
263
 
334
- // Limpar listeners de scroll se existirem
335
- if (scrollListener) {
336
- window.removeEventListener('scroll', scrollListener, true);
337
-
338
- // Remover listeners dos elementos de scroll do modal
339
- modalScrollElements.forEach(element => {
340
- element.removeEventListener('scroll', scrollListener, true);
341
- });
342
- modalScrollElements = [];
343
-
344
- scrollListener = null;
264
+ const removeScrollListener = () => {
265
+ window.removeEventListener('scroll', updatePosition);
266
+
267
+ const scrollableElements = getScrollableElements();
268
+ scrollableElements.forEach(element => {
269
+ element.removeEventListener('scroll', updatePosition);
270
+ });
271
+ };
272
+
273
+ if (isControlled.value) {
274
+ isVisible.value = props.value || false;
275
+ }
276
+
277
+ watch(
278
+ () => props.value,
279
+ newValue => {
280
+ if (isControlled.value) {
281
+ isVisible.value = newValue || false;
282
+
283
+ if (isVisible.value) {
284
+ nextTick(() => {
285
+ if (tooltipRef.value) {
286
+ moveToBody(tooltipRef.value);
287
+ updatePosition();
288
+ addScrollListener();
289
+ }
290
+ });
291
+ } else {
292
+ removeScrollListener();
293
+ }
294
+ }
345
295
  }
296
+ );
346
297
 
347
- if (hasBeenBoostrapped) {
348
- document.querySelector('body').removeChild(popup.value);
298
+ onBeforeUnmount(() => {
299
+ if (tooltipRef.value && containerRef.value) {
300
+ moveToContainer(tooltipRef.value, containerRef.value);
349
301
  }
302
+ removeScrollListener();
350
303
  });
351
304
 
352
305
  return {
353
- parent,
354
- popup,
355
- activator,
356
- showOver,
357
- toggleComponent,
358
- externalControl,
359
- hasPosition,
306
+ containerRef,
307
+ activatorRef,
308
+ tooltipRef,
309
+ isVisible,
310
+ isControlled,
360
311
  hasTitle,
361
- styles,
362
- onOver,
363
- onOut,
364
- onClose,
312
+ showCloseButton,
313
+ tooltipClasses,
314
+ tooltipStyles,
315
+ arrowStyles,
316
+ show,
317
+ hide,
318
+ close,
365
319
  };
366
320
  },
367
321
  });
368
322
  </script>
369
323
 
370
324
  <style lang="scss" scoped>
371
- @import './Tooltip';
325
+ @import './Tooltip.scss';
326
+
327
+ .tooltip-container {
328
+ display: inline-block;
329
+ position: relative;
330
+ }
331
+
332
+ .tooltip-activator {
333
+ display: inline-block;
334
+ }
372
335
  </style>