improve email input control
This commit is contained in:
parent
94a28806e1
commit
d4aa16773d
|
|
@ -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",
|
||||
|
|
|
|||
83
resources/assets/src/components/EmailSuggestion.tsx
Normal file
83
resources/assets/src/components/EmailSuggestion.tsx
Normal 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
|
||||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
58
resources/assets/tests/components/EmailSuggestion.test.tsx
Normal file
58
resources/assets/tests/components/EmailSuggestion.test.tsx
Normal 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()
|
||||
})
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
- 更改获取角色的 API
|
||||
- 头像和预览图的格式改为 WebP
|
||||
- 减少不必要的 SQL 查询语句
|
||||
- 改进邮箱地址输入框
|
||||
|
||||
## 修复
|
||||
|
||||
|
|
|
|||
57
yarn.lock
57
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user