1+ // helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider component
12import React , { useRef } from 'react' ;
23import propTypes from 'prop-types' ;
34
@@ -10,9 +11,10 @@ import {
1011 createHatchedBackground
1112} from '../common' ;
1213import useControlledOrUncontrolled from '../common/hooks/useControlledOrUncontrolled' ;
14+ import useForkRef from '../common/hooks/useForkRef' ;
15+ import { useIsFocusVisible } from '../common/hooks/focusVisible' ;
1316import Cutout from '../Cutout/Cutout' ;
1417
15- // helper functions and event handling basically copied from Material UI (https://github.com/mui-org/material-ui) Slider component
1618function trackFinger ( event , touchId ) {
1719 if ( touchId . current !== undefined && event . changedTouches ) {
1820 for ( let i = 0 ; i < event . changedTouches . length ; i += 1 ) {
@@ -82,19 +84,51 @@ function roundValueToStep(value, step, min) {
8284 const nearest = Math . round ( ( value - min ) / step ) * step + min ;
8385 return Number ( nearest . toFixed ( getDecimalPrecision ( step ) ) ) ;
8486}
87+ function focusThumb ( sliderRef ) {
88+ if ( ! sliderRef . current . contains ( document . activeElement ) ) {
89+ sliderRef . current . querySelector ( `#swag` ) . focus ( ) ;
90+ }
91+ }
8592const Wrapper = styled . div `
8693 display: inline-block;
8794 position: relative;
8895 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+
89111 ${ ( { vertical, size } ) =>
90112 vertical
91113 ? css `
92114 height: ${ size } ;
93115 margin-right: 1.5rem;
116+ &:before {
117+ left: -2px;
118+ top: -15px;
119+ height: calc(100% + 30px);
120+ width: ${ ( { hasMarks } ) => ( hasMarks ? '41px' : '39px' ) } ;
121+ }
94122 `
95123 : css `
96124 width: ${ size } ;
97125 margin-bottom: 1.5rem;
126+ &:before {
127+ top: -2px;
128+ left: -15px;
129+ width: calc(100% + 30px);
130+ height: ${ ( { hasMarks } ) => ( hasMarks ? '41px' : '39px' ) } ;
131+ }
98132 ` }
99133
100134 pointer-events: ${ ( { isDisabled } ) => ( isDisabled ? 'none' : 'auto' ) } ;
@@ -220,38 +254,124 @@ const Mark = styled.div`
220254 ` }
221255` ;
222256
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 ;
240275 const Groove = variant === 'flat' ? StyledFlatGroove : StyledGroove ;
276+
241277 const [ valueDerived , setValueState ] = useControlledOrUncontrolled ( {
242278 value,
243279 defaultValue
244280 } ) ;
245281
282+ const {
283+ isFocusVisible,
284+ onBlurVisible,
285+ ref : focusVisibleRef
286+ } = useIsFocusVisible ( ) ;
287+ const [ focusVisible , setFocusVisible ] = React . useState ( false ) ;
246288 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+
247304 const touchId = React . useRef ( ) ;
248305
249306 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+ } ) ;
255375
256376 const getNewValue = React . useCallback (
257377 finger => {
@@ -288,7 +408,9 @@ const Slider = ({
288408 }
289409 const newValue = getNewValue ( finger ) ;
290410
411+ focusThumb ( sliderRef ) ;
291412 setValueState ( newValue ) ;
413+ setFocusVisible ( true ) ;
292414
293415 if ( onChange ) {
294416 onChange ( newValue ) ;
@@ -302,6 +424,7 @@ const Slider = ({
302424 }
303425
304426 const newValue = getNewValue ( finger ) ;
427+
305428 if ( onChangeCommitted ) {
306429 onChangeCommitted ( newValue ) ;
307430 }
@@ -322,8 +445,11 @@ const Slider = ({
322445 event . preventDefault ( ) ;
323446 const finger = trackFinger ( event , touchId ) ;
324447 const newValue = getNewValue ( finger ) ;
448+ focusThumb ( sliderRef ) ;
325449
326450 setValueState ( newValue ) ;
451+ setFocusVisible ( true ) ;
452+
327453 if ( onChange ) {
328454 onChange ( newValue ) ;
329455 }
@@ -341,7 +467,10 @@ const Slider = ({
341467 }
342468 const finger = trackFinger ( event , touchId ) ;
343469 const newValue = getNewValue ( finger ) ;
470+ focusThumb ( sliderRef ) ;
471+
344472 setValueState ( newValue ) ;
473+ setFocusVisible ( true ) ;
345474
346475 if ( onChange ) {
347476 onChange ( newValue ) ;
@@ -371,7 +500,9 @@ const Slider = ({
371500 vertical = { vertical }
372501 size = { size }
373502 onMouseDown = { handleMouseDown }
374- ref = { sliderRef }
503+ ref = { handleRef }
504+ isFocused = { focusVisible }
505+ hasMarks = { marks . length }
375506 { ...otherProps }
376507 >
377508 { /* should we keep the hidden input ? */ }
@@ -403,10 +534,12 @@ const Slider = ({
403534 < Groove vertical = { vertical } variant = { variant } />
404535 < Thumb
405536 role = 'slider'
537+ id = 'swag'
406538 style = { {
407539 [ vertical ? 'bottom' : 'left' ] : `${ ( vertical ? - 100 : 0 ) +
408540 ( 100 * valueDerived ) / ( max - min ) } %`
409541 } }
542+ tabIndex = { disabled ? null : 0 }
410543 vertical = { vertical }
411544 variant = { variant }
412545 isDisabled = { disabled }
@@ -415,10 +548,13 @@ const Slider = ({
415548 aria-valuemax = { max }
416549 aria-valuemin = { min }
417550 aria-valuenow = { valueDerived }
551+ onKeyDown = { handleKeyDown }
552+ onFocus = { handleFocus }
553+ onBlur = { handleBlur }
418554 />
419555 </ Wrapper >
420556 ) ;
421- } ;
557+ } ) ;
422558
423559Slider . defaultProps = {
424560 defaultValue : undefined ,
0 commit comments