feat: Fixes Issue #260. Allow use Enter to submit extrinsics (#265)

* WIP: Fixes #260. Autofocus password input field. Press tab after enter password to switch focus to submit

* feat: Clicks Submit Transaction when user presses Enter after entering password

* fix: Remove console logs and no need to focus

* test: Add test setup. setState not changing currentItem from undefined

* fix: Change tabIndex values to number instead of string

* fix: Use Refs instead of JS DOM access

* fix: Add missing newline

* test: Test modal show after setState. Skipped pending resolution of Enzyme issue

* fix: Remove string from tabIndex prop type

* fix: Remove use of SUIEvent. Use React refs not HTML DOM queries

* test: Update tests but skipping since awaiting resolution of enzyme issue

Reference: https://github.com/airbnb/enzyme/issues/1794

* WIP

* fix: Simply call onSend instead of long-winded approach with Refs

* test: Fix tests to use promises instead of callbacks

* fix: Fix tslint error to handle promises correctly

* fix: Remove return since broke functionality. Use await to handle promise. Use key instead of keyCode
This commit is contained in:
Luke Schoen 2018-09-13 11:36:52 +02:00 committed by Jaco Greeff
parent 165fb68e74
commit 189e486ed8
9 changed files with 165 additions and 25 deletions

View File

@ -6,5 +6,8 @@ module.exports = Object.assign({}, config, {
'@polkadot/ui-(app|identicon|keyring|react-rx|react|signer)(.*)$': '<rootDir>/packages/ui-$1/src/$2',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
'\\.(css|less)$': 'empty/object'
}
},
transformIgnorePatterns: [
'node_modules/(?!polkadot-identicon)'
]
});

View File

@ -17,7 +17,7 @@ import Or from './Or';
class Button extends React.PureComponent<ButtonProps> {
render () {
const { children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isNegative = false, isPositive = false, isPrimary = false, onClick, size, style, text } = this.props;
const { children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isNegative = false, isPositive = false, isPrimary = false, onClick, size, style, tabIndex, text } = this.props;
const props = {
basic: isBasic || false,
@ -32,7 +32,8 @@ class Button extends React.PureComponent<ButtonProps> {
primary: isPrimary,
size,
secondary: isBasic && !(isPositive || isPrimary || isNegative || false),
style
style,
tabIndex
};
return isUndefined(text) && isUndefined(children)

View File

@ -18,6 +18,7 @@ export type ButtonProps = BareProps & {
isPrimary?: boolean,
onClick?: () => void | Promise<void>,
size?: Button$Sizes,
tabIndex?: number,
text?: any
};

View File

@ -14,6 +14,7 @@ import Labelled from './Labelled';
type Input$Type = 'number' | 'password' | 'text';
type Props = BareProps & {
autoFocus?: boolean,
children?: React.ReactNode,
defaultValue?: any,
icon?: any, // node?
@ -27,16 +28,14 @@ type Props = BareProps & {
min?: any,
name?: string,
onChange: (value: string) => void,
onKeyDown?: (event: React.KeyboardEvent<Element>) => void,
placeholder?: string,
tabIndex?: number,
type?: Input$Type,
value?: any,
withLabel?: boolean
};
type SUIEvent = {
value: any
};
type State = {
name: string;
};
@ -49,7 +48,7 @@ export default class Input extends React.PureComponent<Props, State> {
};
render () {
const { children, className, defaultValue, icon, isEditable = false, isAction = false, isDisabled = false, isError = false, isHidden = false, label, max, min, name, placeholder, style, type = 'text', value, withLabel } = this.props;
const { autoFocus = false, children, className, defaultValue, icon, isEditable = false, isAction = false, isDisabled = false, isError = false, isHidden = false, label, max, min, name, placeholder, style, tabIndex, type = 'text', value, withLabel } = this.props;
return (
<Labelled
@ -59,6 +58,7 @@ export default class Input extends React.PureComponent<Props, State> {
withLabel={withLabel}
>
<SUIInput
autoFocus={autoFocus}
action={isAction}
className={isEditable ? 'edit icon' : ''}
defaultValue={defaultValue}
@ -75,7 +75,9 @@ export default class Input extends React.PureComponent<Props, State> {
min={min}
name={name || this.state.name}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
placeholder={placeholder}
tabIndex={tabIndex}
type={type}
value={value}
>
@ -94,7 +96,17 @@ export default class Input extends React.PureComponent<Props, State> {
);
}
onChange = (event: React.SyntheticEvent<Element>, { value }: SUIEvent): void => {
this.props.onChange(value);
onChange = (event: React.SyntheticEvent<Element>): void => {
const { onChange } = this.props;
onChange((event.target as HTMLInputElement).value);
}
onKeyDown = (event: React.KeyboardEvent<Element>): void => {
const { onKeyDown } = this.props;
if (onKeyDown) {
onKeyDown(event);
}
}
}

View File

@ -10,6 +10,7 @@ import Button from './Button';
import Input from './Input';
type Props = BareProps & {
autoFocus?: boolean,
defaultValue?: any,
children?: React.ReactNode,
isDisabled?: boolean,
@ -17,6 +18,8 @@ type Props = BareProps & {
label?: string,
name?: string,
onChange: (value: string) => void,
onKeyDown?: (event: React.KeyboardEvent<Element>) => void,
tabIndex?: number,
value: any,
withLabel?: boolean
};
@ -31,11 +34,12 @@ export default class Password extends React.PureComponent<Props, State> {
};
render () {
const { children, className, defaultValue, isDisabled, isError, label, name, onChange, style, value, withLabel } = this.props;
const { autoFocus, children, className, defaultValue, isDisabled, isError, label, name, onChange, onKeyDown, style, tabIndex, value, withLabel } = this.props;
const { isVisible } = this.state;
return (
<Input
autoFocus={autoFocus}
className={className}
defaultValue={defaultValue}
isAction
@ -44,7 +48,9 @@ export default class Password extends React.PureComponent<Props, State> {
label={label}
name={name}
onChange={onChange}
onKeyDown={onKeyDown}
style={style}
tabIndex={tabIndex}
type={
isVisible
? 'text'

View File

@ -0,0 +1,81 @@
// Copyright 2017-2018 @polkadot/ui-app authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
import React from 'react';
import { shallow } from '../../../test/enzyme';
import { Signer } from './Modal';
const mockT = (key, options) => (key);
describe('Signer', () => {
let expectedNextCurrentItemState, fixtureCurrentItemState, fixtureQueueProp, unlockComponent, wrapper;
beforeEach(() => {
fixtureCurrentItemState = {
rpc: {
isSigned: true
},
id: 'test1',
publicKey: new Uint8Array([1]),
status: 'sending'
};
fixtureQueueProp = [{
rpc: {
isSigned: true
},
id: 'test2',
publicKey: new Uint8Array([2]),
status: 'queued'
}];
expectedNextCurrentItemState = fixtureQueueProp[0];
wrapper = shallow(
<Signer
t={mockT}
queue={[]}
/>, {}
);
});
it('creates the element', () => {
return new Promise((resolve) => {
wrapper.setState({
currentItem: fixtureCurrentItemState,
password: '123',
unlockError: null
}, () => {
wrapper.setProps({
queue: fixtureQueueProp
}, resolve);
});
}).then(() => {
wrapper.update();
expect(wrapper).toHaveLength(1);
});
});
// Reference: https://github.com/airbnb/enzyme/issues/1794
it('renders Unlock component with empty password', () => {
return new Promise((resolve) => {
wrapper.setState({
currentItem: fixtureCurrentItemState,
password: '123',
unlockError: null
}, () => {
wrapper.setProps({
queue: fixtureQueueProp
}, resolve);
});
}).then(() => {
wrapper.update();
expect(wrapper.state('currentItem')).toEqual(expectedNextCurrentItemState);
expect(wrapper.find('.ui--signer-Signer')).toHaveLength(1);
unlockComponent = wrapper.find('.ui--signer-Signer').children().at(0).childAt(0);
expect(unlockComponent.props().password).toEqual('');
});
});
});

View File

@ -106,24 +106,29 @@ class Signer extends React.PureComponent<Props, State> {
<Button
isNegative
onClick={this.onCancel}
tabIndex={3}
text={t('extrinsic.cancel', {
defaultValue: 'Cancel'
})}
/>
<Button.Or />
<Button
isPrimary
onClick={this.onSend}
text={
isSigned
? t('extrinsic.signedSend', {
defaultValue: 'Sign and Submit'
})
: t('extrinsic.send', {
defaultValue: 'Submit'
})
}
/>
<div>
<Button
className='ui--signer-Signer-Submit'
isPrimary
onClick={this.onSend}
tabIndex={2}
text={
isSigned
? t('extrinsic.signedSend', {
defaultValue: 'Sign and Submit'
})
: t('extrinsic.send', {
defaultValue: 'Submit'
})
}
/>
</div>
</Button.Group>
</Modal.Actions>
);
@ -153,10 +158,13 @@ class Signer extends React.PureComponent<Props, State> {
return (
<Unlock
autoFocus
error={unlockError && t(unlockError.key, unlockError.value)}
onChange={this.onChangePassword}
onKeyDown={this.onKeyDown}
password={password}
value={currentItem.publicKey}
tabIndex={1}
/>
);
}
@ -189,6 +197,12 @@ class Signer extends React.PureComponent<Props, State> {
});
}
onKeyDown = async (event: React.KeyboardEvent<Element>): Promise<any> => {
if (event.key === 'Enter') {
await this.onSend();
}
}
onCancel = (): void => {
const { queueSetStatus } = this.props;
const { currentItem } = this.state;
@ -246,4 +260,8 @@ const Component: React.ComponentType<any> = translate(
withApi(Signer)
);
export {
Signer
};
export default Component;

View File

@ -13,9 +13,12 @@ import keyring from '@polkadot/ui-keyring/index';
import translate from './translate';
type Props = I18nProps & {
autoFocus?: boolean,
error?: string,
onChange: (password: string) => void,
onKeyDown?: (event: React.KeyboardEvent<Element>) => void,
password: string,
tabIndex?: number,
value?: Uint8Array | null
};
@ -40,7 +43,7 @@ class Unlock extends React.PureComponent<Props, State> {
}
render () {
const { onChange, password, t } = this.props;
const { autoFocus, onChange, onKeyDown, password, t, tabIndex } = this.props;
const { isError, isLocked } = this.state;
if (!isLocked) {
@ -51,12 +54,15 @@ class Unlock extends React.PureComponent<Props, State> {
<div className='ui--signer-Signer-Unlock'>
<div className='ui--row'>
<Password
autoFocus={autoFocus}
className='medium'
isError={isError}
label={t('unlock.password', {
defaultValue: 'unlock account using'
})}
onChange={onChange}
onKeyDown={onKeyDown}
tabIndex={tabIndex}
value={password}
/>
</div>

12
test/enzyme.js Normal file
View File

@ -0,0 +1,12 @@
// Copyright 2017-2018 @polkadot authors & contributors
// This software may be modified and distributed under the terms
// of the ISC license. See the LICENSE file for details.
const Adapter = require('enzyme-adapter-react-16');
const Enzyme = require('enzyme');
Enzyme.configure({
adapter: new Adapter()
});
module.exports = Enzyme;