diff --git a/src/components/SegmentedControl/SegmentedControl.module.scss b/src/components/SegmentedControl/SegmentedControl.module.scss new file mode 100644 index 0000000..94fcad6 --- /dev/null +++ b/src/components/SegmentedControl/SegmentedControl.module.scss @@ -0,0 +1,40 @@ +.SegmentedControl { + overflow: hidden; + width: 100%; + height: 100%; + padding: 2px; + box-sizing: border-box; + border-radius: 44px; + background: var(--background-local-chips-default); +} + +.SegmentedControl__body { + position: relative; + display: flex; + align-items: center; + align-content: stretch; + width: 100%; + height: 100%; + box-sizing: border-box; + border-radius: inherit; +} + +.SegmentedControl__slider { + position: absolute; + inset: 0; + transition: transform 150ms; + border-radius: 40px; + box-sizing: border-box; + background: var(--background-accent-contrast-static); +} + +.SegmentedControl_platform_ios { + border-radius: 9px; + background: var(--background-local-chips-default); +} + +.SegmentedControl_platform_ios .SegmentedControl__slider { + border: 1px solid rgba(0, 0, 0, .04); + border-radius: inherit; + box-shadow: 0 3px 1px 0 rgba(0, 0, 0, .04), 0 3px 8px 0 rgba(0, 0, 0, .12); +} diff --git a/src/components/SegmentedControl/SegmentedControl.stories.tsx b/src/components/SegmentedControl/SegmentedControl.stories.tsx new file mode 100644 index 0000000..b970808 --- /dev/null +++ b/src/components/SegmentedControl/SegmentedControl.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; + +import { hideArgsControl } from '../../../.storybook/shared/args-manager'; +import { SegmentedControl, type SegmentedControlProps } from './index'; + +const meta = { + title: 'Navigation/SegmentedControl', + component: SegmentedControl, + argTypes: hideArgsControl(['children']), + parameters: { + docs: { + description: { + component: ` +Компонент \`SegmentedControl\` используется для выбора одного из нескольких вариантов. +Представляет собой переключатель в виде сегментов, похожий на табы, но более компактный и визуально акцентированный. + +🧠 Полезен в случаях, когда нужно переключать состояние/фильтр или выбирать категорию (например, "Все / Активные / Завершённые"). + ` + } + } + } +} satisfies Meta; + +export default meta; + +const labels = [ + { label: 'Первый', value: 'Первый' }, + { label: 'Второй', value: 'Второй' }, + { label: 'Третий', value: 'Третий' } +]; + +export const Playground: StoryObj = { + render: (args) => { + const [selected, setSelected] = useState(labels[0].value); + + return ( + + {labels.map(({ value, label }) => ( + { + setSelected(value); + }} + > + {label} + + ))} + + ); + }, + decorators: [ + (Component) => ( +
+ +
+ ) + ] +}; diff --git a/src/components/SegmentedControl/SegmentedControl.tsx b/src/components/SegmentedControl/SegmentedControl.tsx new file mode 100644 index 0000000..cc9a4b8 --- /dev/null +++ b/src/components/SegmentedControl/SegmentedControl.tsx @@ -0,0 +1,51 @@ +import { clsx } from 'clsx'; +import { Children, forwardRef, type HTMLAttributes, isValidElement, type ReactElement } from 'react'; + +import { usePlatform } from '../../hooks'; +import type { SegmentedControlItemProps } from './components/SegmentedControlItem'; +import styles from './SegmentedControl.module.scss'; + +export interface SegmentedControlProps extends HTMLAttributes { + children: Array> +} + +export const SegmentedControl = forwardRef(({ + className, + children, + ...restProps +}, forwardedRef) => { + const platform = usePlatform(); + + const childrenArray = Children.toArray(children); + const checkedIndex = childrenArray.findIndex((child) => isValidElement(child) && child.props.selected); + const translateX = `translateX(${100 * checkedIndex}%)`; + + return ( +
+
+ {checkedIndex > -1 && ( +
+ )} + {children} +
+
+ ); +}); + +SegmentedControl.displayName = 'SegmentedControl'; diff --git a/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.module.scss b/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.module.scss new file mode 100644 index 0000000..67f8ee3 --- /dev/null +++ b/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.module.scss @@ -0,0 +1,18 @@ +.SegmentedControlItem { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + flex: 1 1 0; + max-inline-size: 100%; + padding: 10px 24px; + border: none; + border-radius: inherit; + background: transparent; + z-index: var(--layer-base, 1); + color: var(--text-primary); + text-align: center; +} + +.SegmentedControlItem_platform_ios { + padding: 6px 24px; +} diff --git a/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.stories.tsx b/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.stories.tsx new file mode 100644 index 0000000..d139d5a --- /dev/null +++ b/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { SegmentedControlItem, type SegmentedControlItemProps } from './SegmentedControlItem'; + +const meta = { + title: 'Navigation/SegmentedControl/SegmentedControl.Item', + component: SegmentedControlItem +} satisfies Meta; + +export default meta; + +export const Playground: StoryObj = { + args: { + selected: true, + children: 'This is a SegmentedControl.Item' + } +}; diff --git a/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.tsx b/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.tsx new file mode 100644 index 0000000..ab07be0 --- /dev/null +++ b/src/components/SegmentedControl/components/SegmentedControlItem/SegmentedControlItem.tsx @@ -0,0 +1,38 @@ +import { clsx } from 'clsx'; +import { type ButtonHTMLAttributes } from 'react'; + +import { usePlatform } from '../../../../hooks'; +import { Tappable } from '../../../Tappable'; +import { TypographyLabel } from '../../../Typography/parts'; +import styles from './SegmentedControlItem.module.scss'; + +export interface SegmentedControlItemProps extends ButtonHTMLAttributes { + selected?: boolean +} + +export const SegmentedControlItem = ({ + selected, + className, + children, + ...restProps +}: SegmentedControlItemProps): JSX.Element => { + const platform = usePlatform(); + return ( + + + {children} + + + ); +}; + +SegmentedControlItem.displayName = 'SegmentedControl.Item'; diff --git a/src/components/SegmentedControl/components/SegmentedControlItem/index.ts b/src/components/SegmentedControl/components/SegmentedControlItem/index.ts new file mode 100644 index 0000000..bef5a4e --- /dev/null +++ b/src/components/SegmentedControl/components/SegmentedControlItem/index.ts @@ -0,0 +1 @@ +export { SegmentedControlItem, type SegmentedControlItemProps } from './SegmentedControlItem'; diff --git a/src/components/SegmentedControl/index.ts b/src/components/SegmentedControl/index.ts new file mode 100644 index 0000000..beaf002 --- /dev/null +++ b/src/components/SegmentedControl/index.ts @@ -0,0 +1,10 @@ +import { SegmentedControlItem } from './components/SegmentedControlItem'; +import { SegmentedControl } from './SegmentedControl'; + +const SegmentedControlNamespace = Object.assign(SegmentedControl, { Item: SegmentedControlItem }); + +export { SegmentedControlNamespace as SegmentedControl }; +export type { SegmentedControlItemProps } from './components/SegmentedControlItem'; +export type { + SegmentedControlProps +} from './SegmentedControl'; diff --git a/src/components/index.ts b/src/components/index.ts index d4d0946..f2db0a7 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,6 +17,7 @@ export * from './MaxUI'; export * from './Panel'; export * from './Ripple'; export * from './SearchInput'; +export * from './SegmentedControl'; export * from './Spinner'; export * from './SvgButton'; export * from './Switch';