Skip to content
This repository has been archived by the owner on May 10, 2019. It is now read-only.

106: Add Slider and SliderInput components #124

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
102 changes: 102 additions & 0 deletions src/lib/components/Slider/Slider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import PropTypes from 'prop-types';
import ensureValueInRange from '../../utils/ensureValueInRange';
import getClassName from '../../utils/getClassName';
import SliderInput from './SliderInput';

class Slider extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);

const value = ensureValueInRange(props.value, props.max, props.min);
this.state = { value };
}

componentWillReceiveProps(nextProps) {
const { value, max, min } = nextProps;
const newValue = ensureValueInRange(value, max, min);

this.setState({ value: newValue });
}

handleChange(e) {
const { max, min } = this.props;
const targetValue = Number(e.target.value);
const newValue = ensureValueInRange(targetValue, max, min);

this.setState({ value: newValue });
}

// Fix for Chrome and Safari as they don't support CSS slider progress
handleStyle() {
const { value } = this.state;
const { max, min } = this.props;

const ratio = (value - min) / (max - min);
return { backgroundImage: `-webkit-gradient(linear, left top, right top, color-stop(${ratio}, #335280), color-stop(${ratio}, #fff))` };
}

render() {
const {
className, disabled, max, min, showValue, step,
} = this.props;
const { value } = this.state;

let style;
if (/Chrome/i.test(navigator.userAgent) || /Safari/i.test(navigator.userAgent)) {
style = this.handleStyle();
}

const classNames = getClassName({
[className]: className,
'p-slider': true,
}) || undefined;

return (
<div className="p-slider__wrapper">
<input
className={classNames}
disabled={disabled}
max={max}
min={min}
step={step}
style={style}
type="range"
value={value}
onChange={this.handleChange}
/>
{showValue &&
<SliderInput
disabled={disabled}
value={value}
onChange={this.handleChange}
/>}
</div>
);
}
}

Slider.defaultProps = {
className: undefined,
disabled: false,
max: 100,
min: 0,
showValue: false,
step: 1,
value: undefined,
};

Slider.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool,
max: PropTypes.number,
min: PropTypes.number,
showValue: PropTypes.bool,
step: PropTypes.number,
value: PropTypes.number,
};

Slider.displayName = 'Slider';

export default Slider;
32 changes: 32 additions & 0 deletions src/lib/components/Slider/Slider.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { boolean, number } from '@storybook/addon-knobs';
import { withInfo } from '@storybook/addon-info';

import Slider from './Slider';

storiesOf('Slider', module)
.add('Default',
withInfo('The <Slider> component allows a user to select from a specified range of values, where the precise value is not considered important.')(() => (
<Slider
value={number('Value', 0)}
max={number('Max', 100)}
min={number('Min', 0)}
step={number('Step', 1)}
showValue={boolean('Show value', false)}
disabled={boolean('Disabled', false)}
/>),
),
)

.add('With Value',
withInfo('When the "showValue" prop is added to <Slider> an editable input will display, showing a numeric representation of the slider value.')(() => (
<Slider
max={number('Max', 100)}
min={number('Min', 0)}
step={number('Step', 1)}
showValue={boolean('Show value', true)}
disabled={boolean('Disabled', false)}
/>),
),
);
59 changes: 59 additions & 0 deletions src/lib/components/Slider/Slider.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import ReactTestRenderer from 'react-test-renderer';
import { shallow } from 'enzyme';
import Slider from './Slider';

describe('<Slider>', () => {
it('should match the default snapshot', () => {
const slider = ReactTestRenderer.create(
<Slider />);
const json = slider.toJSON();
expect(json).toMatchSnapshot();
});

it('should match the snapshot with showValue prop', () => {
const slider = ReactTestRenderer.create(
<Slider showValue />);
const json = slider.toJSON();
expect(json).toMatchSnapshot();
});

it('should match the snapshot with disabled prop', () => {
const slider = ReactTestRenderer.create(
<Slider disabled />);
const json = slider.toJSON();
expect(json).toMatchSnapshot();
});

it('should render additional classes', () => {
const slider = shallow(<Slider className="class" />);

expect(slider.find('div').at(0).children('input').hasClass('class')).toBe(true);
});

it('should keep value state below max prop', () => {
const slider = shallow(<Slider max={2} />);

slider.setProps({ value: 2 });
expect(slider.state().value).toEqual(2);

slider.setProps({ value: 3 });
expect(slider.state().value).toEqual(2);

slider.setProps({ max: 3 });
expect(slider.state().value).toEqual(3);
});

it('should keep value state above min prop', () => {
const slider = shallow(<Slider min={1} />);

slider.setProps({ value: 1 });
expect(slider.state().value).toEqual(1);

slider.setProps({ value: 0 });
expect(slider.state().value).toEqual(1);

slider.setProps({ min: 0 });
expect(slider.state().value).toEqual(0);
});
});
39 changes: 39 additions & 0 deletions src/lib/components/Slider/SliderInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import getClassName from '../../utils/getClassName';

const SliderInput = (props) => {
const {
className, value, ...otherProps
} = props;

const classNames = getClassName({
[className]: className,
'p-slider__input': true,
}) || undefined;

return (
<input
className={classNames}
type="text"
value={value}
{...otherProps}
/>
);
};

SliderInput.defaultProps = {
className: undefined,
maxLength: 3,
value: undefined,
};

SliderInput.propTypes = {
className: PropTypes.string,
maxLength: PropTypes.number,
value: PropTypes.number,
};

SliderInput.displayName = 'SliderInput';

export default SliderInput;
63 changes: 63 additions & 0 deletions src/lib/components/Slider/__snapshots__/Slider.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Slider> should match the default snapshot 1`] = `
<div
className="p-slider__wrapper"
>
<input
className="p-slider"
disabled={false}
max={100}
min={0}
onChange={[Function]}
step={1}
style={undefined}
type="range"
value={0}
/>
</div>
`;

exports[`<Slider> should match the snapshot with disabled prop 1`] = `
<div
className="p-slider__wrapper"
>
<input
className="p-slider"
disabled={true}
max={100}
min={0}
onChange={[Function]}
step={1}
style={undefined}
type="range"
value={0}
/>
</div>
`;

exports[`<Slider> should match the snapshot with showValue prop 1`] = `
<div
className="p-slider__wrapper"
>
<input
className="p-slider"
disabled={false}
max={100}
min={0}
onChange={[Function]}
step={1}
style={undefined}
type="range"
value={0}
/>
<input
className="p-slider__input"
disabled={false}
maxLength={3}
onChange={[Function]}
type="text"
value={0}
/>
</div>
`;
7 changes: 5 additions & 2 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import SideNav from './components/SideNav/SideNav';
import SideNavBanner from './components/SideNav/SideNavBanner';
import SideNavGroup from './components/SideNav/SideNavGroup';
import SideNavLink from './components/SideNav/SideNavLink';
import Slider from './components/Slider/Slider';
import SliderInput from './components/Slider/SliderInput';
import SteppedList from './components/SteppedList/SteppedList';
import SteppedListItem from './components/SteppedList/SteppedListItem';
import Strip from './components/Strip/Strip';
Expand All @@ -62,6 +64,7 @@ export {
Footer, FooterNav, FooterNavContainer, HeadingIcon, Image, InlineImages, Input, Label, Link, List,
ListItem, ListTree, ListTreeGroup, ListTreeItem, Matrix, MatrixItem, MediaObject, Modal,
MutedHeading, Navigation, NavigationBanner, NavigationLink, Notification, Pagination,
PaginationItem, SideNav, SideNavBanner, SideNavGroup, SideNavLink, SteppedList, SteppedListItem,
Strip, StripColumn, StripRow, Switch, Table, TableCell, TableRow, Tabs, TabsItem, ToolTip,
PaginationItem, SideNav, SideNavBanner, SideNavGroup, SideNavLink, Slider, SliderInput,
SteppedList, SteppedListItem, Strip, StripColumn, StripRow, Switch, Table, TableCell, TableRow,
Tabs, TabsItem, ToolTip,
};
13 changes: 13 additions & 0 deletions src/lib/utils/ensureValueInRange.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function ensureValueInRange(val, max, min) {
if (val) {
if (val <= min) {
return min;
}
if (val >= max) {
return max;
}
} else {
return min;
}
return val;
}