React

Bottom Sheet

Bottom Sheet를 Stackflow와 함께 사용하는 방법을 안내합니다.

Bottom Sheet

Bottom Sheet 컴포넌트에 대해 자세히 알아봅니다.

Making a Bottom Sheet Activity

일반적인 경우 Bottom Sheet를 Activity로 만들어 사용하는 것을 권장합니다.

  • Activity로 관리되므로, 하위 Activity보다 높고 상위 Activity보다는 낮은 z-index를 갖도록 관리하기 쉽습니다.
  • 딥링킹이 가능합니다. (URL 접속으로 Bottom Sheet를 열 수 있습니다.)
  • @stackflow/plugin-basic-ui BottomSheet에서의 마이그레이션이 쉽습니다.
Loading...

Usage

import { useActivityZIndexBase } from "@seed-design/stackflow";
import { useActivity, useFlow, type ActivityComponentType } from "@stackflow/react/future";
// ... more imports

const ActivityBottomSheetSimple: ActivityComponentType<"ActivityBottomSheetSimple"> = () => {
  const { pop } = useFlow();

  return (
    <BottomSheetRoot open={useActivity().isActive} onOpenChange={(open) => !open && pop()}>
      <BottomSheetContent
        title="Activity로 만들어진 BottomSheet"
        layerIndex={useActivityZIndexBase()}
      >
        <BottomSheetFooter>
          <ActionButton onClick={pop}>
            확인
          </ActionButton>
        </BottomSheetFooter>
      </BottomSheetContent>
    </BottomSheetRoot>
  );
};
  1. open prop에 useActivity().isActive를 전달하여 Activity가 활성화될 때 Bottom Sheet가 열리도록 합니다.
  2. onOpenChange를 통해 Bottom Sheet가 닫힐 때 pop()을 실행하여 Activity를 종료합니다.
  3. layerIndex={useActivityZIndexBase()}로 Bottom Sheet Activity의 z-index 기준점을 전달합니다.

Keeping Bottom Sheet Mounted

Bottom Sheet Activity 위에 다른 Activity를 push할 때 Bottom Sheet가 unmount되는 것을 방지하려면,

  • open 상태를 isActive 대신 transitionState로 관리하고
  • modal prop을 isActive로 설정하고
  • onOpenChange 핸들러에서 !isOpen && isActive인 경우 pop()을 실행하도록 합니다.

이 패턴은 다음 상황에서 유용합니다.

  • Bottom Sheet 액티비티 위에 다른 오버레이 컴포넌트 액티비티를 중첩하여 표시하고 싶은 경우
  • 하위 Bottom Sheet 액티비티 내부에 존재하는 Uncontrolled 폼 요소의 상태를 유지하고 싶은 경우
const { isActive, transitionState } = useActivity();

return (
  <BottomSheetRoot
    open={
      transitionState === "enter-active" || transitionState === "enter-done"
    }
    modal={isActive}
    onOpenChange={(open) => !open && isActive && pop()}
  >
    {/* ... */}
  </BottomSheetRoot>
);
  1. opentransitionState로 관리하여 다른 Activity가 위에 push되어도 Bottom Sheet가 unmount되지 않도록 합니다.
  2. modal={isActive}로 Bottom Sheet Activity가 비활성 상태일 때 modalfalse로 설정합니다. 이렇게 하지 않으면, 위에 push된 Activity에서 포커스, 스크린 리더 접근 및 스크롤 등의 상호작용이 동작하지 않습니다.
  3. onOpenChange 핸들러에서 isActive인 경우에만 pop()을 실행하여, 비활성 상태에서의 의도치 않은 Activity 종료를 방지합니다.

Stacking Another Modal Overlay on Top

Bottom Sheet Activity 위에 자체 backdrop을 갖는 또 다른 modal Activity(예: Alert Dialog Activity)를 push해 얹는 경우에는, modal={isActive} 대신 modal을 항상 true로 두는 것을 권장합니다.

modal={isActive} 패턴은 위에 push되는 Activity가 일반 AppScreen일 때 스크롤 등의 상호작용을 보장하기 위한 것이지만, 위로 push되는 Activity가 자체 backdrop을 갖는 modal overlay라면 비활성 시점에 Bottom Sheet의 backdrop이 사라지면서 dim layering이 끊겨 보일 수 있습니다.

Loading...
const { isActive, transitionState } = useActivity();

return (
  <BottomSheetRoot
    open={
      transitionState === "enter-active" || transitionState === "enter-done"
    }
    modal
    onOpenChange={(open) => !open && isActive && pop()}
  >
    {/* ... */}
  </BottomSheetRoot>
);
  1. modal을 항상 true로 두어 위에 또 다른 modal Activity가 push되어도 Bottom Sheet의 backdrop layering이 끊기지 않도록 합니다.
  2. onOpenChangeisActive 가드 덕분에 비활성 상태에서의 outside 상호작용으로 의도치 않게 pop()이 발생하지 않습니다.
언제 `modal={isActive}`를 유지해야 하나요?

위에 push되는 Activity가 Bottom Sheet 영역까지 덮는 backdrop을 갖지 않는 일반 AppScreen이라면, 위 Activity의 스크롤 등 상호작용을 위해 modal={isActive} 가이드가 적합합니다. modal overlay 두 개가 stack되는 경우에 한해 modal을 항상 true로 두는 변형을 사용하세요.

Syncing BottomSheet State with a Step

Bottom Sheet를 Activity로 만들 수 없는 경우, Bottom Sheet가 표시된 상태를 Step으로 만들 수 있습니다.

  • 현재 Activity를 유지하면서도, 뒤로 가기 버튼 등으로 Bottom Sheet를 닫을 수 있습니다.
  • BottomSheetTrigger를 사용하여 Bottom Sheet를 열고 닫을 수 있습니다.
제약 사항

Activity로 만들지 않은 Bottom Sheet에서 다른 Activity를 push하기 전, z-index 문제를 방지하기 위해 Bottom Sheet를 닫으세요.

Bottom Sheet를 닫을 수 없거나, Bottom Sheet를 연 Activity로 돌아왔을 때 Bottom Sheet가 열린 상태를 유지해야 하는 경우 Bottom Sheet를 Activity로 만들어 사용하는 것을 권장합니다.

Activity 간 유려한 트랜지션을 제공하기 위해 하위 AppScreen 요소 중 일부가 상위 AppScreen 요소보다 위에 위치합니다. 이 제약으로 인해, 열린 상태의 Bottom Sheet는 독립적인 Activity로 만들지 않는 경우 하위 Activity와 상위 Activity 사이에 위치시키는 것이 불가능합니다.

Loading...

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: {
      "bottom-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["bottom-sheet"] === "open";

  useEffect(() => {
    if (!isOverlayOpen) {
      setOpen(false);
    }
  }, [isOverlayOpen]);

  const onOpenChange = (newOpen: boolean) => {
    setOpen(newOpen);

    if (newOpen && !isOverlayOpen) {
      pushStep((params) => ({ ...params, "bottom-sheet": "open" }));

      return;
    }

    if (!newOpen && isOverlayOpen) {
      popStep();

      return;
    }
  };

  return (
    <AppScreen>
      <BottomSheetRoot open={open} onOpenChange={onOpenChange}>
        <BottomSheetTrigger asChild>
          <ActionButton>Open</ActionButton>
        </BottomSheetTrigger>
        <Portal>
          <BottomSheetContent
            title="Step으로 관리되는 Bottom Sheet"
            layerIndex={useActivityZIndexBase({ activityOffset: 1 })}
          >
            <BottomSheetFooter>
              <HStack gap="x2">
                <ActionButton onClick={() => setOpen(false)}>취소</ActionButton>
                <ActionButton
                  onClick={() => {
                    setOpen(false); // 다른 Activity로 이동하기 전에는 Bottom Sheet를 닫으세요.
                    push("ActivityNext");
                  }}
                >
                  다음
                </ActionButton>
              </HStack>
            </BottomSheetFooter>
          </BottomSheetContent>
        </Portal>
      </BottomSheetRoot>
    </AppScreen>
  );
};
  1. Portal을 사용하여 Bottom Sheet가 DOM 상 현재 Activity 밖에 렌더링되도록 합니다.
  2. open prop를 관리하고, onOpenChange 핸들러를 통해 Step 상태와 동기화합니다.
  3. 뒤로 가기 버튼 등을 통해 Activity 파라미터가 변경될 때 Bottom Sheet의 open 상태를 동기화합니다.
  4. 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();

  return (
    <AppScreen>
      <BottomSheetRoot {...overlayProps}>
        <BottomSheetTrigger asChild>
          <ActionButton>Open</ActionButton>
        </BottomSheetTrigger>
        <Portal>
          <BottomSheetContent
            title="Step으로 관리되는 Bottom Sheet"
            layerIndex={useActivityZIndexBase({ activityOffset: 1 })}
          >
            <BottomSheetFooter>
              <HStack gap="x2">
                <ActionButton onClick={() => setOpen(false)}>취소</ActionButton>
                <ActionButton
                  onClick={() => {
                    setOpen(false); // 다른 Activity로 이동하기 전에는 Bottom Sheet를 닫으세요.
                    push("ActivityNext");
                  }}
                >
                  다음
                </ActionButton>
              </HStack>
            </BottomSheetFooter>
          </BottomSheetContent>
        </Portal>
      </BottomSheetRoot>
    </AppScreen>
  );
};

About useActivityZIndexBase

useActivityZIndexBase는 각 Activity의 z-index 기준점을 반환하는 훅입니다.

Last updated on

On this page