# Menu URL: /lynx/components/menu Source: https://github.com/daangn/seed-design/blob/dev/docs/content/lynx/components/menu.mdx 사용자가 취할 수 있는 선택지나 액션 리스트를 제공하는 컴포넌트입니다. Lynx Engine 최소 버전: 3.9 사용 XElement: ## Preview ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import IconTrashcanLine from "@karrotmarket/lynx-monochrome-icon/IconTrashcanLine"; import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "@/components/ui/menu"; export default function Example() { return ( 열기 작업 } /> } /> } /> ); } ``` ## Installation - npm: npx @seed-design/cli@latest add ui:menu - pnpm: pnpm dlx @seed-design/cli@latest add ui:menu - yarn: yarn dlx @seed-design/cli@latest add ui:menu - bun: bun x @seed-design/cli@latest add ui:menu ## Usage `MenuContent`는 긴 목록을 스크롤합니다. `container`를 지정하지 않으면 Lynx view 안에, 지정하면 native overlay에 렌더링됩니다. ```tsx import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "@/components/ui/menu"; export function App() { return ( 열기 작업 ); } ``` - `MenuAnchor`는 위치만 정하며 Menu를 열지 않습니다. `open`과 `onOpenChange`로 상태를 제어하세요. - `MenuContent` 안에 별도의 positioner나 scroll area를 추가하지 마세요. - `MenuItem`은 `description`, `prefixIcon`, `suffixIcon`을 지원합니다. ## Props ### `MenuRoot` ### `MenuTrigger` ### `MenuAnchor` 고유 prop이 없습니다. `className`, `style`, `children`을 받습니다. ### `MenuContent` ### `MenuGroup` 고유 prop이 없습니다. `className`, `style`, `children`을 받습니다. ### `MenuGroupLabel` 고유 prop이 없습니다. `className`, `style`, `children`을 받습니다. ### `MenuItem` ## Examples ### Trigger `MenuTrigger`를 탭하면 닫힌 Menu가 열립니다. `MenuItem`을 탭하거나 바깥 영역을 탭하면 닫힙니다. ## Preview ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import IconTrashcanLine from "@karrotmarket/lynx-monochrome-icon/IconTrashcanLine"; import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, } from "@/components/ui/menu"; export default function Example() { return ( 열기 작업 } /> } /> } /> ); } ``` ### Size `size`로 Menu의 크기를 정합니다. 기본값은 `"medium"`입니다. `"responsive"`는 현재 화면 너비를 기준으로 `"small"` 또는 `"medium"`으로 바뀝니다. ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton, HStack } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuGroupLabel, MenuItem, MenuRoot, MenuTrigger, type MenuRootProps, } from "@/components/ui/menu"; function SizeMenu({ size, label }: { size: MenuRootProps["size"]; label: string }) { return ( {label} 작업 } /> } /> ); } export default function Example() { return ( ); } ``` ### With Description `MenuItem`의 `description`에 항목의 부가 설명을 전달합니다. ```tsx import IconArrowUpBracketDownLine from "@karrotmarket/lynx-monochrome-icon/IconArrowUpBracketDownLine"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { return ( 열기 } /> } /> } /> ); } ``` ### Tone `MenuItem`의 `tone="critical"`로 삭제처럼 주의가 필요한 액션을 표시합니다. ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import IconTrashcanLine from "@karrotmarket/lynx-monochrome-icon/IconTrashcanLine"; import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { return ( 열기 } /> } /> } /> ); } ``` ### Disabled Items `disabled` 항목은 탭해도 `bindtap`이 실행되지 않고 Menu를 닫지 않습니다. ```tsx import IconArrowUpBracketDownLine from "@karrotmarket/lynx-monochrome-icon/IconArrowUpBracketDownLine"; import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { return ( 열기 } /> } disabled /> } /> ); } ``` ### Placement `MenuRoot`의 `placement`로 Menu를 기준 요소의 12개 방향에 배치합니다. 기본값은 `"bottom"`입니다. 화면 가장자리에서 공간이 부족하면 native runtime이 반대쪽으로 뒤집거나 경계 안으로 이동합니다. ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { ActionButton, Box, HStack, VStack } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger, type MenuRootProps, } from "@/components/ui/menu"; function PlacementMenu({ placement }: { placement: NonNullable }) { return ( {placement} } /> } /> ); } export default function Example() { return ( ); } ``` ### Anchor `MenuAnchor`는 Menu 위치만 정하고 탭으로 열고 닫지는 않습니다. 이 예제처럼 `open`과 `onOpenChange`로 상태를 제어할 수 있습니다. ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { useState } from "@lynx-js/react"; import { Box, HStack } from "@seed-design/lynx-react"; import { MenuAnchor, MenuContent, MenuGroup, MenuItem, MenuRoot } from "@/components/ui/menu"; import { Switch } from "@/components/ui/switch"; export default function Example() { const [open, setOpen] = useState(false); function handleOpenChange(nextOpen: boolean, details?: { reason?: string }) { "background only"; if (!nextOpen && details?.reason === "interactOutside") return; setOpen(nextOpen); } return ( } /> } /> ); } ``` Menu가 열려 있는 동안에는 화면 전체를 덮는 레이어가 바깥 탭을 받으므로 Anchor 바깥의 Switch를 탭할 수 없습니다. 이 예제는 Menu가 닫힌 상태에서 Switch로 열고, 항목을 선택해 닫는 흐름을 보여줍니다. 바깥 탭을 무시하는 제어는 `onOpenChange`에서 `details.reason === "interactOutside"`를 걸러 구현합니다. ### Match Width `matchReferenceWidth`를 사용하면 Menu 너비가 Trigger 또는 Anchor 너비와 같아집니다. ```tsx import { ActionButton, Box } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { return ( 열기 ); } ``` ### Container `container`를 지정하지 않으면 Menu 레이어는 Lynx view 안의 `position: fixed` view로 렌더링되고, z-index `99`로 같은 화면의 형제 요소 위에 그려집니다. `container="window"`처럼 native container 이름을 지정하면 native overlay에 렌더링되어 Lynx view 밖의 host 화면까지 덮습니다. `overlayLevel`은 `container`를 지정했을 때 overlay의 표시 순서를 정합니다. 두 방식 모두 레이어 바깥을 탭하면 아래 화면에 탭을 전달하지 않고 `"interactOutside"` reason으로 닫힙니다. ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { useState } from "@lynx-js/react"; import { ActionButton, Text, VStack } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const [closeReason, setCloseReason] = useState(null); const [count, setCount] = useState(0); function handleOpenChange(open: boolean, details: { reason: string }) { "background only"; if (!open) setCloseReason(details.reason); } function handleUnderlyingTap() { "background only"; setCount((current) => current + 1); } return ( window에 열기 } /> } /> {`아래 버튼 ${count}`} {`마지막 닫힘 이유: ${closeReason ?? "-"}`} ); } ``` ### `onOpenChange` details `onOpenChange`의 두 번째 인자인 `details`에는 상태를 바꾼 이유와 native event가 들어옵니다. - 열림: `"trigger"` - 닫힘: `"trigger"`, `"interactOutside"`, `"itemClick"` - `container`를 지정한 native overlay가 닫힘을 요청한 경우(Android 뒤로 가기): `"dismiss"`. `container`가 없으면 전달하지 않으며, Android 뒤로 가기는 Menu가 아니라 host 화면을 닫습니다. ```tsx import IconPencilLine from "@karrotmarket/lynx-monochrome-icon/IconPencilLine"; import IconPlusLine from "@karrotmarket/lynx-monochrome-icon/IconPlusLine"; import { useState } from "@lynx-js/react"; import { ActionButton, HStack, VStack } from "@seed-design/lynx-react"; import { MenuContent, MenuGroup, MenuItem, MenuRoot, MenuTrigger } from "@/components/ui/menu"; export default function Example() { const [open, setOpen] = useState(false); const [openReason, setOpenReason] = useState(null); const [closeReason, setCloseReason] = useState(null); function handleOpenChange(nextOpen: boolean, details?: { reason?: string }) { "background only"; setOpen(nextOpen); (nextOpen ? setOpenReason : setCloseReason)(details?.reason ?? null); } return ( 열기 } /> } /> 마지막 열림 이유: {openReason ?? "-"} 마지막 닫힘 이유: {closeReason ?? "-"} ); } ``` ## 웹 버전과의 차이 - Trigger는 자식을 native `view`로 감싸며 `asChild`를 제공하지 않습니다. raw ``처럼 이름을 만들 수 없는 자식에는 `accessibility-label`을 지정하세요. - 키보드 focus, typeahead, ESC 키 닫힘은 지원하지 않습니다. - 바깥 탭은 `"interactOutside"`, native overlay의 닫기 요청은 `"dismiss"` reason으로 전달합니다. - Menu가 열려 있는 동안 화면 전체 레이어가 Anchor 밖의 탭을 받습니다.