Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,53 +1,112 @@
import { type PropsWithChildren, type ReactNode, useEffect } from 'react';
import type { PropsWithChildren } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import type { ManualGesture } from 'react-native-gesture-handler';
import {
runOnJS,
type SharedValue,
useAnimatedReaction
} from 'react-native-reanimated';

import { useStableCallback } from '../../../hooks';
import type { AnimatedStyleProp } from '../../../integrations/reanimated';
import { useMutableValue } from '../../../integrations/reanimated';
import { usePortalContext } from '../../../providers';
import {
CommonValuesContext,
ItemContextProvider,
usePortalContext
} from '../../../providers';
import type { CommonValuesContextType } from '../../../types';
import { getContextProvider } from '../../../utils';
import TeleportedItemCell from './TeleportedItemCell';

const CommonValuesContextProvider = getContextProvider(CommonValuesContext);

type ActiveItemPortalProps = PropsWithChildren<{
itemKey: string;
activationAnimationProgress: SharedValue<number>;
commonValuesContext: CommonValuesContextType;
renderTeleportedItemCell: () => ReactNode;
cellStyle: AnimatedStyleProp;
isActive: SharedValue<boolean>;
gesture: ManualGesture;
onTeleport: (isTeleported: boolean) => void;
}>;

export default function ActiveItemPortal({
activationAnimationProgress,
cellStyle,
children,
commonValuesContext,
gesture,
isActive,
itemKey,
renderTeleportedItemCell
onTeleport
}: ActiveItemPortalProps) {
const { containerId } = commonValuesContext;
const { measurePortalOutlet, teleport } = usePortalContext() ?? {};

const { isTeleported, measurePortalOutlet, teleport } =
usePortalContext() ?? {};
const updateTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const teleportEnabled = useMutableValue(false);

const teleportedItemId = `${containerId}-${itemKey}`;
const renderTeleportedItemCell = useCallback(
() => (
// We have to wrap the TeleportedItemCell in context providers as they won't
// be accessible otherwise, when the item is rendered in the portal outlet
<CommonValuesContextProvider value={commonValuesContext}>
<ItemContextProvider
activationAnimationProgress={activationAnimationProgress}
gesture={gesture}
isActive={isActive}
itemKey={itemKey}>
<TeleportedItemCell
activationAnimationProgress={activationAnimationProgress}
cellStyle={cellStyle}
isActive={isActive}
itemKey={itemKey}>
{children}
</TeleportedItemCell>
</ItemContextProvider>
</CommonValuesContextProvider>
),
[
activationAnimationProgress,
children,
commonValuesContext,
gesture,
isActive,
itemKey,
cellStyle
]
);

useEffect(() => {
if (teleportEnabled.value) {
teleport?.(teleportedItemId, renderTeleportedItemCell());
}
// This is fine, we want to update the teleported item cell only when
// the children change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children]);
const teleportedItemId = `${commonValuesContext.containerId}-${itemKey}`;

const enableTeleport = () => {
const enableTeleport = useStableCallback(() => {
teleport?.(teleportedItemId, renderTeleportedItemCell());
};
onTeleport(true);
});

const disableTeleport = useCallback(() => {
if (updateTimeoutRef.current !== null) {
clearTimeout(updateTimeoutRef.current);
}
teleport?.(teleportedItemId, null);
onTeleport(false);
}, [teleport, teleportedItemId, onTeleport]);

const disableTeleport = () => {
if (teleport) {
runOnJS(teleport)(teleportedItemId, null);
useEffect(() => disableTeleport, [disableTeleport]);

useEffect(() => {
if (isTeleported?.(teleportedItemId)) {
// We have to delay the update in order not to schedule render via this
// useEffect at the same time as the enableTeleport render is scheduled.
// This may happen if the user changes the item style/content via the
// onDragStart callback (e.g. in collapsible items) when we want to
// render the view unchanged at first and change it a while later to
// properly trigger all layout transitions that the item has.
updateTimeoutRef.current = setTimeout(() => {
teleport?.(teleportedItemId, renderTeleportedItemCell());
});
}
};
}, [isTeleported, renderTeleportedItemCell, teleport, teleportedItemId]);

useAnimatedReaction(
() => activationAnimationProgress.value,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PropsWithChildren, ReactNode } from 'react';
import type { PropsWithChildren } from 'react';
import { Fragment, memo, useCallback, useEffect, useState } from 'react';
import type { LayoutChangeEvent } from 'react-native';
import { GestureDetector } from 'react-native-gesture-handler';
Expand All @@ -14,7 +14,6 @@ import type {
} from '../../../integrations/reanimated';
import { useMutableValue } from '../../../integrations/reanimated';
import {
CommonValuesContext,
ItemContextProvider,
useCommonValuesContext,
useDragContext,
Expand All @@ -23,12 +22,8 @@ import {
useMeasurementsContext,
usePortalContext
} from '../../../providers';
import { getContextProvider } from '../../../utils';
import ActiveItemPortal from './ActiveItemPortal';
import ItemCell from './ItemCell';
import TeleportedItemCell from './TeleportedItemCell';

const CommonValuesContextProvider = getContextProvider(CommonValuesContext);

export type DraggableViewProps = PropsWithChildren<{
itemKey: string;
Expand All @@ -49,9 +44,9 @@ function DraggableView({
const { handleItemMeasurement, removeItemMeasurements } =
useMeasurementsContext();
const { handleDragEnd } = useDragContext();
const { activeItemKey, containerId, customHandle } = commonValuesContext;
const { activeItemKey, customHandle } = commonValuesContext;

const [isTeleported, setIsTeleported] = useState(false);
const [isHidden, setIsHidden] = useState(false);
const activationAnimationProgress = useMutableValue(0);
const isActive = useDerivedValue(() => activeItemKey.value === key);
const itemStyles = useItemStyles(key, isActive, activationAnimationProgress);
Expand All @@ -66,24 +61,6 @@ function DraggableView({
};
}, [activationAnimationProgress, handleDragEnd, key, removeItemMeasurements]);

useEffect(() => {
if (!portalContext) {
setIsTeleported(false);
return;
}

const teleportedItemId = `${containerId}-${key}`;
const unsubscribe = portalContext?.subscribe?.(
teleportedItemId,
setIsTeleported
);

return () => {
portalContext?.teleport?.(teleportedItemId, null);
unsubscribe?.();
};
}, [portalContext, containerId, key]);

const onLayout = useCallback(
({
nativeEvent: {
Expand All @@ -93,45 +70,37 @@ function DraggableView({
[handleItemMeasurement, key]
);

const withItemContext = (component: ReactNode) => (
<ItemContextProvider
activationAnimationProgress={activationAnimationProgress}
gesture={gesture}
isActive={isActive}
itemKey={key}>
{component}
</ItemContextProvider>
);

const sharedCellProps = {
activationAnimationProgress,
isActive,
itemKey: key,
onLayout
};

const renderItemCell = (hidden = false) => {
const innerComponent = (
<ItemCell
{...sharedCellProps}
activationAnimationProgress={activationAnimationProgress}
cellStyle={[style, itemStyles]}
entering={itemEntering ?? undefined}
exiting={itemExiting ?? undefined}
hidden={hidden}>
hidden={hidden}
isActive={isActive}
itemKey={key}
onLayout={onLayout}>
<LayoutAnimationConfig skipEntering={false} skipExiting={false}>
{children}
</LayoutAnimationConfig>
</ItemCell>
);

return withItemContext(
customHandle ? (
innerComponent
) : (
<GestureDetector gesture={gesture} userSelect='none'>
{innerComponent}
</GestureDetector>
)
return (
<ItemContextProvider
activationAnimationProgress={activationAnimationProgress}
gesture={gesture}
isActive={isActive}
itemKey={key}>
{customHandle ? (
innerComponent
) : (
<GestureDetector gesture={gesture} userSelect='none'>
{innerComponent}
</GestureDetector>
)}
</ItemContextProvider>
);
};

Expand All @@ -143,28 +112,19 @@ function DraggableView({

// PORTAL CASE

const renderTeleportedItemCell = () => (
// We have to wrap the TeleportedItemCell in context providers as they won't
// be accessible otherwise, when the item is rendered in the portal outlet
<CommonValuesContextProvider value={commonValuesContext}>
{withItemContext(
<TeleportedItemCell {...sharedCellProps} cellStyle={style}>
{children}
</TeleportedItemCell>
)}
</CommonValuesContextProvider>
);

return (
<Fragment>
{/* We cannot unmount this item as its gesture detector must be still
mounted to continue handling the pan gesture */}
{renderItemCell(isTeleported)}
{renderItemCell(isHidden)}
<ActiveItemPortal
activationAnimationProgress={activationAnimationProgress}
cellStyle={style}
commonValuesContext={commonValuesContext}
gesture={gesture}
isActive={isActive}
itemKey={key}
renderTeleportedItemCell={renderTeleportedItemCell}>
onTeleport={setIsHidden}>
{children}
</ActiveItemPortal>
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type ItemCellProps = PropsWithChildren<{
isActive: SharedValue<boolean>;
activationAnimationProgress: SharedValue<number>;
cellStyle: AnimatedStyleProp;
onLayout: (event: LayoutChangeEvent) => void;
onLayout?: (event: LayoutChangeEvent) => void;
hidden?: boolean;
entering?: LayoutAnimation;
exiting?: LayoutAnimation;
Expand Down Expand Up @@ -77,7 +77,7 @@ export default function ItemCell({
dimensionsStyle,
hidden && styles.hidden
]}
onLayout={hidden ? undefined : onLayout}>
onLayout={onLayout}>
{children}
</AnimatedOnLayoutView>
</Animated.View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export default function useStableCallback<C extends AnyFunction>(callback: C) {
callbackRef.current = callback;
}, [callback]);

return useCallback((...args: Parameters<C>) => {
callbackRef.current(...args);
}, []);
return useCallback(
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
(...args: Parameters<C>) => callbackRef.current(...args) as ReturnType<C>,
[]
);
}
Loading
Loading