# Swipeable Menu Sheet URL: /lynx/components/swipeable-menu-sheet Source: https://github.com/daangn/seed-design/blob/dev/docs/content/lynx/components/swipeable-menu-sheet.mdx 사용자 작업에 관련된 선택지를 화면 하단의 드래그 가능한 시트로 제공합니다. Lynx Engine 최소 버전: 3.6 사용 XElement: 사용 가능 버전: @seed-design/lynx-react@0.7.0, @seed-design/lynx-css@0.11.0 ## Preview ```tsx import "./styles"; import { useState } from "@lynx-js/react"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); function handleOpenChange(nextOpen: boolean) { "background only"; setOpen(nextOpen); } return ( {open ? "열림: true" : "열림: false"} 메뉴 열기 ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:swipeable-menu-sheet - pnpm: pnpm dlx @seed-design/cli@latest add ui:swipeable-menu-sheet - yarn: yarn dlx @seed-design/cli@latest add ui:swipeable-menu-sheet - bun: bun x @seed-design/cli@latest add ui:swipeable-menu-sheet ## Usage Registry가 제공하는 `Root`, `Trigger`, `Content`, `Group`, `Item`을 조합합니다. `Trigger`는 자식을 감싸는 native ``를 렌더링하므로 `asChild`처럼 자식을 대체하는 방식은 지원하지 않습니다. ```tsx import { useState } from "@lynx-js/react"; import { ActionButton } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export function App() { const [open, setOpen] = useState(false); function handleEdit() { "background only"; setOpen(false); } return ( 메뉴 열기 ); } ``` `Item`의 `bindtap`은 작업만 수행하며 시트를 자동으로 닫지 않습니다. 작업 후 닫아야 하면 위 예제처럼 `open`을 제어하거나 `SwipeableMenuSheetRoot` ref의 `close()`를 호출하세요. `title`을 제공하지 않는 `Content`에는 native 보조 기술이 읽을 이름을 알 수 있도록 `accessibility-label`을 제공해야 합니다. ## Props ### `SwipeableMenuSheetRoot` `open?: boolean`, `defaultOpen?: boolean`으로 제어·비제어 열린 상태를 선택합니다. `skipAnimation?: boolean`은 열기와 닫기 애니메이션을 건너뜁니다. `onOpenChange`의 형식은 `(open: boolean, details: { reason: "trigger" | "closeButton" | "interactOutside" | "drag" }) => void`입니다. ### `SwipeableMenuSheetTrigger` ### `SwipeableMenuSheetContent` `labelAlign?: "left" | "center"`을 지정하면 그 안의 `Group`과 `Item`의 기본 레이블 정렬이 됩니다. `showCloseButton?: boolean`의 기본값은 `false`입니다. `false`일 때 별도의 숨겨진 닫기 버튼은 렌더링하지 않으며, 사용자는 배경 탭 또는 아래 방향 드래그로 닫을 수 있습니다. ### `SwipeableMenuSheetGroup` `labelAlign?: "left" | "center"`으로 `Content`의 기본 정렬을 해당 그룹에서 덮어쓸 수 있습니다. ### `SwipeableMenuSheetItem` `label`은 필수이며, `description`과 `prefixIcon`을 추가할 수 있습니다. `tone?: "neutral" | "critical"`의 기본값은 `"neutral"`입니다. `labelAlign?: "left" | "center"`은 Item, Group, Content 순서로 해석되는 정렬을 마지막에 덮어씁니다. ## Examples ### Trigger `SwipeableMenuSheetTrigger`를 탭하면 시트가 열립니다. 열린 상태는 배경 탭, 아래 방향 드래그, 또는 `showCloseButton`으로 표시한 닫기 버튼으로 닫을 수 있습니다. ## Preview ```tsx import "./styles"; import { useState } from "@lynx-js/react"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); function handleOpenChange(nextOpen: boolean) { "background only"; setOpen(nextOpen); } return ( {open ? "열림: true" : "열림: false"} 메뉴 열기 ); } ``` ### Controlled `open`과 `onOpenChange`로 Trigger 외의 방식에서도 열린 상태를 제어합니다. 배경 탭과 드래그로 닫힐 때도 새 `open` 값을 반영해야 합니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { useState } from "@lynx-js/react"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); function handleOpen() { "background only"; setOpen(true); } function handleOpenChange(nextOpen: boolean) { "background only"; setOpen(nextOpen); } return ( {open ? "열림 상태: true" : "열림 상태: false"} 상태로 열기 메뉴 열기 } /> ); } ``` ### With Title `Content`의 `title`로 시트 헤더에 제목을 표시합니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 메뉴 열기 } /> } description="친구에게 게시글을 공유할 수 있어요." /> } description="게시글을 다시 상단에 표시해요." /> } /> } tone="critical" /> ); } ``` ### With Title and Description `description`은 제목 아래에 부가 설명을 표시합니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 메뉴 열기 } /> } description="친구에게 게시글을 공유할 수 있어요." /> } description="게시글을 다시 상단에 표시해요." /> } /> } tone="critical" /> ); } ``` ### Close Button `showCloseButton`을 설정하면 시트 하단에 닫기 버튼을 표시합니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 메뉴 열기 } /> ); } ``` ### Label Align `labelAlign`은 Content의 기본값을 Group과 Item에서 차례로 덮어쓸 수 있습니다. #### `labelAlign="left"` with Prefix Icon Prefix icon이 있는 항목은 일반적으로 왼쪽 정렬을 사용합니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 메뉴 열기 } /> } /> } /> } /> } tone="critical" /> ); } ``` #### `labelAlign="center"` without Prefix Icon Prefix icon이 없는 항목은 중앙 정렬을 사용할 수 있습니다. ```tsx import "./styles"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 메뉴 열기 ); } ``` #### Overriding `labelAlign` 그룹 또는 개별 항목의 `labelAlign`으로 상위 정렬을 덮어씁니다. ```tsx import "./styles"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 메뉴 열기 ); } ``` ### `onOpenChange` Details `onOpenChange`의 두 번째 인자는 `{ reason }`입니다. 열릴 때는 `"trigger"`이고, 닫힐 때는 `"closeButton"`, `"interactOutside"`, `"drag"` 중 하나입니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { useState } from "@lynx-js/react"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; type OpenChangeReason = "trigger" | "closeButton" | "interactOutside" | "drag"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); function handleOpenChange(nextOpen: boolean, details: { reason: OpenChangeReason }) { "background only"; setOpen(nextOpen); if (nextOpen) { setOpenReason(details.reason); } else { setCloseReason(details.reason); } } return ( {open ? "열림 상태: true" : "열림 상태: false"} 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} 메뉴 열기 } /> ); } ``` ### Skip Animation `skipAnimation`을 설정하면 enter/exit 애니메이션 없이 즉시 열고 닫습니다. ```tsx import "./styles"; import IconEyeSlashLine from "@karrotmarket/lynx-monochrome-icon/IconEyeSlashLine"; import { ActionButton, useSeedClassName, VStack } from "@seed-design/lynx-react"; import { SwipeableMenuSheetContent, SwipeableMenuSheetGroup, SwipeableMenuSheetItem, SwipeableMenuSheetRoot, SwipeableMenuSheetTrigger, } from "@/components/ui/swipeable-menu-sheet"; export default function Example() { const seedClassName = useSeedClassName({ colorMode: "system" }); return ( 즉시 메뉴 열기 } /> ); } ``` ## Web Version Differences Lynx `SwipeableMenuSheet`는 React 버전과 다음 차이가 있습니다. - **Trigger wrapper**: Trigger는 native `` wrapper를 렌더링하며 `asChild`처럼 자식을 대체하는 API를 제공하지 않습니다. - **Native interaction**: HTML 요소와 `onClick` 대신 native ``/``와 `bindtap`을 사용합니다. - **Accessibility**: DOM ARIA 속성 대신 `accessibility-label`, `accessibility-role-description` 등 Lynx native 접근성 속성을 사용합니다. - **Close fallback**: `showCloseButton={false}`는 숨겨진 닫기 버튼을 만들지 않습니다. - **Close reason**: 키보드 ESC에 해당하는 `escapeKeyDown` reason은 제공하지 않습니다.