Skip to content

Commit 8ddec33

Browse files
committed
refactor(Popper): extract timeout manager and simplify popper logic
1 parent 99bdfdc commit 8ddec33

File tree

2 files changed

+58
-42
lines changed

2 files changed

+58
-42
lines changed

src/composables/popper.ts

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createPopper, type Instance, type Placement, type PositioningStrategy, type VirtualElement } from '@popperjs/core';
22
import { type MaybeElement, unrefElement } from '@vueuse/core';
33
import { onMounted, type Ref, ref, watchEffect } from 'vue';
4+
import { useTimeoutManager } from './timeout-manager';
45

56
export interface PopperOptions {
67
locked?: boolean;
@@ -39,7 +40,7 @@ interface UsePopperReturn {
3940
popper: Ref<MaybeElement>;
4041
popperEnter: Ref<boolean>;
4142
reference: Ref<MaybeElement>;
42-
updatePopper: () => void;
43+
updatePopper: () => Promise<void>;
4344
}
4445

4546
export function usePopper(
@@ -49,65 +50,53 @@ export function usePopper(
4950
closeDelay: Ref<number> = ref(0),
5051
virtualReference?: Ref<Element | VirtualElement>,
5152
): UsePopperReturn {
52-
const reference: Ref<MaybeElement | null> = ref(null);
53-
const popper: Ref<MaybeElement | null> = ref(null);
54-
const instance: Ref<Instance | null> = ref(null);
55-
const open: Ref<boolean> = ref(false);
56-
const openTimeout: Ref<NodeJS.Timeout | undefined> = ref();
57-
const closeTimeout: Ref<NodeJS.Timeout | undefined> = ref();
58-
const popperEnter: Ref<boolean> = ref(false);
59-
const leavePending = ref(false);
53+
const reference = ref<MaybeElement | null>(null);
54+
const popper = ref<MaybeElement | null>(null);
55+
const instance = ref<Instance | null>(null);
56+
const open = ref<boolean>(false);
57+
const popperEnter = ref<boolean>(false);
58+
const leavePending = ref<boolean>(false);
59+
60+
const openTimeoutManager = useTimeoutManager();
61+
const closeTimeoutManager = useTimeoutManager();
6062

6163
const onPopperLeave = () => {
6264
set(popperEnter, false);
6365
};
6466

65-
const updatePopper = () => {
66-
// todo: see making things async/await has any side-effects
67-
get(instance)?.update();
67+
const updatePopper = async (): Promise<void> => {
68+
await get(instance)?.update();
6869
};
6970

7071
const onOpen = (immediate = false) => {
7172
if (get(disabled))
7273
return;
7374

74-
if (get(closeTimeout)) {
75-
clearTimeout(get(closeTimeout));
76-
set(closeTimeout, undefined);
77-
}
75+
closeTimeoutManager.clear();
7876

7977
set(leavePending, false);
80-
if (!get(openTimeout)) {
78+
if (!openTimeoutManager.isActive()) {
8179
set(popperEnter, true);
8280

83-
const timeout = setTimeout(() => {
81+
openTimeoutManager.create(() => {
8482
set(open, true);
85-
set(openTimeout, undefined);
8683
}, immediate ? 0 : get(openDelay));
87-
88-
set(openTimeout, timeout);
8984
}
9085
};
9186

9287
const onClose = (immediate = false) => {
9388
if (get(disabled))
9489
return;
9590

96-
if (get(openTimeout)) {
97-
clearTimeout(get(openTimeout));
98-
set(openTimeout, undefined);
99-
}
91+
openTimeoutManager.clear();
10092

101-
if (!get(closeTimeout)) {
102-
const timeout = setTimeout(() => {
93+
if (!closeTimeoutManager.isActive()) {
94+
closeTimeoutManager.create(() => {
10395
if (!get(open))
10496
onPopperLeave();
10597

10698
set(open, false);
107-
set(closeTimeout, undefined);
10899
}, immediate ? 0 : get(closeDelay));
109-
110-
set(closeTimeout, timeout);
111100
}
112101
};
113102

@@ -176,32 +165,37 @@ export function usePopper(
176165
};
177166
});
178167

179-
function initializePopper(onInvalidate: (cleanupFn: () => void) => void) {
180-
if (!get(popper))
181-
return;
182-
183-
if (!get(reference) && !get(virtualReference))
184-
return;
168+
function getValidElements(): { popperEl: HTMLElement; referenceEl: Element | VirtualElement } | null {
169+
if (!get(popper) || (!get(reference) && !get(virtualReference))) {
170+
return null;
171+
}
185172

186173
const popperEl = unrefElement(popper);
187174
const referenceEl = get(virtualReference) || unrefElement(reference);
188175

189-
if (!(popperEl instanceof HTMLElement))
190-
return;
176+
if (!(popperEl instanceof HTMLElement) || !referenceEl) {
177+
return null;
178+
}
191179

192-
if (!referenceEl)
180+
return { popperEl, referenceEl };
181+
}
182+
183+
function initializePopper(onInvalidate: (cleanupFn: () => void) => void) {
184+
const elements = getValidElements();
185+
if (!elements)
193186
return;
194187

188+
const { popperEl, referenceEl } = elements;
189+
195190
const value = createPopper(referenceEl, popperEl, get(popperConfig));
196191

197192
set(instance, value);
198193
onInvalidate(value.destroy);
199194
}
200195

201196
useResizeObserver([reference, popper], async () => {
202-
const instanceVal = get(instance);
203-
if (get(open) && instanceVal)
204-
await instanceVal.update();
197+
if (isDefined(instance))
198+
await get(instance).update();
205199
});
206200

207201
onMounted(() => {

src/composables/timeout-manager.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { get, set } from '@vueuse/shared';
2+
import { ref } from 'vue';
3+
4+
export function useTimeoutManager() {
5+
const timeout = ref<NodeJS.Timeout>();
6+
7+
const clear = () => {
8+
if (get(timeout)) {
9+
clearTimeout(get(timeout));
10+
set(timeout, undefined);
11+
}
12+
};
13+
14+
const create = (callback: () => void, delay: number) => {
15+
clear();
16+
set(timeout, setTimeout(callback, delay));
17+
};
18+
19+
const isActive = () => !!get(timeout);
20+
21+
return { clear, create, isActive };
22+
}

0 commit comments

Comments
 (0)