diff --git a/docs/components/example/segmented-control-disabled.tsx b/docs/components/example/segmented-control-disabled.tsx index 56e0c309d..f472fb2b0 100644 --- a/docs/components/example/segmented-control-disabled.tsx +++ b/docs/components/example/segmented-control-disabled.tsx @@ -1,10 +1,10 @@ -import { SegmentedControl, Segment } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlSegment } from "seed-design/ui/segmented-control"; export default function SegmentedControlPreview() { return ( - - Hot - New + + Hot + New ); } diff --git a/docs/components/example/segmented-control-fixed-width.tsx b/docs/components/example/segmented-control-fixed-width.tsx index bcf05e44f..fe57fbfbc 100644 --- a/docs/components/example/segmented-control-fixed-width.tsx +++ b/docs/components/example/segmented-control-fixed-width.tsx @@ -1,10 +1,10 @@ -import { SegmentedControl, Segment } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlSegment } from "seed-design/ui/segmented-control"; export default function SegmentedControlFixedWidth() { return ( - - New - Hot + + New + Hot ); } diff --git a/docs/components/example/segmented-control-long-label-fixed-width.tsx b/docs/components/example/segmented-control-long-label-fixed-width.tsx index 4567a6636..aab4a7394 100644 --- a/docs/components/example/segmented-control-long-label-fixed-width.tsx +++ b/docs/components/example/segmented-control-long-label-fixed-width.tsx @@ -1,11 +1,11 @@ -import { SegmentedControl, Segment } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlSegment } from "seed-design/ui/segmented-control"; export default function SegmentedControlLongLabelFixedWidth() { return ( - - 가격 높은 순 - 할인율 높은 순 - 인기 많은 순 + + 가격 높은 순 + 할인율 높은 순 + 인기 많은 순 ); } diff --git a/docs/components/example/segmented-control-long-label.tsx b/docs/components/example/segmented-control-long-label.tsx index 693d6943c..ed9170bdd 100644 --- a/docs/components/example/segmented-control-long-label.tsx +++ b/docs/components/example/segmented-control-long-label.tsx @@ -1,11 +1,11 @@ -import { SegmentedControl, Segment } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlSegment } from "seed-design/ui/segmented-control"; export default function SegmentedControlLongLabel() { return ( - - 가격 높은 순 - 할인율 높은 순 - 인기 많은 순 + + 가격 높은 순 + 할인율 높은 순 + 인기 많은 순 ); } diff --git a/docs/components/example/segmented-control-preview.tsx b/docs/components/example/segmented-control-preview.tsx index 12135dfee..b92fc3745 100644 --- a/docs/components/example/segmented-control-preview.tsx +++ b/docs/components/example/segmented-control-preview.tsx @@ -1,10 +1,10 @@ -import { SegmentedControl, Segment } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlSegment } from "seed-design/ui/segmented-control"; export default function SegmentedControlPreview() { return ( - - Hot - New + + Hot + New ); } diff --git a/docs/content/docs/react/components/segmented-control.mdx b/docs/content/docs/react/components/segmented-control.mdx index 491b1d6db..e91db32ad 100644 --- a/docs/content/docs/react/components/segmented-control.mdx +++ b/docs/content/docs/react/components/segmented-control.mdx @@ -27,7 +27,7 @@ title: Segmented Control ## 예제 @@ -46,7 +46,7 @@ Pill 형태의 `Segment` 한 개는 86px의 최소 너비를 가져요. 제공 ### Fixed Width -`SegmentControl`의 `style` prop에 `width`를 제공해서 직접 너비를 설정할 수 있어요. +`SegmentedControl`의 `style` prop에 `width`를 제공해서 직접 너비를 설정할 수 있어요. diff --git a/docs/package.json b/docs/package.json index 1a5bcc488..a3911d141 100644 --- a/docs/package.json +++ b/docs/package.json @@ -34,7 +34,7 @@ "@seed-design/react": "0.0.0", "@seed-design/react-icon": "^0.7.3", "@seed-design/react-popover": "0.0.0-alpha-20241030023710", - "@seed-design/react-segmented-control": "workspace:^", + "@seed-design/react-segmented-control": "0.0.0", "@seed-design/react-tabs": "0.0.0-alpha-20241209060641", "@seed-design/recipe": "0.0.0-alpha-20241212122822", "@seed-design/rootage-cli": "0.0.0", diff --git a/docs/registry/ui/segmented-control.tsx b/docs/registry/ui/segmented-control.tsx index 6e5e8a61d..24367d19c 100644 --- a/docs/registry/ui/segmented-control.tsx +++ b/docs/registry/ui/segmented-control.tsx @@ -1,101 +1,58 @@ "use client"; import "@seed-design/stylesheet/segmentedControl.css"; -import { - useSegmentedControl, - type SegmentItemProps, - type UseSegmentedControlProps, -} from "@seed-design/react-segmented-control"; -import * as React from "react"; -import clsx from "clsx"; -import { - segmentedControl, - type SegmentedControlVariantProps, -} from "@seed-design/recipe/segmentedControl"; -import type { Assign } from "../util/types"; -import { visuallyHidden } from "../util/visuallyHidden"; - -const SegmentedControlContext = React.createContext<{ - api: ReturnType; -} | null>(null); - -const useSegmentedControlContext = () => { - const context = React.useContext(SegmentedControlContext); - if (!context) - throw new Error("Segment cannot be rendered outside the SegmentedControl"); - return context; -}; +import { SegmentedControl as SeedSegmentedControl } from "@seed-design/react"; +import * as React from "react"; -export type SegmentedControlProps = Assign< - React.HTMLAttributes, - UseSegmentedControlProps -> & - SegmentedControlVariantProps & {}; +export interface SegmentedControlProps extends SeedSegmentedControl.RootProps {} export const SegmentedControl = React.forwardRef< HTMLDivElement, SegmentedControlProps ->(({ className, children, ...otherProps }, ref) => { - const api = useSegmentedControl(otherProps); - - const { rootProps, restProps, indicatorProps } = api; - - const classNames = segmentedControl(); +>(({ children, ...otherProps }, ref) => { + if (!otherProps["aria-label"] && !otherProps["aria-labelledby"]) { + console.warn( + "SegmentedControl component requires either an `aria-label` or `aria-labelledby` attribute.", + ); + } - const [mounted, setMounted] = React.useState(false); - React.useEffect(() => setMounted(true), []); + if (otherProps.value === undefined && otherProps.defaultValue === undefined) { + console.warn( + "SegmentedControl component requires either a `value` or `defaultValue` attribute.", + ); + } return ( -
- {/* TODO */} - {/*
*/} - - {children} - -
-
+ + {children} + + ); }); SegmentedControl.displayName = "SegmentedControl"; -export interface SegmentProps - extends Assign, SegmentItemProps> {} +export interface SegmentedControlSegmentProps + extends SeedSegmentedControl.SegmentProps { + inputProps?: React.InputHTMLAttributes; -export const Segment = React.forwardRef( - ({ className, children, value, ...otherProps }, ref) => { - const { - api: { getSegmentProps }, - } = useSegmentedControlContext(); + rootRef?: React.Ref; +} - const { rootProps, hiddenInputProps, stateProps } = getSegmentProps({ - value, - }); - const classNames = segmentedControl(); - - return ( - - ); - }, -); - -Segment.displayName = "Segment"; +export const SegmentedControlSegment = React.forwardRef< + HTMLInputElement, + SegmentedControlSegmentProps +>(({ children, inputProps, rootRef, ...otherProps }, ref) => { + return ( + + + + {children} + + + {children} + + + ); +}); +SegmentedControlSegment.displayName = "SegmentedControlSegment"; diff --git a/docs/stories/SegmentedControl.stories.tsx b/docs/stories/SegmentedControl.stories.tsx index 65bc4fefd..cf5f16f09 100644 --- a/docs/stories/SegmentedControl.stories.tsx +++ b/docs/stories/SegmentedControl.stories.tsx @@ -1,22 +1,22 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { SegmentedControl, Segment } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlSegment } 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 Component = ({ disabled }: { disabled: boolean }) => { const values = ["dolor", "magna", "sint"]; const [value, setValue] = useState(values[0]); return ( - + {values.map((value) => ( - + {value} - + ))} ); @@ -31,12 +31,23 @@ export default meta; type Story = StoryObj; -const CommonStoryTemplate: Story = { - args: { - defaultValue: "1", +const conditionMap = { + disabled: { + false: { disabled: false }, + true: { disabled: true }, }, +}; + +const CommonStoryTemplate: Story = { render: function Render(args) { - return ; + return ( + + ); }, }; diff --git a/packages/qvism-preset/src/recipes/segmented-control.ts b/packages/qvism-preset/src/recipes/segmented-control.ts index 095e67f22..271db99cc 100644 --- a/packages/qvism-preset/src/recipes/segmented-control.ts +++ b/packages/qvism-preset/src/recipes/segmented-control.ts @@ -8,40 +8,31 @@ const segmentedControl = defineRecipe({ base: { root: { display: "grid", - + boxSizing: "border-box", minWidth: "fit-content", maxWidth: "100%", - padding: vars.base.enabled.root.padding, - position: "relative", + padding: vars.base.enabled.root.padding, + borderRadius: vars.base.enabled.root.cornerRadius, backgroundColor: vars.base.enabled.root.color, - gridTemplateColumns: "repeat(var(--seed-design-segmented-control-count, 0), 1fr)", + gridAutoFlow: "column", + gridAutoColumns: "1fr", - // XXX: css reset 생기면 제거 - boxSizing: "border-box", + alignItems: "center", + + isolation: "isolate", }, segment: { - // XXX: css reset 생기면 제거 - border: "none", - padding: 0, - backgroundColor: "transparent", - font: "inherit", - [pseudo(not(disabled))]: { - cursor: "pointer", - }, - - position: "relative", + display: "grid", minWidth: vars.base.enabled.segment.minWidth, height: vars.base.enabled.segment.height, - zIndex: 10, - borderRadius: vars.base.enabled.segment.cornerRadius, overflow: "hidden", @@ -50,6 +41,10 @@ const segmentedControl = defineRecipe({ lineHeight: vars.base.enabled.segment.lineHeight, + [pseudo(not(disabled))]: { + cursor: "pointer", + }, + [pseudo(not(disabled), active)]: { backgroundColor: vars.base.enabledPressed.segment.color, }, @@ -59,12 +54,10 @@ const segmentedControl = defineRecipe({ }, }, segmentLabel: { - position: "absolute", - insetInline: 0, - transform: "translateY(-50%)", - insetBlockStart: "50%", + gridArea: "1 / 1 / 1 / 1", paddingInline: `calc(${vars.base.enabled.segment.paddingX} - 1px)`, + marginBlock: "auto", textAlign: "center", fontWeight: vars.base.enabled.segment.fontWeight, @@ -88,6 +81,8 @@ const segmentedControl = defineRecipe({ }, }, segmentLabelPlaceholder: { + gridArea: "1 / 1 / 1 / 1", + paddingInline: vars.base.enabled.segment.paddingX, textAlign: "center", @@ -103,6 +98,13 @@ const segmentedControl = defineRecipe({ indicator: { position: "absolute", insetBlock: vars.base.enabled.root.padding, + insetInlineStart: vars.base.enabled.root.padding, + + width: `calc((100% - ${vars.base.enabled.root.padding} * 2) / var(--seed-design-segmented-control-segment-count))`, + transform: + "translateX(calc(var(--seed-design-segmented-control-current-segment-index) * 100%))", + + zIndex: -1, borderRadius: vars.base.enabled.indicator.cornerRadius, diff --git a/packages/react-headless/segmented-control/package.json b/packages/react-headless/segmented-control/package.json index ff9dbcdf7..44b1cef92 100644 --- a/packages/react-headless/segmented-control/package.json +++ b/packages/react-headless/segmented-control/package.json @@ -27,7 +27,6 @@ }, "dependencies": { "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-size": "^1.1.0", "@seed-design/dom-utils": "0.0.0-alpha-20241030023710" }, "devDependencies": { diff --git a/packages/react-headless/segmented-control/src/SegmentedControl.namespace.ts b/packages/react-headless/segmented-control/src/SegmentedControl.namespace.ts new file mode 100644 index 000000000..61a0e3c4d --- /dev/null +++ b/packages/react-headless/segmented-control/src/SegmentedControl.namespace.ts @@ -0,0 +1,8 @@ +export { + SegmentedControlRoot as Root, + SegmentedControlSegment as Segment, + SegmentedControlSegmentHiddenInput as SegmentHiddenInput, + type SegmentedControlRootProps as RootProps, + type SegmentedControlSegmentProps as SegmentProps, + type SegmentedControlSegmentHiddenInputProps as SegmentHiddenInputProps, +} from "./SegmentedControl"; diff --git a/packages/react-headless/segmented-control/src/SegmentedControl.tsx b/packages/react-headless/segmented-control/src/SegmentedControl.tsx new file mode 100644 index 000000000..c552fd895 --- /dev/null +++ b/packages/react-headless/segmented-control/src/SegmentedControl.tsx @@ -0,0 +1,78 @@ +import { mergeProps } from "@seed-design/dom-utils"; +import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive"; +import * as React from "react"; +import { + useSegmentedControl, + type SegmentProps, + type UseSegmentedControlProps, +} from "./useSegmentedControl"; +import { SegmentedControlProvider, useSegmentedControlContext } from "./useSegmentedControlContext"; +import { + SegmentedControlSegmentProvider, + useSegmentedControlSegmentContext, +} from "./useSegmentedControlSegmentContext"; + +export interface SegmentedControlRootProps + extends UseSegmentedControlProps, + PrimitiveProps, + Omit, "defaultValue"> {} + +export const SegmentedControlRoot = React.forwardRef( + (props, ref) => { + const { value, defaultValue, onValueChange, form, name, disabled, ...otherProps } = props; + + const api = useSegmentedControl({ + value, + defaultValue, + onValueChange, + disabled, + form, + name, + }); + const mergedProps = mergeProps(api.rootProps, otherProps); + + return ( + + + + ); + }, +); +SegmentedControlRoot.displayName = "SegmentedControl"; + +export interface SegmentedControlSegmentProps + extends SegmentProps, + PrimitiveProps, + Omit, "value"> {} + +export const SegmentedControlSegment = React.forwardRef< + HTMLLabelElement, + SegmentedControlSegmentProps +>((props, ref) => { + const { value, invalid, disabled, ...otherProps } = props; + const { getSegmentProps } = useSegmentedControlContext(); + const itemProps = getSegmentProps({ value, disabled, invalid }); + const mergedProps = mergeProps(itemProps.rootProps, otherProps); + + return ( + + + + ); +}); +SegmentedControlSegment.displayName = "SegmentedControlSegment"; + +export interface SegmentedControlSegmentHiddenInputProps + extends PrimitiveProps, + React.HTMLAttributes {} + +export const SegmentedControlSegmentHiddenInput = React.forwardRef< + HTMLInputElement, + SegmentedControlSegmentHiddenInputProps +>((props, ref) => { + const { hiddenInputProps } = useSegmentedControlSegmentContext(); + const mergedProps = mergeProps(hiddenInputProps, props); + + return ; +}); +SegmentedControlSegmentHiddenInput.displayName = "SegmentedControlSegmentHiddenInput"; diff --git a/packages/react-headless/segmented-control/src/dom.ts b/packages/react-headless/segmented-control/src/dom.ts index 1a12be272..700157113 100644 --- a/packages/react-headless/segmented-control/src/dom.ts +++ b/packages/react-headless/segmented-control/src/dom.ts @@ -3,12 +3,6 @@ export const getRootId = (id: string) => `segmented-control:${id}:root`; -export const getLabelId = (id: string) => `segmented-control:${id}:label`; - -export const getSegmentRootId = (value: string, id: string) => - `segmented-control:${value}:${id}:segment-root`; -export const getIndicatorId = (id: string) => `segmented-control:${id}:indicator`; - /* Element ----------------------------------------------------------------------- -------------------------------------------------------------------------------- */ @@ -16,9 +10,6 @@ const isClient = typeof window === "object"; export const getRootEl = (id: string) => (isClient ? document.getElementById(getRootId(id)) : null); -export const getSegmentRootEl = (value: string, id: string) => - isClient ? document.getElementById(getSegmentRootId(value, id)) : null; - /* Utils ----------------------------------------------------------------------- -------------------------------------------------------------------------------- */ @@ -29,5 +20,11 @@ export const getAllValues = (id: string) => { return Array.from(el.children) .map((child) => child.getAttribute("data-value")) - .filter(Boolean); + .filter(Boolean) as string[]; +}; + +export const getSegmentIndex = (value: string, id: string) => { + const values = getAllValues(id); + + return values.indexOf(value); }; diff --git a/packages/react-headless/segmented-control/src/index.ts b/packages/react-headless/segmented-control/src/index.ts index 7db10d7f2..4d162da91 100644 --- a/packages/react-headless/segmented-control/src/index.ts +++ b/packages/react-headless/segmented-control/src/index.ts @@ -1,265 +1,20 @@ -import { useControllableState } from "@radix-ui/react-use-controllable-state"; -import { useId, useState } from "react"; - -import { dataAttr, elementProps, inputProps } from "@seed-design/dom-utils"; - -import * as React from "react"; -import * as dom from "./dom"; - -import { useSize } from "@radix-ui/react-use-size"; - -const useLayoutEffect = globalThis?.document ? React.useLayoutEffect : React.useEffect; - -type AtLeastOne }> = Partial & U[keyof U]; - -type UseSegmentedControlStateProps = { - onValueChange?: (value: string) => void; -} & AtLeastOne<{ value: string; defaultValue: string }>; - -function useSegmentedControlState(props: UseSegmentedControlStateProps & { id: string }) { - const [value, setValue] = useControllableState({ - prop: props.value, - defaultProp: props.defaultValue, - onChange: props.onValueChange, - }); - const [hoveredValue, setHoveredValue] = useState(null); - const [activeValue, setActiveValue] = useState(null); - const [focusedValue, setFocusedValue] = useState(null); - const [isFocusVisible, setIsFocusVisible] = useState(false); - - const [rootEl, setRootEl] = React.useState(null); - - const triggerEl = dom.getSegmentRootEl(value, props.id); - const triggerSize = useSize(triggerEl); - - const segmentedControlValues = dom.getAllValues(props.id); - - useLayoutEffect(() => { - setRootEl(dom.getRootEl(props.id)); - }, [props.id]); - - return { - value, - setValue, - rootEl, - triggerSize: { - width: triggerSize?.width || 0, - height: triggerSize?.height || 0, - left: triggerEl?.offsetLeft || 0, - }, - hoveredValue, - setHoveredValue, - activeValue, - setActiveValue, - focusedValue, - setFocusedValue, - isFocusVisible, - setIsFocusVisible, - segmentedControlValues, - }; -} - -export type UseSegmentedControlProps = UseSegmentedControlStateProps & { - disabled?: boolean; - - name?: string; - - form?: string; -}; - -export interface SegmentItemProps { - value: string; -} - -export function useSegmentedControl(props: UseSegmentedControlProps) { - const id = useId(); - const { - value, - setValue, - hoveredValue, - setHoveredValue, - activeValue, - setActiveValue, - focusedValue, - setFocusedValue, - isFocusVisible, - setIsFocusVisible, - rootEl, - triggerSize, - segmentedControlValues, - } = useSegmentedControlState({ ...props, id }); - - const { - disabled, - form, - name, - value: omitValue, - defaultValue: omitDefaultValue, - onValueChange: omitOnValueChange, - ...restProps - } = props; - - const updateIndicatorStyle = React.useCallback(() => { - if (rootEl) { - rootEl.style.setProperty( - "--seed-design-segmented-control-indicator-left", - `${triggerSize.left}px`, - ); - rootEl.style.setProperty( - "--seed-design-segmented-control-indicator-width", - `${triggerSize.width}px`, - ); - } - }, [triggerSize, rootEl]); - - const updateSegmentCount = React.useCallback(() => { - if (rootEl) { - rootEl.style.setProperty( - "--seed-design-segmented-control-count", - `${segmentedControlValues.length}`, - ); - } - }, [segmentedControlValues.length, rootEl]); - - useLayoutEffect(() => { - updateIndicatorStyle(); - - window.addEventListener("resize", updateIndicatorStyle); - return () => { - window.removeEventListener("resize", updateIndicatorStyle); - }; - }, [updateIndicatorStyle]); - - useLayoutEffect(() => { - updateSegmentCount(); - }, [updateSegmentCount]); - - const stateProps = elementProps({ - "data-disabled": dataAttr(disabled), - }); - - return { - value, - setValue, - - stateProps, - restProps, - - rootProps: elementProps({ - id: dom.getRootId(id), - role: "radiogroup", - // "aria-labelledby": dom.getLabelId(id), - ...stateProps, - }), - - // labelProps: elementProps({ - // id: dom.getLabelId(id), - // }), - - indicatorProps: elementProps({ - id: dom.getIndicatorId(id), - }), - - getSegmentProps(segmentItemProps: SegmentItemProps) { - const { value: segmentItemValue, ...segmentItemRestProps } = segmentItemProps; - - const segmentState = { - isDisabled: !!disabled, - isChecked: value === segmentItemValue, - isFocused: focusedValue === segmentItemValue, - isHovered: hoveredValue === segmentItemValue, - isActive: activeValue === segmentItemValue, - }; - - const segmentItemStateProps = { - "data-focus": dataAttr(segmentState.isFocused), - "data-focus-visible": dataAttr(segmentState.isFocused && isFocusVisible), - "data-disabled": dataAttr(segmentState.isDisabled), - "data-checked": dataAttr(segmentState.isChecked), - "data-active": dataAttr(segmentState.isActive), - "data-hover": dataAttr(segmentState.isHovered), - "data-value": segmentItemValue, - }; - - return { - segmentState, - - setFocusedValue, - setIsFocusVisible, - - stateProps: segmentItemStateProps, - restProps: segmentItemRestProps, - - rootProps: elementProps({ - ...segmentItemStateProps, - id: dom.getSegmentRootId(segmentItemValue, id), - onPointerMove() { - if (segmentState.isDisabled) return; - setHoveredValue(segmentItemValue); - }, - onPointerLeave() { - if (segmentState.isDisabled) return; - setHoveredValue(null); - setActiveValue(null); - }, - onPointerDown(event) { - if (segmentState.isDisabled) return; - // On pointerdown, the input blurs and returns focus to the `body`, - // we need to prevent this. - if (segmentState.isFocused && event.pointerType === "mouse") { - event.preventDefault(); - } - setActiveValue(segmentItemValue); - }, - onPointerUp() { - if (segmentState.isDisabled) return; - setActiveValue(null); - }, - onFocus(event) { - setFocusedValue(segmentItemValue); - setIsFocusVisible(event.target.matches(":focus-visible")); - }, - onBlur() { - setFocusedValue(null); - setIsFocusVisible(false); - }, - }), - - hiddenInputProps: inputProps({ - type: "radio", - name: name || id, - form, - value: segmentItemValue, - onChange(event) { - if (segmentState.isDisabled) return; - - if (event.target.checked) { - setValue(segmentItemValue); - } - setIsFocusVisible(event.target.matches(":focus-visible")); - }, - onBlur() { - setFocusedValue(null); - setIsFocusVisible(false); - }, - onFocus(event) { - setFocusedValue(segmentItemValue); - setIsFocusVisible(event.target.matches(":focus-visible")); - }, - onKeyDown(event) { - if (event.key === " ") { - setActiveValue(segmentItemValue); - } - }, - onKeyUp(event) { - if (event.key === " ") { - setActiveValue(null); - } - }, - disabled: segmentState.isDisabled, - defaultChecked: segmentState.isChecked, - }), - }; - }, - }; -} +export { + SegmentedControlRoot, + SegmentedControlSegment, + SegmentedControlSegmentHiddenInput, + type SegmentedControlRootProps, + type SegmentedControlSegmentProps, + type SegmentedControlSegmentHiddenInputProps, +} from "./SegmentedControl"; + +export { + useSegmentedControlContext, + type UseSegmentedControlContext, +} from "./useSegmentedControlContext"; + +export { + useSegmentedControlSegmentContext, + type UseSegmentedControlSegmentContext, +} from "./useSegmentedControlSegmentContext"; + +export * as SegmentedControl from "./SegmentedControl.namespace"; diff --git a/packages/react-headless/segmented-control/src/useSegmentedControl.test.tsx b/packages/react-headless/segmented-control/src/useSegmentedControl.test.tsx index ab6883676..cb026e925 100644 --- a/packages/react-headless/segmented-control/src/useSegmentedControl.test.tsx +++ b/packages/react-headless/segmented-control/src/useSegmentedControl.test.tsx @@ -6,17 +6,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { ReactElement } from "react"; import * as React from "react"; -import { useSegmentedControl, type SegmentItemProps, type UseSegmentedControlProps } from "./index"; - -/** - * @see https://github.com/ZeeCoder/use-resize-observer/issues/40#issuecomment-644536259 - * useSize에서 사용하는 ResizeObserver를 mock으로 대체합니다. - */ -class ResizeObserver { - observe() {} - unobserve() {} - disconnect() {} -} +import { + SegmentedControlRoot as Root, + SegmentedControlSegment as Segment, + SegmentedControlSegmentHiddenInput as HiddenInput, + type SegmentedControlRootProps, + type SegmentedControlSegmentProps, +} from "./SegmentedControl"; afterEach(cleanup); @@ -27,133 +23,69 @@ function setUp(jsx: ReactElement) { }; } -const SegmentedControlContext = React.createContext<{ - api: ReturnType; -} | null>(null); - -const useSegmentedControlContext = () => { - const context = React.useContext(SegmentedControlContext); - if (!context) throw new Error("Segment cannot be rendered outside the SegmentedControl"); - - return context; -}; - -type Assign = Omit & U; - -function SegmentedControl({ - children, - ...otherProps -}: Assign, UseSegmentedControlProps>) { - const api = useSegmentedControl(otherProps); - - const { rootProps, restProps, indicatorProps } = api; - +function SegmentedControl({ children, ...otherProps }: SegmentedControlRootProps) { return ( -
- - {children} - -
-
+ + {children} +
+ ); } -function Segment({ - children, - value, - ...otherProps -}: Assign, SegmentItemProps>) { - const { - api: { getSegmentProps }, - } = useSegmentedControlContext(); - - const { rootProps, hiddenInputProps, stateProps } = getSegmentProps({ value }); - +function SegmentedControlSegment({ children, ...otherProps }: SegmentedControlSegmentProps) { return ( - + + + {children} + ); } -function ControlledSegmentedControl( - props: React.PropsWithChildren>, -) { - const { defaultValue } = props; - const [value, setValue] = React.useState(defaultValue); - const mockSetValue = vi.fn((value) => setValue(value)); - - return ; -} - describe("useSegmentedControl", () => { - window.ResizeObserver = ResizeObserver; - - const FIRST_VALUE = "first"; - const SECOND_VALUE = "second"; - const THIRD_VALUE = "third"; - const values = [FIRST_VALUE, SECOND_VALUE, THIRD_VALUE]; - - it("should render correctly", () => { - const { getByTestId } = setUp( - - {values.map((value) => ( - - ))} - , - ); - - for (const value of values) { - const input = getByTestId(value); - expect(input).toBeInTheDocument(); - } - }); - - it("should change value on click", async () => { - const { user, getByTestId } = setUp( - - {values.map((value) => ( - - ))} - , - ); - - const firstControl = getByTestId(FIRST_VALUE); - const secondControl = getByTestId(SECOND_VALUE); + const values = ["first", "second", "third"]; - await user.click(secondControl); - - expect(firstControl).not.toHaveAttribute("checked"); - expect(secondControl).toHaveAttribute("checked"); - }); + describe("uncontrolled", () => { + it("should render correctly", () => { + const { getByTestId } = setUp( + + {values.map((value) => ( + + ))} + , + ); - it("should onValueChange be called", async () => { - const handleValueChange = vi.fn(); + for (const value of values) { + const input = getByTestId(value); + expect(input).toBeInTheDocument(); + } + }); - const { user, getByTestId } = setUp( - - {values.map((value) => ( - - ))} - , - ); + it("should change value on click", async () => { + const { user, getByTestId } = setUp( + + {values.map((value) => ( + + ))} + , + ); - const secondControl = getByTestId(SECOND_VALUE); + const firstControl = getByTestId(values[0]); + const secondControl = getByTestId(values[1]); - await user.click(secondControl); + expect(firstControl).toBeChecked(); + expect(secondControl).not.toBeChecked(); - expect(handleValueChange).toHaveBeenCalledWith(SECOND_VALUE); - }); + await user.click(secondControl); - describe("disabled prop test", () => { - window.ResizeObserver = ResizeObserver; + expect(firstControl).not.toBeChecked(); + expect(secondControl).toBeChecked(); + }); it("should disabled when disabled prop is true", async () => { const { getByTestId } = setUp( - + {values.map((value) => ( - + ))} , ); @@ -166,73 +98,107 @@ describe("useSegmentedControl", () => { it("should not change value on click when disabled", async () => { const { user, getByTestId } = setUp( - + {values.map((value) => ( - + ))} , ); - const firstControl = getByTestId(FIRST_VALUE); - const secondControl = getByTestId(SECOND_VALUE); + const firstControl = getByTestId(values[0]); + const secondControl = getByTestId(values[1]); + + expect(firstControl).toBeChecked(); + expect(secondControl).not.toBeChecked(); await user.click(secondControl); - expect(firstControl).toHaveAttribute("checked"); - expect(secondControl).not.toHaveAttribute("checked"); + expect(firstControl).toBeChecked(); + expect(secondControl).not.toBeChecked(); }); + }); - it("should not call onValueChange when disabled", async () => { + describe("controlled test", () => { + it("should render correctly with controlled value", () => { + const ControlledSegmentedControl = () => { + const [value, setValue] = React.useState(values[2]); + + return ( + + {values.map((value) => ( + + ))} + + ); + }; + + const { getByTestId } = setUp(); + + const thirdControl = getByTestId(values[2]); + expect(thirdControl).toBeChecked(); + }); + + it("should onValueChange be called", async () => { const handleValueChange = vi.fn(); const { user, getByTestId } = setUp( - + {values.map((value) => ( - + ))} , ); - const secondControl = getByTestId(SECOND_VALUE); + const secondControl = getByTestId(values[1]); await user.click(secondControl); - expect(handleValueChange).not.toHaveBeenCalled(); + expect(handleValueChange).toHaveBeenCalledWith(values[1]); }); - }); - describe("controlled test", () => { - window.ResizeObserver = ResizeObserver; + it("should change value on click with controlled value", async () => { + const ControlledSegmentedControl = () => { + const [value, setValue] = React.useState(values[1]); - it("should render correctly with controlled value", () => { - const { getByTestId } = setUp( - - {values.map((value) => ( - - ))} - , - ); + return ( + + {values.map((value) => ( + + ))} + + ); + }; + + const { user, getByTestId } = setUp(); + + const secondControl = getByTestId(values[1]); + const thirdControl = getByTestId(values[2]); + + expect(secondControl).toBeChecked(); + expect(thirdControl).not.toBeChecked(); - const secondControl = getByTestId(SECOND_VALUE); - expect(secondControl).toHaveAttribute("checked"); + await user.click(thirdControl); + + expect(secondControl).not.toBeChecked(); + expect(thirdControl).toBeChecked(); }); - it("should change value on click with controlled value", async () => { + it("should not call onValueChange when disabled", async () => { + const handleValueChange = vi.fn(); + const { user, getByTestId } = setUp( - + {values.map((value) => ( - + ))} - , + , ); - const firstControl = getByTestId(FIRST_VALUE); - const secondControl = getByTestId(SECOND_VALUE); + const secondControl = getByTestId(values[1]); await user.click(secondControl); - expect(firstControl).not.toHaveAttribute("checked"); - expect(secondControl).toHaveAttribute("checked"); + expect(handleValueChange).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/react-headless/segmented-control/src/useSegmentedControl.ts b/packages/react-headless/segmented-control/src/useSegmentedControl.ts new file mode 100644 index 000000000..f395fe1a3 --- /dev/null +++ b/packages/react-headless/segmented-control/src/useSegmentedControl.ts @@ -0,0 +1,228 @@ +import { useControllableState } from "@radix-ui/react-use-controllable-state"; +import { useCallback, useId, useLayoutEffect, useState } from "react"; + +import { dataAttr, elementProps, inputProps, visuallyHidden } from "@seed-design/dom-utils"; +import * as dom from "./dom"; + +interface UseSegmentedControlStateProps { + value?: string; + + defaultValue?: string; + + onValueChange?: (value: string) => void; +} + +function useSegmentedControlState(props: UseSegmentedControlStateProps & { id: string }) { + const [value, setValue] = useControllableState({ + prop: props.value, + defaultProp: props.defaultValue, + onChange: props.onValueChange, + }); + + const [hoveredValue, setHoveredValue] = useState(null); + const [activeValue, setActiveValue] = useState(null); + const [focusedValue, setFocusedValue] = useState(null); + const [isFocusVisible, setIsFocusVisible] = useState(false); + + return { + value, + setValue, + hoveredValue, + setHoveredValue, + activeValue, + setActiveValue, + focusedValue, + setFocusedValue, + isFocusVisible, + setIsFocusVisible, + }; +} + +export interface UseSegmentedControlProps extends UseSegmentedControlStateProps { + disabled?: boolean; + + name?: string; + + form?: string; +} + +export interface SegmentProps { + value: string; + + disabled?: boolean; + + invalid?: boolean; +} + +export type UseSegmentedControlReturn = ReturnType; + +export type GetSegmentPropsReturn = ReturnType; + +export function useSegmentedControl(props: UseSegmentedControlProps) { + const id = useId(); + + const { + value, + setValue, + hoveredValue, + setHoveredValue, + activeValue, + setActiveValue, + focusedValue, + setFocusedValue, + isFocusVisible, + setIsFocusVisible, + } = useSegmentedControlState({ ...props, id }); + + const { disabled, form, name } = props; + + const isControlled = props.value !== undefined; + + const stateProps = elementProps({ + "data-disabled": dataAttr(disabled), + }); + + const [rootEl, setRootEl] = useState(null); + + const segmentCount = dom.getAllValues(id).length; + const currentSegmentIndex = value ? dom.getSegmentIndex(value, id) : -1; + + useLayoutEffect(() => { + setRootEl(dom.getRootEl(id)); + }, [id]); + + const updateCurrentIndex = useCallback(() => { + if (rootEl) { + rootEl.style.setProperty( + "--seed-design-segmented-control-current-segment-index", + `${currentSegmentIndex}`, + ); + } + }, [currentSegmentIndex, rootEl]); + + const updateSegmentCount = useCallback(() => { + if (rootEl) { + rootEl.style.setProperty("--seed-design-segmented-control-segment-count", `${segmentCount}`); + } + }, [segmentCount, rootEl]); + + useLayoutEffect(() => { + updateCurrentIndex(); + }, [updateCurrentIndex]); + + useLayoutEffect(() => { + updateSegmentCount(); + }, [updateSegmentCount]); + + return { + value, + setValue, + + stateProps, + + rootProps: elementProps({ + id: dom.getRootId(id), + role: "radiogroup", + ...stateProps, + }), + + getSegmentProps(segmentProps: SegmentProps) { + const { value: itemValue, disabled: itemDisabled, invalid: itemInvalid } = segmentProps; + + const itemState = { + invalid: !!itemInvalid, + disabled: !!itemDisabled || disabled, + checked: value === itemValue, + focused: focusedValue === itemValue, + hovered: hoveredValue === itemValue, + active: activeValue === itemValue, + }; + + const itemStateProps = elementProps({ + "data-focus": dataAttr(itemState.focused), + "data-focus-visible": dataAttr(itemState.focused && isFocusVisible), + "data-disabled": dataAttr(itemState.disabled), + "data-checked": dataAttr(itemState.checked), + "data-active": dataAttr(itemState.active), + "data-hover": dataAttr(itemState.hovered), + "data-invalid": dataAttr(itemState.invalid), + "data-value": itemValue, + }); + + return { + ...itemState, + + setFocusedValue, + setIsFocusVisible, + + stateProps: itemStateProps, + + rootProps: elementProps({ + ...itemStateProps, + + onPointerMove() { + if (itemState.disabled) return; + setHoveredValue(segmentProps.value); + }, + onPointerLeave() { + if (itemState.disabled) return; + setHoveredValue(null); + setActiveValue(null); + }, + onPointerDown(event) { + if (itemState.disabled) return; + // On pointerdown, the input blurs and returns focus to the `body`, + // we need to prevent this. + if (itemState.focused && event.pointerType === "mouse") { + event.preventDefault(); + } + setActiveValue(segmentProps.value); + }, + onPointerUp() { + if (itemState.disabled) return; + setActiveValue(null); + }, + }), + + hiddenInputProps: inputProps({ + type: "radio", + name: name || id, + form, + + value: segmentProps.value, + + onChange(event) { + if (itemState.disabled) return; + + if (event.target.checked) { + setValue(segmentProps.value); + } + setIsFocusVisible(event.target.matches(":focus-visible")); + }, + onBlur() { + setFocusedValue(null); + setIsFocusVisible(false); + }, + onFocus(event) { + setFocusedValue(segmentProps.value); + setIsFocusVisible(event.target.matches(":focus-visible")); + }, + onKeyDown(event) { + if (event.key === " ") { + setActiveValue(segmentProps.value); + } + }, + onKeyUp(event) { + if (event.key === " ") { + setActiveValue(null); + } + }, + disabled: itemState.disabled, + ...(isControlled && { checked: itemState.checked }), + ...(!isControlled && { defaultChecked: itemState.checked }), + style: visuallyHidden, + }), + }; + }, + }; +} diff --git a/packages/react-headless/segmented-control/src/useSegmentedControlContext.tsx b/packages/react-headless/segmented-control/src/useSegmentedControlContext.tsx new file mode 100644 index 000000000..b93e6574e --- /dev/null +++ b/packages/react-headless/segmented-control/src/useSegmentedControlContext.tsx @@ -0,0 +1,21 @@ +import { createContext, useContext } from "react"; +import type { UseSegmentedControlReturn } from "./useSegmentedControl"; + +export interface UseSegmentedControlContext extends UseSegmentedControlReturn {} + +const SegmentedControlContext = createContext(null); + +export const SegmentedControlProvider = SegmentedControlContext.Provider; + +export function useSegmentedControlContext({ + strict = true, +}: { strict?: T } = {}): T extends false + ? UseSegmentedControlContext | null + : UseSegmentedControlContext { + const context = useContext(SegmentedControlContext); + if (!context && strict) { + throw new Error("useSegmentedControlContext must be used within a SegmentedControl"); + } + + return context as UseSegmentedControlContext; +} diff --git a/packages/react-headless/segmented-control/src/useSegmentedControlSegmentContext.tsx b/packages/react-headless/segmented-control/src/useSegmentedControlSegmentContext.tsx new file mode 100644 index 000000000..b62608c00 --- /dev/null +++ b/packages/react-headless/segmented-control/src/useSegmentedControlSegmentContext.tsx @@ -0,0 +1,25 @@ +import { createContext, useContext } from "react"; +import type { GetSegmentPropsReturn } from "./useSegmentedControl"; + +export interface UseSegmentedControlSegmentContext extends GetSegmentPropsReturn {} + +const SegmentedControlSegmentContext = createContext( + null, +); + +export const SegmentedControlSegmentProvider = SegmentedControlSegmentContext.Provider; + +export function useSegmentedControlSegmentContext({ + strict = true, +}: { strict?: T } = {}): T extends false + ? UseSegmentedControlSegmentContext | null + : UseSegmentedControlSegmentContext { + const context = useContext(SegmentedControlSegmentContext); + if (!context && strict) { + throw new Error( + "useSegmentedControlSegmentContext must be used within a SegmentedControlSegment", + ); + } + + return context as UseSegmentedControlSegmentContext; +} diff --git a/packages/react-headless/segmented-control/tsconfig.json b/packages/react-headless/segmented-control/tsconfig.json index 00abb5428..59fbc0a08 100644 --- a/packages/react-headless/segmented-control/tsconfig.json +++ b/packages/react-headless/segmented-control/tsconfig.json @@ -2,9 +2,19 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", + "moduleDetection": "force", "moduleResolution": "Bundler", + "verbatimModuleSyntax": true, + + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "rootDir": "src", "outDir": "lib", - "jsx": "react" + "jsx": "react-jsx" } } diff --git a/packages/react/src/components/SegmentedControl/SegmentedControl.namespace.ts b/packages/react/src/components/SegmentedControl/SegmentedControl.namespace.ts new file mode 100644 index 000000000..b3d1f8552 --- /dev/null +++ b/packages/react/src/components/SegmentedControl/SegmentedControl.namespace.ts @@ -0,0 +1,14 @@ +export { + SegmentedControlIndicator as Indicator, + SegmentedControlRoot as Root, + SegmentedControlSegment as Segment, + SegmentedControlSegmentHiddenInput as SegmentHiddenInput, + SegmentedControlSegmentLabel as SegmentLabel, + SegmentedControlSegmentLabelPlaceholder as SegmentLabelPlaceholder, + type SegmentedControlIndicatorProps as IndicatorProps, + type SegmentedControlRootProps as RootProps, + type SegmentedControlSegmentProps as SegmentProps, + type SegmentedControlSegmentHiddenInputProps as SegmentHiddenInputProps, + type SegmentedControlSegmentLabelProps as SegmentLabelProps, + type SegmentedControlSegmentLabelPlaceholderProps as SegmentLabelPlaceholderProps, +} from "./SegmentedControl"; diff --git a/packages/react/src/components/SegmentedControl/SegmentedControl.tsx b/packages/react/src/components/SegmentedControl/SegmentedControl.tsx new file mode 100644 index 000000000..f81188cf1 --- /dev/null +++ b/packages/react/src/components/SegmentedControl/SegmentedControl.tsx @@ -0,0 +1,62 @@ +import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive"; +import { + SegmentedControl as SegmentedControlPrimitive, + useSegmentedControlSegmentContext, +} from "@seed-design/react-segmented-control"; +import { + segmentedControl, + type SegmentedControlVariantProps, +} from "@seed-design/recipe/segmentedControl"; +import { createStyleContext } from "../../utils/createStyleContext"; +import { createWithStateProps } from "../../utils/createWithStateProps"; + +const { withProvider, withContext } = createStyleContext(segmentedControl); +const withStateProps = createWithStateProps([useSegmentedControlSegmentContext]); + +export interface SegmentedControlRootProps + extends SegmentedControlVariantProps, + SegmentedControlPrimitive.RootProps {} + +export const SegmentedControlRoot = withProvider( + SegmentedControlPrimitive.Root, + "root", +); + +export interface SegmentedControlIndicatorProps + extends PrimitiveProps, + React.HTMLAttributes {} + +export const SegmentedControlIndicator = withContext< + HTMLDivElement, + SegmentedControlIndicatorProps +>(Primitive.div, "indicator"); + +export interface SegmentedControlSegmentProps extends SegmentedControlPrimitive.SegmentProps {} + +export const SegmentedControlSegment = withContext( + SegmentedControlPrimitive.Segment, + "segment", +); + +export interface SegmentedControlSegmentHiddenInputProps + extends SegmentedControlPrimitive.SegmentHiddenInputProps {} + +export const SegmentedControlSegmentHiddenInput = SegmentedControlPrimitive.SegmentHiddenInput; + +export interface SegmentedControlSegmentLabelProps + extends PrimitiveProps, + React.HTMLAttributes {} + +export const SegmentedControlSegmentLabel = withContext< + HTMLSpanElement, + SegmentedControlSegmentLabelProps +>(withStateProps(Primitive.span), "segmentLabel"); + +export interface SegmentedControlSegmentLabelPlaceholderProps + extends PrimitiveProps, + React.HTMLAttributes {} + +export const SegmentedControlSegmentLabelPlaceholder = withContext< + HTMLSpanElement, + SegmentedControlSegmentLabelPlaceholderProps +>(withStateProps(Primitive.span), "segmentLabelPlaceholder"); diff --git a/packages/react/src/components/SegmentedControl/index.ts b/packages/react/src/components/SegmentedControl/index.ts new file mode 100644 index 000000000..2036d3f5d --- /dev/null +++ b/packages/react/src/components/SegmentedControl/index.ts @@ -0,0 +1,16 @@ +export { + SegmentedControlIndicator, + SegmentedControlRoot, + SegmentedControlSegment, + SegmentedControlSegmentHiddenInput, + SegmentedControlSegmentLabel, + SegmentedControlSegmentLabelPlaceholder, + type SegmentedControlIndicatorProps, + type SegmentedControlRootProps, + type SegmentedControlSegmentProps, + type SegmentedControlSegmentHiddenInputProps, + type SegmentedControlSegmentLabelProps, + type SegmentedControlSegmentLabelPlaceholderProps, +} from "./SegmentedControl"; + +export * as SegmentedControl from "./SegmentedControl.namespace"; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 61781bcd5..b3982134d 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -13,6 +13,7 @@ export * from "./HelpBubble"; export * from "./InlineBanner"; export * from "./ProgressCircle"; export * from "./ReactionButton"; +export * from "./SegmentedControl"; export * from "./SelectBox"; export * from "./Skeleton"; export * from "./Switch"; diff --git a/packages/stylesheet/segmentedControl.css b/packages/stylesheet/segmentedControl.css index cff6eecc3..e28d52484 100644 --- a/packages/stylesheet/segmentedControl.css +++ b/packages/stylesheet/segmentedControl.css @@ -1,33 +1,29 @@ .segmentedControl__root { display: grid; + box-sizing: border-box; min-width: fit-content; max-width: 100%; - padding: var(--seed-v3-unit-s1); position: relative; + padding: var(--seed-v3-unit-s1); border-radius: var(--seed-v3-radius-full); background-color: var(--seed-v3-color-bg-neutral-weak); - grid-template-columns: repeat(var(--seed-design-segmented-control-count, 0), 1fr); - box-sizing: border-box; + grid-auto-flow: column; + grid-auto-columns: 1fr; + align-items: center; + isolation: isolate; } .segmentedControl__segment { - border: none; - padding: 0; - background-color: transparent; - font: inherit; -} -.segmentedControl__segment:not(:is(:disabled, [disabled], [data-disabled])) { - cursor: pointer; -} -.segmentedControl__segment { - position: relative; + display: grid; min-width: 86px; height: var(--seed-v3-unit-s8); - z-index: 10; border-radius: var(--seed-v3-radius-full); overflow: hidden; user-select: none; line-height: var(--seed-v3-line-height-s5); } +.segmentedControl__segment:not(:is(:disabled, [disabled], [data-disabled])) { + cursor: pointer; +} .segmentedControl__segment:not(:is(:disabled, [disabled], [data-disabled])):is(:active, [data-active]) { background-color: var(--seed-v3-color-bg-neutral-weak-pressed); } @@ -35,11 +31,9 @@ background-color: var(--seed-v3-color-bg-layer-default-pressed); } .segmentedControl__segmentLabel { - position: absolute; - inset-inline: 0; - transform: translateY(-50%); - inset-block-start: 50%; + grid-area: 1 / 1 / 1 / 1; padding-inline: calc(var(--seed-v3-unit-s4) - 1px); + margin-block: auto; text-align: center; font-weight: var(--seed-v3-font-weight-medium); font-size: var(--seed-v3-font-size-s5); @@ -56,6 +50,7 @@ color: var(--seed-v3-color-fg-disabled); } .segmentedControl__segmentLabelPlaceholder { + grid-area: 1 / 1 / 1 / 1; padding-inline: var(--seed-v3-unit-s4); text-align: center; font-weight: var(--seed-v3-font-weight-bold); @@ -68,6 +63,10 @@ .segmentedControl__indicator { position: absolute; inset-block: var(--seed-v3-unit-s1); + inset-inline-start: var(--seed-v3-unit-s1); + width: calc((100% - var(--seed-v3-unit-s1) * 2) / var(--seed-design-segmented-control-segment-count)); + transform: translateX(calc(var(--seed-design-segmented-control-current-segment-index) * 100%)); + z-index: -1; border-radius: var(--seed-v3-radius-full); background-color: var(--seed-v3-color-bg-layer-default); box-shadow: 0px 1px 6px 0px #0000000d; diff --git a/yarn.lock b/yarn.lock index ad134e75c..967bf3b2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7838,12 +7838,11 @@ __metadata: languageName: unknown linkType: soft -"@seed-design/react-segmented-control@workspace:^, @seed-design/react-segmented-control@workspace:packages/react-headless/segmented-control": +"@seed-design/react-segmented-control@npm:0.0.0, @seed-design/react-segmented-control@workspace:packages/react-headless/segmented-control": version: 0.0.0-use.local resolution: "@seed-design/react-segmented-control@workspace:packages/react-headless/segmented-control" dependencies: "@radix-ui/react-use-controllable-state": "npm:1.0.1" - "@radix-ui/react-use-size": "npm:^1.1.0" "@seed-design/dom-utils": "npm:0.0.0-alpha-20241030023710" nanobundle: "npm:^1.6.0" peerDependencies: @@ -7942,7 +7941,6 @@ __metadata: "@seed-design/react-primitive": "npm:0.0.0" "@seed-design/react-progress": "npm:0.0.0" "@seed-design/react-radio-group": "npm:0.0.0-alpha-20241030023710" - "@seed-design/react-segmented-control": "workspace:^" "@seed-design/react-switch": "npm:0.0.0-alpha-20241030023710" "@seed-design/react-text-field": "npm:0.0.0-alpha-20241030023710" "@seed-design/react-toggle": "npm:0.0.0" @@ -13177,7 +13175,7 @@ __metadata: "@seed-design/react": "npm:0.0.0" "@seed-design/react-icon": "npm:^0.7.3" "@seed-design/react-popover": "npm:0.0.0-alpha-20241030023710" - "@seed-design/react-segmented-control": "workspace:^" + "@seed-design/react-segmented-control": "npm:0.0.0" "@seed-design/react-tabs": "npm:0.0.0-alpha-20241209060641" "@seed-design/recipe": "npm:0.0.0-alpha-20241212122822" "@seed-design/rootage-cli": "npm:0.0.0"