feat: improved range slider

This commit is contained in:
Sara Vieira 2017-09-18 12:12:01 +01:00 committed by Sérgio Ramos
parent 4c666bb438
commit 621f4c72f4
31 changed files with 2556 additions and 2125 deletions

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders <Filters /> without throwing 1`] = `
.c6 {
.c12 {
font-family: sans-serif;
font-size: 100%;
line-height: 1.15;
@ -49,47 +49,47 @@ exports[`renders <Filters /> without throwing 1`] = `
font-weight: 600;
}
.c6::-moz-focus-inner,
.c6[type='button']::-moz-focus-inner,
.c6[type='reset']::-moz-focus-inner,
.c6[type='submit']::-moz-focus-inner {
.c12::-moz-focus-inner,
.c12[type='button']::-moz-focus-inner,
.c12[type='reset']::-moz-focus-inner,
.c12[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
.c6:-moz-focusring,
.c6[type='button']:-moz-focusring,
.c6[type='reset']:-moz-focusring,
.c6[type='submit']:-moz-focusring {
.c12:-moz-focusring,
.c12[type='button']:-moz-focusring,
.c12[type='reset']:-moz-focusring,
.c12[type='submit']:-moz-focusring {
outline: 0.0625rem dotted ButtonText;
}
.c6:focus {
.c12:focus {
outline: 0;
text-decoration: none;
}
.c6:hover {
.c12:hover {
border: solid 0.0625rem;
}
.c6:active,
.c6:active:hover,
.c6:active:focus {
.c12:active,
.c12:active:hover,
.c12:active:focus {
background-image: none;
outline: 0;
}
.c6[disabled] {
.c12[disabled] {
cursor: not-allowed;
pointer-events: none;
}
.c6 + button {
.c12 + button {
margin-left: 1.25rem;
}
.c7 {
.c13 {
font-family: sans-serif;
font-size: 100%;
line-height: 1.15;
@ -136,43 +136,43 @@ exports[`renders <Filters /> without throwing 1`] = `
font-weight: 600;
}
.c7::-moz-focus-inner,
.c7[type='button']::-moz-focus-inner,
.c7[type='reset']::-moz-focus-inner,
.c7[type='submit']::-moz-focus-inner {
.c13::-moz-focus-inner,
.c13[type='button']::-moz-focus-inner,
.c13[type='reset']::-moz-focus-inner,
.c13[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
.c7:-moz-focusring,
.c7[type='button']:-moz-focusring,
.c7[type='reset']:-moz-focusring,
.c7[type='submit']:-moz-focusring {
.c13:-moz-focusring,
.c13[type='button']:-moz-focusring,
.c13[type='reset']:-moz-focusring,
.c13[type='submit']:-moz-focusring {
outline: 0.0625rem dotted ButtonText;
}
.c7:focus {
.c13:focus {
outline: 0;
text-decoration: none;
}
.c7:hover {
.c13:hover {
border: solid 0.0625rem;
}
.c7:active,
.c7:active:hover,
.c7:active:focus {
.c13:active,
.c13:active:hover,
.c13:active:focus {
background-image: none;
outline: 0;
}
.c7[disabled] {
.c13[disabled] {
cursor: not-allowed;
pointer-events: none;
}
.c7 + button {
.c13 + button {
margin-left: 1.25rem;
}
@ -187,14 +187,25 @@ exports[`renders <Filters /> without throwing 1`] = `
font-weight: bold;
}
.c3 .input-range {
position: relative;
width: calc(100% - 18px);
margin: auto;
min-height: 0.625rem;
.c7 {
font-weight: 600;
font-size: 0.625rem;
color: #464646;
position: absolute;
top: 1rem;
right: auto;
}
.c3 .slider {
.c9 {
font-weight: 600;
font-size: 0.625rem;
color: #464646;
position: absolute;
top: 1rem;
right: 1px;
}
.c8 {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
@ -205,57 +216,56 @@ exports[`renders <Filters /> without throwing 1`] = `
display: block;
height: 0.875rem;
width: 0.875rem;
-webkit-transform: translateY(-50%) translateX(-50%);
-ms-transform: translateY(-50%) translateX(-50%);
transform: translateY(-50%) translateX(-50%);
-webkit-transform: translateY(-50%) translateX(-1%);
-ms-transform: translateY(-50%) translateX(-1%);
transform: translateY(-50%) translateX(-1%);
outline: none;
position: absolute;
top: 50%;
top: 0;
margin-top: 0.125rem;
}
.c3 .slider::active {
.c8::active {
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
.c3 .slider::focus {
.c8::focus {
box-shadow: 0 0 0 5px rgba(63,81,181,0.2);
}
.c3 .disabled .track {
background: #D8D8D8;
}
.c3 .disabled .slider {
background: #CCC;
border: 1px solid #CCC;
box-shadow: none;
-webkit-transform: none;
-ms-transform: none;
transform: none;
}
.c3 .min,
.c3 .max {
display: none;
}
.c3 .value {
top: 0.5rem;
.c10 {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: #FFFFFF;
border: 2px solid #bdbdbd;
border-radius: 50%;
cursor: pointer;
display: block;
height: 0.875rem;
width: 0.875rem;
-webkit-transform: translateY(-50%) translateX(-99%);
-ms-transform: translateY(-50%) translateX(-99%);
transform: translateY(-50%) translateX(-99%);
outline: none;
position: absolute;
top: 0;
margin-top: 0.125rem;
}
.c3 .value .label-container {
font-weight: 600;
font-size: 0.625rem;
color: #464646;
left: -50%;
position: relative;
.c10::active {
-webkit-transform: scale(1.3);
-ms-transform: scale(1.3);
transform: scale(1.3);
}
.c3 .track {
.c10::focus {
box-shadow: 0 0 0 5px rgba(63,81,181,0.2);
}
.c5 {
background: #D8D8D8;
cursor: pointer;
display: block;
@ -263,13 +273,18 @@ exports[`renders <Filters /> without throwing 1`] = `
position: relative;
}
.c3 .active-track {
.c6 {
background: #364ACD;
height: 100%;
position: absolute;
}
.c4 {
position: relative;
min-height: 0.625rem;
}
.c3 {
margin-bottom: 0.625rem;
margin-top: 0.75rem;
}
@ -291,7 +306,7 @@ exports[`renders <Filters /> without throwing 1`] = `
margin-right: 2.25rem;
}
.c5 {
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -320,39 +335,28 @@ exports[`renders <Filters /> without throwing 1`] = `
<section
className="c2"
>
<div
className="c3"
>
<div>
<label
className="c4 c1"
className="c3 c1"
htmlFor=""
>
GB RAM
</label>
<div
aria-disabled={false}
className="input-range"
className="c4"
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<span
className="min"
>
<span
className="label-container"
>
0.256
</span>
</span>
<div
className="track"
className="c5"
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<div
className="active-track"
className="c6"
style={
Object {
"left": "0%",
@ -360,117 +364,89 @@ exports[`renders <Filters /> without throwing 1`] = `
}
}
/>
<span>
<span
className="slider-container"
style={
Object {
"left": "0%",
"position": "absolute",
}
}
>
<span
className="value"
>
<span
className="label-container"
className="c7"
type="min"
>
0.256
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={50.688}
aria-valuemin={0.256}
aria-valuenow={0.256}
className="slider"
className="c8"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
tabIndex="0"
/>
</span>
<span
className="slider-container"
style={
Object {
"left": "100%",
"left": "0%",
"position": "absolute",
}
}
>
tabIndex="0"
type="min"
/>
</span>
<span>
<span
className="value"
>
<span
className="label-container"
className="c9"
type="max"
>
50.688
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={50.688}
aria-valuemin={0.256}
aria-valuenow={50.688}
className="slider"
className="c10"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
style={
Object {
"left": "100%",
"position": "absolute",
}
}
tabIndex="0"
type="max"
/>
</span>
</div>
<span
className="max"
>
<span
className="label-container"
>
50.688
</span>
</span>
</div>
</div>
<div
className="c3"
>
<div>
<label
className="c4 c1"
className="c3 c1"
htmlFor=""
>
vCPUs
</label>
<div
aria-disabled={false}
className="input-range"
className="c4"
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<span
className="min"
>
<span
className="label-container"
>
0.25
</span>
</span>
<div
className="track"
className="c5"
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<div
className="active-track"
className="c6"
style={
Object {
"left": "0%",
@ -478,117 +454,89 @@ exports[`renders <Filters /> without throwing 1`] = `
}
}
/>
<span>
<span
className="slider-container"
style={
Object {
"left": "0%",
"position": "absolute",
}
}
>
<span
className="value"
>
<span
className="label-container"
className="c7"
type="min"
>
0.25
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={3.25}
aria-valuemin={0.25}
aria-valuenow={0.25}
className="slider"
className="c8"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
tabIndex="0"
/>
</span>
<span
className="slider-container"
style={
Object {
"left": "100%",
"left": "0%",
"position": "absolute",
}
}
>
tabIndex="0"
type="min"
/>
</span>
<span>
<span
className="value"
>
<span
className="label-container"
className="c9"
type="max"
>
3.25
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={3.25}
aria-valuemin={0.25}
aria-valuenow={3.25}
className="slider"
className="c10"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
style={
Object {
"left": "100%",
"position": "absolute",
}
}
tabIndex="0"
type="max"
/>
</span>
</div>
<span
className="max"
>
<span
className="label-container"
>
3.25
</span>
</span>
</div>
</div>
<div
className="c3"
>
<div>
<label
className="c4 c1"
className="c3 c1"
htmlFor=""
>
TB Disk
</label>
<div
aria-disabled={false}
className="input-range"
className="c4"
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<span
className="min"
>
<span
className="label-container"
>
0.01
</span>
</span>
<div
className="track"
className="c5"
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<div
className="active-track"
className="c6"
style={
Object {
"left": "0%",
@ -596,117 +544,89 @@ exports[`renders <Filters /> without throwing 1`] = `
}
}
/>
<span>
<span
className="slider-container"
style={
Object {
"left": "0%",
"position": "absolute",
}
}
>
<span
className="value"
>
<span
className="label-container"
className="c7"
type="min"
>
0.01
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={107.26}
aria-valuemin={0.01}
aria-valuenow={0.01}
className="slider"
className="c8"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
tabIndex="0"
/>
</span>
<span
className="slider-container"
style={
Object {
"left": "100%",
"left": "0%",
"position": "absolute",
}
}
>
tabIndex="0"
type="min"
/>
</span>
<span>
<span
className="value"
>
<span
className="label-container"
className="c9"
type="max"
>
107.26
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={107.26}
aria-valuemin={0.01}
aria-valuenow={107.26}
className="slider"
className="c10"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
style={
Object {
"left": "100%",
"position": "absolute",
}
}
tabIndex="0"
type="max"
/>
</span>
</div>
<span
className="max"
>
<span
className="label-container"
>
107.26
</span>
</span>
</div>
</div>
<div
className="c3"
>
<div>
<label
className="c4 c1"
className="c3 c1"
htmlFor=""
>
$/hr
</label>
<div
aria-disabled={false}
className="input-range"
className="c4"
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<span
className="min"
>
<span
className="label-container"
>
0.016
</span>
</span>
<div
className="track"
className="c5"
onMouseDown={[Function]}
onTouchStart={[Function]}
>
<div
className="active-track"
className="c6"
style={
Object {
"left": "0%",
@ -714,112 +634,95 @@ exports[`renders <Filters /> without throwing 1`] = `
}
}
/>
<span>
<span
className="slider-container"
style={
Object {
"left": "0%",
"position": "absolute",
}
}
>
<span
className="value"
>
<span
className="label-container"
className="c7"
type="min"
>
0.016
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={0.525}
aria-valuemin={0.016}
aria-valuenow={0.016}
className="slider"
className="c8"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
tabIndex="0"
/>
</span>
<span
className="slider-container"
style={
Object {
"left": "100%",
"left": "0%",
"position": "absolute",
}
}
>
tabIndex="0"
type="min"
/>
</span>
<span>
<span
className="value"
>
<span
className="label-container"
className="c9"
type="max"
>
0.525
</span>
</span>
<div
aria-controls={undefined}
aria-labelledby={undefined}
aria-valuemax={0.525}
aria-valuemin={0.016}
aria-valuenow={0.525}
className="slider"
className="c10"
draggable="false"
onKeyDown={[Function]}
onMouseDown={[Function]}
onTouchStart={[Function]}
role="slider"
style={
Object {
"left": "100%",
"position": "absolute",
}
}
tabIndex="0"
type="max"
/>
</span>
</div>
<span
className="max"
>
<span
className="label-container"
>
0.525
</span>
</span>
</div>
</div>
</section>
<section
className="c5"
className="c11"
>
<div>
<button
className="c6"
className="c12"
onClick={[Function]}
selected={false}
>
Compute Optimized
</button>
<button
className="c6"
className="c12"
onClick={[Function]}
selected={false}
>
General Purpose
</button>
<button
className="c6"
className="c12"
onClick={[Function]}
selected={false}
>
Memory Optimized
</button>
<button
className="c6"
className="c12"
onClick={[Function]}
selected={false}
>
@ -827,7 +730,7 @@ exports[`renders <Filters /> without throwing 1`] = `
</button>
</div>
<button
className="c7"
className="c13"
disabled={false}
onClick={undefined}
>

View File

@ -1,9 +1,21 @@
import React, { Component } from 'react';
import { Row } from 'react-styled-flexboxgrid';
import styled from 'styled-components';
import { SectionNav } from '@components/navigation';
import { Filters } from '@components/filters';
import PackagesHOC from '@containers/packages';
import { Message, Breadcrumb, BreadcrumbItem, Anchor } from 'joyent-ui-toolkit';
import {
Message,
Breadcrumb,
BreadcrumbItem,
Anchor,
Button
} from 'joyent-ui-toolkit';
const Main = styled.main`
/* Prettier stahp */
margin-bottom: 40px;
`
class Home extends Component {
constructor(props) {
@ -38,10 +50,7 @@ class Home extends Component {
onFilterChange({
...filters,
groups: [
...otherGroups,
{ name: group.name, selected: !group.selected }
]
groups: [...otherGroups, { name: group.name, selected: !group.selected }]
});
}
@ -60,7 +69,7 @@ class Home extends Component {
) : null;
return (
<main>
<Main>
<SectionNav />
<Breadcrumb>
<BreadcrumbItem>Instances</BreadcrumbItem>
@ -81,7 +90,11 @@ class Home extends Component {
<Row>
<PackagesHOC />
</Row>
</main>
<Row>
<Button secondary>Cancel</Button>
<Button>Save and Continue</Button>
</Row>
</Main>
);
}
}

View File

@ -68,9 +68,11 @@ exports[`renders <Header /> without throwing 1`] = `
>
<a
href="/"
name="Go to home"
onClick={[Function]}
>
<img
alt="Triton Logo"
className="c3"
src="test-file-mock"
/>

View File

@ -14,8 +14,8 @@ const StyledLogo = Img.extend`
const NavHeader = () => (
<Header>
<HeaderBrand>
<Link to="/">
<StyledLogo src={Logo} />
<Link to="/" name="Go to home">
<StyledLogo src={Logo} alt="Triton Logo"/>
</Link>
</HeaderBrand>
</Header>

View File

@ -253,7 +253,7 @@ exports[`renders <Packages /> without throwing 1`] = `
}
}
<ul
<section
className="c0"
>
<div
@ -760,5 +760,5 @@ exports[`renders <Packages /> without throwing 1`] = `
</div>
</div>
</div>
</ul>
</section>
`;

View File

@ -5,7 +5,7 @@ import { Col } from 'react-styled-flexboxgrid';
import Package from '@components/package';
const ListStyled = styled.ul`
const ListStyled = styled.section`
display: flex;
min-width: 100%;
list-style: none;

View File

@ -68,9 +68,11 @@ exports[`renders <Header /> without throwing 1`] = `
>
<a
href="/"
name="Go to home"
onClick={[Function]}
>
<img
alt="Triton Logo"
className="c3"
src="test-file-mock"
/>

View File

@ -253,7 +253,7 @@ exports[`renders <PackagesHOC /> without throwing 1`] = `
}
}
<ul
<section
className="c0"
>
<div
@ -1768,5 +1768,5 @@ exports[`renders <PackagesHOC /> without throwing 1`] = `
</div>
</div>
</div>
</ul>
</section>
`;

View File

@ -1,5 +1,5 @@
{
"presets": "joyent-portal",
"presets": ["joyent-portal"],
"plugins": [
"styled-components",
["inline-react-svg", {

View File

@ -10,22 +10,16 @@
"lint:css": "echo 0",
"lint-ci:css": "echo 0",
"lint:js": "eslint . --fix",
"lint-ci:js":
"eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/ui-toolkit.xml",
"lint-ci:js": "eslint . --format junit --output-file $CIRCLE_TEST_REPORTS/lint/ui-toolkit.xml",
"lint": "redrun -s lint:*",
"lint-ci": "redrun -s lint-ci:*",
"test": "echo 0",
"test-ci": "echo 0",
"copy-fonts":
"rm -rf dist; mkdir -p dist/es/typography; mkdir -p dist/umd/typography; cp -r src/typography/libre-franklin dist/es/typography; cp -r src/typography/libre-franklin dist/umd/typography",
"compile-watch:es":
"NODE_ENV=development babel src --out-dir dist/es --source-maps inline --watch",
"compile:es":
"NODE_ENV=development babel src --out-dir dist/es --source-maps inline",
"compile:umd":
"cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline",
"compile-watch:umd":
"cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline --watch",
"copy-fonts": "rm -rf dist; mkdir -p dist/es/typography; mkdir -p dist/umd/typography; cp -r src/typography/libre-franklin dist/es/typography; cp -r src/typography/libre-franklin dist/umd/typography",
"compile-watch:es": "NODE_ENV=development babel src --out-dir dist/es --source-maps inline --watch",
"compile:es": "NODE_ENV=development babel src --out-dir dist/es --source-maps inline",
"compile:umd": "cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline",
"compile-watch:umd": "cross-env NODE_ENV=test babel src --out-dir dist/umd --source-maps inline --watch",
"compile": "redrun -p compile:*",
"watch": "redrun copy-fonts && redrun -p compile-watch:*",
"styleguide:build": "cross-env NODE_ENV=production styleguidist build",
@ -64,6 +58,7 @@
"unitcalc": "^1.0.8"
},
"devDependencies": {
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"csso": "^3.1.1",
"eslint": "^4.5.0",
"eslint-config-joyent-portal": "2.0.0",
@ -88,8 +83,7 @@
"stylelint": "^8.0.0",
"stylelint-config-primer": "^2.0.1",
"stylelint-config-standard": "^17.0.0",
"stylelint-processor-styled-components":
"styled-components/stylelint-processor-styled-components#2a33b5f",
"stylelint-processor-styled-components": "styled-components/stylelint-processor-styled-components#2a33b5f",
"svgo": "^0.7.2",
"tinycolor2": "^1.4.1",
"title-case": "^2.1.1",

View File

@ -10,16 +10,3 @@
onChange={value => console.log(value)}
>vCPUs</Slider>
```
### Normal Slider
```
<Slider
minValue={10}
maxValue={100}
step={5}
value={0}
onChangeComplete={value => console.log(value)}
onChange={value => console.log(value)}
>Price</Slider>
```

View File

@ -1,57 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import InputRange from 'react-input-range';
import InputRange from './react-input-range';
import remcalc from 'remcalc';
import theme from '../theme';
import FormLabel from '../form/label';
import {
sliderStyles,
disabledStyles,
trackStyles,
activeStyles,
rangeStyles
} from './inputStyles';
const SliderStyled = styled.div`
.input-range {
${rangeStyles};
}
.slider {
${sliderStyles};
}
.disabled {
${disabledStyles};
}
.min,
.max {
display: none;
}
.value {
top: ${remcalc(8)};
position: absolute;
.label-container {
font-weight: 600;
font-size: ${remcalc(10)};
color: ${theme.secondary};
left: -50%;
position: relative;
}
}
.track {
${trackStyles};
}
.active-track {
${activeStyles};
}
`;
const Label = styled(FormLabel)`
margin-bottom: ${remcalc(10)};
@ -79,7 +32,7 @@ class Slider extends Component {
const { minValue, maxValue, value } = this.state;
const { children, ...rest } = this.props;
return (
<SliderStyled>
<div>
<Label>{children}</Label>
<InputRange
{...rest}
@ -88,7 +41,7 @@ class Slider extends Component {
value={value}
onChange={value => this.changeValue(value)}
/>
</SliderStyled>
</div>
);
}
}
@ -103,18 +56,6 @@ Slider.propTypes = {
formatLabel: PropTypes.func,
ariaLabelledby: PropTypes.string,
ariaControls: PropTypes.string,
classNames: PropTypes.shape({
activeTrack: PropTypes.string,
disabledInputRange: PropTypes.string,
inputRange: PropTypes.string,
labelContainer: PropTypes.string,
maxLabel: PropTypes.string,
minLabel: PropTypes.string,
slider: PropTypes.string,
sliderContainer: PropTypes.string,
track: PropTypes.string,
valueLabel: PropTypes.string
}),
disabled: PropTypes.bool,
draggableTrack: PropTypes.bool,
onChangeStart: PropTypes.func,
@ -129,19 +70,7 @@ Slider.defaultProps = {
? Math.round(value).toFixed(3)
: value,
onChangeStart: () => {},
step: 1,
classNames: {
activeTrack: 'active-track',
disabledInputRange: 'disabled-range',
inputRange: 'input-range',
labelContainer: 'label-container',
maxLabel: 'max',
minLabel: 'min',
sliderContainer: 'slider-container',
track: 'track',
valueLabel: 'value',
slider: 'slider'
}
step: 1
};
export default Slider;

View File

@ -1,61 +0,0 @@
import { css } from 'styled-components';
import remcalc from 'remcalc';
import theme from '../theme';
export const sliderStyles = css`
appearance: none;
background: ${theme.white};
border: 2px solid ${theme.greyLight};
border-radius: 50%;
cursor: pointer;
display: block;
height: ${remcalc(14)};
width: ${remcalc(14)};
transform: translateY(-50%) translateX(-50%);
outline: none;
position: absolute;
top: 50%;
margin-top: ${remcalc(2)};
&::active {
transform: scale(1.3);
}
&::focus {
box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2);
}
`;
export const disabledStyles = css`
.track {
background: ${theme.grey};
}
.slider {
background: ${theme.greyDark};
border: 1px solid ${theme.greyDark};
box-shadow: none;
transform: none;
}
`;
export const trackStyles = css`
background: ${theme.grey};
cursor: pointer;
display: block;
height: ${remcalc(4)};
position: relative;
`;
export const activeStyles = css`
background: ${theme.blue};
height: 100%;
position: absolute;
`;
export const rangeStyles = css`
position: relative;
width: calc(100% - 18px);
margin: auto;
min-height: ${remcalc(10)};
`;

View File

@ -0,0 +1,3 @@
import InputRange from './input-range/input-range';
export default InputRange;

View File

@ -0,0 +1,725 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as valueTransformer from './value-transformer';
import rangePropType from './range-prop-type';
import valuePropType from './value-prop-type';
import Slider from './slider';
import Track from './track';
import { captialize, distanceTo, isDefined, isObject, length } from '../utils';
import styled from 'styled-components';
import remcalc from 'remcalc';
export const RangeStyled = styled.div`
position: relative;
min-height: ${remcalc(10)};
`;
/**
* A React component that allows users to input numeric values within a range
* by dragging its sliders.
*/
export default class InputRange extends Component {
/**
* @ignore
* @override
* @return {Object}
*/
static get propTypes() {
return {
ariaLabelledby: PropTypes.string,
ariaControls: PropTypes.string,
classNames: PropTypes.objectOf(PropTypes.string),
disabled: PropTypes.bool,
draggableTrack: PropTypes.bool,
formatLabel: PropTypes.func,
maxValue: rangePropType,
minValue: rangePropType,
name: PropTypes.string,
onChangeStart: PropTypes.func,
onChange: PropTypes.func.isRequired,
onChangeComplete: PropTypes.func,
step: PropTypes.number,
value: valuePropType
};
}
/**
* @ignore
* @override
* @return {Object}
*/
static get defaultProps() {
return {
disabled: false,
maxValue: 10,
minValue: 0,
step: 1
};
}
/**
* @param {Object} props
* @param {string} [props.ariaLabelledby]
* @param {string} [props.ariaControls]
* @param {InputRangeClassNames} [props.classNames]
* @param {boolean} [props.disabled = false]
* @param {Function} [props.formatLabel]
* @param {number|Range} [props.maxValue = 10]
* @param {number|Range} [props.minValue = 0]
* @param {string} [props.name]
* @param {string} props.onChange
* @param {Function} [props.onChangeComplete]
* @param {Function} [props.onChangeStart]
* @param {number} [props.step = 1]
* @param {number|Range} props.value
*/
constructor(props) {
super(props);
/**
* @private
* @type {?number}
*/
this.startValue = null;
/**
* @private
* @type {?Component}
*/
this.node = null;
/**
* @private
* @type {?Component}
*/
this.trackNode = null;
/**
* @private
* @type {bool}
*/
this.isSliderDragging = false;
this.handleSliderDrag = this.handleSliderDrag.bind(this);
this.handleTrackDrag = this.handleTrackDrag.bind(this);
this.handleTrackMouseDown = this.handleTrackMouseDown.bind(this);
this.handleInteractionStart = this.handleInteractionStart.bind(this);
this.handleInteractionEnd = this.handleInteractionEnd.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
}
/**
* @ignore
* @override
* @return {void}
*/
componentWillUnmount() {
this.removeDocumentMouseUpListener();
this.removeDocumentTouchEndListener();
}
/**
* Return the bounding rect of the track
* @private
* @return {ClientRect}
*/
getTrackClientRect() {
return this.trackNode.getClientRect();
}
/**
* Return the slider key closest to a point
* @private
* @param {Point} position
* @return {string}
*/
getKeyByPosition(position) {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
const positions = valueTransformer.getPositionsFromValues(
values,
this.props.minValue,
this.props.maxValue,
this.getTrackClientRect()
);
if (this.isMultiValue()) {
const distanceToMin = distanceTo(position, positions.min);
const distanceToMax = distanceTo(position, positions.max);
if (distanceToMin < distanceToMax) {
return 'min';
}
}
return 'max';
}
/**
* Return all the slider keys
* @private
* @return {string[]}
*/
getKeys() {
if (this.isMultiValue()) {
return ['min', 'max'];
}
return ['max'];
}
/**
* Return true if the difference between the new and the current value is
* greater or equal to the step amount of the component
* @private
* @param {Range} values
* @return {boolean}
*/
hasStepDifference(values) {
const currentValues = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
return (
length(values.min, currentValues.min) >= this.props.step ||
length(values.max, currentValues.max) >= this.props.step
);
}
/**
* Return true if the component accepts a min and max value
* @private
* @return {boolean}
*/
isMultiValue() {
return isObject(this.props.value);
}
/**
* Return true if the range is within the max and min value of the component
* @private
* @param {Range} values
* @return {boolean}
*/
isWithinRange(values) {
if (this.isMultiValue()) {
return (
values.min >= this.props.minValue &&
values.max <= this.props.maxValue &&
values.min < values.max
);
}
return (
values.max >= this.props.minValue && values.max <= this.props.maxValue
);
}
/**
* Return true if the new value should trigger a render
* @private
* @param {Range} values
* @return {boolean}
*/
shouldUpdate(values) {
return this.isWithinRange(values) && this.hasStepDifference(values);
}
/**
* Update the position of a slider
* @private
* @param {string} key
* @param {Point} position
* @return {void}
*/
updatePosition(key, position) {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
const positions = valueTransformer.getPositionsFromValues(
values,
this.props.minValue,
this.props.maxValue,
this.getTrackClientRect()
);
positions[key] = position;
this.updatePositions(positions);
}
/**
* Update the positions of multiple sliders
* @private
* @param {Object} positions
* @param {Point} positions.min
* @param {Point} positions.max
* @return {void}
*/
updatePositions(positions) {
const values = {
min: valueTransformer.getValueFromPosition(
positions.min,
this.props.minValue,
this.props.maxValue,
this.getTrackClientRect()
),
max: valueTransformer.getValueFromPosition(
positions.max,
this.props.minValue,
this.props.maxValue,
this.getTrackClientRect()
)
};
const transformedValues = {
min: valueTransformer.getStepValueFromValue(values.min, this.props.step),
max: valueTransformer.getStepValueFromValue(values.max, this.props.step)
};
this.updateValues(transformedValues);
}
/**
* Update the value of a slider
* @private
* @param {string} key
* @param {number} value
* @return {void}
*/
updateValue(key, value) {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
values[key] = value;
this.updateValues(values);
}
/**
* Update the values of multiple sliders
* @private
* @param {Range|number} values
* @return {void}
*/
updateValues(values) {
if (!this.shouldUpdate(values)) {
return;
}
this.props.onChange(this.isMultiValue() ? values : values.max);
}
/**
* Increment the value of a slider by key name
* @private
* @param {string} key
* @return {void}
*/
incrementValue(key) {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
const value = values[key] + this.props.step;
this.updateValue(key, value);
}
/**
* Decrement the value of a slider by key name
* @private
* @param {string} key
* @return {void}
*/
decrementValue(key) {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
const value = values[key] - this.props.step;
this.updateValue(key, value);
}
/**
* Listen to mouseup event
* @private
* @return {void}
*/
addDocumentMouseUpListener() {
this.removeDocumentMouseUpListener();
this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
}
/**
* Listen to touchend event
* @private
* @return {void}
*/
addDocumentTouchEndListener() {
this.removeDocumentTouchEndListener();
this.node.ownerDocument.addEventListener('touchend', this.handleTouchEnd);
}
/**
* Stop listening to mouseup event
* @private
* @return {void}
*/
removeDocumentMouseUpListener() {
this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
}
/**
* Stop listening to touchend event
* @private
* @return {void}
*/
removeDocumentTouchEndListener() {
this.node.ownerDocument.removeEventListener(
'touchend',
this.handleTouchEnd
);
}
/**
* Handle any "mousemove" event received by the slider
* @private
* @param {SyntheticEvent} event
* @param {string} key
* @return {void}
*/
handleSliderDrag(event, key) {
if (this.props.disabled) {
return;
}
const position = valueTransformer.getPositionFromEvent(
event,
this.getTrackClientRect()
);
this.isSliderDragging = true;
requestAnimationFrame(() => this.updatePosition(key, position));
}
/**
* Handle any "mousemove" event received by the track
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleTrackDrag(event, prevEvent) {
if (
this.props.disabled ||
!this.props.draggableTrack ||
this.isSliderDragging
) {
return;
}
const { maxValue, minValue, value: { max, min } } = this.props;
const position = valueTransformer.getPositionFromEvent(
event,
this.getTrackClientRect()
);
const value = valueTransformer.getValueFromPosition(
position,
minValue,
maxValue,
this.getTrackClientRect()
);
const stepValue = valueTransformer.getStepValueFromValue(
value,
this.props.step
);
const prevPosition = valueTransformer.getPositionFromEvent(
prevEvent,
this.getTrackClientRect()
);
const prevValue = valueTransformer.getValueFromPosition(
prevPosition,
minValue,
maxValue,
this.getTrackClientRect()
);
const prevStepValue = valueTransformer.getStepValueFromValue(
prevValue,
this.props.step
);
const offset = prevStepValue - stepValue;
const transformedValues = {
min: min - offset,
max: max - offset
};
this.updateValues(transformedValues);
}
/**
* Handle any "mousedown" event received by the track
* @private
* @param {SyntheticEvent} event
* @param {Point} position
* @return {void}
*/
handleTrackMouseDown(event, position) {
if (this.props.disabled) {
return;
}
const { maxValue, minValue, value: { max, min } } = this.props;
event.preventDefault();
const value = valueTransformer.getValueFromPosition(
position,
minValue,
maxValue,
this.getTrackClientRect()
);
const stepValue = valueTransformer.getStepValueFromValue(
value,
this.props.step
);
if (!this.props.draggableTrack || stepValue > max || stepValue < min) {
this.updatePosition(this.getKeyByPosition(position), position);
}
}
/**
* Handle the start of any mouse/touch event
* @private
* @return {void}
*/
handleInteractionStart() {
if (this.props.onChangeStart) {
this.props.onChangeStart(this.props.value);
}
if (this.props.onChangeComplete && !isDefined(this.startValue)) {
this.startValue = this.props.value;
}
}
/**
* Handle the end of any mouse/touch event
* @private
* @return {void}
*/
handleInteractionEnd() {
if (this.isSliderDragging) {
this.isSliderDragging = false;
}
if (!this.props.onChangeComplete || !isDefined(this.startValue)) {
return;
}
if (this.startValue !== this.props.value) {
this.props.onChangeComplete(this.props.value);
}
this.startValue = null;
}
/**
* Handle any "keydown" event received by the component
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleKeyDown(event) {
this.handleInteractionStart(event);
}
/**
* Handle any "keyup" event received by the component
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleKeyUp(event) {
this.handleInteractionEnd(event);
}
/**
* Handle any "mousedown" event received by the component
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleMouseDown(event) {
this.handleInteractionStart(event);
this.addDocumentMouseUpListener();
}
/**
* Handle any "mouseup" event received by the component
* @private
* @param {SyntheticEvent} event
*/
handleMouseUp(event) {
this.handleInteractionEnd(event);
this.removeDocumentMouseUpListener();
}
/**
* Handle any "touchstart" event received by the component
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleTouchStart(event) {
this.handleInteractionStart(event);
this.addDocumentTouchEndListener();
}
/**
* Handle any "touchend" event received by the component
* @private
* @param {SyntheticEvent} event
*/
handleTouchEnd(event) {
this.handleInteractionEnd(event);
this.removeDocumentTouchEndListener();
}
/**
* Return JSX of sliders
* @private
* @return {JSX.Element}
*/
renderSliders() {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
const percentages = valueTransformer.getPercentagesFromValues(
values,
this.props.minValue,
this.props.maxValue
);
return this.getKeys().map(key => {
const value = values[key];
const percentage = percentages[key];
let { maxValue, minValue } = this.props;
if (key === 'min') {
maxValue = values.max;
} else {
minValue = values.min;
}
const slider = (
<Slider
ariaLabelledby={this.props.ariaLabelledby}
ariaControls={this.props.ariaControls}
classNames={this.props.classNames}
formatLabel={this.props.formatLabel}
key={key}
maxValue={maxValue}
minValue={minValue}
onSliderDrag={this.handleSliderDrag}
percentage={percentage}
type={key}
value={value}
/>
);
return slider;
});
}
/**
* Return JSX of hidden inputs
* @private
* @return {JSX.Element}
*/
renderHiddenInputs() {
if (!this.props.name) {
return [];
}
const isMultiValue = this.isMultiValue();
const values = valueTransformer.getValueFromProps(this.props, isMultiValue);
return this.getKeys().map(key => {
const value = values[key];
const name = isMultiValue
? `${this.props.name}${captialize(key)}`
: this.props.name;
return <input key={key} type="hidden" name={name} value={value} />;
});
}
/**
* @ignore
* @override
* @return {JSX.Element}
*/
render() {
const values = valueTransformer.getValueFromProps(
this.props,
this.isMultiValue()
);
const percentages = valueTransformer.getPercentagesFromValues(
values,
this.props.minValue,
this.props.maxValue
);
return (
<RangeStyled
aria-disabled={this.props.disabled}
ref={node => {
this.node = node;
}}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onMouseDown={this.handleMouseDown}
onTouchStart={this.handleTouchStart}
>
<Track
classNames={this.props.classNames}
draggableTrack={this.props.draggableTrack}
ref={trackNode => {
this.trackNode = trackNode;
}}
percentages={percentages}
onTrackDrag={this.handleTrackDrag}
onTrackMouseDown={this.handleTrackMouseDown}
>
{this.renderSliders()}
</Track>
{this.renderHiddenInputs()}
</RangeStyled>
);
}
}

View File

@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import remcalc from 'remcalc';
import theme from '../../../theme';
const Span = styled.span`
font-weight: 600;
font-size: ${remcalc(10)};
color: ${theme.secondary};
position: absolute;
top: ${remcalc(8)};
right: ${props => (props.type === 'max' ? '1px' : 'auto')};
`;
/**
* @ignore
* @param {Object} props
* @param {InputRangeClassNames} props.classNames
* @param {Function} props.formatLabel
* @param {string} props.type
*/
export default function Label(props) {
const labelValue = props.formatLabel
? props.formatLabel(props.children, props.type)
: props.children;
return <Span type={props.type}>{labelValue}</Span>;
}
/**
* @type {Object}
* @property {Function} children
* @property {Function} classNames
* @property {Function} formatLabel
* @property {Function} type
*/
Label.propTypes = {
children: PropTypes.node.isRequired,
classNames: PropTypes.objectOf(PropTypes.string).isRequired,
formatLabel: PropTypes.func,
type: PropTypes.string.isRequired
};

View File

@ -0,0 +1,18 @@
import { isNumber } from '../utils';
/**
* @ignore
* @param {Object} props - React component props
* @return {?Error} Return Error if validation fails
*/
export default function rangePropType(props) {
const { maxValue, minValue } = props;
if (!isNumber(minValue) || !isNumber(maxValue)) {
return new Error('"minValue" and "maxValue" must be a number');
}
if (minValue >= maxValue) {
return new Error('"minValue" must be smaller than "maxValue"');
}
}

View File

@ -0,0 +1,309 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Label from './label';
import styled from 'styled-components';
import remcalc from 'remcalc';
import theme from '../../../theme';
export const SliderStyled = styled.div`
appearance: none;
background: ${theme.white};
border: 2px solid ${theme.greyLight};
border-radius: 50%;
cursor: pointer;
display: block;
height: ${remcalc(14)};
width: ${remcalc(14)};
transform: ${props =>
props.type === 'max'
? 'translateY(-50%) translateX(-99%)'
: 'translateY(-50%) translateX(-1%)'};
outline: none;
position: absolute;
top: 0;
margin-top: ${remcalc(2)};
&::active {
transform: scale(1.3);
}
&::focus {
box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2);
}
`;
/**
* @ignore
*/
export default class Slider extends Component {
/**
* Accepted propTypes of Slider
* @override
* @return {Object}
* @property {Function} ariaLabelledby
* @property {Function} ariaControls
* @property {Function} className
* @property {Function} formatLabel
* @property {Function} maxValue
* @property {Function} minValue
* @property {Function} onSliderDrag
* @property {Function} onSliderKeyDown
* @property {Function} percentage
* @property {Function} type
* @property {Function} value
*/
static get propTypes() {
return {
ariaLabelledby: PropTypes.string,
ariaControls: PropTypes.string,
classNames: PropTypes.objectOf(PropTypes.string).isRequired,
formatLabel: PropTypes.func,
maxValue: PropTypes.number,
minValue: PropTypes.number,
onSliderDrag: PropTypes.func.isRequired,
onSliderKeyDown: PropTypes.func.isRequired,
percentage: PropTypes.number.isRequired,
type: PropTypes.string.isRequired,
value: PropTypes.number.isRequired
};
}
/**
* @param {Object} props
* @param {string} [props.ariaLabelledby]
* @param {string} [props.ariaControls]
* @param {InputRangeClassNames} props.classNames
* @param {Function} [props.formatLabel]
* @param {number} [props.maxValue]
* @param {number} [props.minValue]
* @param {Function} props.onSliderKeyDown
* @param {Function} props.onSliderDrag
* @param {number} props.percentage
* @param {number} props.type
* @param {number} props.value
*/
constructor(props) {
super(props);
/**
* @private
* @type {?Component}
*/
this.node = null;
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchMove = this.handleTouchMove.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
/**
* @ignore
* @override
* @return {void}
*/
componentWillUnmount() {
this.removeDocumentMouseMoveListener();
this.removeDocumentMouseUpListener();
this.removeDocumentTouchEndListener();
this.removeDocumentTouchMoveListener();
}
/**
* @private
* @return {Object}
*/
getStyle() {
const perc = (this.props.percentage || 0) * 100;
const style = {
position: 'absolute',
left: `${perc}%`
};
return style;
}
/**
* Listen to mousemove event
* @private
* @return {void}
*/
addDocumentMouseMoveListener() {
this.removeDocumentMouseMoveListener();
this.node.ownerDocument.addEventListener('mousemove', this.handleMouseMove);
}
/**
* Listen to mouseup event
* @private
* @return {void}
*/
addDocumentMouseUpListener() {
this.removeDocumentMouseUpListener();
this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
}
/**
* Listen to touchmove event
* @private
* @return {void}
*/
addDocumentTouchMoveListener() {
this.removeDocumentTouchMoveListener();
this.node.ownerDocument.addEventListener('touchmove', this.handleTouchMove);
}
/**
* Listen to touchend event
* @private
* @return {void}
*/
addDocumentTouchEndListener() {
this.removeDocumentTouchEndListener();
this.node.ownerDocument.addEventListener('touchend', this.handleTouchEnd);
}
/**
* @private
* @return {void}
*/
removeDocumentMouseMoveListener() {
this.node.ownerDocument.removeEventListener(
'mousemove',
this.handleMouseMove
);
}
/**
* @private
* @return {void}
*/
removeDocumentMouseUpListener() {
this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
}
/**
* @private
* @return {void}
*/
removeDocumentTouchMoveListener() {
this.node.ownerDocument.removeEventListener(
'touchmove',
this.handleTouchMove
);
}
/**
* @private
* @return {void}
*/
removeDocumentTouchEndListener() {
this.node.ownerDocument.removeEventListener(
'touchend',
this.handleTouchEnd
);
}
/**
* @private
* @return {void}
*/
handleMouseDown() {
this.addDocumentMouseMoveListener();
this.addDocumentMouseUpListener();
}
/**
* @private
* @return {void}
*/
handleMouseUp() {
this.removeDocumentMouseMoveListener();
this.removeDocumentMouseUpListener();
}
/**
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleMouseMove(event) {
this.props.onSliderDrag(event, this.props.type);
}
/**
* @private
* @return {void}
*/
handleTouchStart() {
this.addDocumentTouchEndListener();
this.addDocumentTouchMoveListener();
}
/**
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleTouchMove(event) {
this.props.onSliderDrag(event, this.props.type);
}
/**
* @private
* @return {void}
*/
handleTouchEnd() {
this.removeDocumentTouchMoveListener();
this.removeDocumentTouchEndListener();
}
/**
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleKeyDown(event) {
this.props.onSliderKeyDown(event, this.props.type);
}
/**
* @override
* @return {JSX.Element}
*/
render() {
const style = this.getStyle();
const props = this.props;
return (
<span
ref={node => {
this.node = node;
}}
>
<Label formatLabel={props.formatLabel} type={props.type}>
{props.value}
</Label>
<SliderStyled
type={props.type}
style={style}
aria-labelledby={props.ariaLabelledby}
aria-controls={props.ariaControls}
aria-valuemax={props.maxValue}
aria-valuemin={props.minValue}
aria-valuenow={props.value}
draggable="false"
onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown}
onTouchStart={this.handleTouchStart}
role="slider"
tabIndex="0"
/>
</span>
);
}
}

View File

@ -0,0 +1,210 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import remcalc from 'remcalc';
import theme from '../../../theme';
export const TrackStyled = styled.div`
background: ${theme.grey};
cursor: pointer;
display: block;
height: ${remcalc(4)};
position: relative;
`;
const ActiveTrack = styled.div`
background: ${theme.blue};
height: 100%;
position: absolute;
`;
/**
* @ignore
*/
export default class Track extends Component {
/**
* @override
* @return {Object}
* @property {Function} children
* @property {Function} classNames
* @property {Boolean} draggableTrack
* @property {Function} onTrackDrag
* @property {Function} onTrackMouseDown
* @property {Function} percentages
*/
static get propTypes() {
return {
children: PropTypes.node.isRequired,
classNames: PropTypes.objectOf(PropTypes.string).isRequired,
draggableTrack: PropTypes.bool,
onTrackDrag: PropTypes.func,
onTrackMouseDown: PropTypes.func.isRequired,
percentages: PropTypes.objectOf(PropTypes.number).isRequired
};
}
/**
* @param {Object} props
* @param {InputRangeClassNames} props.classNames
* @param {Boolean} props.draggableTrack
* @param {Function} props.onTrackDrag
* @param {Function} props.onTrackMouseDown
* @param {number} props.percentages
*/
constructor(props) {
super(props);
/**
* @private
* @type {?Component}
*/
this.node = null;
this.trackDragEvent = null;
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleTouchStart = this.handleTouchStart.bind(this);
}
/**
* @private
* @return {ClientRect}
*/
getClientRect() {
return this.node.getBoundingClientRect();
}
/**
* @private
* @return {Object} CSS styles
*/
getActiveTrackStyle() {
const width = `${(this.props.percentages.max - this.props.percentages.min) *
100}%`;
const left = `${this.props.percentages.min * 100}%`;
return { left, width };
}
/**
* Listen to mousemove event
* @private
* @return {void}
*/
addDocumentMouseMoveListener() {
this.removeDocumentMouseMoveListener();
this.node.ownerDocument.addEventListener('mousemove', this.handleMouseMove);
}
/**
* Listen to mouseup event
* @private
* @return {void}
*/
addDocumentMouseUpListener() {
this.removeDocumentMouseUpListener();
this.node.ownerDocument.addEventListener('mouseup', this.handleMouseUp);
}
/**
* @private
* @return {void}
*/
removeDocumentMouseMoveListener() {
this.node.ownerDocument.removeEventListener(
'mousemove',
this.handleMouseMove
);
}
/**
* @private
* @return {void}
*/
removeDocumentMouseUpListener() {
this.node.ownerDocument.removeEventListener('mouseup', this.handleMouseUp);
}
/**
* @private
* @param {SyntheticEvent} event
* @return {void}
*/
handleMouseMove(event) {
if (!this.props.draggableTrack) {
return;
}
if (this.trackDragEvent !== null) {
this.props.onTrackDrag(event, this.trackDragEvent);
}
this.trackDragEvent = event;
}
/**
* @private
* @return {void}
*/
handleMouseUp() {
if (!this.props.draggableTrack) {
return;
}
this.removeDocumentMouseMoveListener();
this.removeDocumentMouseUpListener();
this.trackDragEvent = null;
}
/**
* @private
* @param {SyntheticEvent} event - User event
*/
handleMouseDown(event) {
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
const trackClientRect = this.getClientRect();
const position = {
x: clientX - trackClientRect.left,
y: 0
};
this.props.onTrackMouseDown(event, position);
if (this.props.draggableTrack) {
this.addDocumentMouseMoveListener();
this.addDocumentMouseUpListener();
}
}
/**
* @private
* @param {SyntheticEvent} event - User event
*/
handleTouchStart(event) {
event.preventDefault();
this.handleMouseDown(event);
}
/**
* @override
* @return {JSX.Element}
*/
render() {
const activeTrackStyle = this.getActiveTrackStyle();
return (
<TrackStyled
onMouseDown={this.handleMouseDown}
onTouchStart={this.handleTouchStart}
innerRef={node => {
this.node = node;
}}
>
<ActiveTrack style={activeTrackStyle} />
{this.props.children}
</TrackStyled>
);
}
}

View File

@ -0,0 +1,23 @@
import { isNumber, isObject } from '../utils';
/**
* @ignore
* @param {Object} props
* @return {?Error} Return Error if validation fails
*/
export default function valuePropType(props, propName) {
const { maxValue, minValue } = props;
const value = props[propName];
if (!isNumber(value) && (!isObject(value) || !isNumber(value.min) || !isNumber(value.max))) {
return new Error(`"${propName}" must be a number or a range object`);
}
if (isNumber(value) && (value < minValue || value > maxValue)) {
return new Error(`"${propName}" must be in between "minValue" and "maxValue"`);
}
if (isObject(value) && (value.min < minValue || value.min > maxValue || value.max < minValue || value.max > maxValue)) {
return new Error(`"${propName}" must be in between "minValue" and "maxValue"`);
}
}

View File

@ -0,0 +1,144 @@
import { clamp } from '../utils';
/**
* Convert a point into a percentage value
* @ignore
* @param {Point} position
* @param {ClientRect} clientRect
* @return {number} Percentage value
*/
export function getPercentageFromPosition(position, clientRect) {
const length = clientRect.width;
const sizePerc = position.x / length;
return sizePerc || 0;
}
/**
* Convert a point into a model value
* @ignore
* @param {Point} position
* @param {number} minValue
* @param {number} maxValue
* @param {ClientRect} clientRect
* @return {number}
*/
export function getValueFromPosition(position, minValue, maxValue, clientRect) {
const sizePerc = getPercentageFromPosition(position, clientRect);
const valueDiff = maxValue - minValue;
return minValue + (valueDiff * sizePerc);
}
/**
* Convert props into a range value
* @ignore
* @param {Object} props
* @param {boolean} isMultiValue
* @return {Range}
*/
export function getValueFromProps(props, isMultiValue) {
if (isMultiValue) {
return { ...props.value };
}
return {
min: props.minValue,
max: props.value,
};
}
/**
* Convert a model value into a percentage value
* @ignore
* @param {number} value
* @param {number} minValue
* @param {number} maxValue
* @return {number}
*/
export function getPercentageFromValue(value, minValue, maxValue) {
const validValue = clamp(value, minValue, maxValue);
const valueDiff = maxValue - minValue;
const valuePerc = (validValue - minValue) / valueDiff;
return valuePerc || 0;
}
/**
* Convert model values into percentage values
* @ignore
* @param {Range} values
* @param {number} minValue
* @param {number} maxValue
* @return {Range}
*/
export function getPercentagesFromValues(values, minValue, maxValue) {
return {
min: getPercentageFromValue(values.min, minValue, maxValue),
max: getPercentageFromValue(values.max, minValue, maxValue),
};
}
/**
* Convert a value into a point
* @ignore
* @param {number} value
* @param {number} minValue
* @param {number} maxValue
* @param {ClientRect} clientRect
* @return {Point} Position
*/
export function getPositionFromValue(value, minValue, maxValue, clientRect) {
const length = clientRect.width;
const valuePerc = getPercentageFromValue(value, minValue, maxValue);
const positionValue = valuePerc * length;
return {
x: positionValue,
y: 0,
};
}
/**
* Convert a range of values into points
* @ignore
* @param {Range} values
* @param {number} minValue
* @param {number} maxValue
* @param {ClientRect} clientRect
* @return {Range}
*/
export function getPositionsFromValues(values, minValue, maxValue, clientRect) {
return {
min: getPositionFromValue(values.min, minValue, maxValue, clientRect),
max: getPositionFromValue(values.max, minValue, maxValue, clientRect),
};
}
/**
* Convert an event into a point
* @ignore
* @param {Event} event
* @param {ClientRect} clientRect
* @return {Point}
*/
export function getPositionFromEvent(event, clientRect) {
const length = clientRect.width;
const { clientX } = event.touches ? event.touches[0] : event;
return {
x: clamp(clientX - clientRect.left, 0, length),
y: 0,
};
}
/**
* Convert a value into a step value
* @ignore
* @param {number} value
* @param {number} valuePerStep
* @return {number}
*/
export function getStepValueFromValue(value, valuePerStep) {
return Math.round(value / valuePerStep) * valuePerStep;
}

View File

@ -0,0 +1,9 @@
/**
* Captialize a string
* @ignore
* @param {string} string
* @return {string}
*/
export default function captialize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View File

@ -0,0 +1,11 @@
/**
* Clamp a value between a min and max value
* @ignore
* @param {number} value
* @param {number} min
* @param {number} max
* @return {number}
*/
export default function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}

View File

@ -0,0 +1,13 @@
/**
* Calculate the distance between pointA and pointB
* @ignore
* @param {Point} pointA
* @param {Point} pointB
* @return {number} Distance
*/
export default function distanceTo(pointA, pointB) {
const xDiff = (pointB.x - pointA.x) ** 2;
const yDiff = (pointB.y - pointA.y) ** 2;
return Math.sqrt(xDiff + yDiff);
}

View File

@ -0,0 +1,7 @@
export { default as captialize } from './captialize';
export { default as clamp } from './clamp';
export { default as distanceTo } from './distance-to';
export { default as isDefined } from './is-defined';
export { default as isNumber } from './is-number';
export { default as isObject } from './is-object';
export { default as length } from './length';

View File

@ -0,0 +1,9 @@
/**
* Check if a value is defined
* @ignore
* @param {*} value
* @return {boolean}
*/
export default function isDefined(value) {
return value !== undefined && value !== null;
}

View File

@ -0,0 +1,9 @@
/**
* Check if a value is a number
* @ignore
* @param {*} value
* @return {boolean}
*/
export default function isNumber(value) {
return typeof value === 'number';
}

View File

@ -0,0 +1,9 @@
/**
* Check if a value is an object
* @ignore
* @param {*} value
* @return {boolean}
*/
export default function isObject(value) {
return value !== null && typeof value === 'object';
}

View File

@ -0,0 +1,10 @@
/**
* Calculate the absolute difference between two numbers
* @ignore
* @param {number} numA
* @param {number} numB
* @return {number}
*/
export default function length(numA, numB) {
return Math.abs(numA - numB);
}

1011
yarn.lock

File diff suppressed because it is too large Load Diff