fix(858): country combobox fix (#318)

keep-around/af30b88d367751c9e05a735e4a0467a96238ef47
Mirlan 5 years ago committed by GitHub
parent 5050f12507
commit bec9a081f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 63
      src/features/Combobox/hooks/index.tsx
  2. 6
      src/features/Combobox/types.tsx
  3. 25
      src/features/FormStore/hooks/useFormState.tsx
  4. 32
      src/features/UserAccount/components/PagePersonalInfo/hooks/useUserInfo.tsx
  5. 52
      src/features/UserAccount/components/PagePersonalInfo/hooks/useUserInfoForm.tsx
  6. 8
      src/features/UserAccount/components/PagePersonalInfo/index.tsx

@ -9,6 +9,7 @@ import {
useCallback, useCallback,
useRef, useRef,
useEffect, useEffect,
useMemo,
} from 'react' } from 'react'
import toLower from 'lodash/toLower' import toLower from 'lodash/toLower'
@ -26,26 +27,14 @@ const isOptionClicked = (target: HTMLElement | null) => (
target?.getAttribute('role') === 'option' target?.getAttribute('role') === 'option'
) )
const useQuery = <T extends Option>({ onChange, value }: Props<T>) => { export const useCombobox = <T extends Option>({
const [query, setQuery] = useState('') onBlur,
onChange,
const onQueryChange = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => { onSelect,
setQuery(target.value) options,
}, []) selected,
value: query,
return { }: Props<T>) => {
onQueryChange: onChange ?? onQueryChange,
query: value ?? query,
setQuery,
}
}
export const useCombobox = <T extends Option>(props: Props<T>) => {
const {
onSelect,
options,
} = props
const inputFieldRef = useRef<HTMLInputElement>(null) const inputFieldRef = useRef<HTMLInputElement>(null)
const [index, setIndex] = useState(0) const [index, setIndex] = useState(0)
@ -54,12 +43,6 @@ export const useCombobox = <T extends Option>(props: Props<T>) => {
popoverRef, popoverRef,
} = useKeyboardScroll() } = useKeyboardScroll()
const {
onQueryChange,
query,
setQuery,
} = useQuery(props)
const { const {
close, close,
isOpen, isOpen,
@ -67,11 +50,15 @@ export const useCombobox = <T extends Option>(props: Props<T>) => {
toggle, toggle,
} = useToggle() } = useToggle()
const results = matchSort( const filteredOptions = useMemo(() => (selected ? options : matchSort(
options, options,
'name', 'name',
query, query,
) )), [
options,
query,
selected,
])
const findOptionByName = useCallback((optionName: string) => ( const findOptionByName = useCallback((optionName: string) => (
find( find(
@ -80,22 +67,25 @@ export const useCombobox = <T extends Option>(props: Props<T>) => {
) || null ) || null
), [options]) ), [options])
const onQueryChange = (e: ChangeEvent<HTMLInputElement>) => {
onChange?.(e)
onSelect?.(null)
}
const onOptionSelect = useCallback((option: string, e?: BaseSyntheticEvent) => { const onOptionSelect = useCallback((option: string, e?: BaseSyntheticEvent) => {
e?.stopPropagation() e?.stopPropagation()
const selectedOption = findOptionByName(option) const selectedOption = findOptionByName(option)
setQuery(selectedOption?.name || '')
onSelect?.(selectedOption) onSelect?.(selectedOption)
close() close()
}, [ }, [
close, close,
findOptionByName, findOptionByName,
onSelect, onSelect,
setQuery,
]) ])
const onOutsideClick = (event: MouseEvent) => { const onOutsideClick = (event: MouseEvent) => {
if (event.target !== inputFieldRef.current) { if (event.target !== inputFieldRef.current) {
onOptionSelect(query) close()
} }
} }
@ -106,16 +96,17 @@ export const useCombobox = <T extends Option>(props: Props<T>) => {
if (isOptionClicked(target)) return if (isOptionClicked(target)) return
onOptionSelect(query) onOptionSelect(query)
onBlur?.(event)
} }
useEffect(() => { useEffect(() => {
const lastElementIndex = size(results) - 1 const lastElementIndex = size(filteredOptions) - 1
if (index < 0) setIndex(0) if (index < 0) setIndex(0)
if (index >= lastElementIndex) setIndex(lastElementIndex) if (index >= lastElementIndex) setIndex(lastElementIndex)
}, [ }, [
index, index,
options, options,
results, filteredOptions,
]) ])
const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => { const onKeyDown = useCallback((event: KeyboardEvent<HTMLInputElement>) => {
@ -128,14 +119,14 @@ export const useCombobox = <T extends Option>(props: Props<T>) => {
} else if (event.key === 'ArrowDown') { } else if (event.key === 'ArrowDown') {
setIndex(index + 1) setIndex(index + 1)
} else if (event.key === 'Enter') { } else if (event.key === 'Enter') {
onSelect?.(results[index]) onSelect?.(filteredOptions[index])
close() close()
inputFieldRef.current?.blur() inputFieldRef.current?.blur()
} }
}, [ }, [
close, close,
index, index,
results, filteredOptions,
onSelect, onSelect,
onKeyDownScroll, onKeyDownScroll,
]) ])
@ -151,7 +142,7 @@ export const useCombobox = <T extends Option>(props: Props<T>) => {
onOutsideClick, onOutsideClick,
onQueryChange, onQueryChange,
open, open,
options: results, options: filteredOptions,
popoverRef, popoverRef,
query, query,
toggle, toggle,

@ -1,13 +1,11 @@
import type { import type {
InputHTMLAttributes, InputHTMLAttributes,
ChangeEvent, ChangeEvent,
ReactNode,
} from 'react' } from 'react'
import type { CustomStyles } from 'features/Common' import type { CustomStyles } from 'features/Common'
export type Option = { export type Option = {
children?: ReactNode,
id: number, id: number,
name: string, name: string,
} }
@ -16,6 +14,7 @@ export type Props<T> = Pick<InputHTMLAttributes<HTMLInputElement>, (
| 'disabled' | 'disabled'
| 'title' | 'title'
| 'placeholder' | 'placeholder'
| 'onBlur'
)> & { )> & {
customListStyles?: CustomStyles, customListStyles?: CustomStyles,
error?: string | null, error?: string | null,
@ -26,6 +25,7 @@ export type Props<T> = Pick<InputHTMLAttributes<HTMLInputElement>, (
onChange?: (event: ChangeEvent<HTMLInputElement>) => void, onChange?: (event: ChangeEvent<HTMLInputElement>) => void,
onSelect?: (option: T | null) => void, onSelect?: (option: T | null) => void,
options: Array<T>, options: Array<T>,
value?: string, selected?: boolean,
value: string,
withError?: boolean, withError?: boolean,
} }

@ -4,8 +4,6 @@ import { useCallback, useState } from 'react'
import isObject from 'lodash/isObject' import isObject from 'lodash/isObject'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import omit from 'lodash/omit' import omit from 'lodash/omit'
import { useLexicsStore } from '../../LexicsStore'
import { Name } from '../../UserAccount/components/PagePersonalInfo/hooks/useUserInfoForm'
type FieldState = { type FieldState = {
error: string | null, error: string | null,
@ -16,25 +14,22 @@ type FormState = {[formId: string]: FieldState}
type SimilarState = {[formId: string]: string} type SimilarState = {[formId: string]: string}
export const useFormState = () => { export const useFormState = () => {
const { suffix } = useLexicsStore()
let init = JSON.parse(localStorage.getItem('init') as string) let init = JSON.parse(localStorage.getItem('init') as string)
const country = init === null ? '' : init.country[`name_${suffix}` as Name]
const countryID = init === null ? '' : init.country.id const countryID = init === null ? '' : init.country.id
if (init !== null) { if (init !== null) {
init.address_line1 = init.address_line1 === null || undefined ? '' : init.address_line1 init.address_line1 = init.address_line1 ?? ''
init.address_line2 = init.address_line2 === null || undefined ? '' : init.address_line2 init.address_line2 = init.address_line2 ?? ''
if (init.city_id !== undefined) { if (init.city_id !== undefined) {
init.city_id = init.city_id === null || undefined ? '' : init.city_id.toString() init.city_id = init.city_id ?? ''
} }
init.city = init.city === null || undefined ? '' : init.city init.city = init.city ?? ''
init.country = country
init.countryId = `${countryID}` init.countryId = `${countryID}`
init.cityId = '' init.cityId = init.city_id ?? ''
init.firstname = init.firstname === null || undefined ? '' : init.firstname init.firstname = init.firstname ?? ''
init.lastname = init.lastname === null || undefined ? '' : init.lastname init.lastname = init.lastname ?? ''
init.region = init.region === null || undefined ? '' : init.region init.region = init.region ?? ''
init.postalCode = 0 init.postal_code = String(init.postal_code) ?? ''
init = omit(init, ['city_id', 'email', 'password', 'postal_code']) init = omit(init, ['city_id', 'email', 'password', 'postalCode'])
} }
const [formState, setFormState] = useState<FormState>({}) const [formState, setFormState] = useState<FormState>({})
const [stateForSimilar, setStateForSimilar] = useState<SimilarState>(init) const [stateForSimilar, setStateForSimilar] = useState<SimilarState>(init)

@ -11,30 +11,33 @@ import { formIds } from 'config/form'
import { getUserInfo } from 'requests/getUserInfo' import { getUserInfo } from 'requests/getUserInfo'
import { saveUserInfo } from 'requests/saveUserInfo' import { saveUserInfo } from 'requests/saveUserInfo'
import { City, getCountryCities } from 'requests/getCountryCities' import { City, getCountryCities } from 'requests/getCountryCities'
import { useLexicsStore } from 'features/LexicsStore'
import type { SelectedCountry } from './useCountries' import type { SelectedCountry } from './useCountries'
import type { Name } from './useUserInfoForm'
import { useUserInfoForm } from './useUserInfoForm' import { useUserInfoForm } from './useUserInfoForm'
import { useValidateForm } from './useValidateForm' import { useValidateForm } from './useValidateForm'
export const useUserInfo = () => { export const useUserInfo = () => {
const validateForm = useValidateForm() const validateForm = useValidateForm()
const saveButton = useRef<HTMLButtonElement>(null) const saveButton = useRef<HTMLButtonElement>(null)
const { suffix } = useLexicsStore()
const { const {
cities, cities,
countries, countries,
getCities,
onCitySelect, onCitySelect,
onCountryBlur,
onCountrySelect, onCountrySelect,
onPhoneBlur, onPhoneBlur,
onRegionOrCityChange, onRegionOrCityChange,
readFormError, readFormError,
readFormValue, readFormValue,
resetCities,
resetSelectedCity,
selectedCity, selectedCity,
updateFormError, selectedCountry,
setSelectedCountry,
updateFormValue, updateFormValue,
} = useUserInfoForm() } = useUserInfoForm()
const readTrimmedValue = useCallback( const readTrimmedValue = useCallback(
@ -86,7 +89,11 @@ export const useUserInfo = () => {
const convertResponse = () => { const convertResponse = () => {
forEach(response, (value: string | number | SelectedCountry, key: string) => { forEach(response, (value: string | number | SelectedCountry, key: string) => {
if (value && typeof value === 'object') { if (value && typeof value === 'object') {
onCountrySelect(value, false) setSelectedCountry({
...value,
name: value[`name_${suffix}` as Name],
})
updateFormValue(formIds.countryId)(String(value.id))
} else if (value) { } else if (value) {
updateFormValue(key)(String(value)) updateFormValue(key)(String(value))
} }
@ -106,24 +113,25 @@ export const useUserInfo = () => {
localStorage.setItem('init', JSON.stringify(response)) localStorage.setItem('init', JSON.stringify(response))
convertResponse() convertResponse()
}) })
}, [updateFormValue, onCountrySelect]) }, [
suffix,
updateFormValue,
setSelectedCountry,
])
return { return {
cities, cities,
countries, countries,
getCities,
handleSubmit, handleSubmit,
onCitySelect, onCitySelect,
onCountryBlur,
onCountrySelect, onCountrySelect,
onPhoneBlur, onPhoneBlur,
onRegionOrCityChange, onRegionOrCityChange,
readFormError, readFormError,
readFormValue, readFormValue,
resetCities,
resetSelectedCity,
saveButton, saveButton,
selectedCity, selectedCountry,
updateFormError,
updateFormValue, updateFormValue,
} }
} }

@ -1,4 +1,4 @@
import type { ChangeEvent, FocusEvent } from 'react' import type { ChangeEvent } from 'react'
import { import {
useEffect, useEffect,
useCallback, useCallback,
@ -6,14 +6,12 @@ import {
import trim from 'lodash/trim' import trim from 'lodash/trim'
import { isValidEmail } from 'helpers/isValidEmail'
import { isValidPhone } from 'helpers/isValidPhone' import { isValidPhone } from 'helpers/isValidPhone'
import { formatPhoneCode } from 'helpers/formatPhoneCode' import { formatPhoneCode } from 'helpers/formatPhoneCode'
import { formIds } from 'config/form' import { formIds } from 'config/form'
import { useForm } from 'features/FormStore' import { useForm } from 'features/FormStore'
import { useLexicsStore } from 'features/LexicsStore'
import type { SelectedCountry } from './useCountries' import type { SelectedCountry } from './useCountries'
import { useCountries } from './useCountries' import { useCountries } from './useCountries'
@ -44,15 +42,6 @@ export const useUserInfoForm = () => {
selectedCity, selectedCity,
} = useCities() } = useCities()
const { suffix } = useLexicsStore()
const onEmailBlur = useCallback(({ target }: FocusEvent<HTMLInputElement>) => {
const email = trim(target.value)
if (email && !isValidEmail(email)) {
updateFormError(formIds.email, 'error_invalid_email_format')
}
}, [updateFormError])
const onPhoneBlur = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => { const onPhoneBlur = useCallback(({ target }: ChangeEvent<HTMLInputElement>) => {
const phone = target.value const phone = target.value
if (phone && !isValidPhone(phone)) { if (phone && !isValidPhone(phone)) {
@ -60,33 +49,40 @@ export const useUserInfoForm = () => {
} }
}, [updateFormError]) }, [updateFormError])
const onCountrySelect = useCallback(( const phone = trim(readFormValue(formIds.phone))
country: SelectedCountry, const onCountrySelect = useCallback((country: SelectedCountry) => {
updatePhoneCode: boolean = true, if (country?.id === selectedCountry?.id) return
) => {
setSelectedCountry(country) setSelectedCountry(country)
const countryName = country ? country[`name_${suffix}` as Name] : ''
updateFormValue(formIds.country)(countryName)
updateFormValue(formIds.countryId)(country?.id ? String(country?.id) : '')
updateFormValue(formIds.cityId)('') updateFormValue(formIds.cityId)('')
updateFormValue(formIds.countryId)(`${country?.id}`)
resetCities() resetCities()
resetSelectedCity() resetSelectedCity()
if (updatePhoneCode) { const selectedCountryCode = formatPhoneCode(selectedCountry?.phone_code || '')
const code = country?.phone_code const hasPhoneNumber = (
? formatPhoneCode(country.phone_code) phone
: '' && selectedCountryCode
&& phone !== selectedCountryCode
)
if (!hasPhoneNumber) {
const code = formatPhoneCode(country?.phone_code || '')
updateFormValue(formIds.phone)(code) updateFormValue(formIds.phone)(code)
} }
}, [ }, [
phone,
selectedCountry,
resetCities, resetCities,
resetSelectedCity, resetSelectedCity,
setSelectedCountry, setSelectedCountry,
updateFormValue, updateFormValue,
suffix,
]) ])
const onCountryBlur = () => {
updateFormValue(formIds.country)(selectedCountry?.name || '')
}
const onRegionOrCityChange = useCallback((fieldName: string) => ( const onRegionOrCityChange = useCallback((fieldName: string) => (
({ target }: ChangeEvent<HTMLInputElement>) => { ({ target }: ChangeEvent<HTMLInputElement>) => {
if (selectedCountry) { if (selectedCountry) {
@ -115,18 +111,16 @@ export const useUserInfoForm = () => {
return { return {
cities, cities,
countries, countries,
getCities,
onCitySelect, onCitySelect,
onCountryBlur,
onCountrySelect, onCountrySelect,
onEmailBlur,
onPhoneBlur, onPhoneBlur,
onRegionOrCityChange, onRegionOrCityChange,
readFormError, readFormError,
readFormValue, readFormValue,
resetCities,
resetSelectedCity,
selectedCity, selectedCity,
updateFormError, selectedCountry,
setSelectedCountry,
updateFormValue, updateFormValue,
} }
} }

@ -11,7 +11,7 @@ import { useUserInfo } from './hooks/useUserInfo'
import { SolidButton } from '../../styled' import { SolidButton } from '../../styled'
import { Form, ButtonWrapper } from './styled' import { Form, ButtonWrapper } from './styled'
const labelWidth = 76 const labelWidth = 104
const { const {
address1, address1,
@ -32,12 +32,14 @@ export const PagePersonalInfo = () => {
countries, countries,
handleSubmit, handleSubmit,
onCitySelect, onCitySelect,
onCountryBlur,
onCountrySelect, onCountrySelect,
onPhoneBlur, onPhoneBlur,
onRegionOrCityChange, onRegionOrCityChange,
readFormError, readFormError,
readFormValue, readFormValue,
saveButton, saveButton,
selectedCountry,
updateFormValue, updateFormValue,
} = useUserInfo() } = useUserInfo()
@ -91,13 +93,15 @@ export const PagePersonalInfo = () => {
withError={false} withError={false}
/> />
<Combobox <Combobox
value={readFormValue(country)} value={selectedCountry?.name ?? readFormValue(country)}
labelLexic='form_country' labelLexic='form_country'
labelWidth={labelWidth} labelWidth={labelWidth}
onChange={updateFormValue(country)} onChange={updateFormValue(country)}
onBlur={onCountryBlur}
options={countries} options={countries}
onSelect={onCountrySelect} onSelect={onCountrySelect}
withError={false} withError={false}
selected={Boolean(selectedCountry)}
/> />
<Input <Input
value={readFormValue(region)} value={readFormValue(region)}

Loading…
Cancel
Save