Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/tangy-banks-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@seed-design/react-drawer": minor
"@seed-design/react": minor
"@seed-design/css": minor
Comment thread
junghyeonsu marked this conversation as resolved.
---

(BREAKING CHANGE: MenuSheet snippet을 다시 설치해야 합니다.) MenuSheet에서 CloseButton을 제거하고 Handle을 추가합니다.

- `MenuSheet.CloseButton`, `MenuSheetCloseButton` export 제거
- `MenuSheet.Handle` 컴포넌트 추가 (bottom-sheet-handle recipe 재사용)
- `MenuSheetTitle`에서 `useDrawerContext` 의존 제거
2 changes: 1 addition & 1 deletion docs/content/docs/migration/deprecations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ description: Deprecated 항목의 현황과 제거 계획을 관리합니다.

| 항목 | 제거 버전 | 대체안 | 비고 |
| ---- | --------- | ------ | ---- |
| - | - | - | - |
| MenuSheet - CloseButton | 1.3.0 | - | Drawer 기반으로 전환, 닫기 버튼 패턴 제거 |
79 changes: 67 additions & 12 deletions docs/content/react/components/menu-sheet.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,56 @@ npx @seed-design/cli@latest add ui:menu-sheet

## Examples

### Trigger

`<MenuSheetTrigger>`는 `asChild` 패턴을 사용해 자식 요소가 MenuSheet를 열 수 있도록 합니다.

`<MenuSheetTrigger>`는 `aria-haspopup="dialog"` 속성을 설정하고, `MenuSheet`의 `open` 상태에 따라 `aria-expanded` 속성을 자동으로 설정합니다. 이 속성은 스크린 리더와 같은 보조 기술에 유용합니다.

<ComponentExample name="react/menu-sheet/trigger">
```json doc-gen:file
{
"file": "examples/react/menu-sheet/trigger.tsx",
"codeblock": true
}
```
</ComponentExample>

### Controlled

Trigger 외의 방식으로 MenuSheet를 열고 닫을 수 있습니다. 이 경우 `open` prop을 사용하여 MenuSheet의 상태를 제어합니다.

<ComponentExample name="react/menu-sheet/controlled">
```json doc-gen:file
{
"file": "examples/react/menu-sheet/controlled.tsx",
"codeblock": true
}
```
</ComponentExample>

### `onOpenChange` Details

`onOpenChange` 두 번째 인자로 `details`가 제공됩니다.

#### `reason`

**닫힐 때** (`open: false`)

- `"escapeKeyDown"`: <kbd>ESC</kbd> 키 사용
- `"interactOutside"`: 외부 영역 클릭
- `"drag"`: 드래그로 닫힘
- `"handleClickOnLastSnapPoint"`: 마지막 스냅 포인트에서 핸들 클릭으로 닫힘

<ComponentExample name="react/menu-sheet/open-change-reason">
```json doc-gen:file
{
"file": "examples/react/menu-sheet/open-change-reason.tsx",
"codeblock": true
}
```
</ComponentExample>

### With Title

`MenuSheetContent`의 `title` prop을 사용하여 시트 헤더에 제목을 표시합니다.
Expand Down Expand Up @@ -134,26 +184,31 @@ npx @seed-design/cli@latest add ui:menu-sheet
```
</ComponentExample>

### `onOpenChange` Details
### Show Handle

`onOpenChange` 두 번째 인자로 `details`가 제공됩니다.
`<MenuSheetContent>`에 `showHandle` prop을 전달하여 Handle 표시 여부를 제어할 수 있습니다.
기본 값은 `true`입니다.

#### `reason`

**열릴 때** (`open: true`)
`showHandle`을 `false`로 설정하면 Handle이 표시되지 않습니다.

- `"trigger"`: `MenuSheetTrigger` (`MenuSheet.Trigger`)로 열림
<ComponentExample name="react/menu-sheet/show-handle">
```json doc-gen:file
{
"file": "examples/react/menu-sheet/show-handle.tsx",
"codeblock": true
}
```
</ComponentExample>

**닫힐 때** (`open: false`)
### Dismissible

- `"closeButton"`: `MenuSheet.CloseButton`으로 닫힘
- `"escapeKeyDown"`: <kbd>ESC</kbd> 키 사용
- `"interactOutside"`: 외부 영역 클릭
`dismissible` prop을 `false`로 설정하면 closeOnEscape, closeOnInteractOutside, draggable 기능이 비활성화됩니다.
의도적으로 MenuSheet를 닫을 수 없게 하고 싶을 때 사용합니다. 이외에는 유저가 MenuSheet를 닫을 수 있는 방법을 제공해야 합니다.

<ComponentExample name="react/menu-sheet/open-change-reason">
<ComponentExample name="react/menu-sheet/dismissible">
```json doc-gen:file
{
"file": "examples/react/menu-sheet/open-change-reason.tsx",
"file": "examples/react/menu-sheet/dismissible.tsx",
"codeblock": true
}
```
Expand Down
38 changes: 38 additions & 0 deletions docs/examples/react/menu-sheet/controlled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
} from "seed-design/ui/menu-sheet";

const MenuSheetControlled = () => {
const [open, setOpen] = useState(false);

const scheduleOpen = () => {
setTimeout(() => {
setOpen(true);
}, 1000);
};

return (
<>
<ActionButton variant="neutralSolid" onClick={scheduleOpen}>
1초 후 열기
</ActionButton>
<MenuSheetRoot open={open} onOpenChange={setOpen}>
<MenuSheetContent title="Actions" aria-label="Menu Sheet">
Comment thread
junghyeonsu marked this conversation as resolved.
<MenuSheetGroup>
<MenuSheetItem label="Action 1" prefixIcon={<IconEyeSlashLine />} />
<MenuSheetItem label="Action 2" prefixIcon={<IconEyeSlashLine />} />
<MenuSheetItem label="Action 3" prefixIcon={<IconEyeSlashLine />} />
</MenuSheetGroup>
</MenuSheetContent>
</MenuSheetRoot>
</>
);
};

export default MenuSheetControlled;
44 changes: 44 additions & 0 deletions docs/examples/react/menu-sheet/dismissible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
MenuSheetTrigger,
} from "seed-design/ui/menu-sheet";

const MenuSheetDismissible = () => {
const [open, setOpen] = useState(false);

return (
<MenuSheetRoot open={open} onOpenChange={setOpen} dismissible={false}>
<MenuSheetTrigger asChild>
<ActionButton variant="neutralSolid">Open</ActionButton>
</MenuSheetTrigger>
<MenuSheetContent title="Actions" aria-label="Menu Sheet">
Comment thread
junghyeonsu marked this conversation as resolved.
<MenuSheetGroup>
<MenuSheetItem
label="Action 1"
prefixIcon={<IconEyeSlashLine />}
onClick={() => setOpen(false)}
/>
<MenuSheetItem
label="Action 2"
prefixIcon={<IconEyeSlashLine />}
onClick={() => setOpen(false)}
/>
<MenuSheetItem
label="닫기"
prefixIcon={<IconEyeSlashLine />}
tone="critical"
onClick={() => setOpen(false)}
/>
</MenuSheetGroup>
</MenuSheetContent>
</MenuSheetRoot>
);
};

export default MenuSheetDismissible;
31 changes: 31 additions & 0 deletions docs/examples/react/menu-sheet/show-handle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon";
import { useState } from "react";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
MenuSheetTrigger,
} from "seed-design/ui/menu-sheet";

const MenuSheetShowHandle = () => {
const [isSheetOpen, setIsSheetOpen] = useState(false);

return (
<MenuSheetRoot open={isSheetOpen} onOpenChange={setIsSheetOpen}>
<MenuSheetTrigger asChild>
<ActionButton variant="neutralSolid">Open</ActionButton>
</MenuSheetTrigger>
<MenuSheetContent title="Actions" aria-label="Menu Sheet" showHandle={false}>
<MenuSheetGroup>
<MenuSheetItem label="Action 1" prefixIcon={<IconEyeSlashLine />} />
<MenuSheetItem label="Action 2" prefixIcon={<IconEyeSlashLine />} />
<MenuSheetItem label="Action 3" prefixIcon={<IconEyeSlashLine />} />
</MenuSheetGroup>
</MenuSheetContent>
</MenuSheetRoot>
);
};

export default MenuSheetShowHandle;
28 changes: 28 additions & 0 deletions docs/examples/react/menu-sheet/trigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { IconEyeSlashLine } from "@karrotmarket/react-monochrome-icon";
import { ActionButton } from "seed-design/ui/action-button";
import {
MenuSheetContent,
MenuSheetGroup,
MenuSheetItem,
MenuSheetRoot,
MenuSheetTrigger,
} from "seed-design/ui/menu-sheet";

const MenuSheetTriggerExample = () => {
return (
<MenuSheetRoot>
<MenuSheetTrigger asChild>
<ActionButton variant="neutralSolid">Open</ActionButton>
</MenuSheetTrigger>
<MenuSheetContent title="Actions" aria-label="Menu Sheet">
<MenuSheetGroup>
<MenuSheetItem label="Action 1" prefixIcon={<IconEyeSlashLine />} />
<MenuSheetItem label="Action 2" prefixIcon={<IconEyeSlashLine />} />
<MenuSheetItem label="Action 3" prefixIcon={<IconEyeSlashLine />} />
</MenuSheetGroup>
</MenuSheetContent>
</MenuSheetRoot>
);
};

export default MenuSheetTriggerExample;
2 changes: 1 addition & 1 deletion docs/public/__registry__/ui/menu-sheet.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"snippets": [
{
"path": "menu-sheet.tsx",
"content": "/**\n * @file ui:menu-sheet\n * @requires @seed-design/react@~1.2.0\n * @requires @seed-design/css@~1.2.0\n **/\n\n\"use client\";\n\nimport { PrefixIcon, MenuSheet as SeedMenuSheet } from \"@seed-design/react\";\nimport { forwardRef } from \"react\";\nimport type * as React from \"react\";\n\nexport interface MenuSheetRootProps extends SeedMenuSheet.RootProps {}\n\n/**\n * @see https://seed-design.io/react/components/menu-sheet\n */\nexport const MenuSheetRoot = (props: MenuSheetRootProps) => {\n const { children, ...otherProps } = props;\n return (\n <SeedMenuSheet.Root closeOnInteractOutside={true} {...otherProps}>\n {children}\n </SeedMenuSheet.Root>\n );\n};\n\nexport interface MenuSheetTriggerProps extends SeedMenuSheet.TriggerProps {}\n\nexport const MenuSheetTrigger = SeedMenuSheet.Trigger;\n\nexport interface MenuSheetContentProps extends Omit<SeedMenuSheet.ContentProps, \"title\"> {\n title?: React.ReactNode;\n\n description?: React.ReactNode;\n\n layerIndex?: number;\n}\n\nexport const MenuSheetContent = forwardRef<HTMLDivElement, MenuSheetContentProps>(\n ({ children, title, description, layerIndex, ...otherProps }, ref) => {\n if (\n !title &&\n !otherProps[\"aria-labelledby\"] &&\n !otherProps[\"aria-label\"] &&\n process.env.NODE_ENV !== \"production\"\n ) {\n console.warn(\n \"MenuSheetContent: aria-labelledby or aria-label should be provided if title is not provided.\",\n );\n }\n\n return (\n <SeedMenuSheet.Positioner style={{ \"--layer-index\": layerIndex } as React.CSSProperties}>\n <SeedMenuSheet.Backdrop />\n <SeedMenuSheet.Content ref={ref} {...otherProps}>\n {(title || description) && (\n <SeedMenuSheet.Header>\n {title && <SeedMenuSheet.Title>{title}</SeedMenuSheet.Title>}\n {description && <SeedMenuSheet.Description>{description}</SeedMenuSheet.Description>}\n </SeedMenuSheet.Header>\n )}\n <SeedMenuSheet.List>{children}</SeedMenuSheet.List>\n <SeedMenuSheet.Footer>\n {/* You may implement your own i18n for dismiss label */}\n <SeedMenuSheet.CloseButton>닫기</SeedMenuSheet.CloseButton>\n </SeedMenuSheet.Footer>\n </SeedMenuSheet.Content>\n </SeedMenuSheet.Positioner>\n );\n },\n);\n\nexport interface MenuSheetGroupProps extends SeedMenuSheet.GroupProps {}\n\nexport const MenuSheetGroup = SeedMenuSheet.Group;\n\nexport interface MenuSheetItemProps extends Omit<SeedMenuSheet.ItemProps, \"children\"> {\n prefixIcon?: React.ReactNode;\n\n label: React.ReactNode;\n\n description?: React.ReactNode;\n}\n\nexport const MenuSheetItem = forwardRef<HTMLButtonElement, MenuSheetItemProps>(\n ({ prefixIcon, label, description, ...props }, ref) => {\n return (\n <SeedMenuSheet.Item ref={ref} {...props}>\n {prefixIcon && <PrefixIcon svg={prefixIcon} />}\n <SeedMenuSheet.ItemContent>\n <SeedMenuSheet.ItemLabel>{label}</SeedMenuSheet.ItemLabel>\n {description && (\n <SeedMenuSheet.ItemDescription>{description}</SeedMenuSheet.ItemDescription>\n )}\n </SeedMenuSheet.ItemContent>\n </SeedMenuSheet.Item>\n );\n },\n);\n\n/**\n * This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.\n * You can extend this snippet however you want.\n */\n",
"content": "/**\n * @file ui:menu-sheet\n * @requires @seed-design/react@~1.2.0\n * @requires @seed-design/css@~1.2.0\n **/\n\n\"use client\";\n\nimport { PrefixIcon, MenuSheet as SeedMenuSheet, VisuallyHidden } from \"@seed-design/react\";\nimport { forwardRef } from \"react\";\nimport type * as React from \"react\";\n\nexport interface MenuSheetRootProps extends SeedMenuSheet.RootProps {}\n\n/**\n * @see https://seed-design.io/react/components/menu-sheet\n */\nexport const MenuSheetRoot = (props: MenuSheetRootProps) => {\n const { children, ...otherProps } = props;\n return (\n <SeedMenuSheet.Root closeOnInteractOutside={true} {...otherProps}>\n {children}\n </SeedMenuSheet.Root>\n );\n};\n\nexport interface MenuSheetTriggerProps extends SeedMenuSheet.TriggerProps {}\n\nexport const MenuSheetTrigger = SeedMenuSheet.Trigger;\n\nexport interface MenuSheetContentProps extends Omit<SeedMenuSheet.ContentProps, \"title\"> {\n title?: React.ReactNode;\n\n description?: React.ReactNode;\n\n layerIndex?: number;\n\n /**\n * @default true\n */\n showHandle?: boolean;\n}\n\nexport const MenuSheetContent = forwardRef<HTMLDivElement, MenuSheetContentProps>(\n ({ children, title, description, layerIndex, showHandle = true, ...otherProps }, ref) => {\n if (\n !title &&\n !otherProps[\"aria-labelledby\"] &&\n !otherProps[\"aria-label\"] &&\n process.env.NODE_ENV !== \"production\"\n ) {\n console.warn(\n \"MenuSheetContent: aria-labelledby or aria-label should be provided if title is not provided.\",\n );\n }\n\n const shouldRenderHeader = title || description;\n\n return (\n <SeedMenuSheet.Positioner style={{ \"--layer-index\": layerIndex } as React.CSSProperties}>\n <SeedMenuSheet.Backdrop />\n <SeedMenuSheet.Content ref={ref} {...otherProps}>\n {showHandle && <SeedMenuSheet.Handle />}\n {shouldRenderHeader && (\n <SeedMenuSheet.Header>\n {title ? (\n <SeedMenuSheet.Title>{title}</SeedMenuSheet.Title>\n ) : (\n <VisuallyHidden asChild>\n <SeedMenuSheet.Title>{otherProps[\"aria-label\"] || \"\"}</SeedMenuSheet.Title>\n </VisuallyHidden>\n )}\n {description && <SeedMenuSheet.Description>{description}</SeedMenuSheet.Description>}\n </SeedMenuSheet.Header>\n )}\n <SeedMenuSheet.List>{children}</SeedMenuSheet.List>\n </SeedMenuSheet.Content>\n </SeedMenuSheet.Positioner>\n );\n },\n);\n\nexport interface MenuSheetGroupProps extends SeedMenuSheet.GroupProps {}\n\nexport const MenuSheetGroup = SeedMenuSheet.Group;\n\nexport interface MenuSheetItemProps extends Omit<SeedMenuSheet.ItemProps, \"children\"> {\n prefixIcon?: React.ReactNode;\n\n label: React.ReactNode;\n\n description?: React.ReactNode;\n}\n\nexport const MenuSheetItem = forwardRef<HTMLButtonElement, MenuSheetItemProps>(\n ({ prefixIcon, label, description, ...props }, ref) => {\n return (\n <SeedMenuSheet.Item ref={ref} {...props}>\n {prefixIcon && <PrefixIcon svg={prefixIcon} />}\n <SeedMenuSheet.ItemContent>\n <SeedMenuSheet.ItemLabel>{label}</SeedMenuSheet.ItemLabel>\n {description && (\n <SeedMenuSheet.ItemDescription>{description}</SeedMenuSheet.ItemDescription>\n )}\n </SeedMenuSheet.ItemContent>\n </SeedMenuSheet.Item>\n );\n },\n);\n\n/**\n * This file is a snippet from SEED Design, helping you get started quickly with @seed-design/* packages.\n * You can extend this snippet however you want.\n */\n",
"dependencies": {
"@seed-design/react": "~1.2.0",
"@seed-design/css": "~1.2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"metadata": {
"id": "action-sheet-close-button",
"name": "Action Sheet Close Button",
"deprecated": "Use menu-sheet-close-button instead."
"deprecated": "No longer used."
},
"data": {
"id": "action-sheet-close-button",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"metadata": {
"id": "extended-action-sheet-close-button",
"name": "Extended Action Sheet Close Button",
"deprecated": "Use menu-sheet-close-button instead."
"deprecated": "No longer used."
},
"data": {
"id": "extended-action-sheet-close-button",
Expand Down
3 changes: 2 additions & 1 deletion docs/public/rootage/components/menu-sheet-close-button.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"kind": "ComponentSpec",
"metadata": {
"id": "menu-sheet-close-button",
"name": "Menu Sheet Close Button"
"name": "Menu Sheet Close Button",
"deprecated": "No longer used. Menu Sheet now uses a handle instead of a close button."
},
"data": {
"id": "menu-sheet-close-button",
Expand Down
7 changes: 7 additions & 0 deletions docs/public/rootage/components/menu-sheet.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"gap": {
"type": "dimension"
},
"paddingTop": {
"type": "dimension"
},
"paddingBottom": {
"type": "dimension"
}
Expand Down Expand Up @@ -226,6 +229,10 @@
"type": "dimension",
"value": "$dimension.x1"
},
"paddingTop": {
"type": "dimension",
"value": "$dimension.x2"
},
"paddingBottom": {
"type": "dimension",
"value": "$dimension.x4"
Expand Down
26 changes: 18 additions & 8 deletions docs/registry/ui/menu-sheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { PrefixIcon, MenuSheet as SeedMenuSheet } from "@seed-design/react";
import { PrefixIcon, MenuSheet as SeedMenuSheet, VisuallyHidden } from "@seed-design/react";
import { forwardRef } from "react";
import type * as React from "react";

Expand Down Expand Up @@ -28,10 +28,15 @@ export interface MenuSheetContentProps extends Omit<SeedMenuSheet.ContentProps,
description?: React.ReactNode;

layerIndex?: number;

/**
* @default true
*/
showHandle?: boolean;
}

export const MenuSheetContent = forwardRef<HTMLDivElement, MenuSheetContentProps>(
({ children, title, description, layerIndex, ...otherProps }, ref) => {
({ children, title, description, layerIndex, showHandle = true, ...otherProps }, ref) => {
if (
!title &&
!otherProps["aria-labelledby"] &&
Expand All @@ -43,21 +48,26 @@ export const MenuSheetContent = forwardRef<HTMLDivElement, MenuSheetContentProps
);
}

const shouldRenderHeader = title || description;

return (
<SeedMenuSheet.Positioner style={{ "--layer-index": layerIndex } as React.CSSProperties}>
<SeedMenuSheet.Backdrop />
<SeedMenuSheet.Content ref={ref} {...otherProps}>
{(title || description) && (
{showHandle && <SeedMenuSheet.Handle />}
{shouldRenderHeader && (
<SeedMenuSheet.Header>
{title && <SeedMenuSheet.Title>{title}</SeedMenuSheet.Title>}
{title ? (
<SeedMenuSheet.Title>{title}</SeedMenuSheet.Title>
) : (
<VisuallyHidden asChild>
<SeedMenuSheet.Title>{otherProps["aria-label"] || ""}</SeedMenuSheet.Title>
</VisuallyHidden>
)}
{description && <SeedMenuSheet.Description>{description}</SeedMenuSheet.Description>}
</SeedMenuSheet.Header>
)}
<SeedMenuSheet.List>{children}</SeedMenuSheet.List>
<SeedMenuSheet.Footer>
{/* You may implement your own i18n for dismiss label */}
<SeedMenuSheet.CloseButton>닫기</SeedMenuSheet.CloseButton>
</SeedMenuSheet.Footer>
</SeedMenuSheet.Content>
</SeedMenuSheet.Positioner>
);
Expand Down
Loading
Loading