|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- 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]),
- /**
- * Is the component active?
- */
- disabled: PropTypes.bool,
- /**
- * Event handler triggered when the component changes value.
- */
- onChange: PropTypes.func,
- }
-
- 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 {boolean} [disabled] - Is the component active?
- * @returns {React.ReactElement} The component elements.
- */
- const Slider: React.FC<Props> = ({
- 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 (
- <Wrapper
- 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
- {...customBaseProps}
- disabled={disabled!}
- style={{
- [parallelDimension]: length,
- [perpendicularDimension]: '2rem',
- padding: orientation === 'horizontal' ? '0.875rem 0.75rem' : '0.75rem 0.875rem',
- cursor: disabled ? 'not-allowed' : undefined,
- }}
- onChange={onChange!}
- >
- <Track>
- <Highlight
- style={{
- [perpendicularDimension]: '100%',
- }}
- />
- <Handle
- style={{
- [perpendicularReference]: '50%',
- transform: `${perpendicularTransform}(-50%)`,
- cursor: disabled ? 'not-allowed' : undefined,
- }}
- />
- </Track>
- </Base>
- </ClickArea>
- </Wrapper>
- )
- }
-
- return (
- <Wrapper
- 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
- {...customFallbackProps}
- style={{
- width: length!,
- }}
- disabled={disabled!}
- onChange={onChange!}
- type="range"
- />
- </ClickArea>
- </TransformWrapper>
- </SizingWrapper>
- </Wrapper>
- )
- }
-
- Slider.propTypes = propTypes
-
- Slider.displayName = 'Slider'
-
- export default Slider
|