import React, { useImperativeHandle, useMemo, useRef, useState, Ref } from 'react' import { Easing, ScrollView } from 'react-native' import { Text } from '../Text' import { View } from '../View' import { useAnimatedVariantStyles } from '../../utils' import { SegmentedControlOption } from './Option' import { SegmentedControlProps, SegmentedControlRef } from './types' import { AnyRecord, IJSX, StyledComponentProps, StyledComponentWithProps, useTheme } from '@codeleap/styles' import { MobileStyleRegistry } from '../../Registry' import { useStylesFor } from '../../hooks' export * from './styles' export * from './types' const DefaultBubble = (props) => { return } const defaultAnimation = { type: 'timing', duration: 200, easing: Easing.linear, } /** * Always fully controlled — there is no internal selected state; `value` and `onValueChange` * are required. The animated bubble position is driven by Reanimated worklets, so the bubble * can only animate when `currentOptionIdx` changes (i.e. `value` changes from the parent). */ export const SegmentedControl = React.forwardRef((props, ref) => { const [themeValues, themeSpacing] = useTheme(store => [store.theme?.values as any, store.theme?.spacing]) const { options = [], onValueChange, debugName, label, value, animation = {}, scrollProps = {}, /** Default divides total screen width equally; override `getItemWidth` when options have variable label lengths or the control doesn't fill the screen. */ getItemWidth = () => (themeValues?.width - themeSpacing?.value?.(4)) / options.length, renderBubble: BubbleView, scrollToCurrentOptionOnMount, renderOption: Option, touchableProps, style, ...viewProps } = { ...SegmentedControl.defaultProps, ...props, } const [bubbleWidth, setBubbleWidth] = useState(0) const _animation = { ...defaultAnimation, ...animation, } const styles = useStylesFor(SegmentedControl.styleRegistryName, style) const scrollRef = useRef(null) function scrollTo(idx: number) { if (!scrollRef.current) return setTimeout(() => { scrollRef.current?.scrollTo({ x: widthStyle.width * idx, y: 0, animated: true }) }) } const widthStyle = useMemo(() => { if (getItemWidth) { const sizes = options.map(getItemWidth) const maxWidth = sizes.sort((a, b) => b - a)[0] return { width: maxWidth } } return { width: bubbleWidth, } }, [options, bubbleWidth]) const currentOptionIdx = options.findIndex(o => o.value === value) || 0 const onPress = (txt: string, idx: number) => { return () => { onValueChange(txt) scrollTo(idx) } } const hasScrolledInitially = useRef(false) useImperativeHandle(ref, () => { if (!scrollRef.current) return null return { ...(scrollRef.current), scrollTo, scrollToCurrent() { if (!scrollRef.current) return scrollTo(currentOptionIdx) }, } as SegmentedControlRef }, [ currentOptionIdx, ]) if (!hasScrolledInitially.current && scrollRef.current && scrollToCurrentOptionOnMount) { scrollTo(currentOptionIdx) hasScrolledInitially.current = true } const bubbleAnimation = useAnimatedVariantStyles({ variantStyles: styles, animatedProperties: [], updater: () => { 'worklet' return { translateX: currentOptionIdx * widthStyle.width, } }, transition: _animation, dependencies: [currentOptionIdx, widthStyle.width], }) /** Tracks the widest option measured during onLayout to set a uniform bubble width when `getItemWidth` is not provided; reset to 0 after each full pass so re-renders don't accumulate stale values. */ const largestWidth = useRef(0) return ( {label ? ( ) : null} } > {options.map((o, idx) => ( ) }) as StyledComponentWithProps SegmentedControl.styleRegistryName = 'SegmentedControl' SegmentedControl.elements = ['wrapper', 'selectedBubble', 'innerWrapper', 'scroll', 'text', 'icon', 'button', 'label', 'badge'] SegmentedControl.rootElement = 'scroll' SegmentedControl.withVariantTypes = (styles: S) => { return SegmentedControl as ((props: StyledComponentProps & { ref?: React.Ref }, typeof styles>) => IJSX) } SegmentedControl.defaultProps = { renderBubble: DefaultBubble, renderOption: SegmentedControlOption, scrollToCurrentOptionOnMount: true, } as Partial MobileStyleRegistry.registerComponent(SegmentedControl)