mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-10 07:05:35 +00:00
Feature/#248 slider keyboard (#257)
* Added tabIndex=0 to <svg> in Slider to allow keyboard focus * further tests for mobile keyboard fix * Proof of concept of way to get mobile keyboard to open for Slider component * Added CSS to slider input box to make it invisible but still usable * Update to hide text input and move it up to same position as slider * Change slider text field type to 'tel" to force numeric-only keyboard on mobile * Added focus/blur handlers and initial styling to show/hide text field component * Update to insure text field values are updated when slider moves, and vice versa. * Added timeout handler for Outfitting sliders to raise keyboard after 1500ms. Doesn't work 100% on iPhone. * Changed touch/mouse down event handlers to change the display value on the parent div, then use componentDidUpdate to set focus on the text box. * Added tap/hold mousedown/hold keyboard display - works on desktop, iOS, and Android
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ nginx.pid
|
|||||||
/bin
|
/bin
|
||||||
env
|
env
|
||||||
*.swp
|
*.swp
|
||||||
|
.project
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ export default class Slider extends React.Component {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
axis: PropTypes.bool,
|
axis: PropTypes.bool,
|
||||||
axisUnit: PropTypes.string,
|
axisUnit: PropTypes.string,//units (T, M, etc.)
|
||||||
max: PropTypes.number,
|
max: PropTypes.number,
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,// function which determins percent value
|
||||||
onResize: PropTypes.func,
|
onResize: PropTypes.func,
|
||||||
percent: PropTypes.number.isRequired,
|
percent: PropTypes.number.isRequired,//value of slider
|
||||||
scale: PropTypes.number
|
scale: PropTypes.number
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,6 +35,11 @@ export default class Slider extends React.Component {
|
|||||||
this._down = this._down.bind(this);
|
this._down = this._down.bind(this);
|
||||||
this._move = this._move.bind(this);
|
this._move = this._move.bind(this);
|
||||||
this._up = this._up.bind(this);
|
this._up = this._up.bind(this);
|
||||||
|
this._keyup = this._keyup.bind(this);
|
||||||
|
this._keydown = this._keydown.bind(this);
|
||||||
|
this._touchstart = this._touchstart.bind(this);
|
||||||
|
this._touchend = this._touchend.bind(this);
|
||||||
|
|
||||||
this._updatePercent = this._updatePercent.bind(this);
|
this._updatePercent = this._updatePercent.bind(this);
|
||||||
this._updateDimensions = this._updateDimensions.bind(this);
|
this._updateDimensions = this._updateDimensions.bind(this);
|
||||||
|
|
||||||
@@ -46,10 +51,12 @@ export default class Slider extends React.Component {
|
|||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
*/
|
*/
|
||||||
_down(event) {
|
_down(event) {
|
||||||
|
|
||||||
let rect = event.currentTarget.getBoundingClientRect();
|
let rect = event.currentTarget.getBoundingClientRect();
|
||||||
this.left = rect.left;
|
this.left = rect.left;
|
||||||
this.width = rect.width;
|
this.width = rect.width;
|
||||||
this._move(event);
|
this._move(event);
|
||||||
|
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,11 +76,67 @@ export default class Slider extends React.Component {
|
|||||||
* @param {Event} event DOM Event
|
* @param {Event} event DOM Event
|
||||||
*/
|
*/
|
||||||
_up(event) {
|
_up(event) {
|
||||||
|
this.sliderInputBox.sliderVal.focus();
|
||||||
|
clearTimeout(this.touchStartTimer);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.left = null;
|
this.left = null;
|
||||||
this.width = null;
|
this.width = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key up handler for keyboard.
|
||||||
|
* display the number field then set focus to it
|
||||||
|
* when "Enter" key is pressed
|
||||||
|
* @param {Event} event Keyboard event
|
||||||
|
*/
|
||||||
|
_keyup(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault();
|
||||||
|
this.sliderInputBox._setDisplay('block');
|
||||||
|
//this.enterTimer = setTimeout(() => this.sliderInputBox.sliderVal.focus(), 10);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Key down handler
|
||||||
|
* increment slider position by +/- 1 when right/left arrow key is pressed or held
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
_keydown(event) {
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
var newVal = this.props.percent*this.props.max + 1;
|
||||||
|
if (newVal <= this.props.max) this.props.onChange(newVal/this.props.max);
|
||||||
|
return;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
var newVal = this.props.percent*this.props.max - 1;
|
||||||
|
if (newVal >= 0) this.props.onChange(newVal/this.props.max);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Touch start handler
|
||||||
|
* @param {Event} event DOM Event
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
_touchstart(event) {
|
||||||
|
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
_touchend(event) {
|
||||||
|
this.sliderInputBox.sliderVal.focus();
|
||||||
|
clearTimeout(this.touchStartTimer);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the user is still dragging
|
* Determine if the user is still dragging
|
||||||
* @param {SyntheticEvent} event Event
|
* @param {SyntheticEvent} event Event
|
||||||
@@ -118,6 +181,7 @@ export default class Slider extends React.Component {
|
|||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -151,16 +215,147 @@ export default class Slider extends React.Component {
|
|||||||
let width = outerWidth - (margin * 2);
|
let width = outerWidth - (margin * 2);
|
||||||
let pctPos = width * this.props.percent;
|
let pctPos = width * this.props.percent;
|
||||||
|
|
||||||
return <svg onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onTouchEnd={this._up} style={style} ref={node => this.node = node}>
|
return <div><svg
|
||||||
|
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onKeyUp={this._keyup} onKeyDown={this._keydown} style={style} ref={node => this.node = node} tabIndex="0">
|
||||||
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
|
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
|
||||||
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
|
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
|
||||||
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
|
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
|
||||||
<rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} />
|
<rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} onTouchEnd={this._touchend} />
|
||||||
{axis && <g style={{ fontSize: '.7em' }}>
|
{axis && <g style={{ fontSize: '.7em' }}>
|
||||||
<text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
|
<text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
|
||||||
<text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
|
<text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
|
||||||
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
|
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
|
||||||
</g>}
|
</g>}
|
||||||
</svg>;
|
</svg>
|
||||||
|
<TextInputBox ref={(tb) => this.sliderInputBox = tb}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
percent={this.props.percent}
|
||||||
|
axisUnit={this.props.axisUnit}
|
||||||
|
scale={this.props.scale}
|
||||||
|
max={this.props.max}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android)
|
||||||
|
**/
|
||||||
|
class TextInputBox extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
axisUnit: PropTypes.string,//units (T, M, etc.)
|
||||||
|
max: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,// function which determins percent value
|
||||||
|
percent: PropTypes.number.isRequired,//value of slider
|
||||||
|
scale: PropTypes.number
|
||||||
|
};
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._handleFocus = this._handleFocus.bind(this);
|
||||||
|
this._handleBlur = this._handleBlur.bind(this);
|
||||||
|
this._handleChange = this._handleChange.bind(this);
|
||||||
|
//this._keydown = this._keydown.bind(this);
|
||||||
|
this._keyup = this._keyup.bind(this);
|
||||||
|
this.state = this._getInitialState();
|
||||||
|
this.percent = this.props.percent;
|
||||||
|
this.max = this.props.max;
|
||||||
|
this.state.inputValue = this.percent * this.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps, nextState) {
|
||||||
|
var nextValue = nextProps.percent * nextProps.max;
|
||||||
|
// See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form
|
||||||
|
if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) {
|
||||||
|
this.setState({ inputValue: nextValue });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
|
||||||
|
if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') {
|
||||||
|
this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) {
|
||||||
|
// they chose a different module
|
||||||
|
this.setState({ inputValue: this.props.max });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) {
|
||||||
|
this.props.onChange(this.state.inputValue/this.props.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInitialState() {
|
||||||
|
return {
|
||||||
|
divStyle: {display:'none'},
|
||||||
|
inputStyle: {width:'4em'},
|
||||||
|
labelStyle: {marginLeft: '.1em'},
|
||||||
|
maxLength:5,
|
||||||
|
size:5,
|
||||||
|
min:0,
|
||||||
|
tabIndex:-1,
|
||||||
|
type:'number',
|
||||||
|
readOnly: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setDisplay(val) {
|
||||||
|
this.setState({
|
||||||
|
divStyle: {display:val}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleFocus() {
|
||||||
|
this.setState({
|
||||||
|
inputValue:this._getValue()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleBlur() {
|
||||||
|
this._setDisplay('none');
|
||||||
|
if (this.state.inputValue !== '') {
|
||||||
|
this.props.onChange(this.state.inputValue/this.props.max);
|
||||||
|
} else {
|
||||||
|
this.state.inputValue = this.props.percent * this.props.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
_getValue() {
|
||||||
|
return this.state.inputValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleChange(event) {
|
||||||
|
if (event.target.value < 0) {
|
||||||
|
this.setState({inputValue: 0});
|
||||||
|
} else if (event.target.value <= this.props.max) {
|
||||||
|
this.setState({inputValue: event.target.value});
|
||||||
|
} else {
|
||||||
|
this.setState({inputValue: this.props.max});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key up handler for input field.
|
||||||
|
* If user hits Enter key, blur/close the input field
|
||||||
|
* @param {Event} event Keyboard event
|
||||||
|
*/
|
||||||
|
_keyup(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
this.sliderVal.blur();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { axisUnit, onChange, percent, scale } = this.props;
|
||||||
|
return <div style={this.state.divStyle}><input style={this.state.inputStyle} value={this._getValue()} min={this.state.min} max={this.props.max} onChange={this._handleChange} onKeyUp={this._keyup} tabIndex={this.state.tabIndex} maxLength={this.state.maxLength} size={this.state.size} onBlur={() => {this._handleBlur()}} onFocus={() => {this._handleFocus()}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/><text className="primary upp" style={this.state.labelStyle}>{this.props.axisUnit}</text></div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user