Skip to content
Closed
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,64 +1,91 @@
import { useCallback, useState } from 'react';
import { StyleSheet, Text } from 'react-native';
import { StyleSheet } from 'react-native';
import Animated, {
LinearTransition,
useAnimatedRef
} from 'react-native-reanimated';
import type { SortableGridRenderItem } from 'react-native-sortables';
import Sortable from 'react-native-sortables';

import { Group, Screen } from '@/components';
import { Screen, Section } from '@/components';
import { IS_WEB } from '@/constants';
import { colors, radius, sizes, spacing, style, text } from '@/theme';

const DATA = Array.from({ length: 20 }, (_, index) => `Item ${index + 1}`);

export default function CollapsibleItemsExample() {
return (
<Screen includeNavBarHeight>
<Section
description='With vertical auto-scroll'
padding='none'
title='Vertical'
fill>
<Example />
</Section>
<Section
description='With horizontal auto-scroll '
padding='none'
title='Horizontal'>
<Example horizontal />
</Section>
</Screen>
);
}

type ExampleProps = {
horizontal?: boolean;
};

function Example({ horizontal }: ExampleProps) {
const [collapsed, setCollapsed] = useState(false);
const scrollableRef = useAnimatedRef<Animated.ScrollView>();

const dimension = horizontal ? 'width' : 'height';

const renderItem = useCallback<SortableGridRenderItem<string>>(
({ item }) => (
<Animated.View
layout={LinearTransition.delay(40)}
style={[styles.card, { height: collapsed ? sizes.lg : sizes.xxxl }]}>
<Animated.Text layout={LinearTransition.delay(40)} style={styles.text}>
layout={LinearTransition}
style={[
styles.card,
{ [dimension]: collapsed ? sizes.lg : sizes.xxxl }
]}>
<Animated.Text layout={LinearTransition} style={styles.text}>
{item}
</Animated.Text>
</Animated.View>
),
[collapsed]
[collapsed, dimension]
);

return (
<Screen>
<Animated.ScrollView
contentContainerStyle={[styles.container, IS_WEB && style.webContent]}
ref={scrollableRef}>
<Group style={styles.group} withMargin={false} bordered center>
<Text style={styles.title}>Above Collapsible Items</Text>
</Group>

<Sortable.Grid
activeItemScale={1.05}
autoScrollMaxOverscroll={[50, 120]}
columnGap={10}
data={DATA}
overDrag='vertical'
overflow='visible'
renderItem={renderItem}
rowGap={10}
scrollableRef={scrollableRef} // TODO - add correct auto scroll support for collapsible items
autoAdjustOffsetDuringDrag
onActiveItemDropped={() => setCollapsed(false)}
onDragStart={() => setCollapsed(true)}
/>
const props = horizontal
? ({
autoScrollDirection: 'horizontal',
columnGap: 10,
rowHeight: sizes.xl,
rows: 1
} as const)
: {
rowGap: 10
};

<Group style={styles.group} withMargin={false} bordered center>
<Text style={styles.title}>Below Collapsible Items</Text>
</Group>
</Animated.ScrollView>
</Screen>
return (
<Animated.ScrollView
contentContainerStyle={[styles.container, IS_WEB && style.webContent]}
horizontal={horizontal}
ref={scrollableRef}>
<Sortable.Grid
{...props}
activeItemScale={1.05}
data={DATA}
renderItem={renderItem}
scrollableRef={scrollableRef}
autoAdjustOffsetDuringDrag
onActiveItemDropped={() => setCollapsed(false)}
onDragStart={() => setCollapsed(true)}
/>
</Animated.ScrollView>
);
}

Expand All @@ -71,20 +98,10 @@ const styles = StyleSheet.create({
justifyContent: 'center'
},
container: {
gap: spacing.md,
padding: spacing.md,
paddingBottom: 120
},
group: {
height: sizes.xxl
padding: spacing.md
},
text: {
...text.label2,
color: colors.white
},
title: {
...text.subHeading2,
marginLeft: spacing.md,
marginTop: spacing.sm
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export default function ActiveItemPortal({
}: ActiveItemPortalProps) {
const { isTeleported, measurePortalOutlet, teleport } =
usePortalContext() ?? {};
const updateTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const teleportEnabled = useMutableValue(false);
const isFirstUpdateRef = useRef(true);

const renderTeleportedItemCell = useCallback(
() => (
Expand Down Expand Up @@ -80,38 +80,42 @@ export default function ActiveItemPortal({
const teleportedItemId = `${commonValuesContext.containerId}-${itemKey}`;

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

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

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());
});
const checkTeleported = () => isTeleported?.(teleportedItemId);
if (!checkTeleported()) return;

const update = () =>
teleport?.(teleportedItemId, renderTeleportedItemCell());

if (isFirstUpdateRef.current) {
isFirstUpdateRef.current = false;
// Needed for proper collapsible items behavior
setTimeout(update);
} else {
update();
}
}, [isTeleported, renderTeleportedItemCell, teleport, teleportedItemId]);

useAnimatedReaction(
() => activationAnimationProgress.value,
(progress, prevProgress) => {
if (prevProgress && progress > prevProgress && !teleportEnabled.value) {
if (
prevProgress !== null &&
progress > prevProgress &&
!teleportEnabled.value
) {
// We have to ensure that the portal outlet ref is measured before the
// teleported item is rendered within it because portal outlet position
// must be known to calculate the teleported item position
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { PropsWithChildren } from 'react';
import { Fragment, memo, useCallback, useEffect, useState } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import type { LayoutChangeEvent } from 'react-native';
import { GestureDetector } from 'react-native-gesture-handler';
import {
Expand Down Expand Up @@ -113,7 +113,7 @@ function DraggableView({
// PORTAL CASE

return (
<Fragment>
<>
{/* We cannot unmount this item as its gesture detector must be still
mounted to continue handling the pan gesture */}
{renderItemCell(isHidden)}
Expand All @@ -127,7 +127,7 @@ function DraggableView({
onTeleport={setIsHidden}>
{children}
</ActiveItemPortal>
</Fragment>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function removeFromPendingTimeouts(id: AnimatedTimeoutID): void {

export function setAnimatedTimeout<F extends AnyFunction>(
callback: F,
delay: number
delay = 0
): AnimatedTimeoutID {
let startTimestamp: number;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ import { useCallback } from 'react';
import type { SharedValue } from 'react-native-reanimated';
import { useDerivedValue } from 'react-native-reanimated';

import type { AdditionalCrossOffsetContextType } from '../../types';
import { useMutableValue } from '../../integrations/reanimated';
import type { AutoOffsetAdjustmentContextType, Vector } from '../../types';
import { calculateSnapOffset } from '../../utils';
import { useCommonValuesContext, useCustomHandleContext } from '../shared';
import { createProvider } from '../utils';
import { calculateActiveItemCrossOffset } from './GridLayoutProvider/utils';

type AdditionalCrossOffsetProviderProps = PropsWithChildren<{
type AutoOffsetAdjustmentProviderProps = PropsWithChildren<{
isVertical: boolean;
columnGap: SharedValue<number>;
rowGap: SharedValue<number>;
numGroups: number;
}>;

const { AdditionalCrossOffsetProvider, useAdditionalCrossOffsetContext } =
createProvider('AdditionalCrossOffset', {
const { AutoOffsetAdjustmentProvider, useAutoOffsetAdjustmentContext } =
createProvider('AutoOffsetAdjustment', {
guarded: false
})<AdditionalCrossOffsetProviderProps, AdditionalCrossOffsetContextType>(({
})<AutoOffsetAdjustmentProviderProps, AutoOffsetAdjustmentContextType>(({
columnGap,
isVertical,
numGroups,
Expand All @@ -35,13 +36,16 @@ const { AdditionalCrossOffsetProvider, useAdditionalCrossOffsetContext } =
itemPositions,
itemWidths,
keyToIndex,
prevActiveItemKey,
snapOffsetX,
snapOffsetY,
touchPosition
} = useCommonValuesContext();
const { activeHandleMeasurements, activeHandleOffset } =
useCustomHandleContext() ?? {};

const wasPreviouslyActive = useMutableValue(false);

let crossCoordinate, crossGap, crossItemSizes;
if (isVertical) {
crossGap = rowGap;
Expand Down Expand Up @@ -118,11 +122,39 @@ const { AdditionalCrossOffsetProvider, useAdditionalCrossOffsetContext } =
return 0;
});

const calculateOffsetShift = useCallback(
(
newItemPositions: Record<string, Vector>,
prevItemPositions: Record<string, Vector>
): null | number => {
'worklet';
const isActive = activeItemKey.value !== null;
const wasActive = wasPreviouslyActive.value;
wasPreviouslyActive.value = isActive;
const key = prevActiveItemKey.value;

if (isActive || !wasActive || key === null) {
return null;
}

const newPos = newItemPositions[key];
const prevPos = prevItemPositions[key];

if (!newPos || !prevPos) {
return null;
}

return newPos[crossCoordinate] - prevPos[crossCoordinate];
},
[activeItemKey, wasPreviouslyActive, prevActiveItemKey, crossCoordinate]
);

return {
value: {
additionalCrossOffset
additionalCrossOffset,
calculateOffsetShift
}
};
});

export { AdditionalCrossOffsetProvider, useAdditionalCrossOffsetContext };
export { AutoOffsetAdjustmentProvider, useAutoOffsetAdjustmentContext };
Loading
Loading