// unit test
it('should type on the onscreen keyboard with correct cursor position', () => {
const mountNode = document.createElement('div');
document.body.appendChild(mountNode);
const component = mount(<Input {...defaultProps} />, {attachTo: mountNode});
const input = document.querySelector('input');
document.querySelector('[data-skbtn="1"]').click();
document.querySelector('[data-skbtn="2"]').click();
document.querySelector('[data-skbtn="3"]').click();
document.querySelector('[data-skbtn="4"]').click();
expect(input.selectionStart).toBe(4);
expect(input.value).toBe('1234');
document.querySelector('[data-skbtn="{bksp}"]').click();
expect(input.selectionStart).toBe(3);
expect(input.value).toBe('123');
input.setSelectionRange(1, 1);
document.querySelector('[data-skbtn="a"]').click();
expect(input.selectionStart).toBe(2);
// currently this cannot be tested. It works fine on screen.
// expect(input.value).toBe('1a23');
component.unmount();
});
import React from 'react';
import * as PropTypes from 'prop-types';
import Keyboard from 'react-simple-keyboard';
import classNames from 'classnames';
import Portal from '@material-ui/core/Portal';
import 'react-simple-keyboard/build/css/index.css';
import styles from './Input.module.scss';
class Input extends React.PureComponent {
static propTypes = {
placeholder: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
value: PropTypes.string,
className: PropTypes.string,
enableVirtualKeyboard: PropTypes.bool,
};
static defaultProps = {
enableVirtualKeyboard: false,
};
state = {
layoutName: 'default',
value: '',
showKeyboard: false,
};
keyboardRef = React.createRef(); // the keyboard div
simpleKeyboardRef = React.createRef(); // the keyboard component
inputRef = React.createRef(); // the input element
showKeyboard = enable => {
if (!this.props.enableVirtualKeyboard) return;
if (this.state.showKeyboard !== enable) {
this.setState({showKeyboard: enable});
if (!enable) {
window.removeEventListener('mousedown', this.handleWindowMousedown);
}
}
};
handleWindowMousedown = e => {
// Clicks outside the keyboard should close the keyboard except the input element
if (
!(this.keyboardRef.current && this.keyboardRef.current.contains(e.target)) &&
!this.inputRef.current.contains(e.target)
) {
this.showKeyboard(false);
}
};
componentWillUnmount() {
window.removeEventListener('mousedown', this.handleWindowMousedown);
}
onChangeKeyboard = input => {
let inputValue = input;
// physical keyboard give input as an event object
if (typeof input === 'object') {
inputValue = input.target.value;
}
const inputElement = this.inputRef.current;
// get the current cursor position and increment to the next position
let cursorNextPosition =
inputElement.selectionStart + inputValue.length - this.state.value.length;
// if the next cursor is 0 at the beginning return the cursor to the back of the input
cursorNextPosition = !cursorNextPosition ? inputValue.length : cursorNextPosition;
this.setState({value: inputValue}, () =>
this.simpleKeyboardRef.keyboard.setInput(inputValue)
);
this.props.onChange(inputValue);
// only adjust cursor when input is from virtual keyboard
if (typeof input !== 'object') {
inputElement.focus();
inputElement.setSelectionRange(cursorNextPosition, cursorNextPosition);
}
};
componentDidUpdate(prevProps) {
// parent clearing the input value
if (this.props.value !== prevProps.value && this.props.value === '') {
this.onChangeKeyboard('');
}
}
onKeyPress = button => {
// handle the shift and caps lock buttons
if (button === '{shift}' || button === '{lock}') this.handleShift();
};
handleShift = () => {
const layoutName = this.state.layoutName;
this.setState({
layoutName: layoutName === 'default' ? 'shift' : 'default',
});
};
render() {
const keyboardClasses = classNames(styles.keyboard, {
[styles.keyboardHide]: !this.state.showKeyboard, // defaults to hide
});
return (
<React.Fragment>
<input
ref={this.inputRef}
className={this.props.className}
value={this.state.value}
placeholder={this.props.placeholder}
onChange={this.onChangeKeyboard}
onFocus={e => {
this.props.onFocus(e.target.value);
this.showKeyboard(true);
}}
/>
<Portal>
<div ref={this.keyboardRef} className={keyboardClasses}>
<Keyboard
ref={r => {
this.simpleKeyboardRef = r;
}}
layoutName={this.state.layoutName}
onChange={this.onChangeKeyboard}
onKeyPress={this.onKeyPress}
preventMouseDownDefault
stopMouseDownPropagation
useMouseEvents
useButtonTag
beforeRender={() =>
window.addEventListener('mousedown', this.handleWindowMousedown)
}
layout={{
default: [
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
'{tab} q w e r t y u i o p [ ] \\',
"{lock} a s d f g h j k l ; ' {enter}",
'{shift} z x c v b n m , . / {shift}',
'@ {space}',
],
shift: [
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
'{tab} Q W E R T Y U I O P { } |',
'{lock} A S D F G H J K L : " {enter}',
'{shift} Z X C V B N M < > ? {shift}',
'{space}',
],
}}
/>
</div>
</Portal>
</React.Fragment>
);
}
}
export default Input;