diff --git a/packages/components/src/range-control/index.js b/packages/components/src/range-control/index.js index 98b85639bd66d9..365eb921e85efa 100644 --- a/packages/components/src/range-control/index.js +++ b/packages/components/src/range-control/index.js @@ -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'; /** @@ -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 { @@ -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 ) }%`; @@ -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 } /> @@ -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; diff --git a/packages/components/src/range-control/stories/index.js b/packages/components/src/range-control/stories/index.js index 28170196098da4..44c7953ece94e9 100644 --- a/packages/components/src/range-control/stories/index.js +++ b/packages/components/src/range-control/stories/index.js @@ -153,6 +153,17 @@ export const customMarks = () => { ); }; +export const multiple = () => { + return ( + + + + + + + ); +}; + const Wrapper = styled.div` padding: 60px 40px; `; diff --git a/packages/components/src/range-control/utils.js b/packages/components/src/range-control/utils.js new file mode 100644 index 00000000000000..4c39afebe99109 --- /dev/null +++ b/packages/components/src/range-control/utils.js @@ -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, + }; +} diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap index 612e4165da1333..5494f5885b5a26 100644 --- a/storybook/test/__snapshots__/index.js.snap +++ b/storybook/test/__snapshots__/index.js.snap @@ -5678,6 +5678,549 @@ input[type='number'].emotion-14 { `; +exports[`Storyshots Components/RangeControl Multiple 1`] = ` +.emotion-16 { + -webkit-tap-highlight-color: transparent; + box-sizing: border-box; + cursor: pointer; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 0; + position: relative; + touch-action: none; + width: 100%; +} + +.emotion-12 { + box-sizing: border-box; + color: #007cba; + display: block; + padding-top: 15px; + position: relative; + width: 100%; + height: 30px; + min-height: 30px; + margin-left: 10px; +} + +.emotion-0 { + box-sizing: border-box; + cursor: pointer; + display: block; + height: 100%; + left: 0; + margin: 0; + opacity: 0; + outline: none; + position: absolute; + right: 0; + top: 0; + width: 100%; +} + +.emotion-2 { + background-color: #d7dade; + box-sizing: border-box; + left: 0; + pointer-events: none; + right: 0; + display: block; + height: 3px; + position: absolute; + margin-top: 14px; + top: 0; +} + +.emotion-4 { + background-color: currentColor; + border-radius: 1px; + box-sizing: border-box; + height: 3px; + pointer-events: none; + display: block; + position: absolute; + margin-top: 14px; + top: 0; +} + +.emotion-8 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + box-sizing: border-box; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + height: 20px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: 5px; + outline: 0; + pointer-events: none; + position: absolute; + top: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 20px; + margin-left: -10px; +} + +.emotion-6 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: white; + border-radius: 50%; + border: 1px solid #7e8993; + box-sizing: border-box; + height: 100%; + outline: 0; + pointer-events: none; + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100%; + border-color: #7e8993; + box-shadow: 0 0 0 rgba(0,0,0,0); +} + +.emotion-10 { + background: #23282d; + border-radius: 3px; + box-sizing: border-box; + color: white; + display: inline-block; + font-size: 11px; + min-width: 32px; + opacity: 0; + padding: 8px; + position: absolute; + text-align: center; + -webkit-transition: opacity 120ms ease; + transition: opacity 120ms ease; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: 0; + margin-top: -4px; + top: -100%; + -webkit-transform: translateX(-50%); + -ms-transform: translateX(-50%); + transform: translateX(-50%); +} + +.emotion-10::after { + border: 6px solid #23282d; + border-left-color: transparent; + border-right-color: transparent; + bottom: -6px; + box-sizing: border-box; + content: ''; + height: 0; + left: 50%; + line-height: 0; + margin-left: -6px; + position: absolute; + width: 0; +} + +.emotion-10::after { + border-bottom: none; + border-top-style: solid; + bottom: -6px; +} + +@media ( prefers-reduced-motion:reduce ) { + .emotion-10 { + -webkit-transition-duration: 0ms; + transition-duration: 0ms; + } +} + +.emotion-14 { + box-sizing: border-box; + display: inline-block; + margin-top: 0; + min-width: 54px; + max-width: 120px; + margin-left: 16px; +} + +input[type='number'].emotion-14 { + height: 30px; + min-height: 30px; +} + +.emotion-72 { + padding: 60px 40px; +} + +
+
+
+ + + + + + + + + + 5 + + + + +
+
+
+
+ + + + + + + + + + 5 + + + + +
+
+
+
+ + + + + + + + + + 5 + + + + +
+
+
+
+ + + + + + + + + + 5 + + + + +
+
+
+`; + exports[`Storyshots Components/RangeControl With Help 1`] = ` .emotion-16 { -webkit-tap-highlight-color: transparent;