improve email input control

This commit is contained in:
Pig Fang 2020-06-22 16:16:09 +08:00
parent 94a28806e1
commit d4aa16773d
9 changed files with 225 additions and 60 deletions

View File

@ -32,6 +32,7 @@
"lodash.debounce": "^4.0.8",
"nanoid": "^3.1.9",
"react": "^16.13.0",
"react-autosuggest": "^10.0.2",
"react-dom": "^16.13.0",
"react-draggable": "^4.4.2",
"react-hot-loader": "^4.12.21",
@ -58,6 +59,7 @@
"@types/js-yaml": "^3.12.4",
"@types/lodash.debounce": "^4.0.6",
"@types/react": "^16.9.35",
"@types/react-autosuggest": "^9.3.14",
"@types/react-dom": "^16.9.8",
"@types/tween.js": "^18.5.0",
"@types/webpack": "^4.41.13",

View File

@ -0,0 +1,83 @@
/** @jsx jsx */
import React, { useState } from 'react'
import Autosuggest from 'react-autosuggest'
import { jsx, css } from '@emotion/core'
import { pointerCursor } from '@/styles/utils'
const styles = css`
.dropdown-menu li {
${pointerCursor}
}
`
const domainNames = ['qq.com', '163.com', 'gmail.com', 'hotmail.com']
type Props = Omit<Autosuggest.InputProps<string>, 'onChange'> & {
onChange(value: string): void
}
const EmailSuggestion: React.FC<Props> = (props) => {
const [suggestions, setSuggestions] = useState<string[]>([])
const handleSuggestionsFetchRequested: Autosuggest.SuggestionsFetchRequested = ({
value,
}) => {
const segments = value.split('@')
setSuggestions(domainNames.map((name) => `${segments[0]}@${name}`))
}
const handleSuggestionsClearRequested = () => {
setSuggestions([])
}
const shouldRenderSuggestions = (value: string) => {
const isSelecting = domainNames.some((name) => value.endsWith(`@${name}`))
return isSelecting || (value.length > 0 && !value.includes('@'))
}
const getSuggestionValue = (value: string) => value
const renderSuggestion = (suggestion: string) => suggestion
const handleChange = (_: React.FormEvent, event: Autosuggest.ChangeEvent) => {
props.onChange(event.newValue)
}
const renderInputComponent = (
props: Omit<Autosuggest.InputProps<string>, 'onChange'>,
) => (
<div className="input-group">
<input className="form-control" {...props} />
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-envelope"></i>
</div>
</div>
</div>
)
return (
<div css={styles}>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
onSuggestionsClearRequested={handleSuggestionsClearRequested}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
shouldRenderSuggestions={shouldRenderSuggestions}
inputProps={Object.assign({}, props, { onChange: handleChange })}
renderInputComponent={renderInputComponent}
theme={{
container: 'mb-3',
suggestion: 'dropdown-item',
suggestionsContainer: 'dropdown',
suggestionsList: `dropdown-menu ${suggestions.length ? 'show' : ''}`,
suggestionHighlighted: 'active',
}}
/>
</div>
)
}
export default EmailSuggestion

View File

@ -6,6 +6,7 @@ import * as fetch from '@/scripts/net'
import urls from '@/scripts/urls'
import Alert from '@/components/Alert'
import Captcha from '@/components/Captcha'
import EmailSuggestion from '@/components/EmailSuggestion'
const Forgot: React.FC = () => {
const [email, setEmail] = useState('')
@ -16,10 +17,6 @@ const Forgot: React.FC = () => {
useEmitMounted()
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value)
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
setWarningMessage('')
@ -41,21 +38,13 @@ const Forgot: React.FC = () => {
return (
<form onSubmit={handleSubmit}>
<div className="input-group mb-3">
<input
type="email"
className="form-control"
placeholder={t('auth.email')}
required
value={email}
onChange={handleEmailChange}
/>
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-envelope"></i>
</div>
</div>
</div>
<EmailSuggestion
type="email"
placeholder={t('auth.email')}
required
value={email}
onChange={setEmail}
/>
<Captcha ref={ref} />

View File

@ -8,6 +8,7 @@ import { showModal } from '@/scripts/notify'
import urls from '@/scripts/urls'
import Alert from '@/components/Alert'
import Captcha from '@/components/Captcha'
import EmailSuggestion from '@/components/EmailSuggestion'
type SuccessfulResponse = {
code: 0
@ -44,12 +45,6 @@ const Login: React.FC = () => {
setHasTooManyFails(blessing.extra.tooManyFails as boolean)
}, [])
const handleIdentificationChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setIdentification(event.target.value)
}
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value)
}
@ -99,21 +94,13 @@ const Login: React.FC = () => {
return (
<form onSubmit={handleSubmit}>
<div className="input-group mb-3">
<input
type="text"
className="form-control"
placeholder={t('auth.identification')}
value={identification}
onChange={handleIdentificationChange}
required
/>
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-envelope"></i>
</div>
</div>
</div>
<EmailSuggestion
type="text"
placeholder={t('auth.identification')}
required
value={identification}
onChange={setIdentification}
/>
<div className="input-group mb-3">
<input
type="password"

View File

@ -8,6 +8,7 @@ import { toast } from '@/scripts/notify'
import urls from '@/scripts/urls'
import Alert from '@/components/Alert'
import Captcha from '@/components/Captcha'
import EmailSuggestion from '@/components/EmailSuggestion'
const Registration: React.FC = () => {
const [email, setEmail] = useState('')
@ -23,10 +24,6 @@ const Registration: React.FC = () => {
useEmitMounted()
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value)
}
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value)
}
@ -79,21 +76,13 @@ const Registration: React.FC = () => {
return (
<form onSubmit={handleSubmit}>
<div className="input-group mb-3">
<input
type="email"
required
className="form-control"
placeholder={t('auth.email')}
value={email}
onChange={handleEmailChange}
/>
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-envelope"></i>
</div>
</div>
</div>
<EmailSuggestion
type="email"
required
placeholder={t('auth.email')}
value={email}
onChange={setEmail}
/>
<div className="input-group mb-3">
<input
type="password"

View File

@ -0,0 +1,58 @@
import React, { useState } from 'react'
import { render, fireEvent } from '@testing-library/react'
import EmailSuggestion from '@/components/EmailSuggestion'
const Wrapper: React.FC = () => {
const [email, setEmail] = useState('')
return <EmailSuggestion value={email} onChange={setEmail} />
}
test('basic typing', () => {
const { getByDisplayValue, queryByText } = render(<Wrapper />)
const input = getByDisplayValue('')
fireEvent.input(input, { target: { value: 'abc' } })
fireEvent.focus(input)
expect(queryByText('abc@qq.com')).toBeInTheDocument()
expect(queryByText('abc@163.com')).toBeInTheDocument()
expect(queryByText('abc@gmail.com')).toBeInTheDocument()
expect(queryByText('abc@hotmail.com')).toBeInTheDocument()
fireEvent.input(input, { target: { value: '' } })
expect(queryByText('abc@qq.com')).not.toBeInTheDocument()
})
test('apply suggestion', () => {
const { getByDisplayValue, getByText } = render(<Wrapper />)
const input = getByDisplayValue('')
fireEvent.input(input, { target: { value: 'abc' } })
fireEvent.focus(input)
fireEvent.click(getByText('abc@hotmail.com'))
expect(input).toHaveValue('abc@hotmail.com')
})
test('do not suggest when `at` is existed', () => {
const { getByDisplayValue, queryByText } = render(<Wrapper />)
const input = getByDisplayValue('')
fireEvent.input(input, { target: { value: 'abc@outlook.com' } })
fireEvent.focus(input)
expect(queryByText('abc@outlook.com@qq.com')).not.toBeInTheDocument()
expect(queryByText('abc@outlook.com@163.com')).not.toBeInTheDocument()
expect(queryByText('abc@outlook.com@gmail.com')).not.toBeInTheDocument()
expect(queryByText('abc@outlook.com@hotmail.com')).not.toBeInTheDocument()
})
test('display suggestions when typing with configured domain names', () => {
const { getByDisplayValue, queryByText } = render(<Wrapper />)
const input = getByDisplayValue('')
fireEvent.input(input, { target: { value: 'abc@hotmail.com' } })
fireEvent.focus(input)
expect(queryByText('abc@qq.com')).toBeInTheDocument()
expect(queryByText('abc@163.com')).toBeInTheDocument()
expect(queryByText('abc@gmail.com')).toBeInTheDocument()
expect(queryByText('abc@hotmail.com')).toBeInTheDocument()
})

View File

@ -52,6 +52,7 @@
- Changed API of retrieving all players.
- Changed format of avatar and 2D preview to WebP.
- Reduced some unnecessary SQL queries.
- Improved email input control.
## Fixed

View File

@ -52,6 +52,7 @@
- 更改获取角色的 API
- 头像和预览图的格式改为 WebP
- 减少不必要的 SQL 查询语句
- 改进邮箱地址输入框
## 修复

View File

@ -1073,6 +1073,13 @@
resolved "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8"
integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==
"@types/react-autosuggest@^9.3.14":
version "9.3.14"
resolved "https://registry.npmjs.org/@types/react-autosuggest/-/react-autosuggest-9.3.14.tgz#0a759db913f28609f390605283747c497af7e931"
integrity sha512-cvGpKaQaNsFbDxTwP56VKVj2FO6SpJ9PsrQtlVzN7aVa/SsMZoQrBLEUx5HQKfIS4Zupb6K4tHmIyTjF7AEcow==
dependencies:
"@types/react" "*"
"@types/react-dom@*":
version "16.9.4"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.4.tgz#0b58df09a60961dcb77f62d4f1832427513420df"
@ -3348,6 +3355,11 @@ es-to-primitive@^1.2.0:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@ -6315,6 +6327,11 @@ oauth-sign@~0.9.0:
resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
object-assign@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=
object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -7251,7 +7268,7 @@ prompts@^2.0.1:
kleur "^3.0.2"
sisteransi "^1.0.0"
prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -7408,6 +7425,27 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-autosuggest@^10.0.2:
version "10.0.2"
resolved "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-10.0.2.tgz#c45cd73a7307026c932cb6a3e19c2fe35bfb6ec4"
integrity sha512-ouI0RJDSgM1FBfK0ZmLC3qUqithIwPVTpnC4JQW4DeId3mH2JnZmkNNDKImhcMrxLbSQRpV/DfTLn0uCs4b27w==
dependencies:
es6-promise "^4.2.8"
prop-types "^15.7.2"
react-autowhatever "^10.2.1"
react-themeable "^1.1.0"
section-iterator "^2.0.0"
shallow-equal "^1.2.1"
react-autowhatever@^10.2.1:
version "10.2.1"
resolved "https://registry.npmjs.org/react-autowhatever/-/react-autowhatever-10.2.1.tgz#a6d421dc6135173efedc249ab7216e4f5b691bcc"
integrity sha512-5gQyoETyBH6GmuW1N1J81CuoAV+Djeg66DEo03xiZOl3WOwJHBP5LisKUvCGOakjrXU4M3hcIvCIqMBYGUmqOA==
dependencies:
prop-types "^15.5.8"
react-themeable "^1.1.0"
section-iterator "^2.0.0"
react-dom@^16.13.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@ -7460,6 +7498,13 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-themeable@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e"
integrity sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=
dependencies:
object-assign "^3.0.0"
react@^16.13.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@ -7862,6 +7907,11 @@ schema-utils@^2.6.5, schema-utils@^2.6.6:
ajv "^6.12.0"
ajv-keywords "^3.4.1"
section-iterator@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a"
integrity sha1-v0RNev7rlK1Dw5rS+yYVFifMuio=
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@ -7984,6 +8034,11 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-equal@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"