Search code examples
javascriptcsstypescriptcss-selectorsstorybook

Applying focus styles to a parent element when a children input is selected but only with keyboard


So i've stumbled upon this issue twice now. I have custom components that sometimes need custom focuses and such.

For example, this radio component needs to somehow make the radio button selected whilst actually setting focus on the parent wrapper. And when pressing enter, it should make the radio have checked="true"

This problem seems to be solvable with using :has() selector with CSS, but it's not widely supported, so I cannot use this.

While using :focus-within on the parent wrapper it almost works, but I also get the focused border onClick, which I don't want. I want it to be focusable by keyboard.

So my question is. Should I make the wrapper a component that passes checked state down to the <input type="radio" />, or can I somehow solve it using HTML and CSS and even typescript?


Solution

  • Finally had to solve it by using typescript. So I'm listening to the current elements keyup event and detecting is the key pressed was Tab and applying -is-focused class to the wrapper.

    I detect off element keypresses with a global event listener and if the targeted element is not current element, it takes the focus off.

    const elemId = `${id}--wrapper`;
    document.getElementById(elemId)?.addEventListener('keyup', (e) => {
      if (e.key === 'Tab') {
        document.getElementById(elemId)?.classList.add('is-focused');
      }
    });
    document.addEventListener('keypress', (e) => {
      const current = e.target as HTMLElement;
    
      if (current?.parentElement?.id !== elemId) {
        document.getElementById(elemId)?.classList.remove('is-focused');
      }
    });
    

    EDIT: The previous solution wasn't obviously very good and it's hard to test using jest. The coverage was absolutely horrible.

    I found a library called React-aria that finally gave me a helping hand. I'm currently using useFocusRing().

    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
    
    import React, { useState } from 'react';
    import classNames from 'classnames';
    import { useFocusRing } from 'react-aria';
    
      const { isFocusVisible, focusProps } = useFocusRing();
    
    <div
      className={classNames('checkbox', {
        'is-focused': isFocusVisible,
      })}
    >
     <input
       {...focusProps}
       type="checkbox"
     />
     
     </div>