@crossed/sheet
Version:
A Cross Platform(Android & iOS) ActionSheet with a robust and flexible api, native performance and zero dependency code for react native. Create anything you want inside ActionSheet.
198 lines (182 loc) • 5.13 kB
text/typescript
/**
* Copyright (c) Paymium.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root of this projects source tree.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
NativeScrollEvent,
NativeSyntheticEvent,
Platform,
} from 'react-native';
import {
DraggableNodeOptions,
LayoutRect,
useDraggableNodesContext,
usePanGestureContext,
} from '../context';
import { EventHandlerSubscription } from '../eventmanager';
export const ScrollState = {
END: -1,
};
const InitialLayoutRect = {
w: 0,
h: 0,
x: 0,
y: 0,
px: 0,
py: 0,
};
export function resolveScrollRef(ref: any) {
// FlatList
if (ref.current?._listRef) {
return ref.current._listRef?._scrollRef;
}
// FlashList
if (ref.current?.rlvRef) {
return ref.current?.rlvRef?._scrollComponent?._scrollViewRef;
}
// SectionList
if (ref.current?._wrapperListRef?._listRef?._scrollRef) {
return ref.current?._wrapperListRef?._listRef?._scrollRef;
}
// ScrollView
return ref.current;
}
export function useDraggable<T>(options?: DraggableNodeOptions) {
const gestureContext = usePanGestureContext();
const draggableNodes = useDraggableNodesContext();
const nodeRef = useRef<T>(null);
const offset = useRef({ x: 0, y: 0 });
const layout = useRef<LayoutRect>(InitialLayoutRect);
useEffect(() => {
const pushNode = () => {
const index = draggableNodes.nodes.current?.findIndex(
(node) => node.ref === nodeRef
);
if (index === undefined || index === -1) {
draggableNodes.nodes.current?.push({
ref: nodeRef,
offset: offset,
rect: layout,
handlerConfig: options,
});
}
};
const popNode = () => {
const index = draggableNodes.nodes.current?.findIndex(
(node) => node.ref === nodeRef
);
if (index === undefined || index > -1) {
draggableNodes.nodes.current?.splice(index as number, 1);
}
};
pushNode();
return () => {
popNode();
};
}, [draggableNodes.nodes, options]);
return {
nodeRef,
offset,
draggableNodes,
layout,
gestureContext,
};
}
/**
* Create a custom scrollable view inside the action sheet.
* The scrollable view must implement `onScroll`, and `onLayout` props.
* @example
* ```tsx
const handlers = useScrollHandlers<RNScrollView>();
return <NativeViewGestureHandler
simultaneousHandlers={handlers.simultaneousHandlers}
>
<ScrollableView
{...handlers}
>
</ScrollableView>
</NativeViewGestureHandler>
* ```
*/
export function useScrollHandlers<T>(options?: DraggableNodeOptions) {
const [, setRender] = useState(false);
const { nodeRef, gestureContext, offset, layout } = useDraggable<T>(options);
const timer = useRef<any>();
const subscription = useRef<EventHandlerSubscription>();
const onMeasure = useCallback(
(x: number, y: number, w: number, h: number, px: number, py: number) => {
layout.current = {
x,
y,
w,
h: h + 10,
px,
py,
};
},
[layout]
);
const measureAndLayout = React.useCallback(() => {
clearTimeout(timer.current);
timer.current = setTimeout(() => {
const ref = resolveScrollRef(nodeRef);
if (Platform.OS == 'web') {
if (!ref) return;
const rect = (ref as HTMLDivElement).getBoundingClientRect();
(ref as HTMLDivElement).style.overflow = 'auto';
onMeasure(rect.x, rect.y, rect.width, rect.height, rect.left, rect.top);
} else {
ref?.measure?.(onMeasure);
}
}, 300);
}, [nodeRef, onMeasure]);
useEffect(() => {
if (Platform.OS === 'web' || !gestureContext.ref) return;
const interval = setInterval(() => {
// Trigger a rerender when gestureContext gets populated.
if (gestureContext.ref.current) {
clearInterval(interval);
setRender(true);
}
}, 10);
}, [gestureContext.ref]);
const memoizedProps = React.useMemo(() => {
return {
ref: nodeRef,
simultaneousHandlers: gestureContext.ref,
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { x, y } = event.nativeEvent.contentOffset;
const maxOffsetX =
event.nativeEvent.contentSize.width - layout.current.w;
const maxOffsetY =
event.nativeEvent.contentSize.height - layout.current.h;
offset.current = {
x: x === maxOffsetX || x > maxOffsetX - 5 ? ScrollState.END : x,
y: y === maxOffsetY || y > maxOffsetY - 5 ? ScrollState.END : y,
};
},
scrollEventThrottle: 1,
onLayout: () => {
measureAndLayout();
subscription.current?.unsubscribe();
subscription.current = gestureContext.eventManager.subscribe(
'onoffsetchange',
() => {
measureAndLayout();
}
);
},
};
}, [
gestureContext.eventManager,
gestureContext.ref,
layout,
measureAndLayout,
nodeRef,
offset,
]);
return memoizedProps;
}