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)