Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][Slider] use native input to trigger native events #12087

Closed
wants to merge 2 commits into from
Closed
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
278 changes: 72 additions & 206 deletions packages/material-ui-lab/src/Slider/Slider.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import React from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';
import keycode from 'keycode';
import classNames from 'classnames';
import withStyles from '@material-ui/core/styles/withStyles';
import ButtonBase from '@material-ui/core/ButtonBase';
import { fade } from '@material-ui/core/styles/colorManipulator';
import clamp from '../utils/clamp';

Expand All @@ -29,6 +26,40 @@ export const style = theme => {
};

return {
// /* Styles for native input implementation */
native: {
zIndex: 10,
WebkitAppearance: 'none',
position: 'relative',
width: '100%',
margin: '10px 0',
padding: '6px 0',
background: 'transparent',
'&$disabled': {
cursor: 'no-drop',
},
'&$vertical': {
WebkitAppearance: 'slider-vertical', // WebKit
writingMode: 'gt-lr', // IE
height: '100%',
margin: '0 10px',
padding: '0 6px',
},
'&$reverse': {
transform: 'scaleX(-1)',
},
'&$vertical$reverse': {
transform: 'scaleY(-1)',
},
'&::-webkit-slider-thumb': {
WebkitAppearance: 'none',
width: 12,
height: 12,
},
'&:focus': {
outline: 'none',
},
},
// /* Styles for root node */
root: {
position: 'relative',
Expand Down Expand Up @@ -95,6 +126,8 @@ export const style = theme => {
/* Thumb styles */
thumb: {
position: 'absolute',
top: '50%',
left: '50%',
zIndex: 2,
transform: 'translate(-50%, -50%)',
width: 12,
Expand Down Expand Up @@ -143,80 +176,22 @@ export const style = theme => {
};
};

function addEventListener(node, event, handler, capture) {
node.addEventListener(event, handler, capture);
return {
remove: function remove() {
node.removeEventListener(event, handler, capture);
},
};
}

function percentToValue(percent, min, max) {
return ((max - min) * percent) / 100 + min;
}

function roundToStep(number, step) {
return Math.round(number / step) * step;
}

function getOffset(node) {
const { pageYOffset, pageXOffset } = global;
const { left, top } = node.getBoundingClientRect();

return {
top: top + pageYOffset,
left: left + pageXOffset,
};
}

function getMousePosition(event) {
if (event.changedTouches && event.changedTouches[0]) {
return {
x: event.changedTouches[0].pageX,
y: event.changedTouches[0].pageY,
};
}

return {
x: event.pageX,
y: event.pageY,
};
}

function calculatePercent(node, event, isVertical, isReverted) {
const { width, height } = node.getBoundingClientRect();
const { top, left } = getOffset(node);
const { x, y } = getMousePosition(event);

const value = isVertical ? y - top : x - left;
const onePercent = (isVertical ? height : width) / 100;

return isReverted ? 100 - clamp(value / onePercent) : clamp(value / onePercent);
}

function preventPageScrolling(event) {
event.preventDefault();
}

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !React.createContext) {
throw new Error('Material-UI: [email protected] or greater is required.');
}

/**
* native implementation research:
* https://caniuse.com/#search=range matches https://material-ui.com/getting-started/supported-platforms/
* reverse: should be possible via css
* vertical: spec != browsersupport https://www.w3.org/TR/html5/sec-forms.html#range-state-typerange
* https://stackoverflow.com/questions/15935837/how-to-display-a-range-input-slider-vertically
* ticks via datalist https://caniuse.com/#search=datalist
*/
class Slider extends React.Component {
state = { currentState: 'initial' };

componentDidMount() {
if (this.container) {
this.container.addEventListener('touchstart', preventPageScrolling, { passive: false });
}
}

componentWillUnmount() {
this.container.removeEventListener('touchstart', preventPageScrolling, { passive: false });
}

static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.disabled) {
return { currentState: 'disabled' };
Expand All @@ -229,45 +204,6 @@ class Slider extends React.Component {
return null;
}

handleKeyDown = event => {
const { min, max, value: currentValue } = this.props;

const onePercent = Math.abs((max - min) / 100);
const step = this.props.step || onePercent;
let value;

switch (keycode(event)) {
case 'home':
value = min;
break;
case 'end':
value = max;
break;
case 'page up':
value = currentValue + onePercent * 10;
break;
case 'page down':
value = currentValue - onePercent * 10;
break;
case 'right':
case 'up':
value = currentValue + step;
break;
case 'left':
case 'down':
value = currentValue - step;
break;
default:
return;
}

event.preventDefault();

value = clamp(value, min, max);

this.emitChange(event, value);
};

handleFocus = () => {
this.setState({ currentState: 'focused' });
};
Expand All @@ -276,82 +212,9 @@ class Slider extends React.Component {
this.setState({ currentState: 'normal' });
};

handleClick = event => {
const { min, max, vertical, reverse } = this.props;
const percent = calculatePercent(this.container, event, vertical, reverse);
const value = percentToValue(percent, min, max);

this.emitChange(event, value, () => {
this.playJumpAnimation();
});
};

handleTouchStart = event => {
this.setState({ currentState: 'activated' });

this.globalMouseUpListener = addEventListener(document, 'touchend', this.handleMouseUp);

if (typeof this.props.onDragStart === 'function') {
this.props.onDragStart(event);
}
};

handleMouseDown = event => {
this.setState({ currentState: 'activated' });

this.globalMouseUpListener = addEventListener(document, 'mouseup', this.handleMouseUp);
this.globalMouseMoveListener = addEventListener(document, 'mousemove', this.handleMouseMove);

if (typeof this.props.onDragStart === 'function') {
this.props.onDragStart(event);
}
};

handleMouseUp = event => {
this.setState({ currentState: 'normal' });

if (this.globalMouseUpListener) {
this.globalMouseUpListener.remove();
}

if (this.globalMouseMoveListener) {
this.globalMouseMoveListener.remove();
}

if (typeof this.props.onDragEnd === 'function') {
this.props.onDragEnd(event);
}
};

handleMouseMove = event => {
const { min, max, vertical, reverse } = this.props;
const percent = calculatePercent(this.container, event, vertical, reverse);
const value = percentToValue(percent, min, max);

this.emitChange(event, value);
};

emitChange(event, rawValue, callback) {
const { step, value: previousValue, onChange, disabled } = this.props;
let value = rawValue;

if (disabled) {
return;
}

if (step) {
value = roundToStep(rawValue, step);
} else {
value = Number(rawValue.toFixed(3));
}

if (typeof onChange === 'function' && value !== previousValue) {
onChange(event, value);

if (typeof callback === 'function') {
callback();
}
}
handleChange = event => {
const { onChange } = this.props;
onChange(event, event.target.value);
}

calculateTrackAfterStyles(percent) {
Expand Down Expand Up @@ -398,6 +261,7 @@ class Slider extends React.Component {
vertical,
reverse,
disabled,
step,
...other
} = this.props;

Expand All @@ -417,6 +281,13 @@ class Slider extends React.Component {
classNameProp,
});

const nativeClasses = classNames(classes.native, {
[classes.vertical]: vertical,
[classes.reverse]: reverse,
[classes.disabled]: disabled,
classNameProp,
});

const trackBeforeClasses = classNames(classes.track, classes.trackBefore, commonClasses, {
[classes.vertical]: vertical,
});
Expand All @@ -436,31 +307,26 @@ class Slider extends React.Component {
const inlineThumbStyles = { [thumbProperty]: `${percent}%` };

return (
<Component
role="slider"
className={rootClasses}
aria-valuenow={value}
aria-valuemin={min}
aria-valuemax={max}
aria-orientation={vertical ? 'vertical' : 'horizontal'}
onClick={this.handleClick}
ref={node => {
this.container = findDOMNode(node);
}}
{...other}
>
<Component className={rootClasses}>
<div className={trackBeforeClasses} style={inlineTrackBeforeStyles} />
<ButtonBase
className={thumbClasses}
disableRipple
style={inlineThumbStyles}
<input
type="range"
className={nativeClasses}
aria-valuenow={value}
aria-valuemin={min}
aria-valuemax={max}
aria-orientation={vertical ? 'vertical' : 'horizontal'}
value={value}
min={min}
max={max}
step={step}
disabled={disabled}
orient={vertical ? 'vertical' : undefined /* Firefox */}
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown}
onTouchStartCapture={this.handleTouchStart}
onTouchMove={this.handleMouseMove}
onFocusVisible={this.handleFocus}
/>
<div className={thumbClasses} style={inlineThumbStyles} />
<div className={trackAfterClasses} style={inlineTrackAfterStyles} />
</Component>
);
Expand Down