Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(PhoneNumber): return country code only when a number is given #2920

Merged
merged 8 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const Empty = () => {
<Field.PhoneNumber
onFocus={(value) => console.log('onFocus', value)}
onBlur={(value) => console.log('onBlur', value)}
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
onCountryCodeChange={(countryCode) =>
console.log('onCountryCodeChange', countryCode)
}
Expand All @@ -25,7 +25,7 @@ export const Placeholder = () => {
<ComponentBox>
<Field.PhoneNumber
placeholder="Call this number"
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
/>
</ComponentBox>
)
Expand All @@ -36,7 +36,7 @@ export const Label = () => {
<ComponentBox>
<Field.PhoneNumber
label="Label text"
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
/>
</ComponentBox>
)
Expand All @@ -48,7 +48,7 @@ export const LabelAndValue = () => {
<Field.PhoneNumber
label="Label text"
value="+47 98765432"
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
/>
</ComponentBox>
)
Expand All @@ -58,7 +58,7 @@ export const WithHelp = () => {
return (
<ComponentBox>
<Field.PhoneNumber
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
help={{
title: 'Help is available',
contents:
Expand All @@ -75,7 +75,7 @@ export const Disabled = () => {
<Field.PhoneNumber
value="+47 12345678"
label="Label text"
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
disabled
/>
</ComponentBox>
Expand All @@ -88,7 +88,7 @@ export const Error = () => {
<Field.PhoneNumber
value="007"
label="Label text"
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
error={new FormError('This is what is wrong...')}
/>
</ComponentBox>
Expand All @@ -101,7 +101,7 @@ export const ValidationRequired = () => {
<Field.PhoneNumber
value="+47 888"
label="Label text"
onChange={(value) => console.log('onChange', value)}
onChange={(...args) => console.log('onChange', ...args)}
required
/>
</ComponentBox>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ render(<Field.PhoneNumber />)
```

There is a corresponding [Value.PhoneNumber](/uilib/extensions/forms/create-component/Value/PhoneNumber) component.

## Value

This component behaves as "one single component". Therefor it combines the country code and the number to a single string during an event callback.

Also, the `value` property should be a string with the country code, separated from the main number by a space.

The component returns the `emptyValue` when no number is set, which defaults to `undefined`.

### Default country code

The default country code is set to `+47`.
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,5 @@ import DataValueReadwriteProperties from '../../data-value-readwrite-properties.

<DataValueReadwriteProperties
type="string"
omit={[
'layout',
'label',
'labelDescription',
'labelSecondary',
'emptyValue',
]}
omit={['layout', 'label', 'labelDescription', 'labelSecondary']}
/>
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export interface AutocompleteProps
*/
keep_value?: boolean;
/**
* Use `true` to not remove the selected value/key on input blur, if it is invalid. By default, the typed value will disappear / replaced by a selected value from the data list during the input field blur. Defaults to `false`.
* Use `true` to not remove selected item on input blur, when the input value is empty. Defaults to `false`.
*/
keep_selection?: boolean;
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useContext, useCallback } from 'react'
import React, { useMemo, useContext, useCallback, useEffect } from 'react'
import { Autocomplete, Flex } from '../../../../components'
import { InputMaskedProps } from '../../../../components/InputMasked'
import classnames from 'classnames'
Expand All @@ -11,7 +11,7 @@ import { pickSpacingProps } from '../../../../components/flex/utils'
import SharedContext from '../../../../shared/Context'

export type Props = FieldHelpProps &
FieldProps<string, undefined> & {
FieldProps<string, undefined | string> & {
countryCodeFieldClassName?: string
numberFieldClassName?: string
countryCodePlaceholder?: string
Expand All @@ -27,9 +27,14 @@ export type Props = FieldHelpProps &
noAnimation?: boolean
}

// Important for the default value to be defined here, and not after the useDataValue call, to avoid the UI jumping
// back to +47 once the user empty the field so handleChange send out undefined.
const defaultCountryCode = '+47'

function PhoneNumber(props: Props) {
const sharedContext = useContext(SharedContext)
const tr = sharedContext?.translation.Forms
const lang = sharedContext.locale?.split('-')[0]

const errorMessages = useMemo(
() => ({
Expand All @@ -40,9 +45,6 @@ function PhoneNumber(props: Props) {
)

const defaultProps: Partial<Props> = {
// Important for the default value to be defined here, and not after the useDataValue call, to avoid the UI jumping
// back to +47 once the user empty the field so handleChange send out undefined.
value: '+47',
errorMessages,
}
const preparedProps: Props = {
Expand All @@ -58,7 +60,6 @@ function PhoneNumber(props: Props) {
placeholder,
countryCodeLabel,
label = sharedContext?.translation.Forms.phoneNumberLabel,
value,
numberMask,
emptyValue,
info,
Expand All @@ -74,61 +75,116 @@ function PhoneNumber(props: Props) {
handleFocus,
handleBlur,
handleChange,
updateValue,
onCountryCodeChange,
onNumberChange,
} = useDataValue(preparedProps)

const [, countryCode, phoneNumber] =
value !== undefined
? value.match(/^(\+[^ ]+)? ?(.*)$/)
: [undefined, '', '']
const countryCodeRef = React.useRef(null)
const phoneNumberRef = React.useRef(null)
const dataRef = React.useRef(null)
const langRef = React.useRef(lang)

/**
* We do not process the whole country list at the first render.
* Only when the Autocomplete opens (focus).
* To achieve this, we use memo instead of effect to update refs in sync.
*
* We set or update the data list depending on if the countrycode changes or lang changes.
* We then update countryCode and phoneNumber when value changes.
*/
useMemo(() => {
const [countryCode, phoneNumber] = splitValue(props.value)
phoneNumberRef.current = phoneNumber

if (
!countryCodeRef.current ||
(countryCode && countryCode !== countryCodeRef.current)
) {
countryCodeRef.current = countryCode || defaultCountryCode

dataRef.current = getCountryData({
lang,
filter: countryCodeRef.current,
})
}

const singleCountryCodeData = useMemo(() => {
return getCountryData({
lang: sharedContext.locale?.split('-')[0],
filter: countryCode,
})
}, [])
if (lang !== langRef.current) {
langRef.current = lang
dataRef.current = getCountryData({
lang,
})
}
}, [props.value, lang])

/**
* On external value change, update the internal,
* only so onFocus and onBlur does have correct (eventually empty) value.
*/
useEffect(() => {
const [countryCode, phoneNumber] = splitValue(props.value)
const newValue = phoneNumber
? joinValue([countryCode, phoneNumber])
: emptyValue
updateValue(newValue)
}, [props.value, emptyValue, updateValue])

const handleCountryCodeChange = useCallback(
({ data }: { data: { selectedKey: string } }) => {
const countryCode = data?.selectedKey?.trim() ?? emptyValue
const countryCode = data?.selectedKey?.trim() || emptyValue
const phoneNumber = phoneNumberRef.current || emptyValue
countryCodeRef.current = countryCode

if (!countryCode && !phoneNumber) {
handleChange?.(emptyValue)
onCountryCodeChange?.(emptyValue)
return
}
/**
* To ensure, we actually call onChange every time,
* even if the value is undefined
*/
updateValue('invalidate')

handleChange(
phoneNumber ? joinValue([countryCode, phoneNumber]) : emptyValue,
{
countryCode,
phoneNumber,
}
)

handleChange?.([countryCode, phoneNumber].filter(Boolean).join(' '))
onCountryCodeChange?.(countryCode)
},
[phoneNumber, emptyValue, handleChange, onCountryCodeChange]
[emptyValue, updateValue, handleChange, onCountryCodeChange]
)

const handleNumberChange = useCallback(
(phoneNumber: string) => {
if (!countryCode && !phoneNumber) {
handleChange?.(emptyValue)
onNumberChange?.(emptyValue)
return
}
(value: string) => {
const phoneNumber = value || emptyValue
const countryCode = countryCodeRef.current || emptyValue
phoneNumberRef.current = phoneNumber || emptyValue

handleChange(
phoneNumber ? joinValue([countryCode, phoneNumber]) : emptyValue,
{
countryCode,
phoneNumber,
}
)

handleChange?.([countryCode, phoneNumber].filter(Boolean).join(' '))
onNumberChange?.(phoneNumber)
},
[countryCode, emptyValue, handleChange, onNumberChange]
[emptyValue, handleChange, onNumberChange]
)

const onFocusHandler = ({ dataList, updateData }) => {
// because there can be more than one country with same cdc
if (dataList.length < 10) {
updateData(
getCountryData({ lang: sharedContext.locale?.split('-')[0] })
)
}
handleFocus()
}
const onFocusHandler = useCallback(
({ updateData }) => {
if (dataRef.current.length < 10) {
dataRef.current = getCountryData({
lang,
})
updateData(dataRef.current)
}
handleFocus()
},
[handleFocus, lang]
)

return (
<FieldBlock
Expand All @@ -151,15 +207,15 @@ function PhoneNumber(props: Props) {
countryCodeLabel ??
sharedContext?.translation.Forms.countryCodeLabel
}
data={singleCountryCodeData}
value={countryCode}
data={dataRef.current}
value={countryCodeRef.current}
disabled={disabled}
on_focus={onFocusHandler}
on_blur={handleBlur}
on_change={handleCountryCodeChange}
independent_width
search_numbers
keep_value_and_selection
keep_selection
no_animation={props.noAnimation}
stretch={width === 'stretch'}
/>
Expand Down Expand Up @@ -192,7 +248,7 @@ function PhoneNumber(props: Props) {
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleNumberChange}
value={phoneNumber}
value={phoneNumberRef.current}
info={info}
warning={warning}
error={error}
Expand Down Expand Up @@ -232,5 +288,17 @@ function getCountryData({ lang = 'en', filter = null } = {}) {
.map((country) => makeObject(country, lang))
}

function splitValue(value: string) {
return (
typeof value === 'string'
? value.match(/^(\+[^ ]+)? ?(.*)$/)
: [undefined, '', '']
).slice(1)
}

function joinValue(array: Array<string>) {
return array.filter(Boolean).join(' ')
}

PhoneNumber._supportsSpacingProps = true
export default PhoneNumber
Loading
Loading