@@ -7,7 +7,7 @@ import { TextInput } from '../TextInput';
77import { Tooltip , TooltipProps } from '../Tooltip' ;
88import cssSliderValue from '@patternfly/react-tokens/dist/esm/c_slider_value' ;
99import cssFormControlWidthChars from '@patternfly/react-tokens/dist/esm/c_slider__value_c_form_control_width_chars' ;
10- import { getLanguageDirection } from '../../helpers/util' ;
10+ import { formatLocalizedDecimal , getLanguageDirection , parseLocalizedDecimal } from '../../helpers/util' ;
1111
1212/** Properties for creating custom steps in a slider. These properties should be passed in as
1313 * an object within an array to the slider component's customSteps property.
@@ -93,6 +93,8 @@ export interface SliderProps extends Omit<React.HTMLProps<HTMLDivElement>, 'onCh
9393 thumbAriaValueText ?: string ;
9494 /** Current value of the slider. */
9595 value ?: number ;
96+ /** Locale string used for input formatting and parsing. See https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag for more information. */
97+ locale ?: string ;
9698}
9799
98100const getPercentage = ( current : number , max : number ) => ( 100 * current ) / max ;
@@ -126,13 +128,22 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
126128 showBoundaries = true ,
127129 'aria-describedby' : ariaDescribedby ,
128130 'aria-labelledby' : ariaLabelledby ,
131+ locale,
129132 ...props
130133} : SliderProps ) => {
131134 const sliderRailRef = useRef < HTMLDivElement > ( undefined ) ;
132135 const thumbRef = useRef < HTMLDivElement > ( undefined ) ;
136+ const isInputFocusedRef = useRef ( false ) ;
137+ let formatter : Intl . NumberFormat ;
138+ try {
139+ formatter = new Intl . NumberFormat ( locale ) ;
140+ } catch {
141+ formatter = new Intl . NumberFormat ( ) ; // grabs default locale
142+ }
133143
134144 const [ localValue , setValue ] = useState ( value ) ;
135145 const [ localInputValue , setLocalInputValue ] = useState ( inputValue ) ;
146+ const [ inputDisplayValue , setInputDisplayValue ] = useState ( ( ) => formatLocalizedDecimal ( inputValue , formatter ) ) ;
136147
137148 let isRTL : boolean ;
138149
@@ -144,46 +155,107 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
144155 setValue ( value ) ;
145156 } , [ value ] ) ;
146157
158+ const updateInputDisplay = useCallback ( ( numericValue : number ) => {
159+ setInputDisplayValue ( formatLocalizedDecimal ( numericValue , formatter ) ) ;
160+ } , [ ] ) ;
161+
162+ const updateInputFromSliderValue = useCallback (
163+ ( numericValue : number ) => {
164+ if ( ! isInputVisible ) {
165+ return ;
166+ }
167+
168+ setLocalInputValue ( numericValue ) ;
169+ if ( ! isInputFocusedRef . current ) {
170+ updateInputDisplay ( numericValue ) ;
171+ }
172+ } ,
173+ [ isInputVisible , updateInputDisplay ]
174+ ) ;
175+
176+ const setLocalInputValueWithDisplay = useCallback < React . Dispatch < React . SetStateAction < number > > > (
177+ ( nextValue ) => {
178+ setLocalInputValue ( ( previousValue ) => {
179+ const resolvedValue = typeof nextValue === 'function' ? nextValue ( previousValue ) : nextValue ;
180+
181+ if ( ! isInputFocusedRef . current ) {
182+ updateInputDisplay ( resolvedValue ) ;
183+ }
184+
185+ return resolvedValue ;
186+ } ) ;
187+ } ,
188+ [ updateInputDisplay ]
189+ ) ;
190+
147191 useEffect ( ( ) => {
148- setLocalInputValue ( inputValue ) ;
149- } , [ inputValue ] ) ;
192+ if ( ! isInputFocusedRef . current ) {
193+ setLocalInputValue ( inputValue ) ;
194+ updateInputDisplay ( inputValue ) ;
195+ }
196+ } , [ inputValue , updateInputDisplay ] ) ;
150197
151198 let diff = 0 ;
152199 let snapValue : number ;
153200
154201 // calculate style value percentage
155202 const stylePercent = ( ( localValue - min ) * 100 ) / ( max - min ) ;
156203 const style = { [ cssSliderValue . name ] : `${ stylePercent } %` } as React . CSSProperties ;
157- const widthChars = useMemo ( ( ) => localInputValue . toString ( ) . length , [ localInputValue ] ) ;
204+ const widthChars = useMemo ( ( ) => inputDisplayValue . length || 1 , [ inputDisplayValue ] ) ;
158205 const inputStyle = { [ cssFormControlWidthChars . name ] : widthChars } as React . CSSProperties ;
159206
160207 const onChangeHandler = ( event : React . FormEvent < HTMLInputElement > , value : string ) => {
161- const newValue = Number ( value ) ;
162- setLocalInputValue ( newValue ) ;
208+ setInputDisplayValue ( value ) ;
163209
164- isInputLive && onChange && onChange ( event , localValue , newValue , setLocalInputValue ) ;
210+ const parsedValue = parseLocalizedDecimal ( value , formatter ) ;
211+
212+ if ( ! Number . isNaN ( parsedValue ) ) {
213+ setLocalInputValue ( parsedValue ) ;
214+ isInputLive && onChange && onChange ( event , localValue , parsedValue , setLocalInputValueWithDisplay ) ;
215+ }
165216 } ;
166217
167218 const handleKeyPressOnInput = ( event : React . KeyboardEvent ) => {
168219 if ( event . key === 'Enter' ) {
169220 event . preventDefault ( ) ;
221+ const parsedValue = parseLocalizedDecimal ( inputDisplayValue , formatter ) ;
222+
223+ if ( ! Number . isNaN ( parsedValue ) ) {
224+ setLocalInputValue ( parsedValue ) ;
225+ updateInputDisplay ( parsedValue ) ;
226+ }
227+
170228 if ( onChange ) {
171- onChange ( event , localValue , localInputValue , setLocalInputValue ) ;
229+ onChange (
230+ event ,
231+ localValue ,
232+ Number . isNaN ( parsedValue ) ? localInputValue : parsedValue ,
233+ setLocalInputValueWithDisplay
234+ ) ;
172235 }
173236 }
174237 } ;
175238
176- const onInputFocus = ( e : any ) => {
239+ const onInputFocus = ( e : React . SyntheticEvent < HTMLInputElement > ) => {
177240 e . stopPropagation ( ) ;
241+ isInputFocusedRef . current = true ;
178242 } ;
179243
180244 const onThumbClick = ( ) => {
181245 thumbRef . current . focus ( ) ;
182246 } ;
183247
184248 const onBlur = ( event : React . FocusEvent < HTMLInputElement > ) => {
249+ isInputFocusedRef . current = false ;
250+
251+ const parsedValue = parseLocalizedDecimal ( inputDisplayValue , formatter ) ;
252+ const resolvedInputValue = Number . isNaN ( parsedValue ) ? localInputValue : parsedValue ;
253+
254+ setLocalInputValue ( resolvedInputValue ) ;
255+ updateInputDisplay ( resolvedInputValue ) ;
256+
185257 if ( onChange ) {
186- onChange ( event , localValue , localInputValue , setLocalInputValue ) ;
258+ onChange ( event , localValue , resolvedInputValue , setLocalInputValueWithDisplay ) ;
187259 }
188260 } ;
189261
@@ -240,8 +312,9 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
240312 if ( snapValue && ! areCustomStepsContinuous ) {
241313 thumbRef . current . style . setProperty ( cssSliderValue . name , `${ snapValue } %` ) ;
242314 setValue ( snapValue ) ;
315+ updateInputFromSliderValue ( snapValue ) ;
243316 if ( onChange ) {
244- onChange ( e , snapValue ) ;
317+ onChange ( e , snapValue , undefined , setLocalInputValueWithDisplay ) ;
245318 }
246319 }
247320 } ;
@@ -308,16 +381,22 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
308381 }
309382
310383 // Call onchange callback
384+ const resolvedValue = snapValue !== undefined ? snapValue : newValue ;
385+ updateInputFromSliderValue ( resolvedValue ) ;
386+
311387 if ( onChange ) {
312- if ( snapValue !== undefined ) {
313- onChange ( e , snapValue ) ;
314- } else {
315- onChange ( e , newValue ) ;
316- }
388+ onChange ( e , resolvedValue , undefined , setLocalInputValueWithDisplay ) ;
317389 }
318390 } ;
319391
320- const callbackThumbMove = useCallback ( handleThumbMove , [ min , max , customSteps , onChange ] ) ;
392+ const callbackThumbMove = useCallback ( handleThumbMove , [
393+ min ,
394+ max ,
395+ customSteps ,
396+ onChange ,
397+ updateInputFromSliderValue ,
398+ setLocalInputValueWithDisplay
399+ ] ) ;
321400 const callbackThumbUp = useCallback ( handleThumbDragEnd , [ min , max , customSteps , onChange ] ) ;
322401
323402 const handleThumbKeys = ( e : React . KeyboardEvent ) => {
@@ -373,8 +452,9 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
373452 if ( newValue !== localValue ) {
374453 thumbRef . current . style . setProperty ( cssSliderValue . name , `${ newValue } %` ) ;
375454 setValue ( newValue ) ;
455+ updateInputFromSliderValue ( newValue ) ;
376456 if ( onChange ) {
377- onChange ( e , newValue ) ;
457+ onChange ( e , newValue , undefined , setLocalInputValueWithDisplay ) ;
378458 }
379459 }
380460 } ;
@@ -383,8 +463,9 @@ export const Slider: React.FunctionComponent<SliderProps> = ({
383463 const textInput = (
384464 < TextInput
385465 isDisabled = { isDisabled }
386- type = "number"
387- value = { localInputValue }
466+ type = "text"
467+ inputMode = "decimal"
468+ value = { inputDisplayValue }
388469 aria-label = { inputAriaLabel }
389470 onKeyDown = { handleKeyPressOnInput }
390471 onChange = { onChangeHandler }
0 commit comments