Skip to content

Commit

Permalink
Merge pull request #147 from kodiak-packages/145-slider
Browse files Browse the repository at this point in the history
Add slider component
  • Loading branch information
RobinWijnant authored Dec 2, 2020
2 parents 261e8d0 + aeed7a0 commit 74b0fb2
Show file tree
Hide file tree
Showing 14 changed files with 467 additions and 1 deletion.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"dependencies": {
"@popperjs/core": "^2.4.4",
"classnames": "^2.2.6",
"react-compound-slider": "^3.3.1",
"react-feather": "^2.0.8",
"react-popper": "^2.2.3",
"react-portal": "^4.2.1",
Expand Down
42 changes: 42 additions & 0 deletions src/components/Slider/Handle/Handle.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.container {
position: absolute;
top: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
cursor: grab;
transform: translate(-50%, -50%);
}

.handle {
width: 4px;
height: 4px;
border: 8px solid var(--color-neutral-7);
border-radius: 50%;
transition: all 0.3s;
}

.dragging .handle,
.container:hover .handle {
background-color: var(--color-neutral-7);
}

.container.disabled .handle {
border-color: var(--color-neutral-3);
}

.dragging .handle {
transform: scale(1.4);
}

.tooltip {
position: absolute;
bottom: 38px;
display: inline-block;
padding: 6px 12px;
border-radius: var(--border-radius-small);
background-color: var(--color-neutral-3);
}
56 changes: 56 additions & 0 deletions src/components/Slider/Handle/Handle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { ReactNode } from 'react';
import { GetHandleProps, SliderItem } from 'react-compound-slider';
import cn from 'classnames';

import styles from './Handle.module.css';

type Props = {
domain: number[];
handle: SliderItem;
isActive: boolean;
getHandleProps: GetHandleProps;
isDisabled?: boolean;
showTooltip: boolean;
formatValue?: (value: number) => ReactNode;
};

export const Handle: React.FC<Props> = ({
domain: [min, max],
handle: { id, value, percent },
isActive = false,
isDisabled = false,
showTooltip = true,
formatValue = (n) => String(n),
getHandleProps,
}: Props) => {
const classNames = cn(styles.container, {
[styles.dragging]: isActive,
[styles.disabled]: isDisabled,
});

return (
<>
<div
className={classNames}
style={{
left: `${percent}%`,
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...getHandleProps(id)}
>
{showTooltip && isActive && <div className={styles.tooltip}>{formatValue(value)}</div>}
<div
role="slider"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
style={{
left: `${percent}%`,
}}
className={styles.handle}
/>
</div>
</>
);
};
63 changes: 63 additions & 0 deletions src/components/Slider/Slider.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
name: Slider
menu: Components
route: /slider
---

import { Playground, Props } from 'docz';
import { Slider } from '../../index.ts'

# Slider

A slider component lets the user pick a value between a minimum and maximum value.

## Examples

### Minimal slider

<Playground>
{() => {
const [value, setValue] = React.useState(50);
return <Slider name='minimal-slider' valueBoundaries={[0,100]} value={value} onChange={setValue} />
}}
</Playground>

### Slider with decimal values

<Playground>
{() => {
const [value, setValue] = React.useState(1);
return <Slider name='decimal-slider' valueBoundaries={[0,10]} value={value} onChange={setValue} numberOfDecimals={2} stepSize={0.05} />
}}
</Playground>

### Slider with text boundaries

<Playground>
{() => {
const [value, setValue] = React.useState(5);
return <Slider name='text-boundaries-slider' valueBoundaries={[0,10]} textBoundaries={['Not accurate at all', 'Very accurate']} value={value} stepSize={1} onChange={setValue} />
}}
</Playground>

### Slider without tooltip

<Playground>
{() => {
const [value, setValue] = React.useState(50);
return <Slider name='no-tooltip-slider' valueBoundaries={[0,100]} value={value} onChange={setValue} showTooltip={false} />
}}
</Playground>

### Disabled slider

<Playground>
{() => {
const [value, setValue] = React.useState(50);
return <Slider name='disabled-slider' valueBoundaries={[0,100]} value={value} onChange={setValue} isDisabled />
}}
</Playground>

## API

<Props of={Slider} />
19 changes: 19 additions & 0 deletions src/components/Slider/Slider.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.sliderWrapper {
box-sizing: content-box;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
touch-action: none;
}

.slider {
position: relative;
width: 100%;
height: 10px;
margin-top: 5px;
}

.sliderWrapper.disabled {
background-color: var(--colorBaseNeutral300);
}
28 changes: 28 additions & 0 deletions src/components/Slider/Slider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from 'react';
import { render } from '@testing-library/react';

import Slider from './Slider';

describe('Slider', () => {
const defaultProps: React.ComponentProps<typeof Slider> = {
onChange: jest.fn(),
value: 50,
valueBoundaries: [0, 100],
};

test('default snapshot', () => {
const { asFragment } = render(<Slider {...defaultProps} />);
expect(asFragment()).toMatchSnapshot();
});

test('3 decimals should be displayed', () => {
const { queryByText } = render(<Slider {...defaultProps} numberOfDecimals={3} />);
expect(queryByText('100.000')).not.toBe(null);
});

test('text boundaries should be displayed', () => {
const { queryByText } = render(<Slider {...defaultProps} textBoundaries={['left', 'right']} />);
expect(queryByText('left')).not.toBe(null);
expect(queryByText('right')).not.toBe(null);
});
});
91 changes: 91 additions & 0 deletions src/components/Slider/Slider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import { Handles, Rail, Slider as CompoundSlider, Ticks } from 'react-compound-slider';
import classnames from 'classnames';

import { Handle } from './Handle/Handle';
import { SliderRail } from './SliderRail/SliderRail';
import { Tick } from './Tick/Tick';

import cssReset from '../../css-reset.module.css';
import styles from './Slider.module.css';

type Props = {
value: number;
onChange: (value: number) => void;
valueBoundaries: [number, number];
textBoundaries?: [string, string];
stepSize?: number;
numberOfDecimals?: number;
isDisabled?: boolean;
showTooltip?: boolean;
};

const Slider: React.FC<Props> = ({
value,
onChange,
valueBoundaries,
textBoundaries,
stepSize,
numberOfDecimals = 0,
isDisabled = false,
showTooltip = true,
}: Props) => {
const formatValue = (val: number) =>
(Math.round(val * (10 ** numberOfDecimals || 1)) / (10 ** numberOfDecimals || 1)).toFixed(
numberOfDecimals,
);
const minValue = valueBoundaries[0];
const maxValue = valueBoundaries[1];
const classNames = classnames(cssReset.ventura, styles.sliderWrapper, {
[styles.disabled]: isDisabled,
});

return (
<div className={classNames}>
<CompoundSlider
step={stepSize ?? (maxValue - minValue) / 100}
domain={valueBoundaries}
className={styles.slider}
onChange={(values) => onChange(values[0])}
values={[value]}
disabled={isDisabled}
>
<Rail>{({ getRailProps }) => <SliderRail getRailProps={getRailProps} />}</Rail>
<Handles>
{({ handles, activeHandleID, getHandleProps }) => (
<div className="slider-handles">
{handles.map((handle) => (
<Handle
key={handle.id}
handle={handle}
domain={valueBoundaries}
isActive={handle.id === activeHandleID}
formatValue={formatValue}
getHandleProps={getHandleProps}
isDisabled={isDisabled}
showTooltip={showTooltip}
/>
))}
</div>
)}
</Handles>
<Ticks count={2} values={valueBoundaries}>
{({ ticks }) => (
<div className="slider-ticks">
{ticks.map((tick, index) => (
<Tick
key={tick.id}
tick={tick}
formatValue={formatValue}
textValue={textBoundaries?.[index]}
/>
))}
</div>
)}
</Ticks>
</CompoundSlider>
</div>
);
};

export default Slider;
16 changes: 16 additions & 0 deletions src/components/Slider/SliderRail/SliderRail.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.container {
position: absolute;
width: 100%;
height: 100%;
cursor: pointer;
}

.rail {
position: absolute;
width: 100%;
height: 4px;
pointer-events: none;
background-color: var(--color-neutral-3);
border-radius: 7px;
transform: translate(0, -50%);
}
16 changes: 16 additions & 0 deletions src/components/Slider/SliderRail/SliderRail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { GetRailProps } from 'react-compound-slider';

import styles from './SliderRail.module.css';

type Props = {
getRailProps: GetRailProps;
};

export const SliderRail: React.FC<Props> = ({ getRailProps }: Props) => (
<>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div className={styles.container} {...getRailProps()} />
<div className={styles.rail} />
</>
);
27 changes: 27 additions & 0 deletions src/components/Slider/Tick/Tick.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.tick {
position: absolute;
height: 40px;
width: 50%;
bottom: 0;
padding: 0 10px 0 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
bottom: -12px;
}

.alignRight {
text-align: right;
transform: translateX(-100%);
padding: 0 0 0 10px;
}

.text {
color: var(--color-neutral-6);
margin-bottom: 13px;
display: block;
}

.value {
display: block;
}
Loading

0 comments on commit 74b0fb2

Please sign in to comment.