Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-ui-core): add onSwipe* hooks and add --stackflow-swipe-back-ratio css var #548

Merged
merged 6 commits into from
Dec 18, 2024
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
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
13 changes: 9 additions & 4 deletions extensions/plugin-basic-ui/src/components/AppBar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { useActions } from "@stackflow/react";
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { forwardRef, useRef } from "react";

import {
useActivityDataAttributes,
useAppBarTitleMaxWidth,
useMounted,
useNullableActivity,
} from "@stackflow/react-ui-core";
import { assignInlineVars } from "@vanilla-extract/dynamic";
import { forwardRef, useRef } from "react";
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 @@ -89,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 @@ -299,6 +302,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
Loading