import * as React from 'react' ; import { clamp } from '@radix-ui/number' ; import { composeEventHandlers } from '@radix-ui/primitive' ; import { useComposedRefs } from '@radix-ui/react-compose-refs' ; import { createContextScope } from '@radix-ui/react-context' ; import { useControllableState } from '@radix-ui/react-use-controllable-state' ; import { useDirection } from '@radix-ui/react-use-direction' ; import { usePrevious } from '@radix-ui/react-use-previous' ; import { useSize } from '@radix-ui/react-use-size' ; import { Primitive } from '@radix-ui/react-primitive' ; import { createCollection } from '@radix-ui/react-collection' ; import type * as Radix from '@radix-ui/react-primitive' ; import type { Scope } from '@radix-ui/react-context' ; type Direction = 'ltr' | 'rtl' ; const PAGE_KEYS = [ 'PageUp' , 'PageDown' ] ; const ARROW_KEYS = [ 'ArrowUp' , 'ArrowDown' , 'ArrowLeft' , 'ArrowRight' ] ; const BACK_KEYS = { ltr : [ 'ArrowDown' , 'Home' , 'ArrowLeft' , 'PageDown' ] , rtl : [ 'ArrowDown' , 'Home' , 'ArrowRight' , 'PageDown' ] , } ; const SLIDER_NAME = 'Slider' ; const [ Collection , useCollection , createCollectionScope ] = createCollection < SliderThumbElement > ( SLIDER_NAME ) ; type ScopedProps < P > = P & { __scopeSlider ? : Scope ; } ; const [ createSliderContext , createSliderScope ] = createContextScope ( SLIDER_NAME , [ createCollectionScope , ] ) ; type SliderContextValue = { disabled ? : boolean ; min : number ; max : number ; values : number [ ] ; valueIndexToChangeRef : React . MutableRefObject < number > ; thumbs : Set < SliderThumbElement > ; orientation : SliderProps [ 'orientation' ] ; } ; const [ SliderProvider , useSliderContext ] = createSliderContext < SliderContextValue > ( SLIDER_NAME ) ; type SliderElement = SliderHorizontalElement | SliderVerticalElement ; interface SliderProps extends Omit < SliderHorizontalProps | SliderVerticalProps , keyof SliderOrientationPrivateProps | 'defaultValue' > { name ? : string ; disabled ? : boolean ; orientation ? : React . AriaAttributes [ 'aria-orientation' ] ; dir ? : Direction ; min ? : number ; max ? : number ; step ? : number ; minStepsBetweenThumbs ? : number ; value ? : number [ ] ; defaultValue ? : number [ ] ; onValueChange ? ( value ) : void ; } const Slider = React . forwardRef < SliderElement , SliderProps > ( ( props , forwardedRef ) => { const { name , min = 0 , max = 100 , step = 1 , orientation = 'horizontal' , disabled = false , minStepsBetweenThumbs = 0 , defaultValue = [ min ] , value , onValueChange = ( ) => { } , ... sliderProps } = props ; const [ slider , setSlider ] = React . useState < HTMLSpanElement | null > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ( node ) => setSlider ( node ) ) ; const thumbRefs = React . useRef < SliderContextValue [ 'thumbs' ] > ( new Set ( ) ) ; const valueIndexToChangeRef = React . useRef < number > ( 0 ) ; const isHorizontal = orientation === 'horizontal' ; const isFormControl = slider ? Boolean ( slider . closest ( 'form' ) ) : true ; const SliderOrientation = isHorizontal ? SliderHorizontal : SliderVertical ; const [ values = [ ] , setValues ] = useControllableState ( { prop : value , defaultProp : defaultValue , onChange : ( value ) => { const thumbs = [ ... thumbRefs . current ] ; thumbs [ valueIndexToChangeRef . current ] ?. focus ( ) ; onValueChange ( value ) ; } , } ) ; function handleSlideStart ( value ) { const closestIndex = getClosestValueIndex ( values , value ) ; updateValues ( value , closestIndex ) ; } function handleSlideMove ( value ) { updateValues ( value , valueIndexToChangeRef . current ) ; } function updateValues ( value , atIndex ) { const decimalCount = getDecimalCount ( step ) ; const snapToStep = roundValue ( Math . round ( ( value - min ) / step ) * step + min , decimalCount ) ; const nextValue = clamp ( snapToStep , [ min , max ] ) ; setValues ( ( prevValues = [ ] ) => { const nextValues = getNextSortedValues ( prevValues , nextValue , atIndex ) ; if ( hasMinStepsBetweenValues ( nextValues , minStepsBetweenThumbs * step ) ) { valueIndexToChangeRef . current = nextValues . indexOf ( nextValue ) ; return String ( nextValues ) === String ( prevValues ) ? prevValues : nextValues ; } else { return prevValues ; } } ) ; } return ( < SliderProvider scope = { props . __scopeSlider } disabled = { disabled } min = { min } max = { max } valueIndexToChangeRef = { valueIndexToChangeRef } thumbs = { thumbRefs . current } values = { values } orientation = { orientation } > < Collection . Provider scope = { props . __scopeSlider } > < Collection . Slot scope = { props . __scopeSlider } > < SliderOrientation aria - disabled = { disabled } data - disabled = { disabled ? '' : undefined } { ... sliderProps } ref = { composedRefs } min = { min } max = { max } onSlideStart = { disabled ? undefined : handleSlideStart } onSlideMove = { disabled ? undefined : handleSlideMove } onHomeKeyDown = { ( ) => ! disabled && updateValues ( min , 0 ) } onEndKeyDown = { ( ) => ! disabled && updateValues ( max , values . length - 1 ) } onStepKeyDown = { ( { event , direction : stepDirection } ) => { if ( ! disabled ) { const isPageKey = PAGE_KEYS . includes ( event . key ) ; const isSkipKey = isPageKey || ( event . shiftKey && ARROW_KEYS . includes ( event . key ) ) ; const multiplier = isSkipKey ? 10 : 1 ; const atIndex = valueIndexToChangeRef . current ; const value = values [ atIndex ] ; const stepInDirection = step * multiplier * stepDirection ; updateValues ( value + stepInDirection , atIndex ) ; } } } /> < /Collection.Slot> < /Collection.Provider> { isFormControl && values . map ( ( value , index ) => ( < BubbleInput key = { index } name = { name ? name + ( values . length > 1 ? '[]' : '' ) : undefined } value = { value } / > ) ) } < /SliderProvider>); } ) ; Slider . displayName = SLIDER_NAME ; const [ SliderOrientationProvider , useSliderOrientationContext ] = createSliderContext < { startEdge : 'bottom' | 'left' | 'right' ; endEdge : 'top' | 'right' | 'left' ; size : keyof NonNullable < ReturnType < typeof useSize > > ; direction : number ; } > ( SLIDER_NAME , { startEdge : 'left' , endEdge : 'right' , size : 'width' , direction : 1 , } ) ; type SliderOrientationPrivateProps = { min : number ; max : number ; onSlideStart ? ( value ) : void ; onSlideMove ? ( value ) : void ; onHomeKeyDown ( event ) : void ; onEndKeyDown ( event ) : void ; onStepKeyDown ( step ) : void ; } ; interface SliderOrientationProps extends Omit < SliderImplProps , keyof SliderImplPrivateProps > , SliderOrientationPrivateProps { } type SliderHorizontalElement = SliderImplElement ; interface SliderHorizontalProps extends SliderOrientationProps { dir ? : Direction ; } const SliderHorizontal = React . forwardRef < SliderHorizontalElement , SliderHorizontalProps > ( ( props , forwardedRef ) => { const { min , max , dir , onSlideStart , onSlideMove , onStepKeyDown , ... sliderProps } = props ; const [ slider , setSlider ] = React . useState < SliderImplElement | null > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ( node ) => setSlider ( node ) ) ; const rectRef = React . useRef < ClientRect > ( ) ; const direction = useDirection ( slider , dir ) ; const isDirectionLTR = direction === 'ltr' ; function getValueFromPointer ( pointerPosition ) { const rect = rectRef . current || slider ! . getBoundingClientRect ( ) ; const input = [ 0 , rect . width ] ; const output = isDirectionLTR ? [ min , max ] : [ max , min ] ; const value = linearScale ( input , output ) ; rectRef . current = rect ; return value ( pointerPosition - rect . left ) ; } return ( < SliderOrientationProvider scope = { props . __scopeSlider } startEdge = { isDirectionLTR ? 'left' : 'right' } endEdge = { isDirectionLTR ? 'right' : 'left' } direction = { isDirectionLTR ? 1 : - 1 } size = "width" > < SliderImpl data - orientation = "horizontal" { ... sliderProps } ref = { composedRefs } style = { { ... sliderProps . style , [ '--radix-slider-thumb-transform' as any ] : 'translateX(-50%)' , } } onSlideStart = { ( event ) => { const value = getValueFromPointer ( event . clientX ) ; onSlideStart ?. ( value ) ; } } onSlideMove = { ( event ) => { const value = getValueFromPointer ( event . clientX ) ; onSlideMove ?. ( value ) ; } } onSlideEnd = { ( ) => ( rectRef . current = undefined ) } onStepKeyDown = { ( event ) => { const isBackKey = BACK_KEYS [ direction ] . includes ( event . key ) ; onStepKeyDown ?. ( { event , direction : isBackKey ? - 1 : 1 } ) ; } } /> < /SliderOrientationProvider>); } ) ; type SliderVerticalElement = SliderImplElement ; interface SliderVerticalProps extends SliderOrientationProps { } const SliderVertical = React . forwardRef < SliderVerticalElement , SliderVerticalProps > ( ( props , forwardedRef ) => { const { min , max , onSlideStart , onSlideMove , onStepKeyDown , ... sliderProps } = props ; const sliderRef = React . useRef < SliderImplElement > ( null ) ; const ref = useComposedRefs ( forwardedRef , sliderRef ) ; const rectRef = React . useRef < ClientRect > ( ) ; function getValueFromPointer ( pointerPosition ) { const rect = rectRef . current || sliderRef . current ! . getBoundingClientRect ( ) ; const input = [ 0 , rect . height ] ; const output = [ max , min ] ; const value = linearScale ( input , output ) ; rectRef . current = rect ; return value ( pointerPosition - rect . top ) ; } return ( < SliderOrientationProvider scope = { props . __scopeSlider } startEdge = "bottom" endEdge = "top" size = "height" direction = { 1 } > < SliderImpl data - orientation = "vertical" { ... sliderProps } ref = { ref } style = { { ... sliderProps . style , [ '--radix-slider-thumb-transform' as any ] : 'translateY(50%)' , } } onSlideStart = { ( event ) => { const value = getValueFromPointer ( event . clientY ) ; onSlideStart ?. ( value ) ; } } onSlideMove = { ( event ) => { const value = getValueFromPointer ( event . clientY ) ; onSlideMove ?. ( value ) ; } } onSlideEnd = { ( ) => ( rectRef . current = undefined ) } onStepKeyDown = { ( event ) => { const isBackKey = BACK_KEYS . ltr . includes ( event . key ) ; onStepKeyDown ?. ( { event , direction : isBackKey ? - 1 : 1 } ) ; } } /> < /SliderOrientationProvider>); } ) ; type SliderImplElement = React . ElementRef < typeof Primitive . span > ; type PrimitiveDivProps = Radix . ComponentPropsWithoutRef < typeof Primitive . div > ; type SliderImplPrivateProps = { onSlideStart ( event ) : void ; onSlideMove ( event ) : void ; onSlideEnd ( event ) : void ; onHomeKeyDown ( event ) : void ; onEndKeyDown ( event ) : void ; onStepKeyDown ( event ) : void ; } ; interface SliderImplProps extends PrimitiveDivProps , SliderImplPrivateProps { } const SliderImpl = React . forwardRef < SliderImplElement , SliderImplProps > ( ( props , forwardedRef ) => { const { __scopeSlider , onSlideStart , onSlideMove , onSlideEnd , onHomeKeyDown , onEndKeyDown , onStepKeyDown , ... sliderProps } = props ; const context = useSliderContext ( SLIDER_NAME , __scopeSlider ) ; return ( < Primitive . span { ... sliderProps } ref = { forwardedRef } onKeyDown = { composeEventHandlers ( props . onKeyDown , ( event ) => { if ( event . key === 'Home' ) { onHomeKeyDown ( event ) ; event . preventDefault ( ) ; } else if ( event . key === 'End' ) { onEndKeyDown ( event ) ; event . preventDefault ( ) ; } else if ( PAGE_KEYS . concat ( ARROW_KEYS ) . includes ( event . key ) ) { onStepKeyDown ( event ) ; event . preventDefault ( ) ; } } ) } onPointerDown = { composeEventHandlers ( props . onPointerDown , ( event ) => { const target = event . target as HTMLElement ; target . setPointerCapture ( event . pointerId ) ; event . preventDefault ( ) ; if ( context . thumbs . has ( target ) ) { target . focus ( ) ; } else { onSlideStart ( event ) ; } } ) } onPointerMove = { composeEventHandlers ( props . onPointerMove , ( event ) => { const target = event . target as HTMLElement ; if ( target . hasPointerCapture ( event . pointerId ) ) onSlideMove ( event ) ; } ) } onPointerUp = { composeEventHandlers ( props . onPointerUp , ( event ) => { const target = event . target as HTMLElement ; if ( target . hasPointerCapture ( event . pointerId ) ) { target . releasePointerCapture ( event . pointerId ) ; onSlideEnd ( event ) ; } } ) } />); } ) ; const TRACK_NAME = 'SliderTrack' ; type SliderTrackElement = React . ElementRef < typeof Primitive . span > ; type PrimitiveSpanProps = Radix . ComponentPropsWithoutRef < typeof Primitive . span > ; interface SliderTrackProps extends PrimitiveSpanProps { } const SliderTrack = React . forwardRef < SliderTrackElement , SliderTrackProps > ( ( props , forwardedRef ) => { const { __scopeSlider , ... trackProps } = props ; const context = useSliderContext ( TRACK_NAME , __scopeSlider ) ; return ( < Primitive . span data - disabled = { context . disabled ? '' : undefined } data - orientation = { context . orientation } { ... trackProps } ref = { forwardedRef } / > ) ; } ) ; SliderTrack . displayName = TRACK_NAME ; const RANGE_NAME = 'SliderRange' ; type SliderRangeElement = React . ElementRef < typeof Primitive . span > ; interface SliderRangeProps extends PrimitiveSpanProps { } const SliderRange = React . forwardRef < SliderRangeElement , SliderRangeProps > ( ( props , forwardedRef ) => { const { __scopeSlider , ... rangeProps } = props ; const context = useSliderContext ( RANGE_NAME , __scopeSlider ) ; const orientation = useSliderOrientationContext ( RANGE_NAME , __scopeSlider ) ; const ref = React . useRef < HTMLSpanElement > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ref ) ; const valuesCount = context . values . length ; const percentages = context . values . map ( ( value ) => convertValueToPercentage ( value , context . min , context . max ) ) ; const offsetStart = valuesCount > 1 ? Math . min ( ... percentages ) : 0 ; const offsetEnd = 100 - Math . max ( ... percentages ) ; return ( < Primitive . span data - orientation = { context . orientation } data - disabled = { context . disabled ? '' : undefined } { ... rangeProps } ref = { composedRefs } style = { { ... props . style , [ orientation . startEdge ] : offsetStart + '%' , [ orientation . endEdge ] : offsetEnd + '%' , } } />); } ) ; SliderRange . displayName = RANGE_NAME ; const THUMB_NAME = 'SliderThumb' ; type SliderThumbElement = SliderThumbImplElement ; interface SliderThumbProps extends Omit < SliderThumbImplProps , 'index' > { } const SliderThumb = React . forwardRef < SliderThumbElement , SliderThumbProps > ( ( props , forwardedRef ) => { const getItems = useCollection ( props . __scopeSlider ) ; const [ thumb , setThumb ] = React . useState < SliderThumbImplElement | null > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ( node ) => setThumb ( node ) ) ; const index = React . useMemo ( ( ) => ( thumb ? getItems ( ) . findIndex ( ( item ) => item . ref . current === thumb ) : - 1 ) , [ getItems , thumb ] ) ; return < SliderThumbImpl { ... props } ref = { composedRefs } index = { index } / > ; } ) ; type SliderThumbImplElement = React . ElementRef < typeof Primitive . span > ; interface SliderThumbImplProps extends PrimitiveSpanProps { index : number ; } const SliderThumbImpl = React . forwardRef < SliderThumbImplElement , SliderThumbImplProps > ( ( props , forwardedRef ) => { const { __scopeSlider , index , ... thumbProps } = props ; const context = useSliderContext ( THUMB_NAME , __scopeSlider ) ; const orientation = useSliderOrientationContext ( THUMB_NAME , __scopeSlider ) ; const [ thumb , setThumb ] = React . useState < HTMLSpanElement | null > ( null ) ; const composedRefs = useComposedRefs ( forwardedRef , ( node ) => setThumb ( node ) ) ; const size = useSize ( thumb ) ; const value = context . values [ index ] as number | undefined ; const percent = value === undefined ? 0 : convertValueToPercentage ( value , context . min , context . max ) ; const label = getLabel ( index , context . values . length ) ; const orientationSize = size ?. [ orientation . size ] ; const thumbInBoundsOffset = orientationSize ? getThumbInBoundsOffset ( orientationSize , percent , orientation . direction ) : 0 ; React . useEffect ( ( ) => { if ( thumb ) { context . thumbs . add ( thumb ) ; return ( ) => { context . thumbs . delete ( thumb ) ; } ; } } , [ thumb , context . thumbs ] ) ; return ( < span style = { { transform : 'var(--radix-slider-thumb-transform)' , position : 'absolute' , [ orientation . startEdge ] : `calc(${ percent }% + ${ thumbInBoundsOffset }px)` , } } > < Collection . ItemSlot scope = { props . __scopeSlider } > < Primitive . span role = "slider" aria - label = { props [ 'aria-label' ] || label } aria - valuemin = { context . min } aria - valuenow = { value } aria - valuemax = { context . max } aria - orientation = { context . orientation } data - orientation = { context . orientation } data - disabled = { context . disabled ? '' : undefined } tabIndex = { context . disabled ? undefined : 0 } { ... thumbProps } ref = { composedRefs } style = { value === undefined ? { display : 'none' } : props . style } onFocus = { composeEventHandlers ( props . onFocus , ( ) => { context . valueIndexToChangeRef . current = index ; } ) } /> < /Collection.ItemSlot> < /span>); } ) ; SliderThumb . displayName = THUMB_NAME ; const BubbleInput = ( props ) => { const { value , ... inputProps } = props ; const ref = React . useRef < HTMLInputElement > ( null ) ; const prevValue = usePrevious ( value ) ; React . useEffect ( ( ) => { const input = ref . current ! ; const inputProto = window . HTMLInputElement . prototype ; const descriptor = Object . getOwnPropertyDescriptor ( inputProto , 'value' ) as PropertyDescriptor ; const setValue = descriptor . set ; if ( prevValue !== value && setValue ) { const event = new Event ( 'input' , { bubbles : true } ) ; setValue . call ( input , value ) ; input . dispatchEvent ( event ) ; } } , [ prevValue , value ] ) ; /**
     * We purposefully do not use `type="hidden"` here otherwise forms that
     * wrap it will not be able to access its value via the FormData API.
     *
     * We purposefully do not add the `value` attribute here to allow the value
     * to be set programatically and bubble to any parent form `onChange` event.
     * Adding the `value` will cause React to consider the programatic
     * dispatch a duplicate and it will get swallowed.
     */ return < input style = { { display : 'none' } } { ... inputProps } ref = { ref } defaultValue = { value } / > ; } ; function getNextSortedValues ( prevValues = [ ] , nextValue , atIndex ) { const nextValues = [ ... prevValues ] ; nextValues [ atIndex ] = nextValue ; return nextValues . sort ( ( a , b ) => a - b ) ; } function convertValueToPercentage ( value , min , max ) { const maxSteps = max - min ; const percentPerStep = 100 / maxSteps ; return percentPerStep * ( value - min ) ; } /**
 * Returns a label for each thumb when there are two or more thumbs
 */ function getLabel ( index , totalValues ) { if ( totalValues > 2 ) { return `Value ${ index + 1 } of ${ totalValues }` ; } else if ( totalValues === 2 ) { return [ 'Minimum' , 'Maximum' ] [ index ] ; } else { return undefined ; } } example function getClosestValueIndex ( values , nextValue ) { if ( values . length === 1 ) return 0 ; const distances = values . map ( ( value ) => Math . abs ( value - nextValue ) ) ; const closestDistance = Math . min ( ... distances ) ; return distances . indexOf ( closestDistance ) ; } /**
 * Offsets the thumb centre point while sliding to ensure it remains
 * within the bounds of the slider when reaching the edges
 */ function getThumbInBoundsOffset ( width , left , direction ) { const halfWidth = width / 2 ; const halfPercent = 50 ; const offset = linearScale ( [ 0 , halfPercent ] , [ 0 , halfWidth ] ) ; return ( halfWidth - offset ( left ) * direction ) * direction ; } example function getStepsBetweenValues ( values ) { return values . slice ( 0 , - 1 ) . map ( ( value , index ) => values [ index + 1 ] - value ) ; } example example function hasMinStepsBetweenValues ( values , minStepsBetweenValues ) { if ( minStepsBetweenValues > 0 ) { const stepsBetweenValues = getStepsBetweenValues ( values ) ; const actualMinStepsBetweenValues = Math . min ( ... stepsBetweenValues ) ; return actualMinStepsBetweenValues >= minStepsBetweenValues ; } return true ; } function linearScale ( input , output ) { return ( value ) => { if ( input [ 0 ] === input [ 1 ] || output [ 0 ] === output [ 1 ] ) return output [ 0 ] ; const ratio = ( output [ 1 ] - output [ 0 ] ) / ( input [ 1 ] - input [ 0 ] ) ; return output [ 0 ] + ratio * ( value - input [ 0 ] ) ; } ; } function getDecimalCount ( value ) { return ( String ( value ) . split ( '.' ) [ 1 ] || '' ) . length ; } function roundValue ( value , decimalCount ) { const rounder = Math . pow ( 10 , decimalCount ) ; return Math . round ( value * rounder ) / rounder ; } const Root = Slider ; const Track = SliderTrack ; const Range = SliderRange ; const Thumb = SliderThumb ; export { createSliderScope , Slider , SliderTrack , SliderRange , SliderThumb , Root , Track , Range , Thumb , } ; export type { SliderProps , SliderTrackProps , SliderRangeProps , SliderThumbProps } ;