import * as React from 'react' import * as PropTypes from 'prop-types' import * as ReachSlider from '@reach/slider' import styled from 'styled-components' import stringify from '../../services/stringify' const Wrapper = styled('div')({ position: 'relative', display: 'inline-block', verticalAlign: 'top', ':focus-within': { '--color-accent': 'var(--color-active, Highlight)', }, }) Wrapper.displayName = 'div' const Base = styled(ReachSlider.SliderInput)({ boxSizing: 'border-box', position: 'absolute', bottom: 0, right: 0, ':active': { cursor: 'grabbing', }, }) Base.displayName = 'input' const Track = styled(ReachSlider.SliderTrack)({ borderRadius: '0.125rem', position: 'relative', width: '100%', height: '100%', '::before': { display: 'block', position: 'absolute', content: "''", opacity: 0.25, width: '100%', height: '100%', backgroundColor: 'currentColor', borderRadius: '0.125rem', }, }) const Handle = styled(ReachSlider.SliderHandle)({ cursor: 'grab', width: '1.25rem', height: '1.25rem', backgroundColor: 'var(--color-accent, blue)', borderRadius: '50%', zIndex: 1, transformOrigin: 'center', outline: 0, position: 'relative', '::before': { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', content: "''", borderRadius: '50%', opacity: 0.5, pointerEvents: 'none', }, ':focus::before': { boxShadow: '0 0 0 0.25rem var(--color-accent, blue)', }, }) const Highlight = styled(ReachSlider.SliderTrackHighlight)({ backgroundColor: 'var(--color-accent, blue)', borderRadius: '0.125rem', }) const SizingWrapper = styled('span')({ width: '100%', display: 'inline-block', verticalAlign: 'top', position: 'relative', }) SizingWrapper.displayName = 'SizingWrapper' const TransformWrapper = styled('span')({ display: 'inline-block', verticalAlign: 'top', transformOrigin: 'top left', }) TransformWrapper.displayName = 'TransformWrapper' const LabelWrapper = styled('span')({ color: 'var(--color-accent, blue)', boxSizing: 'border-box', fontSize: '0.75em', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 'bolder', zIndex: 2, pointerEvents: 'none', transformOrigin: 'top left', position: 'absolute', top: 0, left: 0, transitionProperty: 'color', }) LabelWrapper.displayName = 'span' const FallbackTrack = styled('span')({ padding: '1.875rem 0.75rem 0.875rem', boxSizing: 'border-box', position: 'absolute', top: 0, left: 0, pointerEvents: 'none', '::before': { display: 'block', content: "''", opacity: 0.25, width: '100%', height: '100%', backgroundColor: 'currentColor', borderRadius: '0.125rem', }, }) const FallbackSlider = styled('input')({ boxSizing: 'border-box', backgroundColor: 'transparent', verticalAlign: 'top', margin: 0, width: '100%', height: '2rem', marginTop: '1rem', appearance: 'none', outline: 0, position: 'absolute', left: 0, '::-moz-focus-inner': { outline: 0, border: 0, }, '::-webkit-slider-thumb': { cursor: 'grab', width: '1.25rem', height: '1.25rem', backgroundColor: 'var(--color-accent, blue)', borderRadius: '50%', zIndex: 1, transformOrigin: 'center', outline: 0, position: 'relative', appearance: 'none', }, '::-moz-range-thumb': { cursor: 'grab', border: 0, width: '1.25rem', height: '1.25rem', backgroundColor: 'var(--color-accent, blue)', borderRadius: '50%', zIndex: 1, transformOrigin: 'center', outline: 0, position: 'relative', appearance: 'none', }, }) FallbackSlider.displayName = 'input' const ClickArea = styled('label')({ verticalAlign: 'top', width: '100%', height: '100%', }) ClickArea.displayName = 'label' export type Orientation = 'vertical' | 'horizontal' type Dimension = 'width' | 'height' const propTypes = { /** * The component orientation. */ orientation: PropTypes.oneOf(['vertical', 'horizontal']), /** * Short textual description indicating the nature of the component's value. */ label: PropTypes.any, /** * CSS size for the component length. */ length: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), /** * Is the component active? */ disabled: PropTypes.bool, /** * Event handler triggered when the component changes value. */ onChange: PropTypes.func, } type Props = PropTypes.InferProps /** * Component for inputting numeric values in a graphical manner. * * The component is styled using client-side scripts. When the component is rendered server-side, * the component falls back into the original `` element. * * @see {@link //reacttraining.com/reach-ui/slider/#sliderinput|Reach UI Slider} for the client-side implementation. * @param {'vertical' | 'horizontal'} [orientation] - The component orientation. * @param {*} [label] - Short textual description indicating the nature of the component's value. * @param {string | number} [length] - CSS size for the component length. * @param {boolean} [disabled] - Is the component active? * @returns {React.ReactElement} The component elements. */ const Slider: React.FC = ({ orientation = 'horizontal', label = '', length = '16rem', disabled = false, onChange, }) => { const [isClient, setIsClient] = React.useState(false) React.useEffect(() => { window.document.body.style.setProperty('--reach-slider', '1') setIsClient(true) }, []) const parallelDimension: Dimension = orientation === 'horizontal' ? 'width' : 'height' const perpendicularDimension: Dimension = orientation === 'horizontal' ? 'height' : 'width' const perpendicularReference = orientation === 'horizontal' ? 'top' : 'left' const perpendicularTransform = orientation === 'horizontal' ? 'translateY' : 'translateX' const customBaseProps: any = { orientation, } const customFallbackProps: any = { orient: orientation, } if (isClient) { return ( {stringify(label)} ) } return ( {stringify(label)} {stringify(label).length > 0 && ' '} ) } Slider.propTypes = propTypes Slider.displayName = 'Slider' export default Slider