|
- 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<Orientation>(['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<typeof propTypes>
-
- /**
- * 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 `<input type="range">` 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<Props> = ({
- 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 (
- <Wrapper
- className={className!}
- style={{
- opacity: disabled ? 0.5 : undefined,
- [parallelDimension]: length!,
- }}
- >
- <ClickArea>
- <SizingWrapper
- style={{
- [parallelDimension]: length!,
- [perpendicularDimension]: '3rem',
- }}
- >
- <TransformWrapper
- style={{
- [parallelDimension]: length!,
- [perpendicularDimension]: '100%',
- transform:
- orientation === 'horizontal'
- ? undefined
- : `rotate(-90deg) translateX(calc(${
- typeof length! === 'number' ? `${length as number}px` : (length as string)
- } * -1))`,
- }}
- >
- <LabelWrapper
- style={{
- maxWidth: length!,
- }}
- >
- {stringify(label)}
- </LabelWrapper>
- </TransformWrapper>
- </SizingWrapper>
- <Base
- {...etcProps}
- orientation={orientation!}
- disabled={disabled!}
- style={{
- [parallelDimension]: length,
- [perpendicularDimension]: '2rem',
- padding: orientation === 'horizontal' ? '0.875rem 0.75rem' : '0.75rem 0.875rem',
- cursor: disabled ? 'not-allowed' : undefined,
- }}
- >
- <Track>
- <Highlight
- style={{
- [perpendicularDimension]: '100%',
- }}
- />
- <Handle
- style={{
- [perpendicularReference]: '50%',
- transform: `${perpendicularTransform}(-50%)`,
- cursor: disabled ? 'not-allowed' : undefined,
- }}
- />
- </Track>
- </Base>
- </ClickArea>
- </Wrapper>
- )
- }
-
- return (
- <Wrapper
- className={className!}
- style={{
- opacity: disabled ? 0.5 : undefined,
- [parallelDimension]: length!,
- }}
- >
- <SizingWrapper
- style={{
- [parallelDimension]: length!,
- [perpendicularDimension]: '3rem',
- }}
- >
- <TransformWrapper
- style={{
- [parallelDimension]: length!,
- [perpendicularDimension]: '100%',
- transform:
- orientation === 'horizontal'
- ? undefined
- : `rotate(-90deg) translateX(calc(${typeof length! === 'number' ? `${length}px` : length} * -1))`,
- }}
- >
- <FallbackTrack
- style={{
- width: length!,
- height: '3rem',
- padding: '1.875rem 0.75rem 0.875rem',
- }}
- />
- <ClickArea>
- <LabelWrapper
- style={{
- maxWidth: length!,
- }}
- >
- {stringify(label)}
- </LabelWrapper>
- {stringify(label).length > 0 && ' '}
- <FallbackSlider
- {...etcProps}
- style={{
- width: length!,
- }}
- disabled={disabled!}
- type="range"
- />
- </ClickArea>
- </TransformWrapper>
- </SizingWrapper>
- </Wrapper>
- )
- }
-
- Slider.propTypes = propTypes
-
- Slider.displayName = 'Slider'
-
- export default Slider
|