Swipeable Menu Sheet
사용자 작업에 관련된 선택지를 화면 하단의 드래그 가능한 시트로 제공합니다.
<overlay>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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<text className="swipeable-menu-sheet-preview__status">
{open ? "열림: true" : "열림: false"}
</text>
<SwipeableMenuSheetRoot open={open} onOpenChange={handleOpenChange}>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="게시글 관리" description="원하는 작업을 선택해 주세요.">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="수정하기" />
<SwipeableMenuSheetItem
label="공유하기"
description="친구에게 게시글을 공유할 수 있어요."
/>
<SwipeableMenuSheetItem label="끌어올리기" />
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" />
<SwipeableMenuSheetItem label="삭제하기" tone="critical" />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Installation
npx @seed-design/cli@latest add ui:swipeable-menu-sheetpnpm dlx @seed-design/cli@latest add ui:swipeable-menu-sheetyarn dlx @seed-design/cli@latest add ui:swipeable-menu-sheetbun x @seed-design/cli@latest add ui:swipeable-menu-sheet의존성 설치
npm install @seed-design/lynx-reactyarn add @seed-design/lynx-reactpnpm add @seed-design/lynx-reactbun add @seed-design/lynx-react아래 코드를 복사 후 붙여넣고 사용하세요
/**
* @file ui:swipeable-menu-sheet
* @requires @seed-design/lynx-react@>=0.7.0 <1.0.0
* @requires @seed-design/lynx-css@>=0.11.0 <1.0.0
**/
import * as React from "@lynx-js/react";
import {
PrefixIcon,
SwipeableMenuSheet as SeedSwipeableMenuSheet,
type LynxIconElementProps,
} from "@seed-design/lynx-react";
export interface SwipeableMenuSheetRootProps extends SeedSwipeableMenuSheet.RootProps {}
export type SwipeableMenuSheetRootRef = SeedSwipeableMenuSheet.RootRef;
/**
* @see https://seed-design.io/lynx/components/swipeable-menu-sheet
*/
export const SwipeableMenuSheetRoot = React.forwardRef<
SwipeableMenuSheetRootRef,
SwipeableMenuSheetRootProps
>((props, ref) => {
return <SeedSwipeableMenuSheet.Root ref={ref} {...props} />;
});
SwipeableMenuSheetRoot.displayName = "SwipeableMenuSheetRoot";
export interface SwipeableMenuSheetTriggerProps extends SeedSwipeableMenuSheet.TriggerProps {}
export const SwipeableMenuSheetTrigger = SeedSwipeableMenuSheet.Trigger;
export interface SwipeableMenuSheetContentProps extends SeedSwipeableMenuSheet.ContentProps {
title?: React.ReactNode;
description?: React.ReactNode;
/**
* @default false
*
* Lynx는 웹과 달리 숨겨진 close button fallback을 제공하지 않습니다.
*/
showCloseButton?: boolean;
}
/**
* Positioner / Backdrop / Handle / Header / List를 내부에서 조립해
* 단일 컴포넌트로 메뉴 시트를 구성할 수 있도록 합니다.
*
* @see https://seed-design.io/lynx/components/swipeable-menu-sheet
*/
export const SwipeableMenuSheetContent = React.forwardRef<unknown, SwipeableMenuSheetContentProps>(
(
{
children,
title,
description,
showCloseButton = false,
"accessibility-label": accessibilityLabel,
...otherProps
},
ref,
) => {
if (!title && !accessibilityLabel && process.env.NODE_ENV !== "production") {
console.warn(
"SwipeableMenuSheetContent requires `accessibility-label` when `title` is not provided.",
);
}
const shouldRenderHeader = title != null || description != null;
return (
<SeedSwipeableMenuSheet.Positioner>
<SeedSwipeableMenuSheet.Backdrop />
<SeedSwipeableMenuSheet.Content
ref={ref}
accessibility-label={accessibilityLabel}
{...otherProps}
>
<SeedSwipeableMenuSheet.Handle />
{shouldRenderHeader ? (
<SeedSwipeableMenuSheet.Header>
{title != null ? (
<SeedSwipeableMenuSheet.Title>{title}</SeedSwipeableMenuSheet.Title>
) : null}
{description != null ? (
<SeedSwipeableMenuSheet.Description>
{description}
</SeedSwipeableMenuSheet.Description>
) : null}
</SeedSwipeableMenuSheet.Header>
) : null}
<SeedSwipeableMenuSheet.List>{children}</SeedSwipeableMenuSheet.List>
{showCloseButton ? (
<SeedSwipeableMenuSheet.Footer>
<SeedSwipeableMenuSheet.CloseButton accessibility-label="닫기">
닫기
</SeedSwipeableMenuSheet.CloseButton>
</SeedSwipeableMenuSheet.Footer>
) : null}
</SeedSwipeableMenuSheet.Content>
</SeedSwipeableMenuSheet.Positioner>
);
},
);
SwipeableMenuSheetContent.displayName = "SwipeableMenuSheetContent";
export interface SwipeableMenuSheetGroupProps extends SeedSwipeableMenuSheet.GroupProps {}
export const SwipeableMenuSheetGroup = SeedSwipeableMenuSheet.Group;
export interface SwipeableMenuSheetItemProps
extends Omit<SeedSwipeableMenuSheet.ItemProps, "children"> {
prefixIcon?: React.ReactElement<LynxIconElementProps>;
label: React.ReactNode;
description?: React.ReactNode;
}
export const SwipeableMenuSheetItem = React.forwardRef<unknown, SwipeableMenuSheetItemProps>(
(
{ prefixIcon, label, description, "accessibility-label": accessibilityLabel, ...otherProps },
ref,
) => {
return (
<SeedSwipeableMenuSheet.Item
ref={ref}
accessibility-label={accessibilityLabel ?? (typeof label === "string" ? label : undefined)}
{...otherProps}
>
{prefixIcon != null ? <PrefixIcon icon={prefixIcon} /> : null}
<SeedSwipeableMenuSheet.ItemContent>
<SeedSwipeableMenuSheet.ItemLabel>{label}</SeedSwipeableMenuSheet.ItemLabel>
{description != null ? (
<SeedSwipeableMenuSheet.ItemDescription>
{description}
</SeedSwipeableMenuSheet.ItemDescription>
) : null}
</SeedSwipeableMenuSheet.ItemContent>
</SeedSwipeableMenuSheet.Item>
);
},
);
SwipeableMenuSheetItem.displayName = "SwipeableMenuSheetItem";
/**
* This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.
* You can extend this snippet however you want.
*/
Usage
Registry가 제공하는 Root, Trigger, Content, Group, Item을 조합합니다. Trigger는 자식을 감싸는 native <view>를 렌더링하므로 asChild처럼 자식을 대체하는 방식은 지원하지 않습니다.
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 (
<SwipeableMenuSheetRoot open={open} onOpenChange={setOpen}>
<SwipeableMenuSheetTrigger accessibility-label="게시글 관리 메뉴 열기">
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="게시글 관리">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="수정하기" bindtap={handleEdit} />
<SwipeableMenuSheetItem label="삭제하기" tone="critical" />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
);
}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입니다.
Prop
Type
open?boolean | undefineddefaultOpen?boolean | undefinedonOpenChange?((open: boolean, details: SwipeableMenuSheetOpenChangeDetails) => void) | undefinedSwipeableMenuSheetTrigger
Prop
Type
style?CSSProperties | undefinedchildren?React.ReactNodeclassName?string | undefinedbindtap?EventHandler<BaseTouchEvent<Target>> | undefinedSwipeableMenuSheetContent
labelAlign?: "left" | "center"을 지정하면 그 안의 Group과 Item의 기본 레이블 정렬이 됩니다. showCloseButton?: boolean의 기본값은 false입니다. false일 때 별도의 숨겨진 닫기 버튼은 렌더링하지 않으며, 사용자는 배경 탭 또는 아래 방향 드래그로 닫을 수 있습니다.
Prop
Type
title?React.ReactNodedescription?React.ReactNodelabelAlign?SwipeableMenuSheetLabelAlign | undefinedSwipeableMenuSheetGroup
labelAlign?: "left" | "center"으로 Content의 기본 정렬을 해당 그룹에서 덮어쓸 수 있습니다.
Prop
Type
labelAlign?SwipeableMenuSheetLabelAlign | undefinedstyle?CSSProperties | undefinedchildren?React.ReactNodeclassName?string | undefinedSwipeableMenuSheetItem
label은 필수이며, description과 prefixIcon을 추가할 수 있습니다. tone?: "neutral" | "critical"의 기본값은 "neutral"입니다. labelAlign?: "left" | "center"은 Item, Group, Content 순서로 해석되는 정렬을 마지막에 덮어씁니다.
Prop
Type
prefixIcon?React.ReactElement<LynxIconElementProps, string | React.JSXElementConstructor<any>> | undefinedlabelReact.ReactNodedescription?React.ReactNodeclassName?string | undefinedstyle?CSSProperties | undefinedbindtap?EventHandler<BaseTouchEvent<Target>> | undefinedmain-thread:bindtap?EventHandler<BaseTouchEvent<Element>> | undefinedtone?SwipeableMenuSheetItemTone | undefinedlabelAlign?SwipeableMenuSheetLabelAlign | undefinedExamples
Trigger
SwipeableMenuSheetTrigger를 탭하면 시트가 열립니다. 열린 상태는 배경 탭, 아래 방향 드래그, 또는 showCloseButton으로 표시한 닫기 버튼으로 닫을 수 있습니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<text className="swipeable-menu-sheet-preview__status">
{open ? "열림: true" : "열림: false"}
</text>
<SwipeableMenuSheetRoot open={open} onOpenChange={handleOpenChange}>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="게시글 관리" description="원하는 작업을 선택해 주세요.">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="수정하기" />
<SwipeableMenuSheetItem
label="공유하기"
description="친구에게 게시글을 공유할 수 있어요."
/>
<SwipeableMenuSheetItem label="끌어올리기" />
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" />
<SwipeableMenuSheetItem label="삭제하기" tone="critical" />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Controlled
open과 onOpenChange로 Trigger 외의 방식에서도 열린 상태를 제어합니다. 배경 탭과 드래그로 닫힐 때도 새 open 값을 반영해야 합니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<text className="swipeable-menu-sheet-preview__status">
{open ? "열림 상태: true" : "열림 상태: false"}
</text>
<ActionButton variant="neutralWeak" bindtap={handleOpen} disabled={open}>
상태로 열기
</ActionButton>
<SwipeableMenuSheetRoot open={open} onOpenChange={handleOpenChange}>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="제어되는 메뉴">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" prefixIcon={<IconEyeSlashLine />} />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}With Title
Content의 title로 시트 헤더에 제목을 표시합니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="게시글 관리">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="수정하기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem
label="공유하기"
prefixIcon={<IconEyeSlashLine />}
description="친구에게 게시글을 공유할 수 있어요."
/>
<SwipeableMenuSheetItem
label="끌어올리기"
prefixIcon={<IconEyeSlashLine />}
description="게시글을 다시 상단에 표시해요."
/>
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem
label="삭제하기"
prefixIcon={<IconEyeSlashLine />}
tone="critical"
/>
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}With Title and Description
description은 제목 아래에 부가 설명을 표시합니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="게시글 관리" description="원하는 작업을 선택해 주세요.">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="수정하기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem
label="공유하기"
prefixIcon={<IconEyeSlashLine />}
description="친구에게 게시글을 공유할 수 있어요."
/>
<SwipeableMenuSheetItem
label="끌어올리기"
prefixIcon={<IconEyeSlashLine />}
description="게시글을 다시 상단에 표시해요."
/>
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem
label="삭제하기"
prefixIcon={<IconEyeSlashLine />}
tone="critical"
/>
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Close Button
showCloseButton을 설정하면 시트 하단에 닫기 버튼을 표시합니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="닫기 버튼이 있는 메뉴" showCloseButton>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" prefixIcon={<IconEyeSlashLine />} />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Label Align
labelAlign은 Content의 기본값을 Group과 Item에서 차례로 덮어쓸 수 있습니다.
labelAlign="left" with Prefix Icon
Prefix icon이 있는 항목은 일반적으로 왼쪽 정렬을 사용합니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent accessibility-label="아이콘이 있는 메뉴">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="게시글 숨기기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem label="알림 끄기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem label="관심 없음" prefixIcon={<IconEyeSlashLine />} />
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="게시글 신고하기" prefixIcon={<IconEyeSlashLine />} />
<SwipeableMenuSheetItem
label="게시글 삭제하기"
prefixIcon={<IconEyeSlashLine />}
tone="critical"
/>
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}labelAlign="center" without Prefix Icon
Prefix icon이 없는 항목은 중앙 정렬을 사용할 수 있습니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent accessibility-label="아이콘이 없는 메뉴" labelAlign="center">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="게시글 수정하기" />
<SwipeableMenuSheetItem label="게시글 공유하기" />
<SwipeableMenuSheetItem label="게시글 끌어올리기" />
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="게시글 숨기기" />
<SwipeableMenuSheetItem label="게시글 삭제하기" tone="critical" />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Overriding labelAlign
그룹 또는 개별 항목의 labelAlign으로 상위 정렬을 덮어씁니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent
accessibility-label="레이블 정렬 예제 메뉴"
labelAlign="center"
>
<SwipeableMenuSheetGroup labelAlign="left">
<SwipeableMenuSheetItem label="그룹의 왼쪽 정렬" />
<SwipeableMenuSheetItem label="항목의 가운데 정렬" labelAlign="center" />
<SwipeableMenuSheetItem label="그룹의 왼쪽 정렬" />
</SwipeableMenuSheetGroup>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="콘텐츠의 가운데 정렬" />
<SwipeableMenuSheetItem label="항목의 왼쪽 정렬" labelAlign="left" tone="critical" />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}onOpenChange Details
onOpenChange의 두 번째 인자는 { reason }입니다. 열릴 때는 "trigger"이고, 닫힐 때는 "closeButton", "interactOutside", "drag" 중 하나입니다.
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<OpenChangeReason | null>(null);
const [closeReason, setCloseReason] = useState<OpenChangeReason | null>(null);
function handleOpenChange(nextOpen: boolean, details: { reason: OpenChangeReason }) {
"background only";
setOpen(nextOpen);
if (nextOpen) {
setOpenReason(details.reason);
} else {
setCloseReason(details.reason);
}
}
return (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<text className="swipeable-menu-sheet-preview__status">
{open ? "열림 상태: true" : "열림 상태: false"}
</text>
<text className="swipeable-menu-sheet-preview__status">
마지막 열림 이유: {openReason ?? "-"}
</text>
<text className="swipeable-menu-sheet-preview__status">
마지막 닫힘 이유: {closeReason ?? "-"}
</text>
<SwipeableMenuSheetRoot open={open} onOpenChange={handleOpenChange}>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="메뉴" showCloseButton>
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" prefixIcon={<IconEyeSlashLine />} />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Skip Animation
skipAnimation을 설정하면 enter/exit 애니메이션 없이 즉시 열고 닫습니다.
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 (
<view className={`${seedClassName} docs-lynx-swipeable-menu-sheet-root`}>
<VStack className="swipeable-menu-sheet-preview" gap="x3">
<SwipeableMenuSheetRoot skipAnimation>
<SwipeableMenuSheetTrigger>
<ActionButton variant="neutralSolid">즉시 메뉴 열기</ActionButton>
</SwipeableMenuSheetTrigger>
<SwipeableMenuSheetContent title="애니메이션 없이 열리는 메뉴">
<SwipeableMenuSheetGroup>
<SwipeableMenuSheetItem label="숨기기" prefixIcon={<IconEyeSlashLine />} />
</SwipeableMenuSheetGroup>
</SwipeableMenuSheetContent>
</SwipeableMenuSheetRoot>
</VStack>
</view>
);
}Web Version Differences
Lynx SwipeableMenuSheet는 React 버전과 다음 차이가 있습니다.
- Trigger wrapper: Trigger는 native
<view>wrapper를 렌더링하며asChild처럼 자식을 대체하는 API를 제공하지 않습니다. - Native interaction: HTML 요소와
onClick대신 native<view>/<text>와bindtap을 사용합니다. - Accessibility: DOM ARIA 속성 대신
accessibility-label,accessibility-role-description등 Lynx native 접근성 속성을 사용합니다. - Close fallback:
showCloseButton={false}는 숨겨진 닫기 버튼을 만들지 않습니다. - Close reason: 키보드 ESC에 해당하는
escapeKeyDownreason은 제공하지 않습니다.
Last updated on