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]), /** * Class name used for styling. */ className: PropTypes.string, /** * Is the component active? */ disabled: PropTypes.bool, } 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 {string} [className] - Class name used for styling. * @param {boolean} [disabled] - Is the component active? * @param {object} etcProps - The component props. * @returns {React.ReactElement} The component elements. */ const Slider: React.FC = ({ orientation = 'horizontal', label = '', length = '16rem', className = '', disabled = false, ...etcProps }) => { 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' if (isClient) { return ( {stringify(label)} ) } return ( {stringify(label)} {stringify(label).length > 0 && ' '} ) } Slider.propTypes = propTypes Slider.displayName = 'Slider' export default Slider