From 0eb175fec6783eefb741089c4b5c247481041953 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 8 Jul 2022 17:39:38 +1000
Subject: [PATCH] First pass porting G2 Slider component
---
packages/components/src/slider/index.ts | 2 +
.../src/slider/slider/component.tsx | 32 +
packages/components/src/slider/slider/hook.ts | 119 +++
.../components/src/slider/stories/index.tsx | 44 ++
packages/components/src/slider/styles.ts | 176 +++++
.../slider/test/__snapshots__/index.tsx.snap | 739 ++++++++++++++++++
packages/components/src/slider/test/index.tsx | 98 +++
packages/components/src/slider/types.ts | 26 +
.../components/src/utils/config-values.js | 107 +--
packages/components/src/utils/flow.js | 55 ++
packages/components/src/utils/interpolate.ts | 105 +++
packages/components/src/utils/style-mixins.js | 1 +
12 files changed, 1458 insertions(+), 46 deletions(-)
create mode 100644 packages/components/src/slider/index.ts
create mode 100644 packages/components/src/slider/slider/component.tsx
create mode 100644 packages/components/src/slider/slider/hook.ts
create mode 100644 packages/components/src/slider/stories/index.tsx
create mode 100644 packages/components/src/slider/styles.ts
create mode 100644 packages/components/src/slider/test/__snapshots__/index.tsx.snap
create mode 100644 packages/components/src/slider/test/index.tsx
create mode 100644 packages/components/src/slider/types.ts
create mode 100644 packages/components/src/utils/flow.js
create mode 100644 packages/components/src/utils/interpolate.ts
diff --git a/packages/components/src/slider/index.ts b/packages/components/src/slider/index.ts
new file mode 100644
index 0000000000000..b850d3a85c069
--- /dev/null
+++ b/packages/components/src/slider/index.ts
@@ -0,0 +1,2 @@
+export { default as Slider } from './slider/component';
+export { useSlider } from './slider/hook';
diff --git a/packages/components/src/slider/slider/component.tsx b/packages/components/src/slider/slider/component.tsx
new file mode 100644
index 0000000000000..a2e39ad3b52da
--- /dev/null
+++ b/packages/components/src/slider/slider/component.tsx
@@ -0,0 +1,32 @@
+/**
+ * Internal dependencies
+ */
+import { contextConnect, WordPressComponentProps } from '../../ui/context';
+import { useSlider } from './hook';
+
+import type { SliderProps } from '../types';
+
+const UnconnectedSlider = (
+ props: WordPressComponentProps< SliderProps, 'input', false >,
+ forwardedRef: React.ForwardedRef< any >
+) => {
+ const inputProps = useSlider( props );
+ return ;
+};
+
+/**
+ * `Slider` is a form component that lets users choose a value within a range.
+ *
+ * @example
+ * ```jsx
+ * import { Slider } from `@wordpress/components`
+ *
+ * function Example() {
+ * return (
+ *
+ * );
+ * }
+ * ```
+ */
+export const Slider = contextConnect( UnconnectedSlider, 'Slider' );
+export default Slider;
diff --git a/packages/components/src/slider/slider/hook.ts b/packages/components/src/slider/slider/hook.ts
new file mode 100644
index 0000000000000..21e286cc42c8b
--- /dev/null
+++ b/packages/components/src/slider/slider/hook.ts
@@ -0,0 +1,119 @@
+/**
+ * WordPress dependencies
+ */
+import { useCallback, useMemo, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import * as styles from '../styles';
+import { useContextSystem, WordPressComponentProps } from '../../ui/context';
+import { useControlledValue } from '../../utils/hooks';
+import { useCx } from '../../utils/hooks/use-cx';
+import { useFormGroupContextId } from '../../ui/form-group';
+import { parseCSSUnitValue, createCSSUnitValue } from '../../utils/unit-values';
+import { isValueNumeric } from '../../utils/values';
+import { interpolate } from '../../utils/interpolate';
+
+import type { SliderProps } from '../types';
+
+const noop = () => {};
+
+export function useSlider(
+ props: WordPressComponentProps< SliderProps, 'input', false >
+) {
+ const {
+ className,
+ defaultValue,
+ error,
+ onBlur = noop,
+ onChange: onChangeProp = noop,
+ onFocus = noop,
+ id: idProp,
+ isFocused: isFocusedProp = false,
+ max = 100,
+ min = 0,
+ size = 'medium',
+ style,
+ value: valueProp,
+ ...otherProps
+ } = useContextSystem( props, 'Slider' );
+
+ const [ _value, onChange ] = useControlledValue( {
+ defaultValue,
+ onChange: onChangeProp,
+ value: valueProp,
+ } );
+ const [ value, initialUnit ] = parseCSSUnitValue( `${ _value }` );
+
+ const id = useFormGroupContextId( idProp );
+ const [ isFocused, setIsFocused ] = useState( isFocusedProp );
+
+ const handleOnBlur = useCallback(
+ ( event ) => {
+ onBlur( event );
+ setIsFocused( false );
+ },
+ [ onBlur ]
+ );
+
+ const handleOnChange = useCallback(
+ ( event ) => {
+ const nextValue = parseFloat( event.target.value );
+ if ( ! isValueNumeric( nextValue ) ) {
+ return;
+ }
+
+ let next = `${ nextValue }`;
+
+ if ( initialUnit ) {
+ next = createCSSUnitValue( nextValue, initialUnit );
+ }
+
+ onChange( next );
+ },
+ [ onChange, initialUnit ]
+ );
+
+ const handleOnFocus = useCallback(
+ ( event ) => {
+ onFocus( event );
+ setIsFocused( true );
+ },
+ [ onFocus ]
+ );
+
+ const currentValue = interpolate(
+ value,
+ [ parseFloat( `${ min }` ), parseFloat( `${ max }` ) ],
+ [ 0, 100 ]
+ );
+ const componentStyles = { ...style, '--progress': `${ currentValue }%` };
+
+ // Generate dynamic class names.
+ const cx = useCx();
+ const classes = useMemo( () => {
+ return cx(
+ styles.slider,
+ error && styles.error,
+ styles[ size ],
+ isFocused && styles.focused,
+ error && isFocused && styles.focusedError,
+ className
+ );
+ }, [ className, cx, error, isFocused, size ] );
+
+ return {
+ ...otherProps,
+ className: classes,
+ id: id ? `${ id }` : undefined,
+ max,
+ min,
+ onBlur: handleOnBlur,
+ onChange: handleOnChange,
+ onFocus: handleOnFocus,
+ style: componentStyles,
+ type: 'range',
+ value,
+ };
+}
diff --git a/packages/components/src/slider/stories/index.tsx b/packages/components/src/slider/stories/index.tsx
new file mode 100644
index 0000000000000..eaa9774703977
--- /dev/null
+++ b/packages/components/src/slider/stories/index.tsx
@@ -0,0 +1,44 @@
+/**
+ * External dependencies
+ */
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { Slider } from '../';
+
+const meta: ComponentMeta< typeof Slider > = {
+ title: 'Components (Experimental)/Slider',
+ component: Slider,
+ argTypes: { onChange: { action: 'onChange' } },
+ parameters: {
+ controls: { expanded: true, exclude: [ 'heading' ] },
+ docs: { source: { state: 'open' } },
+ },
+};
+export default meta;
+
+const DefaultTemplate: ComponentStory< typeof Slider > = ( {
+ onChange,
+ value: valueProp,
+ ...args
+} ) => {
+ const [ value, setValue ] = useState( valueProp );
+ const handleChange = ( newValue ) => {
+ setValue( newValue );
+ onChange?.( newValue );
+ };
+
+ return ;
+};
+
+export const Default: ComponentStory< typeof Slider > = DefaultTemplate.bind(
+ {}
+);
+Default.args = {};
diff --git a/packages/components/src/slider/styles.ts b/packages/components/src/slider/styles.ts
new file mode 100644
index 0000000000000..6e79eb33bf056
--- /dev/null
+++ b/packages/components/src/slider/styles.ts
@@ -0,0 +1,176 @@
+/**
+ * External dependencies
+ */
+import { css } from '@emotion/react';
+
+/**
+ * Internal dependencies
+ */
+import { COLORS, CONFIG, flow } from '../utils';
+import { space } from '../ui/utils/space';
+
+const boxShadow = flow(
+ [
+ '0 0 0',
+ CONFIG.controlPseudoBoxShadowFocusWidth,
+ CONFIG.surfaceBackgroundColor,
+ ],
+ [
+ '0 0 0',
+ `calc(${ CONFIG.controlPseudoBoxShadowFocusWidth } + 1px)`,
+ COLORS.admin.theme,
+ ]
+);
+const errorBoxShadow = flow(
+ [
+ '0 0 0',
+ CONFIG.controlPseudoBoxShadowFocusWidth,
+ CONFIG.surfaceBackgroundColor,
+ ],
+ [
+ '0 0 0',
+ `calc(${ CONFIG.controlPseudoBoxShadowFocusWidth } + 1px)`,
+ COLORS.alert.red,
+ ]
+);
+
+function getFocusBoxShadow( color = boxShadow ) {
+ return css`
+ &::-webkit-slider-thumb {
+ box-shadow: ${ color };
+ }
+ &::-moz-range-thumb {
+ box-shadow: ${ color };
+ }
+ `;
+}
+
+export const focusedError = css`
+ ${ getFocusBoxShadow( errorBoxShadow ) };
+`;
+
+export const slider = css`
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: ${ CONFIG.controlBorderRadius };
+ cursor: pointer;
+ display: block;
+ height: ${ CONFIG.controlHeight };
+ max-width: 100%;
+ min-width: 0;
+ padding: ${ space( 1 ) };
+ width: 100%;
+
+ &:focus {
+ outline: none;
+ }
+
+ &::-moz-focus-outer {
+ border: 0;
+ }
+
+ &::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ ${ COLORS.admin.theme } calc( var( --progress ) ),
+ ${ CONFIG.controlBackgroundDimColor } calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+
+ *:disabled& {
+ background: ${ CONFIG.controlBackgroundDimColor };
+ }
+ }
+ &::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ ${ COLORS.admin.theme } calc( var( --progress ) ),
+ ${ CONFIG.controlBackgroundDimColor } calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+
+ *:disabled& {
+ background: ${ CONFIG.controlBackgroundDimColor };
+ }
+ }
+
+ &::-webkit-slider-thumb {
+ appearance: none;
+ background-color: ${ CONFIG.sliderThumbBackgroundColor };
+ border: 1px solid ${ CONFIG.sliderThumbBorderColor };
+ border-radius: 50%;
+ box-shadow: ${ CONFIG.sliderThumbBoxShadow };
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ transition: box-shadow ease ${ CONFIG.transitionDurationFast };
+
+ *:disabled& {
+ background: ${ COLORS.ui.textDisabled };
+ border-color: ${ COLORS.ui.textDisabled };
+ }
+ }
+ &::-moz-range-thumb {
+ appearance: none;
+ background-color: ${ CONFIG.sliderThumbBackgroundColor };
+ border: 1px solid ${ CONFIG.sliderThumbBorderColor };
+ border-radius: 50%;
+ box-shadow: ${ CONFIG.sliderThumbBoxShadow };
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ transition: box-shadow ease ${ CONFIG.transitionDurationFast };
+ will-change: transform;
+
+ *:disabled& {
+ background: ${ COLORS.ui.textDisabled };
+ border-color: ${ COLORS.ui.textDisabled };
+ }
+ }
+
+ &:focus {
+ ${ getFocusBoxShadow() }
+ }
+`;
+
+export const focused = css`
+ ${ getFocusBoxShadow() }
+`;
+
+export const error = css`
+ &::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ ${ CONFIG.controlDestructiveBorderColor } calc( var( --progress ) ),
+ ${ CONFIG.controlBackgroundDimColor } calc( var( --progress ) )
+ );
+ }
+ &::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ ${ CONFIG.controlDestructiveBorderColor } calc( var( --progress ) ),
+ ${ CONFIG.controlBackgroundDimColor } calc( var( --progress ) )
+ );
+ }
+
+ &::-webkit-slider-thumb {
+ background-color: ${ CONFIG.controlDestructiveBorderColor };
+ border: 1px solid ${ CONFIG.controlDestructiveBorderColor };
+ }
+ &::-moz-range-thumb {
+ background-color: ${ CONFIG.controlDestructiveBorderColor };
+ border: 1px solid ${ CONFIG.controlDestructiveBorderColor };
+ }
+
+ &:focus {
+ ${ getFocusBoxShadow( errorBoxShadow ) };
+ }
+`;
diff --git a/packages/components/src/slider/test/__snapshots__/index.tsx.snap b/packages/components/src/slider/test/__snapshots__/index.tsx.snap
new file mode 100644
index 0000000000000..df8cad0ecf185
--- /dev/null
+++ b/packages/components/src/slider/test/__snapshots__/index.tsx.snap
@@ -0,0 +1,739 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Slider should render correctly 1`] = `
+.emotion-0 {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ cursor: pointer;
+ display: block;
+ height: 36px;
+ max-width: 100%;
+ min-width: 0;
+ padding: calc(4px * 1);
+ width: 100%;
+}
+
+.emotion-0:focus {
+ outline: none;
+}
+
+.emotion-0::-moz-focus-outer {
+ border: 0;
+}
+
+.emotion-0::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+}
+
+*:disabled.emotion-0::-webkit-slider-runnable-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+}
+
+*:disabled.emotion-0::-webkit-slider-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0::-moz-range-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+.emotion-0:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+
+`;
+
+exports[`Slider should render max 1`] = `
+.emotion-0 {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ cursor: pointer;
+ display: block;
+ height: 36px;
+ max-width: 100%;
+ min-width: 0;
+ padding: calc(4px * 1);
+ width: 100%;
+}
+
+.emotion-0:focus {
+ outline: none;
+}
+
+.emotion-0::-moz-focus-outer {
+ border: 0;
+}
+
+.emotion-0::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+}
+
+*:disabled.emotion-0::-webkit-slider-runnable-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+}
+
+*:disabled.emotion-0::-webkit-slider-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0::-moz-range-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+.emotion-0:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+
+`;
+
+exports[`Slider should render min 1`] = `
+.emotion-0 {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ cursor: pointer;
+ display: block;
+ height: 36px;
+ max-width: 100%;
+ min-width: 0;
+ padding: calc(4px * 1);
+ width: 100%;
+}
+
+.emotion-0:focus {
+ outline: none;
+}
+
+.emotion-0::-moz-focus-outer {
+ border: 0;
+}
+
+.emotion-0::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+}
+
+*:disabled.emotion-0::-webkit-slider-runnable-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+}
+
+*:disabled.emotion-0::-webkit-slider-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0::-moz-range-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+.emotion-0:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+
+`;
+
+exports[`Slider should render size 1`] = `
+.emotion-0 {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ cursor: pointer;
+ display: block;
+ height: 36px;
+ max-width: 100%;
+ min-width: 0;
+ padding: calc(4px * 1);
+ width: 100%;
+}
+
+.emotion-0:focus {
+ outline: none;
+}
+
+.emotion-0::-moz-focus-outer {
+ border: 0;
+}
+
+.emotion-0::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+}
+
+*:disabled.emotion-0::-webkit-slider-runnable-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+}
+
+*:disabled.emotion-0::-webkit-slider-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0::-moz-range-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+.emotion-0:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+
+`;
+
+exports[`Slider should render unit value 1`] = `
+.emotion-0 {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ cursor: pointer;
+ display: block;
+ height: 36px;
+ max-width: 100%;
+ min-width: 0;
+ padding: calc(4px * 1);
+ width: 100%;
+}
+
+.emotion-0:focus {
+ outline: none;
+}
+
+.emotion-0::-moz-focus-outer {
+ border: 0;
+}
+
+.emotion-0::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+}
+
+*:disabled.emotion-0::-webkit-slider-runnable-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+}
+
+*:disabled.emotion-0::-webkit-slider-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0::-moz-range-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+.emotion-0:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+
+`;
+
+exports[`Slider should render value 1`] = `
+.emotion-0 {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 2px;
+ cursor: pointer;
+ display: block;
+ height: 36px;
+ max-width: 100%;
+ min-width: 0;
+ padding: calc(4px * 1);
+ width: 100%;
+}
+
+.emotion-0:focus {
+ outline: none;
+}
+
+.emotion-0::-moz-focus-outer {
+ border: 0;
+}
+
+.emotion-0::-webkit-slider-runnable-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+}
+
+*:disabled.emotion-0::-webkit-slider-runnable-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-moz-range-track {
+ background: linear-gradient(
+ to right,
+ var( --wp-admin-theme-color, #00669b) calc( var( --progress ) ),
+ #f3f4f5 calc( var( --progress ) )
+ );
+ border-radius: 2px;
+ height: 2px;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-track {
+ background: #f3f4f5;
+}
+
+.emotion-0::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+}
+
+*:disabled.emotion-0::-webkit-slider-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0::-moz-range-thumb {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ -ms-appearance: none;
+ appearance: none;
+ background-color: var( --wp-admin-theme-color, #00669b);
+ border: 1px solid transparent;
+ border-radius: 50%;
+ box-shadow: none;
+ cursor: pointer;
+ height: 12px;
+ margin-top: -5px;
+ opacity: 1;
+ width: 12px;
+ -webkit-transition: box-shadow ease 160ms;
+ transition: box-shadow ease 160ms;
+ will-change: transform;
+}
+
+*:disabled.emotion-0::-moz-range-thumb {
+ background: #8d96a0;
+ border-color: #8d96a0;
+}
+
+.emotion-0:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+.emotion-0:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 2px #fff,0 0 0 calc(2px + 1px) var( --wp-admin-theme-color, #00669b);
+}
+
+
+`;
diff --git a/packages/components/src/slider/test/index.tsx b/packages/components/src/slider/test/index.tsx
new file mode 100644
index 0000000000000..b1f001e568b13
--- /dev/null
+++ b/packages/components/src/slider/test/index.tsx
@@ -0,0 +1,98 @@
+/**
+ * External dependencies
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+
+/**
+ * WordPress dependencies
+ */
+import React from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { Slider } from '../index';
+
+const renderSlider = ( props = {} ) => {
+ // Disabled because of our rule restricting literal IDs, preferring
+ // `withInstanceId`. In this case, it's fine to use literal IDs.
+ // eslint-disable-next-line no-restricted-syntax
+ return render( );
+};
+
+const rerenderSlider = ( props = {}, rerender ) => {
+ // Disabled because of our rule restricting literal IDs, preferring
+ // `withInstanceId`. In this case, it's fine to use literal IDs.
+ // eslint-disable-next-line no-restricted-syntax
+ return rerender( );
+};
+
+const getSliderInput = (): HTMLInputElement => {
+ return screen.getByRole( 'slider' );
+};
+
+describe( 'Slider', () => {
+ test( 'should render correctly', () => {
+ const { container } = renderSlider();
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render min', () => {
+ const { container } = renderSlider( { min: 5 } );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render max', () => {
+ const { container } = renderSlider( { max: 50 } );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render size', () => {
+ const { container } = renderSlider( { size: 'small' } );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render value', () => {
+ const { container } = renderSlider( { value: 40 } );
+ expect( container.firstChild ).toMatchSnapshot();
+ } );
+
+ test( 'should render unit value', () => {
+ const { container } = renderSlider( { value: '40px' } );
+ const input = getSliderInput();
+
+ expect( container.firstChild ).toMatchSnapshot();
+ expect( input.value ).toEqual( '40' );
+ } );
+
+ test( 'should include unit in onChange callback (if value contains unit)', () => {
+ let value = '40px';
+ const setValue = ( next ) => ( value = next );
+
+ renderSlider( { onChange: setValue, value } );
+ const input = getSliderInput();
+ fireEvent.change( input, { target: { value: 13 } } );
+
+ // onChange callback value
+ expect( value ).toBe( '13px' );
+ } );
+
+ test( 'should change unit in onChange callback, if incoming value unit changes', () => {
+ let value = '40px';
+ const setValue = ( next ) => ( value = next );
+
+ const { rerender } = renderSlider( { onChange: setValue, value } );
+ const input = getSliderInput();
+
+ expect( input.value ).toBe( '40' );
+
+ rerenderSlider( { onChange: setValue, value: '100%' }, rerender );
+
+ expect( input.value ).toBe( '100' );
+
+ fireEvent.change( input, { target: { value: 13 } } );
+
+ // onChange callback value
+ expect( value ).toBe( '13%' );
+ } );
+} );
diff --git a/packages/components/src/slider/types.ts b/packages/components/src/slider/types.ts
new file mode 100644
index 0000000000000..28d7bbe3188bc
--- /dev/null
+++ b/packages/components/src/slider/types.ts
@@ -0,0 +1,26 @@
+export type SliderProps = {
+ /**
+ * Default value for input.
+ */
+ defaultValue?: string;
+ /**
+ * Renders an error state.
+ *
+ * @default false
+ */
+ error?: boolean;
+ /**
+ * Renders focused styles.
+ *
+ * @default false
+ */
+ isFocused?: boolean;
+ /**
+ * Callback function when the `value` is committed.
+ */
+ onChange?: ( value: string ) => void;
+ /**
+ * The Slider's current value.
+ */
+ value?: string;
+};
diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js
index 8f3e044261d73..66280ecabde3d 100644
--- a/packages/components/src/utils/config-values.js
+++ b/packages/components/src/utils/config-values.js
@@ -14,11 +14,14 @@ const CONTROL_PROPS = {
controlPaddingXLarge: `calc(${ CONTROL_PADDING_X } * 1.3334)`,
controlPaddingXSmall: `calc(${ CONTROL_PADDING_X } / 1.3334)`,
controlBackgroundColor: COLORS.white,
+ controlBackgroundDimColor: COLORS.ui.backgroundDisabled,
controlBorderRadius: '2px',
controlBorderColor: COLORS.gray[ 700 ],
controlBoxShadow: 'transparent',
controlBorderColorHover: COLORS.gray[ 700 ],
controlBoxShadowFocus: `0 0 0 0.5px ${ COLORS.admin.theme }`,
+ controlBoxShadowFocusSize: '0.5px',
+ controlPseudoBoxShadowFocusWidth: '2px',
controlDestructiveBorderColor: COLORS.alert.red,
controlHeight: CONTROL_HEIGHT,
controlHeightXSmall: `calc( ${ CONTROL_HEIGHT } * 0.6 )`,
@@ -27,6 +30,12 @@ const CONTROL_PROPS = {
controlHeightXLarge: `calc( ${ CONTROL_HEIGHT } * 1.4 )`,
};
+const SLIDER_PROPS = {
+ sliderThumbBorderColor: 'transparent',
+ sliderThumbBoxShadow: 'none',
+ sliderThumbBackgroundColor: COLORS.admin.theme,
+};
+
const TOGGLE_GROUP_CONTROL_PROPS = {
toggleGroupControlBackgroundColor: CONTROL_PROPS.controlBackgroundColor,
toggleGroupControlBorderColor: COLORS.ui.border,
@@ -39,49 +48,55 @@ const TOGGLE_GROUP_CONTROL_PROPS = {
// Using Object.assign to avoid creating circular references when emitting
// TypeScript type declarations.
-export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, {
- colorDivider: 'rgba(0, 0, 0, 0.1)',
- colorScrollbarThumb: 'rgba(0, 0, 0, 0.2)',
- colorScrollbarThumbHover: 'rgba(0, 0, 0, 0.5)',
- colorScrollbarTrack: 'rgba(0, 0, 0, 0.04)',
- elevationIntensity: 1,
- radiusBlockUi: '2px',
- borderWidth: '1px',
- borderWidthFocus: '1.5px',
- borderWidthTab: '4px',
- spinnerSize: 16,
- fontSize: '13px',
- fontSizeH1: 'calc(2.44 * 13px)',
- fontSizeH2: 'calc(1.95 * 13px)',
- fontSizeH3: 'calc(1.56 * 13px)',
- fontSizeH4: 'calc(1.25 * 13px)',
- fontSizeH5: '13px',
- fontSizeH6: 'calc(0.8 * 13px)',
- fontSizeInputMobile: '16px',
- fontSizeMobile: '15px',
- fontSizeSmall: 'calc(0.92 * 13px)',
- fontSizeXSmall: 'calc(0.75 * 13px)',
- fontLineHeightBase: '1.2',
- fontWeight: 'normal',
- fontWeightHeading: '600',
- gridBase: '4px',
- cardBorderRadius: '2px',
- cardPaddingXSmall: `${ space( 2 ) }`,
- cardPaddingSmall: `${ space( 4 ) }`,
- cardPaddingMedium: `${ space( 4 ) } ${ space( 6 ) }`,
- cardPaddingLarge: `${ space( 6 ) } ${ space( 8 ) }`,
- surfaceBackgroundColor: COLORS.white,
- surfaceBackgroundSubtleColor: '#F3F3F3',
- surfaceBackgroundTintColor: '#F5F5F5',
- surfaceBorderColor: 'rgba(0, 0, 0, 0.1)',
- surfaceBorderBoldColor: 'rgba(0, 0, 0, 0.15)',
- surfaceBorderSubtleColor: 'rgba(0, 0, 0, 0.05)',
- surfaceBackgroundTertiaryColor: COLORS.white,
- surfaceColor: COLORS.white,
- transitionDuration: '200ms',
- transitionDurationFast: '160ms',
- transitionDurationFaster: '120ms',
- transitionDurationFastest: '100ms',
- transitionTimingFunction: 'cubic-bezier(0.08, 0.52, 0.52, 1)',
- transitionTimingFunctionControl: 'cubic-bezier(0.12, 0.8, 0.32, 1)',
-} );
+export default Object.assign(
+ {},
+ CONTROL_PROPS,
+ SLIDER_PROPS,
+ TOGGLE_GROUP_CONTROL_PROPS,
+ {
+ colorDivider: 'rgba(0, 0, 0, 0.1)',
+ colorScrollbarThumb: 'rgba(0, 0, 0, 0.2)',
+ colorScrollbarThumbHover: 'rgba(0, 0, 0, 0.5)',
+ colorScrollbarTrack: 'rgba(0, 0, 0, 0.04)',
+ elevationIntensity: 1,
+ radiusBlockUi: '2px',
+ borderWidth: '1px',
+ borderWidthFocus: '1.5px',
+ borderWidthTab: '4px',
+ spinnerSize: 16,
+ fontSize: '13px',
+ fontSizeH1: 'calc(2.44 * 13px)',
+ fontSizeH2: 'calc(1.95 * 13px)',
+ fontSizeH3: 'calc(1.56 * 13px)',
+ fontSizeH4: 'calc(1.25 * 13px)',
+ fontSizeH5: '13px',
+ fontSizeH6: 'calc(0.8 * 13px)',
+ fontSizeInputMobile: '16px',
+ fontSizeMobile: '15px',
+ fontSizeSmall: 'calc(0.92 * 13px)',
+ fontSizeXSmall: 'calc(0.75 * 13px)',
+ fontLineHeightBase: '1.2',
+ fontWeight: 'normal',
+ fontWeightHeading: '600',
+ gridBase: '4px',
+ cardBorderRadius: '2px',
+ cardPaddingXSmall: `${ space( 2 ) }`,
+ cardPaddingSmall: `${ space( 4 ) }`,
+ cardPaddingMedium: `${ space( 4 ) } ${ space( 6 ) }`,
+ cardPaddingLarge: `${ space( 6 ) } ${ space( 8 ) }`,
+ surfaceBackgroundColor: COLORS.white,
+ surfaceBackgroundSubtleColor: '#F3F3F3',
+ surfaceBackgroundTintColor: '#F5F5F5',
+ surfaceBorderColor: 'rgba(0, 0, 0, 0.1)',
+ surfaceBorderBoldColor: 'rgba(0, 0, 0, 0.15)',
+ surfaceBorderSubtleColor: 'rgba(0, 0, 0, 0.05)',
+ surfaceBackgroundTertiaryColor: COLORS.white,
+ surfaceColor: COLORS.white,
+ transitionDuration: '200ms',
+ transitionDurationFast: '160ms',
+ transitionDurationFaster: '120ms',
+ transitionDurationFastest: '100ms',
+ transitionTimingFunction: 'cubic-bezier(0.08, 0.52, 0.52, 1)',
+ transitionTimingFunctionControl: 'cubic-bezier(0.12, 0.8, 0.32, 1)',
+ }
+);
diff --git a/packages/components/src/utils/flow.js b/packages/components/src/utils/flow.js
new file mode 100644
index 0000000000000..618d5ef79f6ce
--- /dev/null
+++ b/packages/components/src/utils/flow.js
@@ -0,0 +1,55 @@
+/** @typedef {number | string} FlowValue */
+
+/**
+ * Combines CSS values. Useful for complex shorthand values,
+ * functions (e.g. calc()), and mixed string/JS values.
+ *
+ * @example
+ * ```
+ * const boxShadow = flow(
+ * '0 1px',
+ * get('boxShadowSpreadValue'),
+ * '2px',
+ * get('boxShadowColor')
+ * )
+ * ```
+ *
+ * ##### Combining groups
+ *
+ * Groups (Array) can be passed into `flow()`, which are combined and
+ * comma separated. Useful for compounded CSS values (e.g. `box-shadow`).
+ *
+ * @example
+ * ```
+ * const boxShadow = flow([
+ * '0 1px',
+ * get('boxShadowSpreadValue'),
+ * '2px',
+ * get('boxShadowColor')
+ * ], [
+ * '0 10px',
+ * get('boxShadowSpreadValue'),
+ * '20px',
+ * get('boxShadowColor')
+ * ]
+ * )
+ * ```
+ *
+ * @param {(FlowValue | FlowValue[])[]} args CSS values to combine.
+ * @return {string} The combined CSS string value.
+ */
+export function flow( ...args ) {
+ /** @type {FlowValue[]} */
+ const results = [];
+
+ for ( const arg of args ) {
+ if ( typeof arg === 'number' || typeof arg === 'string' ) {
+ results.push( arg );
+ }
+ if ( Array.isArray( arg ) ) {
+ results.push( flow( ...arg ), ',' );
+ }
+ }
+
+ return results.join( ' ' ).trim().replace( /,$/, '' );
+}
diff --git a/packages/components/src/utils/interpolate.ts b/packages/components/src/utils/interpolate.ts
new file mode 100644
index 0000000000000..3c46f38363369
--- /dev/null
+++ b/packages/components/src/utils/interpolate.ts
@@ -0,0 +1,105 @@
+/**
+ * Internal dependencies
+ */
+import { clamp } from './math';
+
+/**
+ * Interpolation from:
+ * https://github.com/react-spring/react-spring/blob/master/src/animated/createInterpolator.ts
+ */
+
+function findRange( input: number, inputRange: number[] ): number {
+ let i;
+ for ( i = 1; i < inputRange.length - 1; ++i ) {
+ if ( inputRange[ i ] >= input ) {
+ break;
+ }
+ }
+ return i - 1;
+}
+
+/**
+ * Base interpolate function.
+ *
+ * @param {number} [input=0]
+ * @param {number} [inputMin=0]
+ * @param {number} [inputMax=1]
+ * @param {number} [outputMin=0]
+ * @param {number} [outputMax=1]
+ *
+ * @return {number} The interpolated value.
+ */
+function baseInterpolate(
+ input: number = 0,
+ inputMin: number = 0,
+ inputMax: number = 1,
+ outputMin: number = 0,
+ outputMax: number = 1
+) {
+ let result = input;
+
+ if ( outputMin === outputMax ) return outputMin;
+ if ( inputMin === inputMax )
+ return input <= inputMin ? outputMin : outputMax;
+
+ // Input Range
+ if ( inputMin === -Infinity ) result = -result;
+ else if ( inputMax === Infinity ) result = result - inputMin;
+ else result = ( result - inputMin ) / ( inputMax - inputMin );
+
+ // Output Range
+ if ( outputMin === -Infinity ) result = -result;
+ else if ( outputMax === Infinity ) result = result + outputMin;
+ else result = result * ( outputMax - outputMin ) + outputMin;
+
+ let clampMin = outputMin;
+ let clampMax = outputMax;
+
+ if ( outputMax < outputMin ) {
+ clampMin = outputMax;
+ clampMax = outputMin;
+ }
+
+ return clamp( result, clampMin, clampMax );
+}
+
+/**
+ * Gets a value based on an input range and an output range.
+ * Can be used for a set of numbers or a set of colors.
+ *
+ * @param {number} [input=0]
+ * @param {[number, number]} [inputRange=[0,1]]
+ * @param {[number, number]} [outputRange=[0,1]]
+ *
+ * @return {number} The interpolated value.
+ */
+export function interpolate(
+ input: number = 0,
+ inputRange: [ number, number ] = [ 0, 1 ],
+ outputRange: [ number, number ] = [ 0, 1 ]
+) {
+ const range = findRange( input, inputRange );
+ const outputRange1 = outputRange[ range ];
+ const outputRange2 = outputRange[ range + 1 ];
+
+ return baseInterpolate(
+ input,
+ inputRange[ range ],
+ inputRange[ range + 1 ],
+ outputRange1,
+ outputRange2
+ );
+}
+
+/**
+ * Gets a rounded value based on an input range and an output range.
+ *
+ * @param {Parameters} args
+ *
+ * @return {number} The rounded interpolated value.
+ */
+export function interpolateRounded(
+ ...args: [ number, [ number, number ], [ number, number ] ]
+) {
+ return Math.round( interpolate( ...args ) );
+}
diff --git a/packages/components/src/utils/style-mixins.js b/packages/components/src/utils/style-mixins.js
index dfacc79b0fb39..007c16e8ac4f9 100644
--- a/packages/components/src/utils/style-mixins.js
+++ b/packages/components/src/utils/style-mixins.js
@@ -3,5 +3,6 @@ export { reduceMotion } from './reduce-motion';
export { rtl } from './rtl';
export { font } from './font';
export { breakpoint } from './breakpoint';
+export { flow } from './flow';
export { default as CONFIG } from './config-values';
export { COLORS } from './colors-values';