|
1 | 1 | <template> |
2 | | - <div class="c-popover"> |
3 | | - <div |
4 | | - ref="trigger" |
5 | | - class="c-popover__trigger" |
6 | | - @click.stop="props.triggerEvent === 'click' ? togglePopover() : null" |
7 | | - @mouseenter="props.triggerEvent === 'hover' ? showPopover() : null" |
8 | | - @mouseleave="props.triggerEvent === 'hover' ? hidePopover() : null" |
9 | | - > |
10 | | - <slot :close="hidePopover" /> |
11 | | - </div> |
12 | | - <Teleport |
13 | | - v-if="!props.disabled && (props.persistent || isVisible)" |
14 | | - to="body" |
15 | | - > |
16 | | - <div |
17 | | - ref="popover" |
18 | | - :style="popoverStyle" |
19 | | - class="c-popover__content" |
20 | | - :class="[`is-placed-${props.placement}`, { 'is-hidden': props.persistent && !isVisible }]" |
21 | | - > |
22 | | - <slot |
23 | | - name="content" |
24 | | - :close="hidePopover" |
25 | | - /> |
26 | | - </div> |
27 | | - </Teleport> |
| 2 | + <div |
| 3 | + ref="trigger" |
| 4 | + class="c-popover__trigger" |
| 5 | + @click="handleClick" |
| 6 | + > |
| 7 | + <slot /> |
| 8 | + </div> |
| 9 | + |
| 10 | + <div |
| 11 | + v-show="isVisible" |
| 12 | + ref="popover" |
| 13 | + class="c-popover__content" |
| 14 | + :class="placement" |
| 15 | + > |
| 16 | + <slot |
| 17 | + name="content" |
| 18 | + :close="closePopover" |
| 19 | + /> |
28 | 20 | </div> |
29 | 21 | </template> |
30 | 22 |
|
31 | 23 | <script lang="ts" setup> |
32 | 24 | import { |
33 | | - computed, onBeforeUnmount, ref, watch, |
| 25 | + ref, watch, onMounted, onBeforeUnmount |
34 | 26 | } from 'vue'; |
35 | | -import type { PopoverPlacement } from '@/ui-kit/popover/types/PopoverPlacement'; |
36 | | -import type { PopoverTrigger } from '@/ui-kit/popover/types/PopoverTrigger'; |
37 | | -import useScroll from "~/components/shared/utils/scroll"; |
| 27 | +import type { Instance, Placement } from '@popperjs/core'; |
| 28 | +import { createPopper } from '@popperjs/core'; |
38 | 29 |
|
39 | 30 | const props = withDefaults(defineProps<{ |
40 | | - placement?: PopoverPlacement, |
41 | | - disabled?: boolean, |
42 | | - spacing?: number, |
| 31 | + placement?: Placement, |
| 32 | + triggerEvent?: 'click' | 'hover', |
43 | 33 | visibility?: boolean, |
44 | | - triggerEvent?: PopoverTrigger, |
45 | | - persistent?: boolean; |
| 34 | + spacing?: number, |
| 35 | + disabled?: boolean, |
46 | 36 | }>(), { |
47 | 37 | placement: 'bottom-start', |
48 | | - disabled: false, |
49 | | - spacing: 4, |
50 | | - visibility: false, |
51 | 38 | triggerEvent: 'click', |
52 | | - persistent: false, |
| 39 | + visibility: false, |
| 40 | + spacing: 4, |
| 41 | + disabled: false, |
53 | 42 | }); |
54 | 43 |
|
55 | | -const emit = defineEmits<{(e: 'update:visibility', value: boolean): void }>(); |
56 | | -
|
57 | | -const {scrollTop} = useScroll(); |
| 44 | +const emit = defineEmits(['update:visibility']); |
58 | 45 |
|
59 | | -const trigger = ref(null); |
60 | | -const popover = ref(null); |
| 46 | +const trigger = ref<HTMLElement | null>(null); |
| 47 | +const popover = ref<HTMLElement | null>(null); |
| 48 | +const popperInstance = ref<Instance | null>(null); |
61 | 49 | const isVisible = ref(props.visibility); |
62 | 50 |
|
63 | | -watch(() => props.visibility, (newValue) => { |
64 | | - isVisible.value = newValue; |
65 | | -}, { immediate: true }); |
66 | | -
|
67 | | -watch(isVisible, (newValue) => { |
68 | | - emit('update:visibility', newValue); |
| 51 | +watch(() => props.visibility, (val) => { |
| 52 | + isVisible.value = val; |
69 | 53 | }); |
| 54 | +watch(isVisible, (val) => emit('update:visibility', val)); |
| 55 | +
|
| 56 | +const createPopperInstance = () => { |
| 57 | + if (trigger.value && popover.value) { |
| 58 | + popperInstance.value = createPopper(trigger.value, popover.value, { |
| 59 | + strategy: 'fixed', |
| 60 | + placement: props.placement, |
| 61 | + modifiers: [ |
| 62 | + { |
| 63 | + name: 'offset', |
| 64 | + options: { |
| 65 | + offset: [0, props.spacing], |
| 66 | + }, |
| 67 | + }, |
| 68 | + ], |
| 69 | + }); |
| 70 | + } |
| 71 | +}; |
70 | 72 |
|
71 | | -const togglePopover = () => { |
72 | | - isVisible.value = !isVisible.value; |
73 | | - if (isVisible.value) addOutsideClickListener(); |
74 | | - else removeOutsideClickListener(); |
| 73 | +const destroyPopperInstance = () => { |
| 74 | + popperInstance.value?.destroy(); |
| 75 | + popperInstance.value = null; |
75 | 76 | }; |
76 | 77 |
|
77 | | -const showPopover = () => { |
| 78 | +const openPopover = async () => { |
78 | 79 | isVisible.value = true; |
79 | | - addOutsideClickListener(); |
80 | 80 | }; |
81 | 81 |
|
82 | | -const hidePopover = () => { |
| 82 | +const closePopover = () => { |
83 | 83 | isVisible.value = false; |
84 | | - removeOutsideClickListener(); |
| 84 | + document.removeEventListener('click', handleClickOutside); |
85 | 85 | }; |
86 | 86 |
|
87 | | -const handleClickOutside = (event: Event) => { |
88 | | - if ( |
89 | | - !trigger.value?.contains(event.target) |
90 | | - && !popover.value?.contains(event.target) |
91 | | - ) { |
92 | | - isVisible.value = false; |
93 | | - removeOutsideClickListener(); |
| 87 | +const handleClick = (e: Event) => { |
| 88 | + e.stopPropagation(); |
| 89 | + if (props.triggerEvent === 'click') { |
| 90 | + if (isVisible.value) { |
| 91 | + closePopover(); |
| 92 | + } else { |
| 93 | + openPopover(); |
| 94 | + } |
94 | 95 | } |
95 | 96 | }; |
96 | 97 |
|
97 | | -const addOutsideClickListener = () => { |
98 | | - document.addEventListener('click', handleClickOutside); |
99 | | -}; |
100 | | -
|
101 | | -const removeOutsideClickListener = () => { |
102 | | - document.removeEventListener('click', handleClickOutside); |
| 98 | +const handleClickOutside = (e: Event) => { |
| 99 | + if ( |
| 100 | + popover.value |
| 101 | + && !popover.value.contains(e.target as Node) |
| 102 | + && !trigger.value?.contains(e.target as Node) |
| 103 | + ) { |
| 104 | + closePopover(); |
| 105 | + } |
103 | 106 | }; |
104 | 107 |
|
105 | | -const popoverStyle = computed(() => { |
106 | | - if (!trigger.value || !popover.value) return {}; |
107 | | -
|
108 | | - const triggerRect = trigger.value.getBoundingClientRect(); |
109 | | - return { |
110 | | - '--lfx-popover-trigger-top': `${(triggerRect.top + scrollTop.value) / 16}rem`, |
111 | | - '--lfx-popover-trigger-left': `${triggerRect.left / 16}rem`, |
112 | | - '--lfx-popover-trigger-right': `${triggerRect.right / 16}rem`, |
113 | | - '--lfx-popover-trigger-bottom': `${triggerRect.bottom / 16}rem`, |
114 | | - '--lfx-popover-trigger-width': `${trigger.value.offsetWidth / 16}rem`, |
115 | | - '--lfx-popover-trigger-height': `${trigger.value.offsetHeight / 16}rem`, |
116 | | - '--lfx-popover-content-width': `${popover.value.offsetWidth / 16}rem`, |
117 | | - '--lfx-popover-content-height': `${popover.value.offsetHeight / 16}rem`, |
118 | | - '--lfx-popover-spacing': `${props.spacing / 16}rem`, |
119 | | - }; |
| 108 | +onMounted(() => { |
| 109 | + createPopperInstance(); |
| 110 | + if (props.triggerEvent === 'hover') { |
| 111 | + trigger.value?.addEventListener('mouseenter', openPopover); |
| 112 | + trigger.value?.addEventListener('mouseleave', closePopover); |
| 113 | + } |
120 | 114 | }); |
121 | 115 |
|
122 | 116 | onBeforeUnmount(() => { |
123 | | - removeOutsideClickListener(); |
| 117 | + destroyPopperInstance(); |
| 118 | + if (props.triggerEvent === 'hover') { |
| 119 | + trigger.value?.removeEventListener('mouseenter', openPopover); |
| 120 | + trigger.value?.removeEventListener('mouseleave', closePopover); |
| 121 | + } |
124 | 122 | }); |
125 | 123 | </script> |
126 | 124 |
|
|
0 commit comments