Skip to content

Commit

Permalink
update Presence component
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed Jul 24, 2023
1 parent 4733172 commit 95480b6
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 93 deletions.
10 changes: 7 additions & 3 deletions packages/radix-vue/src/Collapsible/CollapsibleContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,25 @@ const props = defineProps<CollapsibleContentProps>();
</script>

<template>
<Presence ref="presentRef" :present="injectedValue!.open.value">
<Presence
ref="presentRef"
:force-mount="true"
:present="injectedValue!.open.value"
>
<PrimitiveDiv
ref="primitiveElement"
v-bind="$attrs"
:as-child="props.asChild"
:data-state="injectedValue?.open.value ? 'open' : 'closed'"
:data-disabled="injectedValue?.disabled?.value ? 'true' : undefined"
:id="injectedValue?.contentId"
:hidden="!injectedValue?.open"
:hidden="!presentRef?.present"
:style="{
[`--radix-collapsible-content-height`]: `${height}px`,
[`--radix-collapsible-content-width`]: `${width}px`,
}"
>
<slot />
<slot v-if="presentRef?.present" />
</PrimitiveDiv>
</Presence>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ const itemContext = inject(NAVIGATION_MENU_ITEM_INJECTION_KEY);
const { primitiveElement, currentElement: triggerElement } =
usePrimitiveElement();
const triggerId = makeTriggerId(context!.baseId, itemContext!.value);
const contentId = makeContentId(context!.baseId, itemContext!.value);
let triggerId = ref("");
let contentId = ref("");
const hasPointerMoveOpenedRef = ref(false);
const wasClickCloseRef = ref(false);
const open = computed(() => itemContext?.value === context?.modelValue.value);
onMounted(() => {
itemContext!.triggerRef = triggerElement;
triggerId.value = makeTriggerId(context!.baseId, itemContext!.value);
contentId.value = makeContentId(context!.baseId, itemContext!.value);
});
const handlePointerEnter = () => {
Expand All @@ -50,6 +53,7 @@ const handlePointerMove = (ev: PointerEvent) => {
hasPointerMoveOpenedRef.value
)
return;
context!.onTriggerEnter(itemContext!.value);
hasPointerMoveOpenedRef.value = true;
}
Expand Down
28 changes: 16 additions & 12 deletions packages/radix-vue/src/Presence/Presence.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,46 @@ import {
ref,
toRefs,
useSlots,
vShow,
withDirectives,
type Directive,
type VNode,
} from "vue";
import { usePresence } from "./usePresence";
import { syncRef } from "@vueuse/core";
interface PresenceProps {
// children: HTMLElement | ((props: { present: boolean }) => HTMLElement);
present: boolean;
forceMount?: boolean;
}
const props = defineProps<PresenceProps>();
const { present } = toRefs(props);
const { present, forceMount } = toRefs(props);
const slots = useSlots();
let isLocalPresence = ref(false);
const isLocalPresence = ref(props.forceMount || props.present);
const node = ref<HTMLElement>();
const { isPresent } = usePresence(present, node);
const vTest: Directive = {
const vPresence: Directive = {
created(el) {
const { isPresent } = usePresence(present, el);
syncRef(isLocalPresence, isPresent, { direction: "rtl" });
node.value = el;
},
};
const render = () =>
// @ts-ignore
withDirectives(slots.default?.()?.[0], [
[vTest],
[vShow, isLocalPresence.value],
]);
forceMount.value || present.value || isLocalPresence.value
? withDirectives(
slots.default?.({ present: isLocalPresence })?.[0] as VNode,
[[vPresence]]
)
: null;
defineExpose({
present: isLocalPresence,
});
</script>

<template>
<render></render>
<render />
</template>
158 changes: 82 additions & 76 deletions packages/radix-vue/src/Presence/usePresence.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useStateMachine } from "@/shared";
import { ref, watchEffect, type Ref, computed, nextTick } from "vue";
import { ref, type Ref, computed, nextTick, watch, onUnmounted } from "vue";

export function usePresence(present: Ref<boolean>, node: HTMLElement) {
export function usePresence(
present: Ref<boolean>,
node: Ref<HTMLElement | undefined>
) {
const stylesRef = ref<CSSStyleDeclaration>({} as any);
const prevPresentRef = ref(present.value);
const prevAnimationNameRef = ref<string>("none");
const initialState = present.value ? "mounted" : "unmounted";

Expand All @@ -21,98 +23,102 @@ export function usePresence(present: Ref<boolean>, node: HTMLElement) {
},
});

// watchEffect(() => {
// const currentAnimationName = getAnimationName(node);
// prevAnimationNameRef.value =
// state.value === "mounted" ? currentAnimationName : "none";
// });
watch(
present,
async (currentPresent, prevPresent) => {
const hasPresentChanged = prevPresent !== currentPresent;
await nextTick();
if (hasPresentChanged) {
const prevAnimationName = prevAnimationNameRef.value;
const currentAnimationName = getAnimationName(node.value);

watchEffect(async () => {
const styles = stylesRef.value;
const wasPresent = prevPresentRef.value;
const hasPresentChanged = wasPresent !== present.value;
await nextTick();
if (hasPresentChanged) {
const prevAnimationName = prevAnimationNameRef.value;
const currentAnimationName = getAnimationName(node);
if (present.value) {
dispatch("MOUNT");
} else if (
currentAnimationName === "none" ||
styles?.display === "none"
) {
// If there is no exit animation or the element is hidden, animations won't run
// so we unmount instantly
dispatch("UNMOUNT");
} else {
/**
* When `present` changes to `false`, we check changes to animation-name to
* determine whether an animation has started. We chose this approach (reading
* computed styles) because there is no `animationrun` event and `animationstart`
* fires after `animation-delay` has expired which would be too late.
*/
const isAnimating = prevAnimationName !== currentAnimationName;
// console.log(isAnimating, prevAnimationName, currentAnimationName);
if (wasPresent && isAnimating) {
dispatch("ANIMATION_OUT");
} else {
if (currentPresent) {
dispatch("MOUNT");
} else if (
currentAnimationName === "none" ||
stylesRef.value?.display === "none"
) {
// If there is no exit animation or the element is hidden, animations won't run
// so we unmount instantly rv
dispatch("UNMOUNT");
} else {
/**
* When `present` changes to `false`, we check changes to animation-name to
* determine whether an animation has started. We chose this approach (reading
* computed styles) because there is no `animationrun` event and `animationstart`
* fires after `animation-delay` has expired which would be too late.
*/
const isAnimating = prevAnimationName !== currentAnimationName;
if (prevPresent && isAnimating) {
dispatch("ANIMATION_OUT");
} else {
dispatch("UNMOUNT");
}
}
}
},
{ immediate: true }
);

prevPresentRef.value = present.value;
/**
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
* make sure we only trigger ANIMATION_END for the currently active animation.
*/
const handleAnimationEnd = (event: AnimationEvent) => {
const currentAnimationName = getAnimationName(node.value);
const isCurrentAnimation = currentAnimationName.includes(
event.animationName
);
if (event.target === node.value && isCurrentAnimation) {
// With React 18 concurrency this update is applied
// a frame after the animation ends, creating a flash of visible content.
// By manually flushing we ensure they sync within a frame, removing the flash.
// ReactDOM.flushSync(() => dispatch("ANIMATION_END"));
dispatch("ANIMATION_END");
}
});
};
const handleAnimationStart = (event: AnimationEvent) => {
if (event.target === node.value) {
// if animation occurred, store its name as the previous animation.
prevAnimationNameRef.value = getAnimationName(node.value);
}
};

if (node) {
/**
* Triggering an ANIMATION_OUT during an ANIMATION_IN will fire an `animationcancel`
* event for ANIMATION_IN after we have entered `unmountSuspended` state. So, we
* make sure we only trigger ANIMATION_END for the currently active animation.
*/
const handleAnimationEnd = (event: AnimationEvent) => {
const currentAnimationName = getAnimationName(node);
const isCurrentAnimation = currentAnimationName.includes(
event.animationName
);
if (event.target === node && isCurrentAnimation) {
// With React 18 concurrency this update is applied
// a frame after the animation ends, creating a flash of visible content.
// By manually flushing we ensure they sync within a frame, removing the flash.
// ReactDOM.flushSync(() => dispatch("ANIMATION_END"));
const watcher = watch(
node,
(newNode, oldNode) => {
if (newNode) {
stylesRef.value = getComputedStyle(newNode);
newNode.addEventListener("animationstart", handleAnimationStart);
newNode.addEventListener("animationcancel", handleAnimationEnd);
newNode.addEventListener("animationend", handleAnimationEnd);
} else {
// Transition to the unmounted state if the node is removed prematurely.
// We avoid doing so during cleanup as the node may change but still exist.
dispatch("ANIMATION_END");

oldNode?.removeEventListener("animationstart", handleAnimationStart);
oldNode?.removeEventListener("animationcancel", handleAnimationEnd);
oldNode?.removeEventListener("animationend", handleAnimationEnd);
}
};
const handleAnimationStart = (event: AnimationEvent) => {
if (event.target === node) {
// if animation occurred, store its name as the previous animation.
prevAnimationNameRef.value = getAnimationName(node);
}
};
node.addEventListener("animationstart", handleAnimationStart);
node.addEventListener("animationcancel", handleAnimationEnd);
node.addEventListener("animationend", handleAnimationEnd);
},
{ immediate: true }
);

// node.removeEventListener("animationstart", handleAnimationStart);
// node.removeEventListener("animationcancel", handleAnimationEnd);
// node.removeEventListener("animationend", handleAnimationEnd);
} else {
// Transition to the unmounted state if the node is removed prematurely.
// We avoid doing so during cleanup as the node may change but still exist.
dispatch("ANIMATION_END");
}
onUnmounted(() => {
watcher();
});

const isPresent = computed(() =>
["mounted", "unmountSuspended"].includes(state.value)
);

stylesRef.value = getComputedStyle(node);

return {
isPresent,
};
}

function getAnimationName(node: HTMLElement) {
function getAnimationName(node?: HTMLElement) {
return node ? getComputedStyle(node).animationName || "none" : "none";
}

0 comments on commit 95480b6

Please sign in to comment.