'Input element losing it's focus after key press when it's controlled from outside the Modal (which uses Portal)

[Solved] My input component is losing focus as soon as I press any key only when its value is controlled from outside the portal

NOTE: I am sorry. While writing this, I found the problem in my code, but I decided to post this anyway

[Reason] I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.

Portal


const root = document.getElementById('root')
const modalRoot = document.getElementById('modal-root')

const Portal = ({ children, className, drawer = false }) => {
  const element = React.useMemo(() => document.createElement('div'), [])

  React.useEffect(() => {
    element.className = clsx('modal', className)
    modalRoot.appendChild(element)

    return () => {
      modalRoot.removeChild(element)
    }
  }, [element, className])

  return ReactDOM.createPortal(children, element)
}

Modal


const Modal = (props) => {
  const { children, show = false, close, className } = props
  const backdrop = React.useRef(null)

  const handleTransitionEnd = React.useCallback(() => setActive(show), [show])

  const handleBackdropClick = React.useCallback(
    ({ target }) => target === backdrop.current && close(),
    []
  )

  const handleKeyUp = React.useCallback(
    ({ key }) => ['Escape'].includes(key) && close(),
    []
  )

  React.useEffect(() => {
    if (backdrop.current) {
      window.addEventListener('keyup', handleKeyUp)
    }

    if (show) {
      root.setAttribute('inert', 'true')
      document.body.style.overflow = 'hidden'

      document.activeElement.blur?.() // ! CULPRIT
    }

    return () => {
      root.removeAttribute('inert')
      document.body.style.overflow = 'auto'
      window.removeEventListener('keyup', handleKeyUp)
    }
  }, [show, close])

  return (
    <>
      {show && (
        <Portal className={className}>
          <div
            ref={backdrop}
            onClick={handleBackdropClick}
            onTransitionEnd={handleTransitionEnd}
            className={clsx('backdrop', show && 'active')}>
            <div className="content">{children}</div>
          </div>
        </Portal>
      )}
    </>
  )
}

Custom Textfield


const TextField = React.forwardRef(
  ({ label, className, ...props }, ref) => {
    return (
      <div className={clsx('textfield', className)}>
        {label && <label>{label}</label>}
        <input ref={ref} {...props} />
      </div>
    )
  }
)


Solution 1:[1]

I was inlining the close function, so the useEffect hook got triggered every time close changed when the component was rendered again due to state changes and thus calling the activeElement.blur() on each keystroke.

In Modal.jsx

...
 React.useEffect(() => {
    ...

    if (show) {
      root.setAttribute('inert', 'true')
      document.body.style.overflow = 'hidden'

      document.activeElement.blur?.() // ! CULPRIT
    }

    ...
  }, [show, close]) // as dependency
...
 <Modal
   show={show}
   close={() => setShow(false)} // this was inlined
   className="some-modal"
>
   ...
</Modal>     

TAKEAWAY

  • Do not inline functions
  • Usually there is no reason to pass a function (pointer) as dependency

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Cyan Froste