Skip to content

Commit

Permalink
fix: text input mask fixes (#2581)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholasguyett committed Sep 8, 2023
1 parent 7ca6f76 commit 84cf4d0
Showing 1 changed file with 61 additions and 49 deletions.
110 changes: 61 additions & 49 deletions src/components/forms/TextInputMask/TextInputMask.tsx
@@ -1,25 +1,53 @@
/* eslint-disable security/detect-object-injection */
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import classnames from 'classnames'
import { TextInput, TextInputProps } from '../TextInput/TextInput'

type MaskProps = {
export type AllProps = TextInputProps & {
mask: string
charset?: string
}

export type AllProps = TextInputProps & MaskProps
function maskString(value: string, mask: string, charset?: string) {
const maskData = charset || mask

const strippedValue = charset
? value.replace(/\W/g, '')
: value.replace(/\D/g, '')
const charIsInteger = (v: string) => !Number.isNaN(parseInt(v, 10))
const charIsLetter = (v: string) => (v ? v.match(/[A-Z]/i) : false)
const maskedNumber = '_#dDmMyY9'
const maskedLetter = 'A'
let newValue = ''
for (let m = 0, v = 0; m < maskData.length; m++) {
const isInt = charIsInteger(strippedValue[v])
const isLet = charIsLetter(strippedValue[v])
const matchesNumber = maskedNumber.indexOf(maskData[m]) >= 0
const matchesLetter = maskedLetter.indexOf(maskData[m]) >= 0
if ((matchesNumber && isInt) || (charset && matchesLetter && isLet)) {
newValue += strippedValue[v++]
} else if (
strippedValue[v] === undefined || // if no characters left and the pattern is non-special character
(!charset && !isInt && matchesNumber) ||
(charset && ((matchesLetter && !isLet) || (matchesNumber && !isLet)))
) {
break
} else {
newValue += maskData[m]
}
}

return newValue
}

export const TextInputMask = ({
id,
name,
type,
className,
validationStatus,
inputSize,
inputRef,
mask,
value: externalValue,
defaultValue,
charset,
onChange,
...inputProps
}: AllProps): React.ReactElement => {
const classes = classnames(
Expand All @@ -29,42 +57,31 @@ export const TextInputMask = ({
className
)

const [inputValue, setInputValue] = useState('')
const [maskValue, setMaskValue] = useState(mask)
const [iValue, setIValue] = useState('')
const [value, setValue] = useState(
// Ensure that this component preserves the expected behavior when a user sets the defaultValue
maskString((externalValue ?? defaultValue ?? ``) as string, mask, charset)
)
useEffect(() => {
// Make sure this component behaves correctly when used as a controlled component
setValue(
maskString(
((externalValue ?? defaultValue) as string) ?? ``,
mask,
charset
)
)
}, [externalValue])
const [maskValue, setMaskValue] = useState(mask.substring(value.length))
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value
const maskData = charset || mask
if (undefined === maskData) return
const strippedValue = charset
? value.replace(/\W/g, '')
: value.replace(/\D/g, '')
const charIsInteger = (v: string) => !Number.isNaN(parseInt(v, 10))
const charIsLetter = (v: string) => (v ? v.match(/[A-Z]/i) : false)
const maskedNumber = '_#dDmMyY9'
const maskedLetter = 'A'
let newValue = ''
for (let m = 0, v = 0; m < maskData.length; m++) {
const isInt = charIsInteger(strippedValue[v])
const isLet = charIsLetter(strippedValue[v])
const matchesNumber = maskedNumber.indexOf(maskData[m]) >= 0
const matchesLetter = maskedLetter.indexOf(maskData[m]) >= 0
if ((matchesNumber && isInt) || (charset && matchesLetter && isLet)) {
newValue += strippedValue[v++]
} else if (
strippedValue[v] === undefined || // if no characters left and the pattern is non-special character
(!charset && !isInt && matchesNumber) ||
(charset && ((matchesLetter && !isLet) || (matchesNumber && !isLet)))
) {
break
} else {
newValue += maskData[m]
}
}
const newValue = maskString(e.target.value, mask, charset)

setMaskValue(mask.substring(newValue.length))
setIValue(newValue)
setInputValue(newValue)
inputProps.onChange
setValue(newValue)

// Ensure the new value is available to upstream onChange listeners
e.target.value = newValue

onChange?.(e)
}

return (
Expand All @@ -73,21 +90,16 @@ export const TextInputMask = ({
className="usa-input-mask--content"
aria-hidden
data-testid={`${id}Mask`}>
<i>{iValue}</i>
<i>{value}</i>
{maskValue}
</span>
<TextInput
data-testid="textInput"
className={classes}
id={id}
name={name}
type={type}
ref={inputRef}
maxLength={mask.length}
onChange={handleChange}
value={inputValue}
validationStatus={validationStatus}
inputSize={inputSize}
value={value}
{...inputProps}
/>
</span>
Expand Down

0 comments on commit 84cf4d0

Please sign in to comment.