Intermediate States for PC, Tablets and Keyboard Navigation

Recently, I've been working to adopt our PC app onto tablets for acceptable user experience. There are two main challenges that I've encountered, one is regarding how to scale such a complex app to fit a smaller screen securely; the other is discussed here, some thoughts about gestures on tablets because of the implied differences in how users interact with the tablets.

I've followed up a relevant topic later on about on focus indicator. When put together, both are about how users interact with the app and its intermediate state. And it's nice to find a simple solution that address the situation properly, including hovering, focus, gesture on tablets, and accessibility for keyboard users.

Background: The implied interaction differences between tablets and desktop PC

Not preciely, but mostly, PC users interact with the screen from a distance and using a fine cursor. The cursor could be a mouse or a trackpad, but users do not directly touch the pointer, and yet they are able to point precisely using the cursor, giving it refined awareness of the point of focus. Before taking any action, the cursor is already indicating the point, giving it a :hover state.

Another way users may interact with the app on (mostly) PC is with a keyboard. Ideally, users should be able to tab through all the interactable elements, and be reminded of the position of the cursor, because the hopping focus is not controlled by themselves.

Tablets users, however, do not often have or use a fine cursor. Instead, they hug the device and are mostly scrolling. When they are not clicking, the fingers are not on the screen. Therefore there is no assumed indicator of where the point of focus is, until some action (tap / click) takes place. The point of action is also not precise as that with a fine cursor.

In short,

PC:

  • fine cursor with hover state
  • common gesture: scrolling, clicking

tablets:

  • coarse cursor, no hover state
  • common gestures: scrolling, tapping, zooming, swiping

In this context, "hovering behavior" refers to the behavior from the UI standpoint, and :hover refers to the implementation of such UI using CSS :hover state. The difference is significant as I will also describe implementing the behavior using React's onMouseEnter and onMouseLeave synthetic events.

Inconsistent triggerers of :hover across different devices

The very specific situation I encountered when adopting our app to the tablet screen is that it has prior assumptions of the PC interactions only, and so many features are already exclusively relying on the hovering behavior. Furthermore, the intrinsic differences between :hover and React's onMouseEnter, onMouseLeave synthetic events did not show up that much while we were on PC only. However, as we expand our scope to tablets, the problem reveals because different devices and browsers treat :hover differently.

Tapping on newer iPads (after iPadOS) will trigger the :hover pseudo class, and is fairly consistent. But older iPads and Android tablets won't. To trigger the :hover pseudo class, users need to touch the tablet surface as if touching their eyeballs.

In more intuitive terms, if an element implements :hover, in some devices 1st click / tap is hover, 2nd click / tap is click, and in some other devices tap = hover, click = click, but the difference between the two is arguable.

Over the course of my project, I've learned to trigger this with fairly positive confidence level. So it is possible to learn and get used to the behavior. But I categorize this as a "poweruser" usage that we cannot assume our normal users will be able to learn or have the patience to figure out. Together with the context that some of our features already exclusively rely on the hover behavior (means the hover state contains critical information), I would opt for React's synthetic events in this situation. However, if we are designing a new feature, I would rather convince upstreams to not design a UI that put critical information in hovering state, due to the potential hazards for tablet users.

Focus state in place of hover for keyboard navigation

Focus state introduces some complexity in another dimension, specifically the conflict between showing a more obvious focus ring for keyboard navigation and not showing such obvious focus ring otherwise. At the point of this writing (Feb 2022) the :focus-visible selector is still problematic across different browsers, will leave this out for now.

But the neat thing I found is that, with React's synthetic events (onMouseEnter, onMouseLeave, onFocus, and onBlur) we can achieve decent UI with accessibility concerns properly addressed.

Here's some pseudocode for a popover implementation:

const MyComponent = () => {
  const [toggleOn, setToggleOn] = useState(false);
  const [onBlurTimeoutId, setOnBlurTimeoutId] = useState();
  show = () => {
    setToggleOn(true);
  };

  hide = () => {
    setToggleOn(false);
  };

  focus = () => {
    if (onBlurTimeoutId) clearTimeout(onBlurTimeoutId);
    show();
  };

  blur = () => {
    setOnBlurTimeoutId(
      setTimeout(() => {
        hide();
      })
    );
  };

  return () => (
    <div // triggerer
      onMouseEnter={show}
      onMouseLeave={hide}
      onFocus={focus}
      onBlur={blur}
      id="triggerer"
    >
      hover me
      <div aria-describedby="triggerer" aria-hidden={!toggleOn}>
        <a>links are focusable (at least in most browsers)</a>
        <a>when focused on links, do not dismiss the popover</a>
      </div>
    </div>
  );
};

This will give the popover precise control for hovering as well as focusing with keyboard tab. And furthermore, when user tab through the popover element (e.g. some sub menu with interact-able elements), the popover won't be dismissed.