@farm-investimentos/front-mfe-components-vue3 1.3.0 → 1.4.0

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