Skip to content

Commit

Permalink
feat(slider): add slider component (#146)
Browse files Browse the repository at this point in the history
* feat(slider): WIP adding slider component

* feat(slider): add correct styling

* feat(slider): make filled track follow the thumb position

* feat(slider): WIP math

* feat(slider): WIP math, and add support for track click

* feat(slider): WIP rounding math

* feat(slider): fix rounding math

* feat(slider): styling and text input option

* feat(slider): add eventedState

* feat(slider): refactor js

* feat(slider): add support for shiftKey as a modifier

* feat(slider): js cleanup

* feat(slider): make stepMulitplier a configurable option

* test(slider): WIP slider tests

* feat(slider): fix focus/active states, and update tests

* feat(slider): fix shiftKey multiplied math, tests

* test(slider): fix slider tests raf bug

* feat(slider): add min/max labels

* test(slider): WIP fix raf conflict

* test(slider): remove raf stub conflict

* feat(slider): fix misaligned track

* feat(slider): fix support for disabled slider

* fix(slider): fix merge conflict

* docs(slider): re-add README

* fix(slider): fix typo failing tests

* fix(slider): PR fixes

* fix(slider): fix visual PR bug
  • Loading branch information
iangfleming committed Jun 14, 2017
1 parent 73b8e0d commit 8b571fb
Show file tree
Hide file tree
Showing 9 changed files with 547 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"minimatch": "^3.0.0",
"minimist": "~1.2.0",
"mocha": "~2.4.4",
"mock-raf": "^1.0.0",
"nodemon": "1.9.1",
"phantomjs-prebuilt": "^2.1.3",
"pump": "^1.0.2",
Expand Down
34 changes: 34 additions & 0 deletions src/components/slider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
### SCSS

#### Modifiers

Use these modifiers with `.bx--slider` class.

| Selector | Description |
|--------------------------|---------------------------------------------------------------------|
| .bx--slider--disabled | Applies disabled styling and prevents JS from running on user input |

### Javascript

#### Options

| Option | Default Selector | Description |
|--------------------------------|------------------------------|---------------------------------------------------------------------------|
| `selectorInit` | `[data-slider]` | The selector to find the Slider element. |
| `selectorTrack` | `.bx--slider__track` | The selector to find the Slider track element. |
| `selectorFilledTrack` | `.bx--slider__filled-track` | The selector to find the Slider filled track element. |
| `selectorThumb` | `.bx--slider__thumb` | The selector to find the Slider thumb element. |
| `selectorInput` | `.bx--slider__input` | The selector to find the Slider input element. |
| `eventBeforeSliderValueChange` | `slider-before-value-change` | The event emitted before the Slider value changes. |
| `eventAfterSliderValueChange` | `slider-after-value-change` | The event emitted when the Slider value changes. |
| `stepMultiplier` | `4` | The multiplier applied to arrow key inputs when the shift key is pressed. |

### FAQ

#### Keydown event

Once `Slider` instance is initialized a user can use the following keys to control the slider.

- `up` and `right` arrow keys increase the slider value by one step
- `down` and `left` arrow keys decrease the slider value by one step
- Pressing `shift` while changing the value of the slider will multiple the change by `Slider.options.stepMultiplier` (the default is 4)
115 changes: 115 additions & 0 deletions src/components/slider/_slider.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//-----------------------------
// Slider
//-----------------------------

@import '../../globals/scss/colors';
@import '../../globals/scss/helper-mixins';
@import '../../globals/scss/layer';
@import '../../globals/scss/typography';
@import '../../globals/scss/layout';
@import '../../globals/scss/import-once';
@import '../form/form';
@import '../text-input/text-input';

@include exports('slider') {
.bx--slider-container {
max-width: rem(600px);
min-width: rem(200px);
display: flex;
align-items: center;
user-select: none;

@media screen and (min-width: 768px) {
min-width: rem(350px);
}
}

.bx--slider {
position: relative;
width: 100%;
margin: 0 1rem;
}

.bx--slider--disabled {
opacity: .5;
}

.bx--slider--disabled .bx--slider__thumb {
&:hover {
transform: translate(-50%, -50%);
}
&:focus {
box-shadow: none;
outline: none;
}
&:active {
background: $brand-01;
transform: translate(-50%, -50%);
}
}

.bx--slider__range-label {
@include font-size('14');
color: $text-02;

&:last-of-type {
margin-right: 1rem;
}
}

.bx--slider__track {
position: absolute;
width: 100%;
height: rem(4px);
background: $ui-05;
cursor: pointer;
transform: translate(0%, -50%);
}

.bx--slider__filled-track {
position: absolute;
width: 100%;
height: rem(4px);
background: $brand-01;
transform-origin: left;
pointer-events: none;
transform: translate(0%, -50%);
}

.bx--slider__thumb {
position: absolute;
height: rem(24px);
width: rem(24px);
background: $brand-01;
border-radius: 50%;
top: 0;
transform: translate(-50%, -50%);
transition: transform 100ms $bx--standard-easing, background 100ms $bx--standard-easing;
cursor: pointer;
outline: none;

&:hover {
transform: translate(-50%, -50%) scale(1.05);
}
&:focus {
@include focus-outline('blurred');
}
&:active {
background: darken($brand-01, 5%);
transform: translate(-50%, -50%) scale(1.25);
}
}

.bx--slider__input {
display: none;
}

.bx-slider-text-input {
max-width: rem(32px);
min-width: 0;
height: 2rem;
padding: 0;
text-align: center;
font-weight: 700;
}
}
16 changes: 16 additions & 0 deletions src/components/slider/slider.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="bx--form-item">
<label for="slider" class="bx--label">Slider Label</label>
<div class="bx--slider-test">
<div class="bx--slider-container">
<span class="bx--slider__range-label">0</span>
<div class="bx--slider" data-slider data-slider-input-box="#slider-input-box">
<div class="bx--slider__track"></div>
<div class="bx--slider__filled-track"></div>
<div class="bx--slider__thumb" tabindex="0"></div>
<input id="slider" class="bx--slider__input" type="range" step="1" min="0" max="100" value="50">
</div>
<span class="bx--slider__range-label">100</span>
<input id="slider-input-box" type="text" class="bx--text-input bx-slider-text-input" placeholder="0">
</div>
</div>
</div>
206 changes: 206 additions & 0 deletions src/components/slider/slider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import mixin from '../../globals/js/misc/mixin';
import createComponent from '../../globals/js/mixins/create-component';
import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
import eventedState from '../../globals/js/mixins/evented-state';
import on from '../../globals/js/misc/on';

class Slider extends mixin(createComponent, initComponentBySearch, eventedState) {
/**
* Slider.
* @extends CreateComponent
* @extends InitComponentBySearch
* @param {HTMLElement} element The element working as an slider.
*/
constructor(element, options) {
super(element, options);

this.sliderActive = false;
this.dragging = false;

this.track = this.element.querySelector(this.options.selectorTrack);
this.filledTrack = this.element.querySelector(this.options.selectorFilledTrack);
this.thumb = this.element.querySelector(this.options.selectorThumb);
this.input = this.element.querySelector(this.options.selectorInput);

if (this.element.dataset.sliderInputBox) {
this.boundInput = this.element.ownerDocument.querySelector(this.element.dataset.sliderInputBox);
this._updateInput();
this.boundInput.addEventListener('change', (evt) => { this.setValue(evt.target.value); });
}

this._updatePosition();

this.thumb.addEventListener('mousedown', () => { this.sliderActive = true; });
this.hDocumentMouseUp = on(this.element.ownerDocument, 'mouseup', () => { this.sliderActive = false; });
this.hDocumentMouseMove = on(this.element.ownerDocument, 'mousemove', (evt) => {
const disabled = this.element.classList.contains('bx--slider--disabled');
if (this.sliderActive === true && !disabled) {
this._updatePosition(evt);
}
});
this.thumb.addEventListener('keydown', (evt) => {
const disabled = this.element.classList.contains('bx--slider--disabled');
if (!disabled) {
this._updatePosition(evt);
}
});
this.track.addEventListener('click', (evt) => {
const disabled = this.element.classList.contains('bx--slider--disabled');
if (!disabled) {
this._updatePosition(evt);
}
});
}

_changeState = (state, detail, callback) => {
callback();
}


_updatePosition(evt) {
const {
left,
newValue,
} = this._calcValue(evt);


if (this.dragging) {
return;
}

this.dragging = true;

requestAnimationFrame(() => {
this.dragging = false;
this.thumb.style.left = `${left}%`;
this.filledTrack.style.transform = `translate(0%, -50%) scaleX(${left / 100})`;
this.input.value = newValue;
this._updateInput();
this.changeState('slider-value-change', { value: newValue });
});
}

_calcValue(evt) {
const {
value,
min,
max,
step,
} = this.getInputProps();

const range = max - min;
const valuePercentage = (((value - min) / range) * 100);

let left;
let newValue;
left = valuePercentage;
newValue = value;

if (evt) {
const { type } = evt;

if (type === 'keydown') {
const direction = {
40: -1, // decreasing
37: -1, // decreasing
38: 1, // increasing
39: 1, // increasing
}[evt.which];

if (direction !== undefined) {
const multiplier = evt.shiftKey === true
? (range / step) / this.options.stepMultiplier
: 1;
const stepMultiplied = step * multiplier;
const stepSize = (stepMultiplied / range) * 100;
left = valuePercentage + (stepSize * direction);
newValue = Number(value) + (stepMultiplied * direction);
}
}
if (type === 'mousemove' || type === 'click') {
const track = this.track.getBoundingClientRect();
const unrounded = ((evt.clientX - track.left) / track.width);
const rounded = Math.round(((range * unrounded) / step)) * step;
left = (((rounded - min) / range) * 100);
newValue = rounded;
}
}

if (newValue <= Number(min)) {
left = 0;
newValue = min;
}
if (newValue >= Number(max)) {
left = 100;
newValue = max;
}

return { left, newValue };
}

_updateInput() {
if (this.boundInput) {
this.boundInput.value = this.input.value;
}
}

getInputProps() {
const values = {
value: this.input.value,
min: this.input.min,
max: this.input.max,
step: this.input.step ? this.input.step : 1,
};
return values;
}

setValue(value) {
this.input.value = value;
this._updatePosition();
}

stepUp() {
this.input.stepUp();
this._updatePosition();
}

stepDown() {
this.input.stepDown();
this._updatePosition();
}

release() {
if (this.hDocumentMouseUp) {
this.hDocumentMouseUp = this.hDocumentMouseUp.release();
}
if (this.hDocumentMouseMove) {
this.hDocumentMouseMove = this.hDocumentMouseMove.release();
}
super.release();
}

/**
* The map associating DOM element and Slider UI instance.
* @type {WeakMap}
*/
static components = new WeakMap();

/**
* The component options.
* If `options` is specified in the constructor,
* properties in this object are overriden for the instance being created.
* @property {string} selectorInit The CSS selector to find slider instances.
*/
static options = {
selectorInit: '[data-slider]',
selectorTrack: '.bx--slider__track',
selectorFilledTrack: '.bx--slider__filled-track',
selectorThumb: '.bx--slider__thumb',
selectorInput: '.bx--slider__input',
eventBeforeSliderValueChange: 'slider-before-value-change',
eventAfterSliderValueChange: 'slider-after-value-change',
stepMultiplier: 4,
}
}

export default Slider;
1 change: 1 addition & 0 deletions src/globals/scss/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ $css--typography: true !default;
@import '../../components/breadcrumb/breadcrumb';
@import '../../components/toolbar/toolbar';
@import '../../components/time-picker/time-picker';
@import '../../components/slider/slider';

//-------------------------------------
// 🙈 Hidden (Not exposed on website)
Expand Down
Loading

0 comments on commit 8b571fb

Please sign in to comment.