Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🛝 CRT Ratio Slider #4470

Merged
merged 6 commits into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Meta, StoryObj } from '@storybook/react'

import { RatioSlider as RatioSlider_ } from './RatioSlider'

export default {
title: 'inputs/Slider/RatioSlider',
component: RatioSlider_,
args: {
min: 0,
max: 100,
step: 10,
defaultValue: 50,
},
argTypes: {
value: { table: { disable: true } },
},
} as Meta

type Args = {
min: number
max: number
step?: number
disabled?: boolean
}

export const RatioSlider: StoryObj<Args> = {}
92 changes: 92 additions & 0 deletions packages/atlas/src/components/_inputs/Slider/RatioSlider.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import styled from '@emotion/styled'

import { cVar, sizes } from '@/styles'

export const Wrapper = styled.div`
position: relative;
width: 100%;
`

const TRANSITION_DURATION = '100ms'

export const Track = styled.svg`
pointer-events: none;
overflow: visible;
height: ${sizes(6)};
width: calc(100% - ${sizes(5)});
margin: 0 ${sizes(5 / 2)} ${sizes(6)};

.rail {
stroke-width: ${sizes(1 / 2)};
stroke: ${cVar('colorCoreNeutral600')};
transition: all ${TRANSITION_DURATION};

&-left {
stroke: ${cVar('colorBackgroundPrimary')};
}

.steps .step--active {
stroke: ${cVar('colorBackgroundPrimary')};
}
}

.knob {
paint-order: stroke;
stroke-width: 0;
stroke: ${cVar('colorBackgroundPrimary')};
fill: ${cVar('colorBackgroundPrimary')};
transition: all ${TRANSITION_DURATION};
}

.cutout-mask {
paint-order: stroke;
stroke-width: 0;
stroke: #000;
fill: #000;
transition: all ${TRANSITION_DURATION};
}

text {
font-size: ${sizes(3)};
user-select: none;

&:nth-of-type(1) {
fill: ${cVar('colorTextMuted')};
text-anchor: start;
}

&:nth-of-type(2) {
fill: ${cVar('colorTextStrong')};
text-anchor: middle;
}

&:nth-of-type(3) {
fill: ${cVar('colorTextMuted')};
text-anchor: end;
}
}
`

export const Range = styled.input`
position: absolute;
width: 100%;
opacity: 0;
height: ${sizes(8)};
cursor: grab;

&:active {
cursor: grabbing;

& + svg .knob {
fill: ${cVar('colorTextStrong')};
}
}

&:active,
&:hover {
& + svg .knob,
& + svg .cutout-mask {
stroke-width: ${sizes(2)};
}
}
`
78 changes: 78 additions & 0 deletions packages/atlas/src/components/_inputs/Slider/RatioSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ChangeEventHandler, forwardRef, useMemo, useState } from 'react'

import { sizes } from '@/styles'

import { Range, Track, Wrapper } from './RatioSlider.styles'

type Props = {
thesan marked this conversation as resolved.
Show resolved Hide resolved
min?: number
max?: number
step?: number
defaultValue?: number
value?: number
onChange?: (value: number) => void
}

export const RatioSlider = forwardRef<HTMLInputElement, Props>(
({ min = 0, max = 100, step = 10, defaultValue = 50, value: controlledValue, onChange }, ref) => {
const [internalValue, setInternalValue] = useState<number>(defaultValue)
const value = controlledValue ?? internalValue

const handleChange: ChangeEventHandler<HTMLInputElement> = useMemo(
() =>
typeof controlledValue === 'undefined'
? (evt) => setInternalValue(Number(evt.target.value))
: (evt) => onChange?.(Number(evt.target.value)),
[controlledValue, onChange]
)

const length = max - min
const valuePercent = `${(value / length) * 100}%`

const steps = useMemo(() => {
const stepPercent = (step / length) * 100
return Array.from({ length: Math.ceil(length / step) + 1 }).map(
(_, index) => `${Math.min(index * stepPercent, 100)}%`
)
}, [step, length])

return (
<Wrapper>
<Range ref={ref} type="range" min={min} max={max} step={step} value={value} onChange={handleChange} />

<Track xmlns="http://www.w3.org/2000/svg">
<circle className="knob" cx={valuePercent} cy={sizes(3)} r={sizes(2)} />

<mask id="cutout-mask">
<rect x="-5%" y="0%" width="110%" height="100%" fill="#fff" />
<circle className="cutout-mask" cx={valuePercent} cy={sizes(3)} r={sizes(3)} />
</mask>

<g className="rail" mask="url(#cutout-mask)">
<line className="rail-left" x1="0%" x2={valuePercent} y1={sizes(3)} y2={sizes(3)} />
<line className="rail-rigth" x1={valuePercent} x2="100%" y1={sizes(3)} y2={sizes(3)} />

<g className="steps">
{steps.map((x, index) => {
const cls = `step step${Math.min(index * step, max) <= internalValue ? '--active' : ''}`
return <line key={index} className={cls} x1={x} x2={x} y1={sizes(3 / 2)} y2={sizes(9 / 2)} radius={4} />
})}
</g>
</g>

<text x="0%" y={sizes(10)}>
{min}%
</text>
<text x="50%" y={sizes(10)}>
{internalValue}%
</text>
<text x="100%" y={sizes(10)}>
{max}%
</text>
</Track>
</Wrapper>
)
}
)

RatioSlider.displayName = 'RatioSlider'
1 change: 1 addition & 0 deletions packages/atlas/src/components/_inputs/Slider/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Slider'
export * from './RatioSlider'
Loading