Skip to content

Commit

Permalink
First pass porting G2 Slider component
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronrobertshaw committed Jul 11, 2022
1 parent 86943ee commit 0eb175f
Show file tree
Hide file tree
Showing 12 changed files with 1,458 additions and 46 deletions.
2 changes: 2 additions & 0 deletions packages/components/src/slider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Slider } from './slider/component';
export { useSlider } from './slider/hook';
32 changes: 32 additions & 0 deletions packages/components/src/slider/slider/component.tsx
Original file line number Diff line number Diff line change
@@ -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 <input { ...inputProps } ref={ forwardedRef } />;
};

/**
* `Slider` is a form component that lets users choose a value within a range.
*
* @example
* ```jsx
* import { Slider } from `@wordpress/components`
*
* function Example() {
* return (
* <Slider />
* );
* }
* ```
*/
export const Slider = contextConnect( UnconnectedSlider, 'Slider' );
export default Slider;
119 changes: 119 additions & 0 deletions packages/components/src/slider/slider/hook.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
44 changes: 44 additions & 0 deletions packages/components/src/slider/stories/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <Slider { ...args } value={ value } onChange={ handleChange } />;
};

export const Default: ComponentStory< typeof Slider > = DefaultTemplate.bind(
{}
);
Default.args = {};
176 changes: 176 additions & 0 deletions packages/components/src/slider/styles.ts
Original file line number Diff line number Diff line change
@@ -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 ) };
}
`;
Loading

0 comments on commit 0eb175f

Please sign in to comment.