Skip to content

Commit

Permalink
RangeControl: Improve initial hover interaction with Tooltip (#20219)
Browse files Browse the repository at this point in the history
* RangeControl: Improve initial hover interaction with Tooltip

This update improves the initial hover/mouseenter experience with the RangeControl's Tooltip. This is achieved be debouncing the interaction. By debouncing, the Tooltip rendering is less jarring when there are multiple `RangeControl` components next to each other.

A Storybook story was added to test and simulate this experience.

Lastly, the utility functions/hooks from `RangeControl` was abstracted to a dedicated `utils.js` file under the component directory.

* Simplifies setValue to be a plain function vs using useCallback

* Remove local ref from useCallback hook

* Remove guard for clearTimeout

* Add clearTimeout callback on unmount

* Improve Tooltip rendering for current RangeControl changes...

...with other RangeControl hover interactions.

* Removed whitespace + adjusted setState naming
  • Loading branch information
Jon Quach authored Feb 20, 2020
1 parent 5d8701c commit 56c043c
Show file tree
Hide file tree
Showing 4 changed files with 657 additions and 87 deletions.
92 changes: 5 additions & 87 deletions packages/components/src/range-control/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,7 @@ import { clamp, noop } from 'lodash';
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import {
useCallback,
useRef,
useEffect,
useState,
forwardRef,
} from '@wordpress/element';
import { useRef, useState, forwardRef } from '@wordpress/element';
import { compose, withInstanceId } from '@wordpress/compose';

/**
Expand All @@ -23,8 +17,8 @@ import { compose, withInstanceId } from '@wordpress/compose';
import BaseControl from '../base-control';
import Button from '../button';
import Icon from '../icon';

import { color } from '../utils/colors';
import { useControlledRangeValue, useDebouncedHoverInteraction } from './utils';
import RangeRail from './rail';
import SimpleTooltip from './tooltip';
import {
Expand Down Expand Up @@ -93,7 +87,9 @@ const BaseRangeControl = forwardRef(
}
};

const isCurrentlyFocused = inputRef.current?.matches( ':focus' );
const isThumbFocused = ! disabled && isFocused;

const fillValue = ( ( value - min ) / ( max - min ) ) * 100;
const fillValueOffset = `${ clamp( fillValue, 0, 100 ) }%`;

Expand Down Expand Up @@ -219,7 +215,7 @@ const BaseRangeControl = forwardRef(
className="components-range-control__tooltip"
inputRef={ inputRef }
renderTooltipContent={ renderTooltipContent }
show={ showTooltip || showTooltip }
show={ isCurrentlyFocused || showTooltip }
style={ offsetStyle }
value={ value }
/>
Expand Down Expand Up @@ -262,84 +258,6 @@ const BaseRangeControl = forwardRef(
}
);

/**
* A float supported clamp function for a specific value.
*
* @param {number} value The value to clamp
* @param {number} min The minimum value
* @param {number} max The maxinum value
* @return {number} A (float) number
*/
function floatClamp( value, min, max ) {
return parseFloat( clamp( value, min, max ) );
}

/**
* Hook to store a clamped value, derived from props.
*/
function useControlledRangeValue( { min, max, value: valueProp = 0 } ) {
const [ value, _setValue ] = useState( floatClamp( valueProp, min, max ) );
const valueRef = useRef( value );

const setValue = useCallback(
( nextValue ) => {
_setValue( floatClamp( nextValue, min, max ) );
},
[ _setValue, min, max ]
);

useEffect( () => {
if ( valueRef.current !== valueProp ) {
setValue( valueProp );
valueRef.current = valueProp;
}
}, [ valueRef, valueProp, setValue ] );

return [ value, setValue ];
}

/**
* Hook to encapsulate the debouncing "hover" to better handle the showing
* and hiding of the Tooltip.
*/
function useDebouncedHoverInteraction( {
onShow = noop,
onHide = noop,
onMouseEnter = noop,
onMouseLeave = noop,
timeout = 250,
} ) {
const [ show, setShow ] = useState( false );
const timeoutRef = useRef();

const handleOnMouseEnter = useCallback( ( event ) => {
onMouseEnter( event );

if ( timeoutRef.current ) {
window.clearTimeout( timeoutRef.current );
}

if ( ! show ) {
setShow( true );
onShow();
}
}, [] );

const handleOnMouseLeave = useCallback( ( event ) => {
onMouseLeave( event );

timeoutRef.current = setTimeout( () => {
setShow( false );
onHide();
}, timeout );
}, [] );

return {
onMouseEnter: handleOnMouseEnter,
onMouseLeave: handleOnMouseLeave,
};
}

export const RangeControlNext = compose( withInstanceId )( BaseRangeControl );

export default RangeControlNext;
11 changes: 11 additions & 0 deletions packages/components/src/range-control/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ export const customMarks = () => {
);
};

export const multiple = () => {
return (
<Wrapper>
<RangeControlWithState />
<RangeControlWithState />
<RangeControlWithState />
<RangeControlWithState />
</Wrapper>
);
};

const Wrapper = styled.div`
padding: 60px 40px;
`;
98 changes: 98 additions & 0 deletions packages/components/src/range-control/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* External dependencies
*/
import { clamp, noop } from 'lodash';

/**
* WordPress dependencies
*/
import { useCallback, useRef, useEffect, useState } from '@wordpress/element';

/**
* A float supported clamp function for a specific value.
*
* @param {number} value The value to clamp
* @param {number} min The minimum value
* @param {number} max The maxinum value
*
* @return {number} A (float) number
*/
function floatClamp( value, min, max ) {
return parseFloat( clamp( value, min, max ) );
}

/**
* Hook to store a clamped value, derived from props.
*/
export function useControlledRangeValue( { min, max, value: valueProp = 0 } ) {
const [ value, setValue ] = useState( floatClamp( valueProp, min, max ) );
const valueRef = useRef( value );

const setClampValue = ( nextValue ) => {
setValue( floatClamp( nextValue, min, max ) );
};

useEffect( () => {
if ( valueRef.current !== valueProp ) {
setClampValue( valueProp );
valueRef.current = valueProp;
}
}, [ valueProp, setClampValue ] );

return [ value, setClampValue ];
}

/**
* Hook to encapsulate the debouncing "hover" to better handle the showing
* and hiding of the Tooltip.
*/
export function useDebouncedHoverInteraction( {
onShow = noop,
onHide = noop,
onMouseEnter = noop,
onMouseLeave = noop,
timeout = 300,
} ) {
const [ show, setShow ] = useState( false );
const timeoutRef = useRef();

const setDebouncedTimeout = useCallback(
( callback ) => {
window.clearTimeout( timeoutRef.current );

timeoutRef.current = setTimeout( callback, timeout );
},
[ timeout ]
);

const handleOnMouseEnter = useCallback( ( event ) => {
onMouseEnter( event );

setDebouncedTimeout( () => {
if ( ! show ) {
setShow( true );
onShow();
}
} );
}, [] );

const handleOnMouseLeave = useCallback( ( event ) => {
onMouseLeave( event );

setDebouncedTimeout( () => {
setShow( false );
onHide();
} );
}, [] );

useEffect( () => {
return () => {
window.clearTimeout( timeoutRef.current );
};
} );

return {
onMouseEnter: handleOnMouseEnter,
onMouseLeave: handleOnMouseLeave,
};
}
Loading

0 comments on commit 56c043c

Please sign in to comment.