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;
+}
+
+
+`;
+
exports[`Storyshots Components/RangeControl With Help 1`] = `
.emotion-16 {
-webkit-tap-highlight-color: transparent;