I have a form input component where I am checking if the input is active to set the active class to the input's label:
import React, { forwardRef, ReactElement, ReactNode, HTMLProps, useImperativeHandle } from 'react'
import styles from './index.module.css'
import classNames from 'classnames'
import { IconFa } from '../icon-fa'
import { useStableId } from '../use-stable-id'
export interface Props extends HTMLProps<HTMLInputElement> {
// An icon component to show on the left.
icon?: ReactNode
// A help text tooltip to show on the right.
help?: string
// A clear text tooltip to show on the right.
clear?: string
// Pass an onHelp click handler to show a help button.
onHelp?: () => void
// Pass an onClear click handler to show a clear button.
onClear?: () => void
errorMessage?: ReactNode
label?: string
hideLabel?: boolean
}
export const FormInput = forwardRef<HTMLInputElement, Props>(function FormInput(props, ref): ReactElement {
const { id, icon, help, hideLabel, errorMessage, onHelp, onClear, ...rest } = props
const internalRef = React.useRef<HTMLInputElement>(null)
useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => internalRef.current)
const stableId = useStableId()
const inputId = id ? id : stableId
const active = document.activeElement === internalRef.current
const inputClassName = classNames(
styles.input,
icon && styles.hasLeftIcon,
(help || onHelp || onClear) && styles.hasRightIcon
)
const labelClassName = classNames(
styles.label,
Boolean(props.value) && styles.hasValue,
props['aria-invalid'] && styles.hasError,
active && styles.active,
props.className
)
return (
<div className={labelClassName}>
{!hideLabel && (
<label className={styles.labelText} htmlFor={inputId}>
{props.label}
{props.required && '*'}
</label>
)}
<div className="relative">
{icon && <div className={styles.leftIcon}>{icon}</div>}
<input
{...rest}
id={inputId}
ref={internalRef}
aria-invalid={props['aria-invalid']}
aria-label={props.hideLabel ? props.label : undefined}
className={inputClassName}
/>
{onClear && <ClearIcon {...props} />}
{!onClear && (help || onHelp) && <HelpIcon {...props} />}
</div>
{props['aria-invalid'] && <span className={styles.error}>{errorMessage}</span>}
</div>
)
})
function HelpIcon(props: Props) {
if (props.onHelp) {
return (
<button type="button" className={styles.rightIcon} aria-label={props.help} onClick={props.onHelp}>
<IconFa icon={['far', 'info-circle']} title={props.help} />
</button>
)
}
return (
<div className={styles.rightIcon} title={props.help}>
<IconFa icon={['far', 'info-circle']} />
</div>
)
}
function ClearIcon(props: Props) {
return (
<button type="button" className={styles.rightIcon} aria-label={props.clear} onClick={props.onClear}>
<IconFa icon={['far', 'times']} title={props.clear} />
</button>
)
}
But, when I do it like this, the active class is added to label only when I start typing not when the input is focused. Right now, since I am using :focus
selector on input, input gets active class on focus, while label gets it only after typing. How can I fix this so that they both get it on focus?
CodePudding user response:
The active
flag is re-evaluated only on component re-render, that is why you see a change only when the state changes (e.g. by typing, which very probably triggers a state change in the parent component if it uses onChange
for example).
In your case, it seems you could just use the onFocus
and onBlur
listeners directly, and have the active
flag as a state:
export const FormInput = forwardRef<HTMLInputElement, Props>(function FormInput(props, ref) {
const [active, setActive] = useState(false)
const labelClassName = classNames(
active && styles.active
)
return (
<div className={labelClassName}>
<input
onFocus={() => setActive(true)}
onBlur={() => setActive(false)}
/>
</div>
)
})