Skip to content

Commit

Permalink
feat: useSwipeable
Browse files Browse the repository at this point in the history
  • Loading branch information
junghyeonsu committed Aug 27, 2024
1 parent 6283b1a commit 3baa1d7
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 109 deletions.
8 changes: 8 additions & 0 deletions examples/stackflow-spa/src/activities/ActivityChipTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,19 @@ const AcitivitiyChipTabs: ActivityComponentType = () => {
<ChipTabTrigger value="1">라벨1</ChipTabTrigger>
<ChipTabTrigger value="2">라벨2</ChipTabTrigger>
<ChipTabTrigger value="3">라벨3</ChipTabTrigger>
<ChipTabTrigger value="4">라벨4</ChipTabTrigger>
<ChipTabTrigger value="5">라벨5</ChipTabTrigger>
<ChipTabTrigger value="6">라벨6</ChipTabTrigger>
<ChipTabTrigger value="7">라벨7</ChipTabTrigger>
</ChipTabTriggerList>
</ChipTabs>
{value === "1" && <div style={commonStyle}>content 1</div>}
{value === "2" && <div style={commonStyle}>content 2</div>}
{value === "3" && <div style={commonStyle}>content 3</div>}
{value === "4" && <div style={commonStyle}>content 4</div>}
{value === "5" && <div style={commonStyle}>content 5</div>}
{value === "6" && <div style={commonStyle}>content 6</div>}
{value === "7" && <div style={commonStyle}>content 7</div>}
</AppScreen>
);
};
Expand Down
61 changes: 9 additions & 52 deletions examples/stackflow-spa/src/design-system/components/ChipTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ const useChipTabsContext = () => {
};

export interface ChipTabsProps
extends Assign<
React.HTMLAttributes<HTMLDivElement>,
Omit<UseTabsProps, "isSwipeable" | "swipeConfig" | "layout">
>,
extends Assign<React.HTMLAttributes<HTMLDivElement>, Omit<UseTabsProps, "layout">>,
Omit<UseLazyContentsProps, "currentValue"> {}

export const ChipTabs = React.forwardRef<HTMLDivElement, ChipTabsProps>((props, ref) => {
Expand All @@ -60,7 +57,7 @@ export const ChipTabs = React.forwardRef<HTMLDivElement, ChipTabsProps>((props,
</div>
);
});
ChipTabs.displayName = "Tabs";
ChipTabs.displayName = "ChipTabs";

export const ChipTabTriggerList = React.forwardRef<
HTMLDivElement,
Expand Down Expand Up @@ -95,7 +92,7 @@ export const ChipTabTriggerList = React.forwardRef<
</div>
);
});
ChipTabTriggerList.displayName = "TabTriggerList";
ChipTabTriggerList.displayName = "ChipTabTriggerList";

export interface ChipTabTriggerProps
extends Assign<React.HTMLAttributes<HTMLButtonElement>, Omit<TriggerProps, "isDisabled">> {}
Expand All @@ -116,68 +113,28 @@ export const ChipTabTrigger = React.forwardRef<HTMLButtonElement, ChipTabTrigger
);
},
);
ChipTabTrigger.displayName = "TabTrigger";
ChipTabTrigger.displayName = "ChipTabTrigger";

export const ChipTabContentList = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, children, ...otherProps }, ref) => {
const { api, classNames } = useChipTabsContext();
const {
tabContentListProps,
tabContentCameraProps,
getDragProps,
currentTabEnabledIndex,
swipeMoveX,
tabEnabledCount,
} = api;
const { contentList, contentCamera } = classNames;
const dragProps = getDragProps();

const getCameraTranslateX = () => {
const MODIFIER = 5;

const currentContentOffsetX = currentTabEnabledIndex * 100;

if (swipeMoveX > 0 && currentTabEnabledIndex === 0) {
return `calc(-${currentContentOffsetX}% + ${swipeMoveX / MODIFIER}px)`;
}

if (swipeMoveX < 0 && currentTabEnabledIndex === tabEnabledCount - 1) {
return `calc(-${currentContentOffsetX}% + ${swipeMoveX / MODIFIER}px)`;
}

return `calc(-${currentContentOffsetX}% + ${swipeMoveX}px)`;
};
const { tabContentListProps } = api;
const { contentList } = classNames;

return (
<div
ref={ref}
{...tabContentListProps}
className={clsx(contentList, className)}
{...otherProps}
style={{
userSelect: "none",
touchAction: "pan-y",
...otherProps.style,
}}
>
<div
{...tabContentCameraProps}
{...dragProps}
className={clsx(contentCamera)}
style={{
willChange: "transform",

transform: `translateX(${getCameraTranslateX()})`,
}}
>
{children}
</div>
{children}
</div>
);
});
ChipTabContentList.displayName = "TabContentList";
ChipTabContentList.displayName = "ChipTabContentList";

export const ChipTabContent = React.forwardRef<
HTMLDivElement,
Expand All @@ -195,4 +152,4 @@ export const ChipTabContent = React.forwardRef<
</div>
);
});
ChipTabContent.displayName = "TabContent";
ChipTabContent.displayName = "ChipTabContent";
16 changes: 13 additions & 3 deletions examples/stackflow-spa/src/design-system/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import clsx from "clsx";
import * as React from "react";
import {
useTabs,
useSwipeable,
type UseTabsProps,
type TriggerProps,
type ContentProps,
Expand All @@ -17,7 +18,7 @@ import "@seed-design/stylesheet/tabs.css";
import "@seed-design/stylesheet/tab.css";

interface TabsContextValue {
api: ReturnType<typeof useTabs>;
api: ReturnType<typeof useTabs> & ReturnType<typeof useSwipeable>;
classNames: ReturnType<typeof tabs>;
shouldRender: (value: string) => boolean;
isSwipeable: boolean;
Expand Down Expand Up @@ -58,12 +59,21 @@ export const Tabs = React.forwardRef<HTMLDivElement, TabsProps>((props, ref) =>
layout = "hug",
size = "small",
} = props;
const api = useTabs(props);
const useTabsProps = useTabs(props);
const useSwipeableProps = useSwipeable({
isSwipeable,
onSwipeLeft: useTabsProps.moveNext,
onSwipeRight: useTabsProps.movePrev,
});
const classNames = tabs({
layout,
});
const { rootProps, value, restProps } = api;
const { rootProps, value, restProps } = useTabsProps;
const { shouldRender } = useLazyContents({ currentValue: value, lazyMode, isLazy });
const api = {
...useTabsProps,
...useSwipeableProps,
};

return (
<div ref={ref} {...rootProps} {...restProps} className={clsx(classNames.root, className)}>
Expand Down
7 changes: 5 additions & 2 deletions packages/react-headless/tabs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useTabs } from "./useTabs";
import { useLazyContents, type UseLazyContentsProps } from "./useLazyContents";
import { useSwipeable } from "./useSwipeable";

import type { ContentProps, TriggerProps, UseTabsProps } from "./types";
import type { UseSwipeableProps } from "./useSwipeable";

export { useLazyContents, useTabs };
export type { ContentProps, TriggerProps, UseTabsProps, UseLazyContentsProps };
export { useLazyContents, useTabs, useSwipeable };
export type { ContentProps, TriggerProps, UseTabsProps, UseLazyContentsProps, UseSwipeableProps };
112 changes: 112 additions & 0 deletions packages/react-headless/tabs/src/useSwipeable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from "react";

import { type FullGestureState, useGesture } from "@use-gesture/react";

interface UseSwipeableStateProps {
/**
* tab swipe 기능 활성화 여부
* @default true
*/
isSwipeable?: boolean;
}

const useSwipeableState = (props: UseSwipeableStateProps) => {
const { isSwipeable } = props;
const [swipeStatus, setSwipeStatus] = React.useState<"idle" | "dragging">("idle");
const [swipeMoveX, setSwipeMoveX] = React.useState<number>(0);

return {
swipeStatus,
swipeMoveX,
onDrag: (
state: Omit<FullGestureState<"drag">, "event"> & {
event: PointerEvent | MouseEvent | TouchEvent | KeyboardEvent;
},
) => {
if (!isSwipeable) return;

setSwipeMoveX(state.movement[0]);
},
onDragStart: () => {
if (!isSwipeable) return;

setSwipeStatus("dragging");
},
onDragEnd: () => {
if (!isSwipeable) return;

setSwipeStatus("idle");
setSwipeMoveX(0);
},
};
};

export type Vector2 = [number, number];

export interface UseSwipeableProps extends UseSwipeableStateProps {
onSwipeLeft?: () => void;
onSwipeRight?: () => void;

swipeConfig?: {
/**
* @default 0.3
* The minimum velocity per axis (in pixels / ms) the drag gesture needs to
* reach before the pointer is released.
*/
velocity?: number | Vector2;
/**
* @default 50
* The minimum distance per axis (in pixels) the drag gesture needs to
* travel to trigger a swipe. Defaults to 50.
*/
distance?: number | Vector2;
/**
* @default 250
* The maximum duration in milliseconds that a swipe is detected. Defaults
* to 250.
*/
duration?: number;
};
}

export const useSwipeable = (props: UseSwipeableProps) => {
const { isSwipeable = true, swipeConfig, onSwipeLeft, onSwipeRight } = props;

const { onDrag, onDragEnd, onDragStart, swipeMoveX, swipeStatus } = useSwipeableState({
isSwipeable,
});

const getDragProps = useGesture(
{
onDragStart,

onDragEnd: ({ swipe: [swipeX] }) => {
if (!isSwipeable) return;

if (swipeX === -1) onSwipeRight?.();
if (swipeX === 1) onSwipeLeft?.();

onDragEnd();
},

onDrag,
},
{
drag: {
preventScrollAxis: "y",
preventDefault: true,
swipe: {
distance: swipeConfig?.distance || 50,
velocity: swipeConfig?.velocity || 0.3,
duration: swipeConfig?.duration || 250,
},
},
},
);

return {
swipeMoveX,
swipeStatus,
getDragProps,
};
};
Loading

0 comments on commit 3baa1d7

Please sign in to comment.