'use client'; import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; import { useAdaptivity } from '../../hooks/useAdaptivity'; import { useConfigDirection } from '../../hooks/useConfigDirection'; import { useCustomEnsuredControl } from '../../hooks/useEnsuredControl'; import { useTabsNavigation } from '../../hooks/useTabsNavigation'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { warnOnce } from '../../lib/warnOnce'; import type { CSSCustomProperties, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import { SegmentedControlOption, type SegmentedControlOptionProps, } from './SegmentedControlOption/SegmentedControlOption'; import styles from './SegmentedControl.module.css'; const sizeYClassNames = { none: styles.sizeYNone, regular: styles.sizeYRegular, }; export type SegmentedControlValue = string | number | undefined; export interface SegmentedControlOptionInterface extends Omit, 'label'> { /** * Вставляет элемент перед основным контентом. * Рекомендуется использовать только иконки с размером 20. */ before?: React.ReactNode; /** * Текст или React-элемент, отображаемый в качестве метки опции. */ label: React.ReactNode; /** * Значение опции, которое будет передано в обработчик onChange при выборе. */ value: SegmentedControlValue; } export interface SegmentedControlProps extends Omit, 'onChange'> { /** * Массив опций для отображения в компоненте. */ options: SegmentedControlOptionInterface[]; /** * Размер компонента. */ size?: 'm' | 'l'; /** * Имя для input-элементов внутри компонента. */ name?: string; /** * Обработчик изменения выбранного значения. */ onChange?: (value: SegmentedControlValue) => void; /** * Текущее выбранное значение (для контролируемого компонента). */ value?: SegmentedControlValue; /** * Значение по умолчанию (для неконтролируемого компонента). */ defaultValue?: SegmentedControlValue; } const warn = warnOnce('SegmentedControl'); /** * @see https://vkui.io/components/segmented-control */ export const SegmentedControl = ({ size = 'l', name, options, defaultValue = options[0]?.value, children, onChange: onChangeProp, value: valueProp, role = 'radiogroup', ...restProps }: SegmentedControlProps): React.ReactNode => { const id = React.useId(); const direction = useConfigDirection(); const isRtl = direction === 'rtl'; const [value, onChange] = useCustomEnsuredControl({ onChange: onChangeProp, value: valueProp, defaultValue, }); const { sizeY = 'none' } = useAdaptivity(); const { tabsRef } = useTabsNavigation(role === 'tablist', isRtl); const actualIndex = options.findIndex((option) => option.value === value); useIsomorphicLayoutEffect(() => { if (actualIndex === -1 && process.env.NODE_ENV === 'development') { warn('defaultValue: такого значения нет среди опций!', 'error'); } }, [actualIndex]); const sliderStyle: CSSCustomProperties = { '--vkui_internal--SegmentedControl_actual_index': String(actualIndex), '--vkui_internal--SegmentedControl_options': String(options.length), }; return (
{actualIndex > -1 &&
} {options.map(({ label, before, ...optionProps }) => { const selected = value === optionProps.value; const onSelect = () => onChange(optionProps.value); const optionRootProps: SegmentedControlOptionProps['rootProps'] = role === 'tablist' ? { 'role': 'tab', 'aria-selected': selected, 'onClick': onSelect, 'tabIndex': optionProps.tabIndex ?? (selected ? 0 : -1), ...optionProps, } : undefined; const optionInputProps: SegmentedControlOptionProps['inputProps'] = role !== 'tablist' ? { role: optionProps.role || (role === 'radiogroup' ? 'radio' : undefined), checked: selected, onChange: onSelect, name: name ?? id, ...optionProps, } : undefined; return ( {label} ); })}
); };