Home > Software design >  React - Checking active element on focus not behaving as expected
React - Checking active element on focus not behaving as expected

Time:09-16

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>
  )
})
  • Related