Skip to content

Commit

Permalink
feat(react-ui-core): add onSwipe* hooks and add `--stackflow-swipe-…
Browse files Browse the repository at this point in the history
…back-ratio` css var (#548)
  • Loading branch information
tonyfromundefined authored Dec 18, 2024
1 parent f1213a0 commit dc35bfc
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 24 deletions.
6 changes: 6 additions & 0 deletions .changeset/silly-rats-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@stackflow/plugin-basic-ui": minor
"@stackflow/react-ui-core": minor
---

feat(react-ui-core, plugin-basic-ui): add `onSwipe*` hooks and add data attributes and css variables
8 changes: 7 additions & 1 deletion extensions/plugin-basic-ui/src/components/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useActions } from "@stackflow/react";
import {
useActivityDataAttributes,
useAppBarTitleMaxWidth,
useMounted,
useNullableActivity,
} from "@stackflow/react-ui-core";
import { assignInlineVars } from "@vanilla-extract/dynamic";
Expand All @@ -9,7 +11,6 @@ import { IconBack, IconClose } from "../assets";
import { useGlobalOptions } from "../basicUIPlugin";
import type { GlobalVars } from "../basicUIPlugin.css";
import { globalVars } from "../basicUIPlugin.css";

import { compactMap } from "../utils";
import * as css from "./AppBar.css";
import * as appScreenCss from "./AppScreen.css";
Expand Down Expand Up @@ -88,6 +89,9 @@ const AppBar = forwardRef<HTMLDivElement, AppBarProps>(
) => {
const actions = useActions();
const activity = useNullableActivity();
const activityDataAttributes = useActivityDataAttributes();

const mounted = useMounted();

const globalOptions = useGlobalOptions();
const globalCloseButton = globalOptions.appBar?.closeButton;
Expand Down Expand Up @@ -292,6 +296,8 @@ const AppBar = forwardRef<HTMLDivElement, AppBarProps>(
[appScreenCss.vars.appBar.center.mainWidth]: `${maxWidth}px`,
}),
)}
data-part="appBar"
{...activityDataAttributes}
>
<div className={css.safeArea} />
<div className={css.container}>
Expand Down
37 changes: 25 additions & 12 deletions extensions/plugin-basic-ui/src/components/AppScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useActions } from "@stackflow/react";
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { createContext, useContext, useMemo, useRef } from "react";

import {
useActivityDataAttributes,
useLazy,
useMounted,
useNullableActivity,
Expand All @@ -11,6 +9,8 @@ import {
useStyleEffectSwipeBack,
useZIndexBase,
} from "@stackflow/react-ui-core";
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { createContext, useContext, useMemo, useRef } from "react";
import { useGlobalOptions } from "../basicUIPlugin";
import type { GlobalVars } from "../basicUIPlugin.css";
import { globalVars } from "../basicUIPlugin.css";
Expand Down Expand Up @@ -60,6 +60,7 @@ const AppScreen: React.FC<AppScreenProps> = ({
}) => {
const globalOptions = useGlobalOptions();
const activity = useNullableActivity();
const activityDataAttributes = useActivityDataAttributes();
const mounted = useMounted();

const { pop } = useActions();
Expand Down Expand Up @@ -146,6 +147,7 @@ const AppScreen: React.FC<AppScreenProps> = ({
dimRef,
edgeRef,
paperRef,
appBarRef,
offset: OFFSET_PX_CUPERTINO,
transitionDuration: globalVars.transitionDuration,
preventSwipeBack:
Expand Down Expand Up @@ -173,8 +175,10 @@ const AppScreen: React.FC<AppScreenProps> = ({

return null;
},
onSwiped() {
pop();
onSwipeEnd({ swiped }) {
if (swiped) {
pop();
}
},
});

Expand Down Expand Up @@ -236,13 +240,15 @@ const AppScreen: React.FC<AppScreenProps> = ({
}),
)}
data-stackflow-component-name="AppScreen"
data-stackflow-activity-id={mounted ? activity?.id : undefined}
data-stackflow-activity-is-active={
mounted ? activity?.isActive : undefined
}
{...activityDataAttributes}
>
{activityEnterStyle !== "slideInLeft" && (
<div className={css.dim} ref={dimRef} />
<div
ref={dimRef}
className={css.dim}
data-part="dim"
{...activityDataAttributes}
/>
)}
{appBar && (
<AppBar
Expand All @@ -255,19 +261,26 @@ const AppScreen: React.FC<AppScreenProps> = ({
)}
<div
key={activity?.id}
ref={paperRef}
className={css.paper({
hasAppBar,
modalPresentationStyle,
activityEnterStyle,
})}
ref={paperRef}
data-part="paper"
{...activityDataAttributes}
>
{children}
</div>
{!activity?.isRoot &&
globalOptions.theme === "cupertino" &&
!isSwipeBackPrevented && (
<div className={css.edge({ hasAppBar })} ref={edgeRef} />
<div
ref={edgeRef}
className={css.edge({ hasAppBar })}
data-part="edge"
{...activityDataAttributes}
/>
)}
</div>
</Context.Provider>
Expand Down
1 change: 1 addition & 0 deletions extensions/react-ui-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from "./useStyleEffectHide";
export * from "./useStyleEffectOffset";
export * from "./useStyleEffectSwipeBack";
export * from "./useZIndexBase";
export * from "./useActivityDataAttributes";
20 changes: 20 additions & 0 deletions extensions/react-ui-core/src/useActivityDataAttributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMounted } from "./useMounted";
import { useNullableActivity } from "./useNullableActivity";

export function useActivityDataAttributes() {
const activity = useNullableActivity();
const mounted = useMounted();

return {
/**
* should be rendered in client-side only to avoid hydration mismatch warning
*/
...(mounted
? {
"data-stackflow-activity-id": activity?.id,
"data-stackflow-activity-is-active": activity?.isActive,
"data-stackflow-activity-transition-state": activity?.transitionState,
}
: null),
};
}
40 changes: 29 additions & 11 deletions extensions/react-ui-core/src/useStyleEffectSwipeBack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ import type { ActivityTransitionState } from "@stackflow/core";
import { useStyleEffect } from "./useStyleEffect";
import { listenOnce, noop } from "./utils";

export const SWIPE_BACK_RATIO_CSS_VAR_NAME = "--stackflow-swipe-back-ratio";

export function useStyleEffectSwipeBack({
dimRef,
edgeRef,
paperRef,
appBarRef,
offset,
transitionDuration,
preventSwipeBack,
getActivityTransitionState,
onSwiped,
onSwipeStart,
onSwipeMove,
onSwipeEnd,
}: {
dimRef: React.RefObject<HTMLDivElement>;
edgeRef: React.RefObject<HTMLDivElement>;
paperRef: React.RefObject<HTMLDivElement>;
appBarRef?: React.RefObject<HTMLDivElement>;
offset: number;
transitionDuration: string;
preventSwipeBack: boolean;
getActivityTransitionState: () => ActivityTransitionState | null;
onSwiped?: () => void;
onSwipeStart?: () => void;
onSwipeMove?: (args: { dx: number; ratio: number }) => void;
onSwipeEnd?: (args: { swiped: boolean }) => void;
}) {
useStyleEffect({
styleName: "swipe-back",
Expand All @@ -36,6 +44,7 @@ export function useStyleEffectSwipeBack({
const $dim = dimRef.current;
const $edge = edgeRef.current;
const $paper = paperRef.current;
const $appBarRef = appBarRef?.current;

let x0: number | null = null;
let t0: number | null = null;
Expand All @@ -62,27 +71,30 @@ export function useStyleEffectSwipeBack({

let _rAFLock = false;

function movePaper(dx: number) {
function movePaper({ dx, ratio }: { dx: number; ratio: number }) {
if (!_rAFLock) {
_rAFLock = true;

requestAnimationFrame(() => {
const p = dx / $paper.clientWidth;

$dim.style.opacity = `${1 - p}`;
$dim.style.opacity = `${1 - ratio}`;
$dim.style.transition = "0s";

$paper.style.overflowY = "hidden";
$paper.style.transform = `translate3d(${dx}px, 0, 0)`;
$paper.style.transition = "0s";

$appBarRef?.style.setProperty(
SWIPE_BACK_RATIO_CSS_VAR_NAME,
String(ratio),
);

refs.forEach((ref) => {
if (!ref.current) {
return;
}

ref.current.style.transform = `translate3d(${
-1 * (1 - p) * offset
-1 * (1 - ratio) * offset
}px, 0, 0)`;
ref.current.style.transition = "0s";

Expand All @@ -106,6 +118,8 @@ export function useStyleEffectSwipeBack({
$paper.style.transform = `translateX(${swiped ? "100%" : "0"})`;
$paper.style.transition = transitionDuration;

$appBarRef?.style.removeProperty(SWIPE_BACK_RATIO_CSS_VAR_NAME);

refs.forEach((ref) => {
if (!ref.current) {
return;
Expand Down Expand Up @@ -192,6 +206,8 @@ export function useStyleEffectSwipeBack({
: undefined,
};
});

onSwipeStart?.();
};

const onTouchMove = (e: TouchEvent) => {
Expand All @@ -202,7 +218,11 @@ export function useStyleEffectSwipeBack({

x = e.touches[0].clientX;

movePaper(x - x0);
const dx = x - x0;
const ratio = dx / $paper.clientWidth;

movePaper({ dx, ratio });
onSwipeMove?.({ dx, ratio });
};

const onTouchEnd = () => {
Expand All @@ -215,9 +235,7 @@ export function useStyleEffectSwipeBack({
const v = (x - x0) / (t - t0);
const swiped = v > 1 || x / $paper.clientWidth > 0.4;

if (swiped) {
onSwiped?.();
}
onSwipeEnd?.({ swiped });

Promise.resolve()
.then(() => resetPaper({ swiped }))
Expand Down

0 comments on commit dc35bfc

Please sign in to comment.