Menu Sheet
Menu Sheet를 Stackflow와 함께 사용하는 방법을 안내합니다.
Menu Sheet
Menu Sheet 컴포넌트에 대해 자세히 알아봅니다.
Making a Menu Sheet Activity
일반적인 경우 Menu Sheet를 Activity로 만들어 사용하는 것을 권장합니다.
- Activity로 관리되므로, 하위 Activity보다 높고 상위 Activity보다는 낮은 z-index를 갖도록 관리하기 쉽습니다.
- 딥링킹이 가능합니다. (URL 접속으로 Menu Sheet를 열 수 있습니다.)
- @stackflow/plugin-basic-ui
BottomSheet에서의 마이그레이션이 쉽습니다.
Loading...
import { VStack } from "@seed-design/react";
import { useFlow, type ActivityComponentType } from "@stackflow/react/future";
import { AppBar, AppBarBackButton, AppBarLeft, AppBarMain } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import { ActionButton } from "seed-design/ui/action-button";
declare module "@stackflow/config" {
interface Register {
ActivityMenuSheetActivity: {};
}
}
const ActivityMenuSheetActivity: ActivityComponentType<"ActivityMenuSheetActivity"> = () => {
const { push } = useFlow();
return (
<AppScreen>
<AppBar>
<AppBarLeft>
<AppBarBackButton />
</AppBarLeft>
<AppBarMain title="Activity" />
</AppBar>
<AppScreenContent>
<VStack p="x5" justify="center" gap="x4">
<ActionButton
variant="neutralSolid"
flexGrow
onClick={() => push("ActivityMenuSheetSimple", {})}
>
ActivityMenuSheetSimple을 Push
</ActionButton>
<ActionButton
variant="neutralWeak"
flexGrow
onClick={() => push("ActivityMenuSheetActivity", {})}
>
지금 열린 이 Activity를 Push
</ActionButton>
</VStack>
</AppScreenContent>
</AppScreen>
);
};
export default ActivityMenuSheetActivity;import { PrefixIcon } from "@seed-design/react";
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { useActivity, useFlow, type ActivityComponentType } from "@stackflow/react/future";
import {
IconPencilLine,
IconPlusLine,
IconTrashcanLine,
} from "@karrotmarket/react-monochrome-icon";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
} from "seed-design/ui/menu-sheet";
import { Snackbar, useSnackbarAdapter } from "seed-design/ui/snackbar";
declare module "@stackflow/config" {
interface Register {
ActivityMenuSheetSimple: {};
}
}
const ActivityMenuSheetSimple: ActivityComponentType<"ActivityMenuSheetSimple"> = () => {
const { pop, push } = useFlow();
const { isActive } = useActivity();
const snackbar = useSnackbarAdapter();
const handleAction = (action: string) => {
snackbar.create({
render: () => <Snackbar variant="positive" message={`선택한 액션: ${action}`} />,
});
pop();
};
const handleClose = (open: boolean) => {
if (!open) {
pop();
}
};
return (
<MenuSheetRoot open={isActive} onOpenChange={handleClose}>
<MenuSheetContent title="Actions" layerIndex={useActivityZIndexBase()}>
<MenuSheetGroup>
<MenuSheetItem onClick={() => handleAction("add")}>
<PrefixIcon svg={<IconPlusLine />} />
추가
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("edit")}>
<PrefixIcon svg={<IconPencilLine />} />
수정
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("delete")} tone="critical">
<PrefixIcon svg={<IconTrashcanLine />} />
삭제
</MenuSheetItem>
</MenuSheetGroup>
<MenuSheetGroup labelAlign="center">
<MenuSheetItem
onClick={() =>
push("ActivityDetail", {
title: "Menu Sheet에서 이동",
body: "Menu Sheet Activity 내부에서 다른 Activity를 push할 수 있습니다.",
})
}
>
Push
</MenuSheetItem>
</MenuSheetGroup>
</MenuSheetContent>
</MenuSheetRoot>
);
};
export default ActivityMenuSheetSimple;Usage
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { useActivity, useFlow, type ActivityComponentType } from "@stackflow/react/future";
// ... more imports
const ActivityMenuSheet: ActivityComponentType<"ActivityMenuSheet"> = () => {
const { pop } = useFlow();
const handleAction = (action: string) => {
console.log("선택한 액션:", action);
pop();
};
return (
<MenuSheetRoot open={useActivity().isActive} onOpenChange={(open) => !open && pop()}>
<MenuSheetContent
title="Actions"
layerIndex={useActivityZIndexBase()}
>
<MenuSheetGroup>
<MenuSheetItem onClick={() => handleAction("add")}>
<PrefixIcon svg={<IconPlusLine />} />
추가
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("edit")}>
<PrefixIcon svg={<IconPencilLine />} />
수정
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("delete")} tone="critical">
<PrefixIcon svg={<IconTrashcanLine />} />
삭제
</MenuSheetItem>
</MenuSheetGroup>
</MenuSheetContent>
</MenuSheetRoot>
);
};openprop에useActivity().isActive를 전달하여 Activity가 활성화될 때 Menu Sheet가 열리도록 합니다.onOpenChange를 통해 Menu Sheet가 닫힐 때pop()을 실행하여 Activity를 종료합니다.layerIndex={useActivityZIndexBase()}로 Menu Sheet Activity의 z-index 기준점을 전달합니다.
Syncing Menu Sheet State with a Step
Menu Sheet를 Activity로 만들 수 없는 경우, Menu Sheet가 표시된 상태를 Step으로 만들 수 있습니다.
- 현재 Activity를 유지하면서도, 뒤로 가기 버튼 등으로 Menu Sheet를 닫을 수 있습니다.
MenuSheetTrigger를 사용하여 Menu Sheet를 열고 닫을 수 있습니다.
제약 사항
Activity로 만들지 않은 Menu Sheet에서 다른 Activity를 push하는 경우, push하기 전 Menu Sheet를 닫으세요.
Menu Sheet를 닫을 수 없거나, Menu Sheet를 연 Activity로 돌아왔을 때 Menu Sheet가 열린 상태를 유지해야 하는 경우 Menu Sheet를 Activity로 만들어 사용하는 것을 권장합니다.
Activity 간 유려한 트랜지션을 제공하기 위해 하위 AppScreen 요소 중 일부가 상위 AppScreen 요소보다 위에 위치합니다. 이 제약으로 인해, 열린 상태의 Menu Sheet는 독립적인 Activity로 만들지 않는 경우 하위 Activity와 상위 Activity 사이에 위치시키는 것이 불가능합니다.
Loading...
import { Portal, VStack } from "@seed-design/react";
import { useActivityZIndexBase } from "@seed-design/stackflow";
import {
useActivityParams,
useFlow,
useStepFlow,
type ActivityComponentType,
} from "@stackflow/react/future";
import { useEffect, useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import { AppBar, AppBarMain } from "seed-design/ui/app-bar";
import { AppScreen, AppScreenContent } from "seed-design/ui/app-screen";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
MenuSheetTrigger,
} from "seed-design/ui/menu-sheet";
import { PrefixIcon } from "@seed-design/react";
import {
IconPencilLine,
IconPlusLine,
IconTrashcanLine,
} from "@karrotmarket/react-monochrome-icon";
import { Snackbar, useSnackbarAdapter } from "seed-design/ui/snackbar";
declare module "@stackflow/config" {
interface Register {
ActivityMenuSheetStep: {
"menu-sheet"?: "open";
};
}
}
const ActivityMenuSheetStep: ActivityComponentType<"ActivityMenuSheetStep"> = () => {
const [open, setOpen] = useState(false);
const { push } = useFlow();
const { pushStep, popStep } = useStepFlow("ActivityMenuSheetStep");
const params = useActivityParams<"ActivityMenuSheetStep">();
const isOverlayOpen = params["menu-sheet"] === "open";
const snackbar = useSnackbarAdapter();
useEffect(() => {
if (!isOverlayOpen) {
setOpen(false);
}
if (isOverlayOpen) {
setOpen(true);
}
}, [isOverlayOpen]);
const onOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (newOpen && !isOverlayOpen) {
pushStep((params) => ({ ...params, "menu-sheet": "open" }));
return;
}
if (!newOpen && isOverlayOpen) {
popStep();
return;
}
};
const handleAction = (action: string) => {
snackbar.create({
render: () => <Snackbar variant="positive" message={`선택한 액션: ${action}`} />,
});
popStep();
};
return (
<AppScreen>
<AppBar>
<AppBarMain title="Step" />
</AppBar>
<AppScreenContent>
<MenuSheetRoot open={open} onOpenChange={onOpenChange}>
<MenuSheetTrigger asChild>
<VStack p="x5" justify="center" gap="x4">
<ActionButton variant="neutralSolid" flexGrow>
Menu Sheet 열기
</ActionButton>
</VStack>
</MenuSheetTrigger>
<Portal>
<MenuSheetContent
title="Step"
layerIndex={useActivityZIndexBase({ activityOffset: 1 })}
>
<MenuSheetGroup>
<MenuSheetItem onClick={() => handleAction("add")}>
<PrefixIcon svg={<IconPlusLine />} />
추가
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("edit")}>
<PrefixIcon svg={<IconPencilLine />} />
수정
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("delete")} tone="critical">
<PrefixIcon svg={<IconTrashcanLine />} />
삭제
</MenuSheetItem>
</MenuSheetGroup>
<MenuSheetGroup labelAlign="center">
<MenuSheetItem
onClick={() => {
// 이 Menu Sheet는 Activity로 만들어지지 않았기 때문에, z-index 정리를 위해
// Menu Sheet를 먼저 닫고 다음 Activity를 push해야 합니다.
popStep();
push("ActivityDetail", {
title: "Menu Sheet에서 이동한 화면",
body: "Menu Sheet를 닫고 이동했습니다.",
});
}}
>
Push
</MenuSheetItem>
</MenuSheetGroup>
</MenuSheetContent>
</Portal>
</MenuSheetRoot>
</AppScreenContent>
</AppScreen>
);
};
export default ActivityMenuSheetStep;Usage
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { Portal } from "@seed-design/react";
import { useActivity, useActivityParams, useFlow, useStepFlow, type ActivityComponentType } from "@stackflow/react/future";
import { useEffect, useState } from "react";
// ... more imports
declare module "@stackflow/config" {
interface Register {
ActivityHome: {
"menu-sheet"?: "open";
};
}
}
const ActivityHome: ActivityComponentType<"ActivityHome"> = () => {
const [open, setOpen] = useState(false);
const { push } = useFlow();
const { pushStep, popStep } = useStepFlow("ActivityHome");
const params = useActivityParams<"ActivityHome">();
const isOverlayOpen = params["menu-sheet"] === "open";
useEffect(() => {
if (!isOverlayOpen) {
setOpen(false);
}
}, [isOverlayOpen]);
const onOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (newOpen && !isOverlayOpen) {
pushStep((params) => ({ ...params, "menu-sheet": "open" }));
return;
}
if (!newOpen && isOverlayOpen) {
popStep();
return;
}
};
const handleAction = (action: string) => {
console.log("선택한 액션:", action);
popStep();
};
return (
<AppScreen>
<MenuSheetRoot open={open} onOpenChange={onOpenChange}>
<MenuSheetTrigger asChild>
<ActionButton>Open</ActionButton>
</MenuSheetTrigger>
<Portal>
<MenuSheetContent
title="Actions"
layerIndex={useActivityZIndexBase({ activityOffset: 1 })}
>
<MenuSheetGroup>
<MenuSheetItem onClick={() => handleAction("add")}>
추가
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("edit")}>
수정
</MenuSheetItem>
<MenuSheetItem
onClick={() => {
popStep(); // 다른 Activity로 이동하기 전에는 Menu Sheet를 닫으세요.
push("ActivityNext");
}}
>
Push
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("delete")} tone="critical">
삭제
</MenuSheetItem>
</MenuSheetGroup>
</MenuSheetContent>
</Portal>
</MenuSheetRoot>
</AppScreen>
);
};Portal을 사용하여 Menu Sheet가 DOM 상 현재 Activity 밖에 렌더링되도록 합니다.openprop를 관리하고,onOpenChange핸들러를 통해 Step 상태와 동기화합니다.- 뒤로 가기 버튼 등을 통해 Activity 파라미터가 변경될 때 Menu Sheet의
open상태를 동기화합니다. layerIndex={useActivityZIndexBase({ activityOffset: 1 })}로 현재 Activity보다 한 단계 높은 z-index 기준점을 전달합니다.
useStepOverlay
#2와 #3을 일반화하여 useStepOverlay를 사용하면 편리합니다. useStepOverlay 구현 예시는 코드를 참고하세요.
import { useActivityZIndexBase } from "@seed-design/stackflow";
import { Portal } from "@seed-design/react";
import { useStepOverlay } from "./use-step-overlay";
// ... more imports
const MyActivity: ActivityComponentType = () => {
const { overlayProps, setOpen } = useStepOverlay();
const { popStep } = useStepFlow("MyActivity");
const { push } = useFlow();
const handleAction = (action: string) => {
console.log("선택한 액션:", action);
popStep();
};
return (
<AppScreen>
<MenuSheetRoot {...overlayProps}>
<MenuSheetTrigger asChild>
<ActionButton>Open</ActionButton>
</MenuSheetTrigger>
<Portal>
<MenuSheetContent
title="Actions"
layerIndex={useActivityZIndexBase({ activityOffset: 1 })}
>
<MenuSheetGroup>
<MenuSheetItem onClick={() => handleAction("add")}>
추가
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("edit")}>
수정
</MenuSheetItem>
<MenuSheetItem
onClick={() => {
popStep(); // 다른 Activity로 이동하기 전에는 Menu Sheet를 닫으세요.
push("ActivityNext");
}}
>
Push
</MenuSheetItem>
<MenuSheetItem onClick={() => handleAction("delete")} tone="critical">
삭제
</MenuSheetItem>
</MenuSheetGroup>
</MenuSheetContent>
</Portal>
</MenuSheetRoot>
</AppScreen>
);
};About useActivityZIndexBase
useActivityZIndexBase는 각 Activity의 z-index 기준점을 반환하는 훅입니다.
Last updated on