1
+ // helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider component
1
2
import React , { useRef } from 'react' ;
2
3
import propTypes from 'prop-types' ;
3
4
@@ -10,9 +11,10 @@ import {
10
11
createHatchedBackground
11
12
} from '../common' ;
12
13
import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled' ;
14
+ import useForkRef from '../common/hooks/useForkRef' ;
15
+ import { useIsFocusVisible } from '../common/hooks/focusVisible' ;
13
16
import Cutout from '../Cutout/Cutout' ;
14
17
15
- // helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider component
16
18
function trackFinger ( event , touchId ) {
17
19
if ( touchId . current !== undefined && event . changedTouches ) {
18
20
for ( let i = 0 ; i < event . changedTouches . length ; i += 1 ) {
@@ -82,19 +84,51 @@ function roundValueToStep(value, step, min) {
82
84
const nearest = Math . round ( ( value - min ) / step ) * step + min ;
83
85
return Number ( nearest . toFixed ( getDecimalPrecision ( step ) ) ) ;
84
86
}
87
+ function focusThumb ( sliderRef ) {
88
+ if ( ! sliderRef . current . contains ( document . activeElement ) ) {
89
+ sliderRef . current . querySelector ( `#swag` ) . focus ( ) ;
90
+ }
91
+ }
85
92
const Wrapper = styled . div `
86
93
display: inline-block;
87
94
position: relative;
88
95
touch-action: none;
96
+ &:before {
97
+ content: '';
98
+ display: inline-block;
99
+ position: absolute;
100
+ top: -2px;
101
+ left: -15px;
102
+ width: calc(100% + 30px);
103
+ height: ${ ( { hasMarks } ) => ( hasMarks ? '41px' : '39px' ) } ;
104
+ ${ ( { isFocused, theme } ) =>
105
+ isFocused &&
106
+ `
107
+ outline: 2px dotted ${ theme . text } ;
108
+ ` }
109
+ }
110
+
89
111
${ ( { vertical, size } ) =>
90
112
vertical
91
113
? css `
92
114
height: ${ size } ;
93
115
margin-right: 1.5rem;
116
+ &:before {
117
+ left: -2px;
118
+ top: -15px;
119
+ height: calc(100% + 30px);
120
+ width: ${ ( { hasMarks } ) => ( hasMarks ? '41px' : '39px' ) } ;
121
+ }
94
122
`
95
123
: css `
96
124
width: ${ size } ;
97
125
margin-bottom: 1.5rem;
126
+ &:before {
127
+ top: -2px;
128
+ left: -15px;
129
+ width: calc(100% + 30px);
130
+ height: ${ ( { hasMarks } ) => ( hasMarks ? '41px' : '39px' ) } ;
131
+ }
98
132
` }
99
133
100
134
pointer-events: ${ ( { isDisabled } ) => ( isDisabled ? 'none' : 'auto' ) } ;
@@ -220,38 +254,124 @@ const Mark = styled.div`
220
254
` }
221
255
` ;
222
256
223
- const Slider = ( {
224
- value,
225
- defaultValue,
226
- step,
227
- min,
228
- max,
229
- size,
230
- marks : marksProp ,
231
- onChange,
232
- onChangeCommitted,
233
- onMouseDown,
234
- name,
235
- vertical,
236
- variant,
237
- disabled,
238
- ...otherProps
239
- } ) => {
257
+ const Slider = React . forwardRef ( function Slider ( props , ref ) {
258
+ const {
259
+ value,
260
+ defaultValue,
261
+ step,
262
+ min,
263
+ max,
264
+ size,
265
+ marks : marksProp ,
266
+ onChange,
267
+ onChangeCommitted,
268
+ onMouseDown,
269
+ name,
270
+ vertical,
271
+ variant,
272
+ disabled,
273
+ ...otherProps
274
+ } = props ;
240
275
const Groove = variant === 'flat' ? StyledFlatGroove : StyledGroove ;
276
+
241
277
const [ valueDerived , setValueState ] = useControlledOrUncontrolled ( {
242
278
value,
243
279
defaultValue
244
280
} ) ;
245
281
282
+ const {
283
+ isFocusVisible,
284
+ onBlurVisible,
285
+ ref : focusVisibleRef
286
+ } = useIsFocusVisible ( ) ;
287
+ const [ focusVisible , setFocusVisible ] = React . useState ( false ) ;
246
288
const sliderRef = useRef ( ) ;
289
+ const handleFocusRef = useForkRef ( focusVisibleRef , sliderRef ) ;
290
+ const handleRef = useForkRef ( ref , handleFocusRef ) ;
291
+
292
+ const handleFocus = useEventCallback ( event => {
293
+ if ( isFocusVisible ( event ) ) {
294
+ setFocusVisible ( true ) ;
295
+ }
296
+ } ) ;
297
+ const handleBlur = useEventCallback ( ( ) => {
298
+ if ( focusVisible !== false ) {
299
+ setFocusVisible ( false ) ;
300
+ onBlurVisible ( ) ;
301
+ }
302
+ } ) ;
303
+
247
304
const touchId = React . useRef ( ) ;
248
305
249
306
const marks =
250
- marksProp === true
251
- ? Array ( 1 + ( max - min ) / step )
252
- . fill ( { label : null } )
253
- . map ( ( mark , i ) => ( { ...mark , value : i * step } ) )
254
- : marksProp ;
307
+ marksProp === true && step !== null
308
+ ? [ ...Array ( Math . floor ( ( max - min ) / step ) + 1 ) ] . map ( ( _ , index ) => ( {
309
+ value : min + step * index
310
+ } ) )
311
+ : marksProp || [ ] ;
312
+
313
+ const handleKeyDown = useEventCallback ( event => {
314
+ const tenPercents = ( max - min ) / 10 ;
315
+ const marksValues = marks . map ( mark => mark . value ) ;
316
+ const marksIndex = marksValues . indexOf ( valueDerived ) ;
317
+ let newValue ;
318
+
319
+ switch ( event . key ) {
320
+ case 'Home' :
321
+ newValue = min ;
322
+ break ;
323
+ case 'End' :
324
+ newValue = max ;
325
+ break ;
326
+ case 'PageUp' :
327
+ if ( step ) {
328
+ newValue = valueDerived + tenPercents ;
329
+ }
330
+ break ;
331
+ case 'PageDown' :
332
+ if ( step ) {
333
+ newValue = valueDerived - tenPercents ;
334
+ }
335
+ break ;
336
+ case 'ArrowRight' :
337
+ case 'ArrowUp' :
338
+ if ( step ) {
339
+ newValue = valueDerived + step ;
340
+ } else {
341
+ newValue =
342
+ marksValues [ marksIndex + 1 ] || marksValues [ marksValues . length - 1 ] ;
343
+ }
344
+ break ;
345
+ case 'ArrowLeft' :
346
+ case 'ArrowDown' :
347
+ if ( step ) {
348
+ newValue = valueDerived - step ;
349
+ } else {
350
+ newValue = marksValues [ marksIndex - 1 ] || marksValues [ 0 ] ;
351
+ }
352
+ break ;
353
+ default :
354
+ return ;
355
+ }
356
+
357
+ // Prevent scroll of the page
358
+ event . preventDefault ( ) ;
359
+ if ( step ) {
360
+ newValue = roundValueToStep ( newValue , step , min ) ;
361
+ }
362
+
363
+ newValue = clamp ( newValue , min , max ) ;
364
+
365
+ setValueState ( newValue ) ;
366
+ setFocusVisible ( true ) ;
367
+
368
+ if ( onChange ) {
369
+ onChange ( newValue ) ;
370
+ }
371
+ if ( onChangeCommitted ) {
372
+ onChangeCommitted ( newValue ) ;
373
+ }
374
+ } ) ;
255
375
256
376
const getNewValue = React . useCallback (
257
377
finger => {
@@ -288,7 +408,9 @@ const Slider = ({
288
408
}
289
409
const newValue = getNewValue ( finger ) ;
290
410
411
+ focusThumb ( sliderRef ) ;
291
412
setValueState ( newValue ) ;
413
+ setFocusVisible ( true ) ;
292
414
293
415
if ( onChange ) {
294
416
onChange ( newValue ) ;
@@ -302,6 +424,7 @@ const Slider = ({
302
424
}
303
425
304
426
const newValue = getNewValue ( finger ) ;
427
+
305
428
if ( onChangeCommitted ) {
306
429
onChangeCommitted ( newValue ) ;
307
430
}
@@ -322,8 +445,11 @@ const Slider = ({
322
445
event . preventDefault ( ) ;
323
446
const finger = trackFinger ( event , touchId ) ;
324
447
const newValue = getNewValue ( finger ) ;
448
+ focusThumb ( sliderRef ) ;
325
449
326
450
setValueState ( newValue ) ;
451
+ setFocusVisible ( true ) ;
452
+
327
453
if ( onChange ) {
328
454
onChange ( newValue ) ;
329
455
}
@@ -341,7 +467,10 @@ const Slider = ({
341
467
}
342
468
const finger = trackFinger ( event , touchId ) ;
343
469
const newValue = getNewValue ( finger ) ;
470
+ focusThumb ( sliderRef ) ;
471
+
344
472
setValueState ( newValue ) ;
473
+ setFocusVisible ( true ) ;
345
474
346
475
if ( onChange ) {
347
476
onChange ( newValue ) ;
@@ -371,7 +500,9 @@ const Slider = ({
371
500
vertical = { vertical }
372
501
size = { size }
373
502
onMouseDown = { handleMouseDown }
374
- ref = { sliderRef }
503
+ ref = { handleRef }
504
+ isFocused = { focusVisible }
505
+ hasMarks = { marks . length }
375
506
{ ...otherProps }
376
507
>
377
508
{ /* should we keep the hidden input ? */ }
@@ -403,10 +534,12 @@ const Slider = ({
403
534
< Groove vertical = { vertical } variant = { variant } />
404
535
< Thumb
405
536
role = 'slider'
537
+ id = 'swag'
406
538
style = { {
407
539
[ vertical ? 'bottom' : 'left' ] : `${ ( vertical ? - 100 : 0 ) +
408
540
( 100 * valueDerived ) / ( max - min ) } %`
409
541
} }
542
+ tabIndex = { disabled ? null : 0 }
410
543
vertical = { vertical }
411
544
variant = { variant }
412
545
isDisabled = { disabled }
@@ -415,10 +548,13 @@ const Slider = ({
415
548
aria-valuemax = { max }
416
549
aria-valuemin = { min }
417
550
aria-valuenow = { valueDerived }
551
+ onKeyDown = { handleKeyDown }
552
+ onFocus = { handleFocus }
553
+ onBlur = { handleBlur }
418
554
/>
419
555
</ Wrapper >
420
556
) ;
421
- } ;
557
+ } ) ;
422
558
423
559
Slider . defaultProps = {
424
560
defaultValue : undefined ,
0 commit comments