From d4aa16773d41d6b8d3f49433058d1baccca7d133 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 22 Jun 2020 16:16:09 +0800 Subject: [PATCH] improve email input control --- package.json | 2 + .../assets/src/components/EmailSuggestion.tsx | 83 +++++++++++++++++++ resources/assets/src/views/auth/Forgot.tsx | 27 ++---- resources/assets/src/views/auth/Login.tsx | 29 ++----- .../assets/src/views/auth/Registration.tsx | 27 ++---- .../tests/components/EmailSuggestion.test.tsx | 58 +++++++++++++ resources/misc/changelogs/en/5.0.0.md | 1 + resources/misc/changelogs/zh_CN/5.0.0.md | 1 + yarn.lock | 57 ++++++++++++- 9 files changed, 225 insertions(+), 60 deletions(-) create mode 100644 resources/assets/src/components/EmailSuggestion.tsx create mode 100644 resources/assets/tests/components/EmailSuggestion.test.tsx diff --git a/package.json b/package.json index 7fd8ad15..90c1e156 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/assets/src/components/EmailSuggestion.tsx b/resources/assets/src/components/EmailSuggestion.tsx new file mode 100644 index 00000000..9241fea1 --- /dev/null +++ b/resources/assets/src/components/EmailSuggestion.tsx @@ -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, 'onChange'> & { + onChange(value: string): void +} + +const EmailSuggestion: React.FC = (props) => { + const [suggestions, setSuggestions] = useState([]) + + 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, 'onChange'>, + ) => ( +
+ +
+
+ +
+
+
+ ) + + return ( +
+ +
+ ) +} + +export default EmailSuggestion diff --git a/resources/assets/src/views/auth/Forgot.tsx b/resources/assets/src/views/auth/Forgot.tsx index 3df7a405..0d27b0fb 100644 --- a/resources/assets/src/views/auth/Forgot.tsx +++ b/resources/assets/src/views/auth/Forgot.tsx @@ -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) => { - setEmail(event.target.value) - } - const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() setWarningMessage('') @@ -41,21 +38,13 @@ const Forgot: React.FC = () => { return (
-
- -
-
- -
-
-
+ diff --git a/resources/assets/src/views/auth/Login.tsx b/resources/assets/src/views/auth/Login.tsx index 7feb17ee..725f1753 100644 --- a/resources/assets/src/views/auth/Login.tsx +++ b/resources/assets/src/views/auth/Login.tsx @@ -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, - ) => { - setIdentification(event.target.value) - } - const handlePasswordChange = (event: React.ChangeEvent) => { setPassword(event.target.value) } @@ -99,21 +94,13 @@ const Login: React.FC = () => { return ( -
- -
-
- -
-
-
+
{ const [email, setEmail] = useState('') @@ -23,10 +24,6 @@ const Registration: React.FC = () => { useEmitMounted() - const handleEmailChange = (event: React.ChangeEvent) => { - setEmail(event.target.value) - } - const handlePasswordChange = (event: React.ChangeEvent) => { setPassword(event.target.value) } @@ -79,21 +76,13 @@ const Registration: React.FC = () => { return ( -
- -
-
- -
-
-
+
{ + const [email, setEmail] = useState('') + + return +} + +test('basic typing', () => { + const { getByDisplayValue, queryByText } = render() + 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() + 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() + 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() + 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() +}) diff --git a/resources/misc/changelogs/en/5.0.0.md b/resources/misc/changelogs/en/5.0.0.md index ff8d9c4f..92837fe0 100644 --- a/resources/misc/changelogs/en/5.0.0.md +++ b/resources/misc/changelogs/en/5.0.0.md @@ -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 diff --git a/resources/misc/changelogs/zh_CN/5.0.0.md b/resources/misc/changelogs/zh_CN/5.0.0.md index 8d4d2d98..89d884dd 100644 --- a/resources/misc/changelogs/zh_CN/5.0.0.md +++ b/resources/misc/changelogs/zh_CN/5.0.0.md @@ -52,6 +52,7 @@ - 更改获取角色的 API - 头像和预览图的格式改为 WebP - 减少不必要的 SQL 查询语句 +- 改进邮箱地址输入框 ## 修复 diff --git a/yarn.lock b/yarn.lock index 62d8f481..b8c41ced 100644 --- a/yarn.lock +++ b/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"