From c728ce3f465a026f64e01e07aa3e19a7276de839 Mon Sep 17 00:00:00 2001 From: Tony Won Date: Wed, 18 Dec 2024 15:57:49 +0900 Subject: [PATCH 1/6] feat(react-ui-core): add `onSwipe*` hooks and add `--stackflow-swipe-back-ratio` css var --- .../src/components/AppScreen.tsx | 12 +++--- .../src/useStyleEffectSwipeBack.ts | 43 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/extensions/plugin-basic-ui/src/components/AppScreen.tsx b/extensions/plugin-basic-ui/src/components/AppScreen.tsx index 7bb81de6..f462ace6 100644 --- a/extensions/plugin-basic-ui/src/components/AppScreen.tsx +++ b/extensions/plugin-basic-ui/src/components/AppScreen.tsx @@ -1,7 +1,4 @@ import { useActions } from "@stackflow/react"; -import { assignInlineVars } from "@vanilla-extract/dynamic"; -import { createContext, useContext, useMemo, useRef } from "react"; - import { useLazy, useMounted, @@ -11,6 +8,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"; @@ -146,6 +145,7 @@ const AppScreen: React.FC = ({ dimRef, edgeRef, paperRef, + appBarRef, offset: OFFSET_PX_CUPERTINO, transitionDuration: globalVars.transitionDuration, preventSwipeBack: @@ -173,8 +173,10 @@ const AppScreen: React.FC = ({ return null; }, - onSwiped() { - pop(); + onSwipeEnd({ swiped }) { + if (swiped) { + pop(); + } }, }); diff --git a/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts b/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts index d6928e9a..6d20c22f 100644 --- a/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts +++ b/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts @@ -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; edgeRef: React.RefObject; paperRef: React.RefObject; + appBarRef?: React.RefObject; 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", @@ -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; @@ -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"; @@ -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; @@ -192,6 +206,8 @@ export function useStyleEffectSwipeBack({ : undefined, }; }); + + onSwipeStart?.(); }; const onTouchMove = (e: TouchEvent) => { @@ -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 = () => { @@ -215,13 +235,10 @@ export function useStyleEffectSwipeBack({ const v = (x - x0) / (t - t0); const swiped = v > 1 || x / $paper.clientWidth > 0.4; - if (swiped) { - onSwiped?.(); - } - Promise.resolve() .then(() => resetPaper({ swiped })) - .then(() => resetState()); + .then(() => resetState()) + .then(() => onSwipeEnd?.({ swiped })); }; $edge.addEventListener("touchstart", onTouchStart, { passive: true }); From fb64548f24c1fad31694e16c4da23d71d6257919 Mon Sep 17 00:00:00 2001 From: Tony Won Date: Wed, 18 Dec 2024 15:58:51 +0900 Subject: [PATCH 2/6] changeset --- .changeset/silly-rats-itch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/silly-rats-itch.md diff --git a/.changeset/silly-rats-itch.md b/.changeset/silly-rats-itch.md new file mode 100644 index 00000000..34887cb2 --- /dev/null +++ b/.changeset/silly-rats-itch.md @@ -0,0 +1,6 @@ +--- +"@stackflow/plugin-basic-ui": minor +"@stackflow/react-ui-core": minor +--- + +feat(react-ui-core): add `onSwipe*` hooks and add `--stackflow-swipe-back-ratio` css var From ae66f4911d04b7ebf0a5e33cd3e504417528a8da Mon Sep 17 00:00:00 2001 From: Tony Won Date: Wed, 18 Dec 2024 16:01:10 +0900 Subject: [PATCH 3/6] fix timing --- extensions/react-ui-core/src/useStyleEffectSwipeBack.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts b/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts index 6d20c22f..2bfb2dff 100644 --- a/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts +++ b/extensions/react-ui-core/src/useStyleEffectSwipeBack.ts @@ -235,10 +235,11 @@ export function useStyleEffectSwipeBack({ const v = (x - x0) / (t - t0); const swiped = v > 1 || x / $paper.clientWidth > 0.4; + onSwipeEnd?.({ swiped }); + Promise.resolve() .then(() => resetPaper({ swiped })) - .then(() => resetState()) - .then(() => onSwipeEnd?.({ swiped })); + .then(() => resetState()); }; $edge.addEventListener("touchstart", onTouchStart, { passive: true }); From a5180118397d016f2b3b7762b237187e73dc164f Mon Sep 17 00:00:00 2001 From: Tony Won Date: Wed, 18 Dec 2024 16:19:41 +0900 Subject: [PATCH 4/6] feat(plugin-basic-ui): add data-attrs --- .../plugin-basic-ui/src/components/AppBar.tsx | 13 ++++++---- .../src/components/AppScreen.tsx | 25 +++++++++++++------ .../src/utils/activityDataAttributes.ts | 19 ++++++++++++++ extensions/plugin-basic-ui/src/utils/index.ts | 1 + 4 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts diff --git a/extensions/plugin-basic-ui/src/components/AppBar.tsx b/extensions/plugin-basic-ui/src/components/AppBar.tsx index 8ce9ae26..5b1c81c1 100644 --- a/extensions/plugin-basic-ui/src/components/AppBar.tsx +++ b/extensions/plugin-basic-ui/src/components/AppBar.tsx @@ -1,17 +1,16 @@ import { useActions } from "@stackflow/react"; -import { assignInlineVars } from "@vanilla-extract/dynamic"; -import { forwardRef, useRef } from "react"; - import { 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 { activityDataAttributes, compactMap } from "../utils"; import * as css from "./AppBar.css"; import * as appScreenCss from "./AppScreen.css"; @@ -90,6 +89,8 @@ const AppBar = forwardRef( const actions = useActions(); const activity = useNullableActivity(); + const mounted = useMounted(); + const globalOptions = useGlobalOptions(); const globalCloseButton = globalOptions.appBar?.closeButton; const globalBackButton = globalOptions.appBar?.backButton; @@ -299,6 +300,8 @@ const AppBar = forwardRef( [appScreenCss.vars.appBar.center.mainWidth]: `${maxWidth}px`, }), )} + data-stackflow-component-name="AppScreen--appBar" + {...activityDataAttributes({ activity, mounted })} >
diff --git a/extensions/plugin-basic-ui/src/components/AppScreen.tsx b/extensions/plugin-basic-ui/src/components/AppScreen.tsx index f462ace6..cbb21aaf 100644 --- a/extensions/plugin-basic-ui/src/components/AppScreen.tsx +++ b/extensions/plugin-basic-ui/src/components/AppScreen.tsx @@ -14,7 +14,7 @@ import { useGlobalOptions } from "../basicUIPlugin"; import type { GlobalVars } from "../basicUIPlugin.css"; import { globalVars } from "../basicUIPlugin.css"; import type { PropOf } from "../utils"; -import { compactMap } from "../utils"; +import { activityDataAttributes, compactMap } from "../utils"; import AppBar from "./AppBar"; import * as css from "./AppScreen.css"; @@ -238,13 +238,15 @@ const AppScreen: React.FC = ({ }), )} data-stackflow-component-name="AppScreen" - data-stackflow-activity-id={mounted ? activity?.id : undefined} - data-stackflow-activity-is-active={ - mounted ? activity?.isActive : undefined - } + {...activityDataAttributes({ activity, mounted })} > {activityEnterStyle !== "slideInLeft" && ( -
+
)} {appBar && ( = ({ )}
{children}
{!activity?.isRoot && globalOptions.theme === "cupertino" && !isSwipeBackPrevented && ( -
+
)}
diff --git a/extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts b/extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts new file mode 100644 index 00000000..10a1be90 --- /dev/null +++ b/extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts @@ -0,0 +1,19 @@ +import type { Activity } from "@stackflow/core"; + +export function activityDataAttributes({ + activity, + mounted, +}: { activity?: Activity | null; mounted?: boolean }) { + 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), + }; +} diff --git a/extensions/plugin-basic-ui/src/utils/index.ts b/extensions/plugin-basic-ui/src/utils/index.ts index c60933ea..5c3cf198 100644 --- a/extensions/plugin-basic-ui/src/utils/index.ts +++ b/extensions/plugin-basic-ui/src/utils/index.ts @@ -1,3 +1,4 @@ +export * from "./activityDataAttributes"; export * from "./compact"; export * from "./compactMap"; export * from "./isBrowser"; From cbc06de42abdbb6b57096d53047b00959c61f279 Mon Sep 17 00:00:00 2001 From: Tony Won Date: Wed, 18 Dec 2024 16:36:32 +0900 Subject: [PATCH 5/6] =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plugin-basic-ui/src/components/AppBar.tsx | 8 +++++--- .../src/components/AppScreen.tsx | 18 ++++++++++-------- extensions/plugin-basic-ui/src/utils/index.ts | 1 - extensions/react-ui-core/src/index.ts | 1 + .../src/useActivityDataAttributes.ts} | 11 ++++++----- 5 files changed, 22 insertions(+), 17 deletions(-) rename extensions/{plugin-basic-ui/src/utils/activityDataAttributes.ts => react-ui-core/src/useActivityDataAttributes.ts} (62%) diff --git a/extensions/plugin-basic-ui/src/components/AppBar.tsx b/extensions/plugin-basic-ui/src/components/AppBar.tsx index 5b1c81c1..3a7a9f6e 100644 --- a/extensions/plugin-basic-ui/src/components/AppBar.tsx +++ b/extensions/plugin-basic-ui/src/components/AppBar.tsx @@ -1,5 +1,6 @@ import { useActions } from "@stackflow/react"; import { + useActivityDataAttributes, useAppBarTitleMaxWidth, useMounted, useNullableActivity, @@ -10,7 +11,7 @@ import { IconBack, IconClose } from "../assets"; import { useGlobalOptions } from "../basicUIPlugin"; import type { GlobalVars } from "../basicUIPlugin.css"; import { globalVars } from "../basicUIPlugin.css"; -import { activityDataAttributes, compactMap } from "../utils"; +import { compactMap } from "../utils"; import * as css from "./AppBar.css"; import * as appScreenCss from "./AppScreen.css"; @@ -88,6 +89,7 @@ const AppBar = forwardRef( ) => { const actions = useActions(); const activity = useNullableActivity(); + const activityDataAttributes = useActivityDataAttributes(); const mounted = useMounted(); @@ -300,8 +302,8 @@ const AppBar = forwardRef( [appScreenCss.vars.appBar.center.mainWidth]: `${maxWidth}px`, }), )} - data-stackflow-component-name="AppScreen--appBar" - {...activityDataAttributes({ activity, mounted })} + data-part="appBar" + {...activityDataAttributes} >
diff --git a/extensions/plugin-basic-ui/src/components/AppScreen.tsx b/extensions/plugin-basic-ui/src/components/AppScreen.tsx index cbb21aaf..655b1ab0 100644 --- a/extensions/plugin-basic-ui/src/components/AppScreen.tsx +++ b/extensions/plugin-basic-ui/src/components/AppScreen.tsx @@ -1,5 +1,6 @@ import { useActions } from "@stackflow/react"; import { + useActivityDataAttributes, useLazy, useMounted, useNullableActivity, @@ -14,7 +15,7 @@ import { useGlobalOptions } from "../basicUIPlugin"; import type { GlobalVars } from "../basicUIPlugin.css"; import { globalVars } from "../basicUIPlugin.css"; import type { PropOf } from "../utils"; -import { activityDataAttributes, compactMap } from "../utils"; +import { compactMap } from "../utils"; import AppBar from "./AppBar"; import * as css from "./AppScreen.css"; @@ -59,6 +60,7 @@ const AppScreen: React.FC = ({ }) => { const globalOptions = useGlobalOptions(); const activity = useNullableActivity(); + const activityDataAttributes = useActivityDataAttributes(); const mounted = useMounted(); const { pop } = useActions(); @@ -238,14 +240,14 @@ const AppScreen: React.FC = ({ }), )} data-stackflow-component-name="AppScreen" - {...activityDataAttributes({ activity, mounted })} + {...activityDataAttributes} > {activityEnterStyle !== "slideInLeft" && (
)} {appBar && ( @@ -265,8 +267,8 @@ const AppScreen: React.FC = ({ modalPresentationStyle, activityEnterStyle, })} - data-stackflow-component-name="AppScreen--paper" - {...activityDataAttributes({ activity, mounted })} + data-part="paper" + {...activityDataAttributes} > {children}
@@ -276,8 +278,8 @@ const AppScreen: React.FC = ({
)}
diff --git a/extensions/plugin-basic-ui/src/utils/index.ts b/extensions/plugin-basic-ui/src/utils/index.ts index 5c3cf198..c60933ea 100644 --- a/extensions/plugin-basic-ui/src/utils/index.ts +++ b/extensions/plugin-basic-ui/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from "./activityDataAttributes"; export * from "./compact"; export * from "./compactMap"; export * from "./isBrowser"; diff --git a/extensions/react-ui-core/src/index.ts b/extensions/react-ui-core/src/index.ts index 3482cc0b..3de7be0e 100644 --- a/extensions/react-ui-core/src/index.ts +++ b/extensions/react-ui-core/src/index.ts @@ -7,3 +7,4 @@ export * from "./useStyleEffectHide"; export * from "./useStyleEffectOffset"; export * from "./useStyleEffectSwipeBack"; export * from "./useZIndexBase"; +export * from "./useActivityDataAttributes"; diff --git a/extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts b/extensions/react-ui-core/src/useActivityDataAttributes.ts similarity index 62% rename from extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts rename to extensions/react-ui-core/src/useActivityDataAttributes.ts index 10a1be90..ed2c6c8c 100644 --- a/extensions/plugin-basic-ui/src/utils/activityDataAttributes.ts +++ b/extensions/react-ui-core/src/useActivityDataAttributes.ts @@ -1,9 +1,10 @@ -import type { Activity } from "@stackflow/core"; +import { useMounted } from "./useMounted"; +import { useNullableActivity } from "./useNullableActivity"; + +export function useActivityDataAttributes() { + const activity = useNullableActivity(); + const mounted = useMounted(); -export function activityDataAttributes({ - activity, - mounted, -}: { activity?: Activity | null; mounted?: boolean }) { return { /** * should be rendered in client-side only to avoid hydration mismatch warning From a1b37eb5637c69f6ea4faf09a058b771f9ee50bd Mon Sep 17 00:00:00 2001 From: Tony Won Date: Wed, 18 Dec 2024 16:37:26 +0900 Subject: [PATCH 6/6] update changeset --- .changeset/silly-rats-itch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/silly-rats-itch.md b/.changeset/silly-rats-itch.md index 34887cb2..989ac9d7 100644 --- a/.changeset/silly-rats-itch.md +++ b/.changeset/silly-rats-itch.md @@ -3,4 +3,4 @@ "@stackflow/react-ui-core": minor --- -feat(react-ui-core): add `onSwipe*` hooks and add `--stackflow-swipe-back-ratio` css var +feat(react-ui-core, plugin-basic-ui): add `onSwipe*` hooks and add data attributes and css variables