Skip to content

Commit

Permalink
Basic slider accessibility (#282)
Browse files Browse the repository at this point in the history
* Makes the handle a focusable element

* Adds keyboard value mutators for all supported key behaviours

* Adds the keyboard event handlers to the `createSlider` method

* Adds the `onKeyboard` event handler in both slider and range

* Updates the snapshot to allow tabindex on the handler

* Updates the Range snapshot to allow tabindex in tests

* Adds tests for all keyboard events for the slider

* Reverts `defaultValue` property back to `value` within the slider tests
  • Loading branch information
byCedric authored and paranoidjk committed Jul 28, 2017
1 parent 9c0ea63 commit 59a5b15
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 7 deletions.
5 changes: 5 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
cursor: -webkit-grabbing;
cursor: grabbing;
}
&:focus {
border-color: tint(@primary-color, 20%);
box-shadow: 0 0 0 5px tint(@primary-color, 50%);
outline: none;
}
}

&-mark {
Expand Down
1 change: 1 addition & 0 deletions src/Handle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default class Handle extends React.Component {
return (
<div
role="slider"
tabIndex="0"
{...ariaProps}
{...restProps}
className={className}
Expand Down
5 changes: 5 additions & 0 deletions src/Range.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import shallowEqual from 'shallowequal';
import warning from 'warning';
import Track from './common/Track';
import createSlider from './common/createSlider';
import * as utils from './utils';
Expand Down Expand Up @@ -136,6 +137,10 @@ class Range extends React.Component {
});
}

onKeyboard() {
warning(true, 'Keyboard support is not yet supported for ranges.');
}

getValue() {
return this.state.bounds;
}
Expand Down
15 changes: 15 additions & 0 deletions src/Slider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ class Slider extends React.Component {
this.onChange({ value });
}

onKeyboard(e) {
const valueMutator = utils.getKeyboardValueMutator(e);

if (valueMutator) {
utils.pauseEvent(e);
const state = this.state;
const oldValue = state.value;
const mutatedValue = valueMutator(oldValue, this.props);
const value = this.trimAlignValue(mutatedValue);
if (value === oldValue) return;

this.onChange({ value });
}
}

getValue() {
return this.state.value;
}
Expand Down
25 changes: 25 additions & 0 deletions src/common/createSlider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ export default function createSlider(Component) {
utils.pauseEvent(e);
}

onFocus = (e) => {
const isVertical = this.props.vertical;

if (utils.isEventFromHandle(e, this.handlesRefs)) {
const handlePosition = utils.getHandleCenterPosition(isVertical, e.target);

this.dragOffset = 0;
this.onStart(handlePosition);
utils.pauseEvent(e);
}
}

onBlur = (e) => {
this.onEnd(e);
};

addDocumentTouchEvents() {
// just work for Chrome iOS Safari and Android Browser
this.onTouchMoveListener = addEventListener(document, 'touchmove', this.onTouchMove);
Expand Down Expand Up @@ -160,6 +176,12 @@ export default function createSlider(Component) {
this.onMove(e, position - this.dragOffset);
}

onKeyDown = (e) => {
if (this.sliderRef && utils.isEventFromHandle(e, this.handlesRefs)) {
this.onKeyboard(e);
}
}

getSliderStart() {
const slider = this.sliderRef;
const rect = slider.getBoundingClientRect();
Expand Down Expand Up @@ -237,6 +259,9 @@ export default function createSlider(Component) {
className={sliderClassName}
onTouchStart={disabled ? noop : this.onTouchStart}
onMouseDown={disabled ? noop : this.onMouseDown}
onKeyDown={disabled ? noop : this.onKeyDown}
onFocus={disabled ? noop : this.onFocus}
onBlur={disabled ? noop : this.onBlur}
style={style}
>
<div
Expand Down
20 changes: 20 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { findDOMNode } from 'react-dom';
import keyCode from 'rc-util/lib/KeyCode';

export function isEventFromHandle(e, handles) {
return Object.keys(handles)
Expand Down Expand Up @@ -70,3 +71,22 @@ export function pauseEvent(e) {
e.stopPropagation();
e.preventDefault();
}

export function getKeyboardValueMutator(e) {
switch (e.keyCode) {
case keyCode.UP:
case keyCode.RIGHT:
return (value, props) => value + props.step;

case keyCode.DOWN:
case keyCode.LEFT:
return (value, props) => value - props.step;

case keyCode.END: return (value, props) => props.max;
case keyCode.HOME: return (value, props) => props.min;
case keyCode.PAGE_UP: return (value, props) => value + props.step * 2;
case keyCode.PAGE_DOWN: return (value, props) => value - props.step * 2;

default: return undefined;
}
}
81 changes: 81 additions & 0 deletions tests/Slider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import React from 'react';
import { render, mount } from 'enzyme';
import { renderToJson } from 'enzyme-to-json';
import keyCode from 'rc-util/lib/KeyCode';
import Slider from '../src/Slider';

describe('Slider', () => {
Expand All @@ -20,4 +21,84 @@ describe('Slider', () => {
expect(trackStyle).toMatch(/width: 50%;/);
expect(trackStyle).toMatch(/visibility: visible;/);
});

it('increments the value when key "up" was pressed', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.UP });

expect(wrapper.state('value')).toBe(51);
});

it('increments the value when key "right" was pressed', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.RIGHT });

expect(wrapper.state('value')).toBe(51);
});

it('increases the value when key "page up" was pressed, by a factor 2', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.PAGE_UP });

expect(wrapper.state('value')).toBe(52);
});

it('decreases the value when key "down" was pressed', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.DOWN });

expect(wrapper.state('value')).toBe(49);
});

it('decreases the value when key "left" was pressed', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.LEFT });

expect(wrapper.state('value')).toBe(49);
});

it('decreases the value when key "page down" was pressed, by a factor 2', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.PAGE_DOWN });

expect(wrapper.state('value')).toBe(48);
});

it('sets the value to minimum when key "home" was pressed', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.HOME });

expect(wrapper.state('value')).toBe(0);
});

it('sets the value to maximum when the key "end" was pressed', () => {
const wrapper = mount(<Slider defaultValue={50} />);
const handler = wrapper.find('.rc-slider-handle');

wrapper.simulate('focus');
handler.simulate('keyDown', { keyCode: keyCode.END });

expect(wrapper.state('value')).toBe(100);
});
});
18 changes: 12 additions & 6 deletions tests/__snapshots__/Range.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,35 @@ exports[`Range should render Multi-Range with correct DOM structure 1`] = `
aria-valuenow="0"
class="rc-slider-handle rc-slider-handle-1"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="rc-slider-handle rc-slider-handle-2"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="rc-slider-handle rc-slider-handle-3"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="rc-slider-handle rc-slider-handle-4"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
class="rc-slider-mark" />
</div>
Expand All @@ -68,15 +72,17 @@ exports[`Range should render Range with correct DOM structure 1`] = `
aria-valuenow="0"
class="rc-slider-handle rc-slider-handle-1"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
aria-disabled="false"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="rc-slider-handle rc-slider-handle-2"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
class="rc-slider-mark" />
</div>
Expand Down
3 changes: 2 additions & 1 deletion tests/__snapshots__/Slider.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ exports[`Slider should render Slider with correct DOM structure 1`] = `
aria-valuenow="0"
class="rc-slider-handle"
role="slider"
style="left:0%;" />
style="left:0%;"
tabindex="0" />
<div
class="rc-slider-mark" />
</div>
Expand Down

0 comments on commit 59a5b15

Please sign in to comment.