import type { Ref } from 'vue'; import { onBeforeUnmount, ref, watch, onMounted } from 'vue'; import raf from '../../_util/raf'; /** * Popup should follow the steps for each component work correctly: * measure - check for the value stretch size * align - let component align the position * aligned - re-align again in case additional className changed the size * afterAlign - choice next step is trigger motion or finished * beforeMotion - should reset motion to invisible so that CSSMotion can do normal motion * motion - play the motion * stable - everything is done */ type PopupStatus = null | 'measure' | 'align' | 'aligned' | 'motion' | 'stable'; type Func = () => void; const StatusQueue: PopupStatus[] = ['measure', 'align', null, 'motion']; export default ( visible: Ref, doMeasure: Func, ): [Ref, (callback?: () => void) => void] => { const status = ref(null); const rafRef = ref(); const destroyRef = ref(false); function setStatus(nextStatus: PopupStatus) { if (!destroyRef.value) { status.value = nextStatus; } } function cancelRaf() { raf.cancel(rafRef.value); } function goNextStatus(callback?: () => void) { cancelRaf(); rafRef.value = raf(() => { // Only align should be manually trigger let newStatus = status.value; switch (status.value) { case 'align': newStatus = 'motion'; break; case 'motion': newStatus = 'stable'; break; default: } setStatus(newStatus); callback?.(); }); } watch( visible, () => { setStatus('measure'); }, { immediate: true, flush: 'post' }, ); onMounted(() => { // Go next status watch( status, () => { switch (status.value) { case 'measure': doMeasure(); break; default: } if (status.value) { rafRef.value = raf(async () => { const index = StatusQueue.indexOf(status.value); const nextStatus = StatusQueue[index + 1]; if (nextStatus && index !== -1) { setStatus(nextStatus); } }); } }, { immediate: true, flush: 'post' }, ); }); onBeforeUnmount(() => { destroyRef.value = true; cancelRaf(); }); return [status, goNextStatus]; };