-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Segmented Control 컴포넌트를 추가해요 (#475)
* feat: wip using `useTabs` (cherry picked from commit ec99733) # Conflicts: # docs/components/example/index.json # docs/public/__registry__/ui/index.json # docs/registry/registry-ui.ts # packages/recipe-generator/preset/src/index.ts # packages/vars/src/component/index.ts * fix: fix styles (cherry picked from commit c065bcc) # Conflicts: # docs/public/__registry__/ui/segmented-control.json * fix: fix styles again (cherry picked from commit 678d3a7) * feat: support adjustable width (cherry picked from commit 5f4d6cd) * feat: dynamic width, placeholder로 overflow 시 layout shift 방지 (cherry picked from commit 3de3849) # Conflicts: # docs/components/example/index.json * fix: overflow 문제 수정 (cherry picked from commit cf7a835) # Conflicts: # docs/components/example/index.json * revert: 불필요한 변경 롤백 (cherry picked from commit 2cbdc0f15a86e84bb211d0234a83999f0aa70ac5) # Conflicts: # docs/components/example/index.json * fix: remove `labelProps` from label placeholder * fix: remove label from being able to select * docs: add stories * refactor: 새 rootage 스키마에 맞게 수정해요 * fix: 스토리 수정 * fix: 폰트 스케일링 대응
- Loading branch information
Showing
19 changed files
with
637 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; | ||
|
||
export default function SegmentedControlFixedWidth() { | ||
return ( | ||
<SegmentedControl style={{ width: "600px" }}> | ||
<SegmentedControlOption value="new">New</SegmentedControlOption> | ||
<SegmentedControlOption value="hot">Hot</SegmentedControlOption> | ||
</SegmentedControl> | ||
); | ||
} |
11 changes: 11 additions & 0 deletions
11
docs/components/example/segmented-control-long-label-fixed-width.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; | ||
|
||
export default function SegmentedControlLongLabelFixedWidth() { | ||
return ( | ||
<SegmentedControl style={{ width: "600px" }}> | ||
<SegmentedControlOption value="price">가격 높은 순</SegmentedControlOption> | ||
<SegmentedControlOption value="discount">할인율 높은 순</SegmentedControlOption> | ||
<SegmentedControlOption value="popularity">인기 많은 순</SegmentedControlOption> | ||
</SegmentedControl> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; | ||
|
||
export default function SegmentedControlLongLabel() { | ||
return ( | ||
<SegmentedControl> | ||
<SegmentedControlOption value="price">가격 높은 순</SegmentedControlOption> | ||
<SegmentedControlOption value="discount">할인율 높은 순</SegmentedControlOption> | ||
<SegmentedControlOption value="popularity">인기 많은 순</SegmentedControlOption> | ||
</SegmentedControl> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"use client"; | ||
|
||
import { useState } from "react"; | ||
import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; | ||
|
||
export default function SegmentedControlPreview() { | ||
const options = ["New", "Hot"]; | ||
const [value, setValue] = useState("New"); | ||
|
||
return ( | ||
<div className="flex flex-col gap-3 items-center text-center"> | ||
<SegmentedControl value={value} defaultValue="New" onValueChange={setValue}> | ||
{options.map((option) => ( | ||
<SegmentedControlOption key={option} value={option}> | ||
{option} | ||
</SegmentedControlOption> | ||
))} | ||
</SegmentedControl> | ||
<div>Selected value: {value}</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
--- | ||
title: Segmented Control | ||
description: 설명 # TODO | ||
--- | ||
|
||
<ComponentExample name="segmented-control-preview" /> | ||
|
||
### 설치 | ||
|
||
<Installation name="segmented-control" /> | ||
|
||
## Props | ||
|
||
### `SegmentedControl` | ||
|
||
<AutoTypeTable | ||
path="./registry/ui/segmented-control.tsx" | ||
name="SegmentedControlProps" | ||
/> | ||
|
||
### `SegmentedControlOption` | ||
|
||
<AutoTypeTable | ||
path="./registry/ui/segmented-control.tsx" | ||
name="SegmentedControlOptionProps" | ||
/> | ||
|
||
## 예제 | ||
|
||
### 최소 너비보다 넓은 옵션 레이블 | ||
|
||
Pill 형태의 `SegmentControlOption` 한 개는 86px의 최소 너비를 가져요. 제공한 옵션 중 이 너비를 초과하는 옵션이 있다면, 가장 긴 옵션의 너비에 모든 옵션의 너비가 맞춰져요. | ||
|
||
<ComponentExample name="segmented-control-long-label" /> | ||
|
||
### Fixed Width | ||
|
||
`SegmentControl`의 `style` prop에 `width`를 제공해서 직접 너비를 설정할 수 있어요. | ||
|
||
<Callout type="info" title="지정한 너비가 무시되는 경우"> | ||
|
||
직접 설정한 너비 안에서 모든 레이블을 overflow 없이 표시할 수 없는 경우, 직접 | ||
지정한 너비 대신 overflow 없이 표시할 수 있는 최소 너비로 자동 조정돼요. | ||
|
||
</Callout> | ||
|
||
<ComponentExample name="segmented-control-fixed-width" /> | ||
|
||
### 최소 너비보다 넓은 옵션 레이블 & Fixed Width | ||
|
||
<ComponentExample name="segmented-control-long-label-fixed-width" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
{ | ||
"name": "segmented-control", | ||
"dependencies": [ | ||
"@seed-design/react-tabs@alpha" | ||
], | ||
"registries": [ | ||
{ | ||
"name": "segmented-control.tsx", | ||
"type": "ui", | ||
"content": "\"use client\";\n\nimport \"@seed-design/stylesheet/segmentedControl.css\";\nimport {\n useTabs,\n type TriggerProps,\n type UseTabsProps,\n} from \"@seed-design/react-tabs\";\nimport * as React from \"react\";\nimport clsx from \"clsx\";\nimport {\n segmentedControl,\n type SegmentedControlVariantProps,\n} from \"@seed-design/recipe/segmentedControl\";\nimport type { Assign } from \"../util/types\";\nexport interface SegmentedControlProps extends SegmentedControlVariantProps {}\n\nconst TabsContext = React.createContext<{\n api: ReturnType<typeof useTabs>;\n} | null>(null);\n\nconst useTabsContext = () => {\n const context = React.useContext(TabsContext);\n if (!context)\n throw new Error(\n \"SegmentedControlOption cannot be rendered outside the SegmentedControl\",\n );\n\n return context;\n};\n\nexport interface SegmentedControlProps\n extends SegmentedControlVariantProps,\n Pick<UseTabsProps, \"value\" | \"defaultValue\" | \"onValueChange\"> {}\n\ntype ReactSegmentedControlProps = SegmentedControlProps &\n Assign<React.HTMLAttributes<HTMLDivElement>, UseTabsProps>;\n\nexport const SegmentedControl = React.forwardRef<\n // HTMLFieldSetElement,\n HTMLDivElement,\n ReactSegmentedControlProps\n>(({ className, children, style, ...otherProps }, ref) => {\n const api = useTabs(otherProps);\n const { tabTriggerListProps, triggerSize, tabIndicatorProps } = api;\n\n const { left, width } = triggerSize;\n\n // TODO: value/defaultvalue 없는 경우 첫 번째 아이템으로 default (tabs 참고)\n\n const classNames = segmentedControl();\n\n return (\n <div\n style={{\n ...style,\n // XXX: tabCount 썼을 때 hydration 문제\n gridTemplateColumns: `repeat(${React.Children.count(children)}, 1fr)`,\n }}\n className={clsx(classNames.root, className)}\n ref={ref}\n {...tabTriggerListProps}\n {...otherProps}\n >\n <TabsContext.Provider value={{ api }}>{children}</TabsContext.Provider>\n <div\n aria-hidden\n className={classNames.indicator}\n {...tabIndicatorProps}\n style={{ left, width }}\n />\n </div>\n );\n});\nSegmentedControl.displayName = \"SegmentedControl\";\n\nexport interface SegmentedControlOptionProps\n extends SegmentedControlVariantProps,\n Omit<TriggerProps, \"isDisabled\"> {}\n\ntype ReactSegmentedControlOptionProps = Assign<\n React.HTMLAttributes<HTMLButtonElement>,\n SegmentedControlOptionProps\n>;\n\nexport const SegmentedControlOption = React.forwardRef<\n HTMLButtonElement,\n ReactSegmentedControlOptionProps\n>(({ className, children, value, ...otherProps }, ref) => {\n const {\n api: { getTabTriggerProps },\n } = useTabsContext();\n\n const { rootProps, labelProps } = getTabTriggerProps({ value });\n\n const classNames = segmentedControl();\n\n return (\n <button\n ref={ref}\n className={clsx(classNames.option, className)}\n {...rootProps}\n {...otherProps}\n >\n <div className={classNames.optionLabel} {...labelProps} tabIndex={-1}>\n {children}\n </div>\n <div aria-hidden className={classNames.optionLabelPlaceholder}>\n {children}\n </div>\n </button>\n );\n});\n\nSegmentedControlOption.displayName = \"SegmentedControlOption\";\n" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
"use client"; | ||
|
||
import "@seed-design/stylesheet/segmentedControl.css"; | ||
import { | ||
useTabs, | ||
type TriggerProps, | ||
type UseTabsProps, | ||
} from "@seed-design/react-tabs"; | ||
import * as React from "react"; | ||
import clsx from "clsx"; | ||
import { | ||
segmentedControl, | ||
type SegmentedControlVariantProps, | ||
} from "@seed-design/recipe/segmentedControl"; | ||
import type { Assign } from "../util/types"; | ||
export interface SegmentedControlProps extends SegmentedControlVariantProps {} | ||
|
||
const TabsContext = React.createContext<{ | ||
api: ReturnType<typeof useTabs>; | ||
} | null>(null); | ||
|
||
const useTabsContext = () => { | ||
const context = React.useContext(TabsContext); | ||
if (!context) | ||
throw new Error( | ||
"SegmentedControlOption cannot be rendered outside the SegmentedControl", | ||
); | ||
|
||
return context; | ||
}; | ||
|
||
export interface SegmentedControlProps | ||
extends SegmentedControlVariantProps, | ||
Pick<UseTabsProps, "value" | "defaultValue" | "onValueChange"> {} | ||
|
||
type ReactSegmentedControlProps = SegmentedControlProps & | ||
Assign<React.HTMLAttributes<HTMLDivElement>, UseTabsProps>; | ||
|
||
export const SegmentedControl = React.forwardRef< | ||
// HTMLFieldSetElement, | ||
HTMLDivElement, | ||
ReactSegmentedControlProps | ||
>(({ className, children, style, ...otherProps }, ref) => { | ||
const api = useTabs(otherProps); | ||
const { tabTriggerListProps, triggerSize, tabIndicatorProps } = api; | ||
|
||
const { left, width } = triggerSize; | ||
|
||
// TODO: value/defaultvalue 없는 경우 첫 번째 아이템으로 default (tabs 참고) | ||
|
||
const classNames = segmentedControl(); | ||
|
||
return ( | ||
<div | ||
style={{ | ||
...style, | ||
// XXX: tabCount 썼을 때 hydration 문제 | ||
gridTemplateColumns: `repeat(${React.Children.count(children)}, 1fr)`, | ||
}} | ||
className={clsx(classNames.root, className)} | ||
ref={ref} | ||
{...tabTriggerListProps} | ||
{...otherProps} | ||
> | ||
<TabsContext.Provider value={{ api }}>{children}</TabsContext.Provider> | ||
<div | ||
aria-hidden | ||
className={classNames.indicator} | ||
{...tabIndicatorProps} | ||
style={{ left, width }} | ||
/> | ||
</div> | ||
); | ||
}); | ||
SegmentedControl.displayName = "SegmentedControl"; | ||
|
||
export interface SegmentedControlOptionProps | ||
extends SegmentedControlVariantProps, | ||
Omit<TriggerProps, "isDisabled"> {} | ||
|
||
type ReactSegmentedControlOptionProps = Assign< | ||
React.HTMLAttributes<HTMLButtonElement>, | ||
SegmentedControlOptionProps | ||
>; | ||
|
||
export const SegmentedControlOption = React.forwardRef< | ||
HTMLButtonElement, | ||
ReactSegmentedControlOptionProps | ||
>(({ className, children, value, ...otherProps }, ref) => { | ||
const { | ||
api: { getTabTriggerProps }, | ||
} = useTabsContext(); | ||
|
||
const { rootProps, labelProps } = getTabTriggerProps({ value }); | ||
|
||
const classNames = segmentedControl(); | ||
|
||
return ( | ||
<button | ||
ref={ref} | ||
className={clsx(classNames.option, className)} | ||
{...rootProps} | ||
{...otherProps} | ||
> | ||
<div className={classNames.optionLabel} {...labelProps} tabIndex={-1}> | ||
{children} | ||
</div> | ||
<div aria-hidden className={classNames.optionLabelPlaceholder}> | ||
{children} | ||
</div> | ||
</button> | ||
); | ||
}); | ||
|
||
SegmentedControlOption.displayName = "SegmentedControlOption"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import { | ||
SegmentedControl, | ||
SegmentedControlOption, | ||
type SegmentedControlProps, | ||
} from "seed-design/ui/segmented-control"; | ||
|
||
import { segmentedControlVariantMap } from "@seed-design/recipe/segmentedControl"; | ||
import { SeedThemeDecorator } from "./components/decorator"; | ||
import { VariantTable } from "./components/variant-table"; | ||
import { useState } from "react"; | ||
|
||
const Component = () => { | ||
const values = ["dolor", "magna", "sint"]; | ||
const [value, setValue] = useState(values[0]); | ||
|
||
return ( | ||
<SegmentedControl value={value} onValueChange={setValue}> | ||
{values.map((value) => ( | ||
<SegmentedControlOption key={value} value={value}> | ||
{value} | ||
</SegmentedControlOption> | ||
))} | ||
</SegmentedControl> | ||
); | ||
}; | ||
|
||
const meta = { | ||
component: SegmentedControl, | ||
decorators: [SeedThemeDecorator], | ||
} satisfies Meta<typeof SegmentedControl>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
const CommonStoryTemplate: Story = { | ||
args: { | ||
defaultValue: "1", | ||
}, | ||
render: function Render(args) { | ||
return <VariantTable Component={Component} variantMap={segmentedControlVariantMap} {...args} />; | ||
}, | ||
}; | ||
|
||
export const LightTheme = CommonStoryTemplate; | ||
|
||
export const DarkTheme = CommonStoryTemplate; | ||
|
||
export const FontScalingExtraSmall = CommonStoryTemplate; | ||
|
||
export const FontScalingExtraExtraExtraLarge = CommonStoryTemplate; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.