-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First pass porting G2 Slider component
- Loading branch information
1 parent
86943ee
commit 0eb175f
Showing
12 changed files
with
1,458 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) }; | ||
} | ||
`; |
Oops, something went wrong.