unify warning and error and add capslock warning (#545)

* unify warning and error and add capslock warning

* Update packages/extension-ui/src/Popup/Signing/Unlock.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Update packages/extension-ui/src/Popup/Derive/SelectParent.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* check capslock using onKeyPress

* lint

* fix tests

* add test for caps-lock

Co-authored-by: Github Actions <action@github.com>
Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Thibaut Sardan 2020-11-25 02:20:07 -10:00 committed by GitHub
parent f60ab0fd47
commit 1cb36bb73c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 130 additions and 79 deletions

View File

@ -60,7 +60,9 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
</div>
{isFirst && (
<>
<Warning className='warningMargin'>{t<string>('Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.')}</Warning>
<Warning className='warningMargin'>
{t<string>('Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.')}
</Warning>
<Button
className='acceptButton'
onClick={_onApprove}

View File

@ -42,6 +42,12 @@ describe('Create Account', () => {
wrapper.update();
};
const capsLockOn = async (input: ReactWrapper): Promise<void> => {
input.simulate('keyPress', { getModifierState: () => true });
await act(flushAllPromises);
wrapper.update();
};
const enterName = (name: string): Promise<void> => type(wrapper.find('input').first(), name);
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
@ -102,7 +108,7 @@ describe('Create Account', () => {
it('after typing less than 3 characters into name input, password input is not visible', async () => {
await enterName('ab');
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('span.error').first().text()).toBe('Account name is too short');
expect(wrapper.find('.warning-message').first().text()).toBe('Account name is too short');
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
@ -122,10 +128,17 @@ describe('Create Account', () => {
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});
it('password with caps lock should show a warning', async () => {
await enterName('abc').then(password('abcde'));
await capsLockOn(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input));
expect(wrapper.find('.warning-message').first().text()).toBe('Warning: Caps lock is on');
});
it('password shorter than 6 characters should be not valid', async () => {
await enterName('abc').then(password('abcde'));
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('span.error').text()).toBe('Password is too short');
expect(wrapper.find('.warning-message').text()).toBe('Password is too short');
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
@ -133,14 +146,14 @@ describe('Create Account', () => {
it('submit button is not visible until both passwords are equal', async () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdeg'));
expect(wrapper.find('span.error').text()).toBe('Passwords do not match');
expect(wrapper.find('.warning-message').text()).toBe('Passwords do not match');
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});
it('submit button is visible when both passwords are equal', async () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
expect(wrapper.find('span.error')).toHaveLength(0);
expect(wrapper.find('.warning-message')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
});
@ -171,7 +184,7 @@ describe('Create Account', () => {
it('first password changes - button is not disabled', async () => {
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
expect(wrapper.find('span.error').text()).toBe('Passwords do not match');
expect(wrapper.find('.warning-message').text()).toBe('Passwords do not match');
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

View File

@ -39,7 +39,9 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
onCopy={_onCopy}
seed={seed}
/>
<Warning>{t<string>("Please write down your wallet's mnemonic seed and keep it in a safe place. The mnemonic can be used to restore your wallet. Keep it carefully to not lose your assets.")}</Warning>
<Warning>
{t<string>("Please write down your wallet's mnemonic seed and keep it in a safe place. The mnemonic can be used to restore your wallet. Keep it carefully to not lose your assets.")}
</Warning>
<VerticalSpace />
<Checkbox
checked={isMnemonicSaved}

View File

@ -115,7 +115,7 @@ describe('Derive', () => {
});
it('No error is visible when first loading the page', () => {
expect(wrapper.find('.error')).toHaveLength(0);
expect(wrapper.find('Warning')).toHaveLength(0);
});
it('"Create derived account" is disabled when password is not set', () => {
@ -133,7 +133,8 @@ describe('Derive', () => {
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('.error')).toHaveLength(1);
expect(wrapper.find('.warning-message')).toHaveLength(1);
expect(wrapper.find('.warning-message').first().text()).toEqual('Wrong password');
});
it('The error disappears when typing a new password and "Create derived account" is enabled', async () => {
@ -147,7 +148,7 @@ describe('Derive', () => {
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('.error')).toHaveLength(0);
expect(wrapper.find('.warning-message')).toHaveLength(0);
});
it('"Create derived account" is enabled when password is set', async () => {
@ -156,7 +157,7 @@ describe('Derive', () => {
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('.error')).toHaveLength(0);
expect(wrapper.find('.warning-message')).toHaveLength(0);
});
it('An error is visible and "Create derived account" is disabled when suri is incorrect', async () => {
@ -169,7 +170,8 @@ describe('Derive', () => {
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(true);
expect(wrapper.find('.error')).toHaveLength(1);
expect(wrapper.find('.warning-message')).toHaveLength(1);
expect(wrapper.find('.warning-message').first().text()).toEqual('Incorrect derivation path');
});
it('The error disappears and "Create derived account" is enabled when typing a new suri', async () => {
@ -183,7 +185,7 @@ describe('Derive', () => {
const button = wrapper.find('[data-button-action="create derived account"] button');
expect(button.prop('disabled')).toBe(false);
expect(wrapper.find('.error')).toHaveLength(0);
expect(wrapper.find('Warning')).toHaveLength(0);
});
it('takes selected address from URL as parent account', () => {

View File

@ -7,7 +7,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState }
import styled from 'styled-components';
import { assert } from '@polkadot/util';
import { AccountContext, ActionContext, Address, ButtonArea, Checkbox, InputWithLabel, Label, NextStepButton, VerticalSpace } from '../../components';
import { AccountContext, ActionContext, Address, ButtonArea, Checkbox, InputWithLabel, Label, NextStepButton, VerticalSpace, Warning } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { validateAccount, validateDerivationPath } from '../../messaging';
import { nextDerivationPath } from '../../util/nextDerivationPath';
@ -132,7 +132,14 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
type='password'
value={parentPassword}
/>
{!!parentPassword && !isProperParentPassword && <div className='error'>{t('Wrong password')}</div>}
{!!parentPassword && !isProperParentPassword && (
<Warning
isBelowInput
isDanger
>
{t('Wrong password')}
</Warning>
)}
</div>
{isProperParentPassword && (
<>
@ -143,7 +150,14 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
parentAddress={parentAddress}
parentPassword={parentPassword}
/>
{!suriPath && <div className='error'>{t('Incorrect derivation path')}</div>}
{!suriPath && (
<Warning
isBelowInput
isDanger
>
{t('Incorrect derivation path')}
</Warning>
)}
</>
)}
</div>
@ -175,8 +189,7 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
);
}
export default styled(SelectParent)(({ theme }: ThemeProps) => `
export default styled(SelectParent)`
.smallerMargin {
margin-top: 0;
margin-bottom: 10px;
@ -190,12 +203,4 @@ export default styled(SelectParent)(({ theme }: ThemeProps) => `
pointer-events: none;
}
}
.error {
display: block;
margin-top: -10px;
font-size: ${theme.labelFontSize};
line-height: ${theme.labelLineHeight};
color: ${theme.errorColor};
}
`);
`;

View File

@ -64,10 +64,7 @@ function Export ({ className, match: { params: { address } } }: Props): React.Re
/>
<div className={className}>
<Address address={address}>
<Warning
className='movedWarning'
isDanger
>
<Warning className='movedWarning'>
{t<string>("You are exporting your account. Keep it safe and don't share it with anyone.")}
</Warning>
<div className='actionArea'>

View File

@ -40,10 +40,7 @@ function Forget ({ className, match: { params: { address } } }: Props): React.Re
/>
<div className={className}>
<Address address={address}>
<Warning
className='movedWarning'
isDanger
>
<Warning className='movedWarning'>
{t<string>('You are about to remove the account. This means that you will not be able to access it via this extension anymore. If you wish to recover it, you would need to use the seed.')}
</Warning>
<div className='actionArea'>

View File

@ -71,7 +71,9 @@ function Request ({ className, metaId, request, url }: Props): React.ReactElemen
</tr>
</Table>
<div className='requestInfo'>
<Warning className='requestWarning'>{t<string>('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.')}</Warning>
<Warning className='requestWarning'>
{t<string>('This approval will add the metadata to your extension instance, allowing future requests to be decoded using this metadata.')}
</Warning>
<Button
className='btnAccept'
onClick={_onApprove}

View File

@ -1,15 +1,12 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '@polkadot/extension-ui/types';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import { InputWithLabel } from '../../components';
import { InputWithLabel, Warning } from '../../components';
import useTranslation from '../../hooks/useTranslation';
interface Props extends ThemeProps {
interface Props {
className?: string;
error?: string | null;
isBusy: boolean;
@ -43,15 +40,16 @@ function Unlock ({ className, error, isBusy, onSign, password, setError, setPass
value={password}
withoutMargin={true}
/>
{error && <div className='error'>{error}</div>}
{error && (
<Warning
isBelowInput
isDanger
>
{error}
</Warning>
)}
</div>
);
}
export default React.memo(styled(Unlock)(({ theme }: ThemeProps) => `
.error {
font-size: ${theme.labelFontSize};
line-height: ${theme.labelLineHeight};
color: ${theme.errorColor};
}
`));
export default React.memo(Unlock);

View File

@ -1,11 +1,12 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import styled from 'styled-components';
import Label from './Label';
import { Input } from './TextInputs';
import Warning from './Warning';
interface Props {
className?: string;
@ -25,11 +26,21 @@ interface Props {
}
function InputWithLabel ({ className, defaultValue, disabled, isError, isFocused, isReadOnly, label = '', onBlur, onChange, onEnter, placeholder, type = 'text', value, withoutMargin }: Props): React.ReactElement<Props> {
const _checkEnter = useCallback(
const [isCapsLock, setIsCapsLock] = useState(false);
const _checkKey = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>): void => {
onEnter && event.key === 'Enter' && onEnter();
if (type === 'password') {
if (event.getModifierState('CapsLock')) {
setIsCapsLock(true);
} else {
setIsCapsLock(false);
}
}
},
[onEnter]
[onEnter, type]
);
const _onChange = useCallback(
@ -52,7 +63,7 @@ function InputWithLabel ({ className, defaultValue, disabled, isError, isFocused
disabled={disabled}
onBlur={onBlur}
onChange={_onChange}
onKeyPress={_checkEnter}
onKeyPress={_checkKey}
placeholder={placeholder}
readOnly={isReadOnly}
spellCheck={false}
@ -60,6 +71,9 @@ function InputWithLabel ({ className, defaultValue, disabled, isError, isFocused
value={value}
withError={isError}
/>
{ isCapsLock && (
<Warning isBelowInput >Warning: Caps lock is on</Warning>
)}
</Label>
);
}
@ -69,5 +83,9 @@ export default styled(InputWithLabel)`
&.withoutMargin {
margin-bottom: 0px;
+ .error {
margin-top: 6px;
}
}
`;

View File

@ -11,9 +11,9 @@ interface Props extends ThemeProps {
const TextInput = css(({ theme, withError }: Props) => `
background: ${theme.inputBackground};
border-color: ${withError ? theme.errorBorderColor : theme.inputBorderColor};
border-radius: ${theme.borderRadius};
border: 1px solid ${theme.inputBorderColor};
border-color: ${withError ? theme.errorBorderColor : theme.inputBorderColor};
box-sizing: border-box;
color: ${withError ? theme.errorColor : theme.textColor};
display: block;

View File

@ -1,13 +1,11 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../types';
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import useIsMounted from '../hooks/useIsMounted';
import { Result, Validator } from '../util/validators';
import Warning from './Warning';
interface BasicProps {
isError?: boolean;
@ -52,17 +50,16 @@ function ValidatedInput<T extends Record<string, unknown>> ({ className, compone
onChange={setValue}
value={value}
/>
{Result.isError(validationResult) && <span className='error'>{validationResult.error.errorDescription}</span>}
{Result.isError(validationResult) && (
<Warning
isBelowInput
isDanger
>
{validationResult.error.errorDescription}
</Warning>
)}
</div>
);
}
export default styled(ValidatedInput)(({ theme }: ThemeProps) => `
.error {
display: block;
margin-top: -10px;
font-size: ${theme.labelFontSize};
line-height: ${theme.labelLineHeight};
color: ${theme.errorColor};
}
`);
export default ValidatedInput;

View File

@ -12,30 +12,48 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
interface Props extends ThemeProps {
children: React.ReactNode;
className?: string;
isBelowInput?: boolean;
isDanger?: boolean;
}
function Warning ({ children, className }: Props): React.ReactElement<Props> {
function Warning ({ children, className = '', isBelowInput, isDanger }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<div>
<FontAwesomeIcon
className='warningImage'
icon={faExclamationTriangle}
/>
</div>
<div>{children}</div>
<div className={`${className} ${isDanger ? 'danger' : ''} ${isBelowInput ? 'belowInput' : ''}`}>
<FontAwesomeIcon
className='warningImage'
icon={faExclamationTriangle}
/>
<div className='warning-message'>{children}</div>
</div>
);
}
export default React.memo(styled(Warning)(({ isDanger, theme }: Props) => `
export default React.memo(styled(Warning)<Props>(({ isDanger, theme }: Props) => `
display: flex;
flex-direction: row;
padding-left: ${isDanger ? '18px' : ''};
padding-left: 18px;
color: ${theme.subTextColor};
margin-right: 20px;
border-left: ${isDanger ? `0.25rem solid ${theme.buttonBackgroundDanger}` : ''};
margin-top: 6px;
border-left: ${`0.25rem solid ${theme.iconWarningColor}`};
&.belowInput {
font-size: ${theme.labelFontSize};
line-height: ${theme.labelLineHeight};
&.danger {
margin-top: -10px;
}
}
&.danger {
border-left-color: ${theme.buttonBackgroundDanger};
}
.warning-message {
display: flex;
align-items: center;
}
.warningImage {
margin: 5px 10px 5px 0;