@farm-investimentos/front-mfe-components 15.14.3 → 15.14.5

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,77 +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">×</span>
18
+ <span v-if="showCloseButton" class="tooltip-close" @click="close">×</span>
32
19
  </div>
33
- <div class="farm-tooltip__content">
20
+
21
+ <div class="tooltip-content">
34
22
  <slot />
35
23
  </div>
36
- <span v-if="hasPosition" class="farm-tooltip__arrow"></span>
37
- </span>
38
- </span>
24
+
25
+ <div class="tooltip-arrow" :style="arrowStyles"></div>
26
+ </div>
27
+ </div>
39
28
  </template>
29
+
40
30
  <script lang="ts">
41
- import { PropType, ref, computed, reactive, onBeforeUnmount, defineComponent, useSlots } from 'vue';
42
- 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';
43
39
 
44
- export type TooltipPosition =
45
- | 'top-left'
46
- | 'top-center'
47
- | 'top-right'
48
- | 'bottom-left'
49
- | 'bottom-center'
50
- | 'bottom-right';
40
+ const ARROW_OFFSET = 18;
51
41
 
52
42
  export default defineComponent({
53
43
  name: 'farm-tooltip',
54
44
  props: {
55
- /**
56
- * Control visibility with v-model
57
- */
58
45
  value: {
59
46
  type: Boolean,
60
47
  default: undefined,
61
48
  },
62
- /**
63
- * Fluid width (grows based on content)
64
- */
65
- fluid: {
66
- type: Boolean,
67
- default: false,
49
+ trigger: {
50
+ type: String as PropType<TooltipTrigger>,
51
+ default: 'hover',
68
52
  },
69
- /**
70
- * Position of the tooltip relative to the activator
71
- */
72
- position: {
73
- type: String as PropType<TooltipPosition>,
74
- default: undefined,
53
+ placement: {
54
+ type: String as PropType<TooltipPlacement>,
55
+ default: 'top-center',
75
56
  validator: (value: string) => {
76
57
  return [
77
58
  'top-left',
@@ -83,237 +64,272 @@ export default defineComponent({
83
64
  ].includes(value);
84
65
  },
85
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
+ },
86
99
  },
87
- setup(props, { emit }) {
88
- const parent = ref(null);
89
- const popup = ref(null);
90
- const activator = ref(null);
91
- const showOver = ref(false);
92
- const styles = reactive({
93
- left: '0',
94
- top: '0',
95
- zIndex: 1,
96
- });
97
- const slots = useSlots();
98
- 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);
99
106
 
100
- const toggleComponent = computed(() => props.value);
101
- const externalControl = computed(() => props.value !== undefined);
102
- const hasPosition = computed(() => !!props.position);
107
+ const isVisible = ref(false);
108
+
109
+ const isControlled = computed(() => props.value !== undefined);
103
110
  const hasTitle = computed(() => !!slots.title);
111
+ const showCloseButton = computed(() => isControlled.value && hasTitle.value);
104
112
 
105
- let hasBeenBoostrapped = false;
106
- let scrollListener = null;
107
- let isInsideModal = false;
108
-
109
- const calculatePosition = () => {
110
- const parentBoundingClientRect = parent.value.getBoundingClientRect();
111
- const activatorBoundingClientRect = activator.value.getBoundingClientRect();
112
- const popupBoundingClientRect = popup.value.getBoundingClientRect();
113
-
114
- const activatorWidth = activatorBoundingClientRect.width;
115
- const activatorHeight = activatorBoundingClientRect.height;
116
- const popupWidth = popupBoundingClientRect.width;
117
- const popupHeight = popupBoundingClientRect.height;
118
-
119
- let left = 0;
120
- let top = 0;
121
-
122
- // Se estiver dentro de um modal, usar coordenadas da viewport (position fixed)
123
- if (isInsideModal) {
124
- if (!props.position) {
125
- left = activatorBoundingClientRect.left + activatorWidth / 2 - popupWidth / 2;
126
- top = activatorBoundingClientRect.top - popupHeight - 8;
127
- } else {
128
- const [verticalPosition, horizontalAlignment] = props.position.split('-');
129
-
130
- switch (horizontalAlignment) {
131
- case 'left':
132
- left = activatorBoundingClientRect.left - 8;
133
- break;
134
- case 'right':
135
- left = activatorBoundingClientRect.left + activatorWidth - popupWidth + 8;
136
- break;
137
- case 'center':
138
- default:
139
- left = activatorBoundingClientRect.left + activatorWidth / 2 - popupWidth / 2;
140
- break;
141
- }
113
+ const normalizedPlacement = computed(() => {
114
+ if (props.position) {
115
+ return props.position as TooltipPlacement;
116
+ }
117
+ return props.placement;
118
+ });
142
119
 
143
- if (verticalPosition === 'top') {
144
- top = activatorBoundingClientRect.top - popupHeight - 8;
145
- } else {
146
- top = activatorBoundingClientRect.top + activatorHeight + 8;
147
- }
148
- }
120
+ const normalizedMaxWidth = computed(() => {
121
+ if (props.fluid) {
122
+ return '300px';
123
+ }
124
+ return props.maxWidth;
125
+ });
149
126
 
150
- // Ajustar para não sair da viewport
151
- if (left < 5) {
152
- left = 5;
153
- } else if (left + popupWidth > window.innerWidth - 5) {
154
- left = window.innerWidth - popupWidth - 5;
155
- }
156
- } else {
157
- // Comportamento original para tooltips fora de modais
158
- if (!props.position) {
159
- left =
160
- parentBoundingClientRect.left +
161
- window.scrollX +
162
- activatorWidth / 2 -
163
- popupWidth / 2;
164
-
165
- top = parentBoundingClientRect.top + window.scrollY - popupHeight - 8;
166
- } else {
167
- const [verticalPosition, horizontalAlignment] = props.position.split('-');
168
-
169
- switch (horizontalAlignment) {
170
- case 'left':
171
- left = parentBoundingClientRect.left + window.scrollX - 8;
172
- break;
173
- case 'right':
174
- left =
175
- parentBoundingClientRect.left +
176
- window.scrollX +
177
- activatorWidth -
178
- popupWidth +
179
- 8;
180
- break;
181
- case 'center':
182
- default:
183
- left =
184
- parentBoundingClientRect.left +
185
- window.scrollX +
186
- activatorWidth / 2 -
187
- popupWidth / 2;
188
- break;
189
- }
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
+ }));
190
135
 
191
- if (verticalPosition === 'top') {
192
- top = parentBoundingClientRect.top + window.scrollY - popupHeight - 8;
193
- } else {
194
- top = parentBoundingClientRect.top + window.scrollY + activatorHeight + 8;
195
- }
196
- }
136
+ const tooltipStyles = computed(() => {
137
+ const styles: Record<string, string> = {
138
+ position: 'fixed',
139
+ zIndex: '9999',
140
+ };
197
141
 
198
- if (left < window.scrollX) {
199
- left = window.scrollX + 5;
200
- } else if (left + popupWidth > window.innerWidth + window.scrollX) {
201
- left = window.innerWidth + window.scrollX - popupWidth - 5;
202
- }
142
+ if (normalizedMaxWidth.value) {
143
+ styles.maxWidth =
144
+ typeof normalizedMaxWidth.value === 'number'
145
+ ? `${normalizedMaxWidth.value}px`
146
+ : normalizedMaxWidth.value;
147
+ styles.width = 'auto';
203
148
  }
204
149
 
205
- return { left, top };
206
- };
150
+ return styles;
151
+ });
207
152
 
208
- const onOver = () => {
209
- // Limpa qualquer timeout de hide
210
- if (hideTimeout) {
211
- clearTimeout(hideTimeout);
212
- hideTimeout = null;
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
+ };
163
+
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';
213
172
  }
214
173
 
215
- showOver.value = true;
216
-
217
- if (!hasBeenBoostrapped) {
218
- document.querySelector('body').appendChild(popup.value);
219
-
220
- // Detectar se está dentro de um modal
221
- isInsideModal = !!parent.value.closest('.farm-modal');
222
-
223
- if (isInsideModal) {
224
- // Usar position fixed para tooltips dentro de modais
225
- popup.value.style.position = 'fixed';
226
- // Adicionar listener de scroll para recalcular posição
227
- scrollListener = () => {
228
- const { left, top } = calculatePosition();
229
- styles.left = `${left}px`;
230
- styles.top = `${top}px`;
231
- };
232
- window.addEventListener('scroll', scrollListener, true);
233
- } else {
234
- // Comportamento original para tooltips fora de modais
235
- 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();
236
197
  }
237
-
238
- const { left, top } = calculatePosition();
239
- styles.left = `${left}px`;
240
- styles.top = `${top}px`;
241
- styles.zIndex = calculateMainZindex();
198
+ });
199
+ };
242
200
 
243
- hasBeenBoostrapped = true;
244
- }
201
+ const hide = () => {
202
+ if (props.disabled || isControlled.value) return;
203
+
204
+ isVisible.value = false;
205
+ emit('hide');
206
+ removeScrollListener();
245
207
  };
246
208
 
247
- const onOut = (event: MouseEvent) => {
248
- // Limpa qualquer timeout anterior
249
- if (hideTimeout) {
250
- clearTimeout(hideTimeout);
251
- hideTimeout = null;
209
+ const close = () => {
210
+ if (isControlled.value) {
211
+ emit('input', false);
212
+ } else {
213
+ hide();
252
214
  }
215
+ };
253
216
 
254
- // Verifica se o relatedTarget está contido no parent
255
- const isRelatedTargetInParent =
256
- event.relatedTarget && parent.value.contains(event.relatedTarget);
257
-
258
- if (!isRelatedTargetInParent) {
259
- // Se não está no parent, agenda o hide com um pequeno delay para evitar flickering
260
- hideTimeout = window.setTimeout(() => {
261
- showOver.value = false;
262
-
263
- // Remover listener de scroll quando tooltip for escondido
264
- if (scrollListener) {
265
- window.removeEventListener('scroll', scrollListener, true);
266
- scrollListener = null;
267
- }
268
-
269
- hideTimeout = null;
270
- }, 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;
271
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`;
272
243
  };
273
244
 
274
- const onClose = () => {
275
- showOver.value = false;
276
- if (externalControl.value) {
277
- 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);
278
251
  }
252
+ return scrollableElementsRef.value;
279
253
  };
280
254
 
281
- onBeforeUnmount(() => {
282
- // Limpa o timeout se existir
283
- if (hideTimeout) {
284
- clearTimeout(hideTimeout);
285
- hideTimeout = null;
286
- }
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
+ };
263
+
264
+ const removeScrollListener = () => {
265
+ window.removeEventListener('scroll', updatePosition);
287
266
 
288
- // Limpar listener de scroll se existir
289
- if (scrollListener) {
290
- window.removeEventListener('scroll', scrollListener, true);
291
- scrollListener = null;
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
+ }
292
295
  }
296
+ );
293
297
 
294
- if (hasBeenBoostrapped) {
295
- document.querySelector('body').removeChild(popup.value);
298
+ onBeforeUnmount(() => {
299
+ if (tooltipRef.value && containerRef.value) {
300
+ moveToContainer(tooltipRef.value, containerRef.value);
296
301
  }
302
+ removeScrollListener();
297
303
  });
298
304
 
299
305
  return {
300
- parent,
301
- popup,
302
- activator,
303
- showOver,
304
- toggleComponent,
305
- externalControl,
306
- hasPosition,
306
+ containerRef,
307
+ activatorRef,
308
+ tooltipRef,
309
+ isVisible,
310
+ isControlled,
307
311
  hasTitle,
308
- styles,
309
- onOver,
310
- onOut,
311
- onClose,
312
+ showCloseButton,
313
+ tooltipClasses,
314
+ tooltipStyles,
315
+ arrowStyles,
316
+ show,
317
+ hide,
318
+ close,
312
319
  };
313
320
  },
314
321
  });
315
322
  </script>
316
323
 
317
324
  <style lang="scss" scoped>
318
- @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
+ }
319
335
  </style>